mirror of
https://github.com/golang/go
synced 2024-11-18 18:44:42 -07:00
a9439ae9c1
ParseGoHandles serve two purposes: they pin cache entries so that redundant calculations are cached, and they allow users to obtain the actual parsed AST. The former is an implementation detail, and the latter turns out to just be an annoyance. Parsed Go files are obtained from two places. By far the most common is from a type checked package. But a type checked package must by definition have already parsed all the files it contains, so the PGH is already computed and cannot have failed. Type checked packages can simply return the parsed file without requiring a separate Check operation. We do want to pin the cache entries in this case, which I've done by holding on to the PGH in cache.pkg. There are some cases where we directly parse a file, such as for the FoldingRange LSP call, which doesn't need type information. Those parses can actually fail, so we do need an error check. But we don't need the PGH; in all cases we are immediately using and discarding it. So it turns out we don't actually need the PGH type at all, at least not in the public API. Instead, we can pass around a concrete struct that has the various pieces of data directly available. This uncovered a bug in typeCheck: it should fail if it encounters any real errors. Change-Id: I203bf2dd79d5d65c01392d69c2cf4f7744fde7fc Reviewed-on: https://go-review.googlesource.com/c/tools/+/244021 Run-TryBot: Heschi Kreinick <heschi@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
332 lines
8.9 KiB
Go
332 lines
8.9 KiB
Go
// Copyright 2020 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package source
|
|
|
|
import (
|
|
"context"
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/internal/event"
|
|
"golang.org/x/tools/internal/lsp/fuzzy"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
)
|
|
|
|
const maxSymbols = 100
|
|
|
|
// WorkspaceSymbols matches symbols across views using the given query,
|
|
// according to the SymbolMatcher matcher.
|
|
//
|
|
// The workspace symbol method is defined in the spec as follows:
|
|
//
|
|
// > The workspace symbol request is sent from the client to the server to
|
|
// > list project-wide symbols matching the query string.
|
|
//
|
|
// It is unclear what "project-wide" means here, but given the parameters of
|
|
// workspace/symbol do not include any workspace identifier, then it has to be
|
|
// assumed that "project-wide" means "across all workspaces". Hence why
|
|
// WorkspaceSymbols receives the views []View.
|
|
//
|
|
// However, it then becomes unclear what it would mean to call WorkspaceSymbols
|
|
// with a different configured SymbolMatcher per View. Therefore we assume that
|
|
// Session level configuration will define the SymbolMatcher to be used for the
|
|
// WorkspaceSymbols method.
|
|
func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) {
|
|
ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
|
|
defer done()
|
|
if query == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
queryMatcher := makeQueryMatcher(matcherType, query)
|
|
seen := make(map[string]struct{})
|
|
var symbols []protocol.SymbolInformation
|
|
outer:
|
|
for _, view := range views {
|
|
knownPkgs, err := view.Snapshot().KnownPackages(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ph := range knownPkgs {
|
|
pkg, err := ph.Check(ctx, view.Snapshot())
|
|
symbolMatcher := makePackageSymbolMatcher(style, pkg, queryMatcher)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := seen[pkg.PkgPath()]; ok {
|
|
continue
|
|
}
|
|
seen[pkg.PkgPath()] = struct{}{}
|
|
for _, pgf := range pkg.CompiledGoFiles() {
|
|
for _, si := range findSymbol(pgf.File.Decls, pkg.GetTypesInfo(), symbolMatcher) {
|
|
mrng, err := posToMappedRange(view, pkg, si.node.Pos(), si.node.End())
|
|
if err != nil {
|
|
event.Error(ctx, "Error getting mapped range for node", err)
|
|
continue
|
|
}
|
|
rng, err := mrng.Range()
|
|
if err != nil {
|
|
event.Error(ctx, "Error getting range from mapped range", err)
|
|
continue
|
|
}
|
|
symbols = append(symbols, protocol.SymbolInformation{
|
|
Name: si.name,
|
|
Kind: si.kind,
|
|
Location: protocol.Location{
|
|
URI: protocol.URIFromSpanURI(mrng.URI()),
|
|
Range: rng,
|
|
},
|
|
ContainerName: pkg.PkgPath(),
|
|
})
|
|
if len(symbols) > maxSymbols {
|
|
break outer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return symbols, nil
|
|
}
|
|
|
|
type symbolInformation struct {
|
|
name string
|
|
kind protocol.SymbolKind
|
|
node ast.Node
|
|
}
|
|
|
|
type matcherFunc func(string) bool
|
|
|
|
func makeQueryMatcher(m SymbolMatcher, query string) matcherFunc {
|
|
switch m {
|
|
case SymbolFuzzy:
|
|
fm := fuzzy.NewMatcher(query)
|
|
return func(s string) bool {
|
|
return fm.Score(s) > 0
|
|
}
|
|
case SymbolCaseSensitive:
|
|
return func(s string) bool {
|
|
return strings.Contains(s, query)
|
|
}
|
|
default:
|
|
q := strings.ToLower(query)
|
|
return func(s string) bool {
|
|
return strings.Contains(strings.ToLower(s), q)
|
|
}
|
|
}
|
|
}
|
|
|
|
// packageSymbolMatcher matches (possibly partially) qualified symbols within a
|
|
// package scope.
|
|
//
|
|
// The given symbolizer controls how symbol names are extracted from the
|
|
// package scope.
|
|
type packageSymbolMatcher struct {
|
|
queryMatcher matcherFunc
|
|
pkg Package
|
|
symbolize symbolizer
|
|
}
|
|
|
|
// symbolMatch returns the package symbol for name that matches the underlying
|
|
// query, or the empty string if no match is found.
|
|
func (s packageSymbolMatcher) symbolMatch(name string) string {
|
|
return s.symbolize(name, s.pkg, s.queryMatcher)
|
|
}
|
|
|
|
func makePackageSymbolMatcher(style SymbolStyle, pkg Package, matcher matcherFunc) func(string) string {
|
|
var s symbolizer
|
|
switch style {
|
|
case DynamicSymbols:
|
|
s = dynamicSymbolMatch
|
|
case FullyQualifiedSymbols:
|
|
s = fullyQualifiedSymbolMatch
|
|
default:
|
|
s = packageSymbolMatch
|
|
}
|
|
return packageSymbolMatcher{queryMatcher: matcher, pkg: pkg, symbolize: s}.symbolMatch
|
|
}
|
|
|
|
// A symbolizer returns a qualified symbol match for the unqualified name
|
|
// within pkg, if one exists, or the empty string if no match is found.
|
|
type symbolizer func(name string, pkg Package, m matcherFunc) string
|
|
|
|
func fullyQualifiedSymbolMatch(name string, pkg Package, matcher matcherFunc) string {
|
|
// TODO: this should probably include pkg.Name() as well.
|
|
fullyQualified := pkg.PkgPath() + "." + name
|
|
if matcher(fullyQualified) {
|
|
return fullyQualified
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func dynamicSymbolMatch(name string, pkg Package, matcher matcherFunc) string {
|
|
pkgQualified := pkg.Name() + "." + name
|
|
if match := shortestMatch(pkgQualified, matcher); match != "" {
|
|
return match
|
|
}
|
|
fullyQualified := pkg.PkgPath() + "." + name
|
|
if match := shortestMatch(fullyQualified, matcher); match != "" {
|
|
return match
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func packageSymbolMatch(name string, pkg Package, matcher matcherFunc) string {
|
|
qualified := pkg.Name() + "." + name
|
|
if matcher(qualified) {
|
|
return qualified
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func shortestMatch(fullPath string, matcher func(string) bool) string {
|
|
pathParts := strings.Split(fullPath, "/")
|
|
dottedParts := strings.Split(pathParts[len(pathParts)-1], ".")
|
|
// First match the smallest package identifier.
|
|
if m := matchRight(dottedParts, ".", matcher); m != "" {
|
|
return m
|
|
}
|
|
// Then match the shortest subpath.
|
|
return matchRight(pathParts, "/", matcher)
|
|
}
|
|
|
|
func matchRight(parts []string, sep string, matcher func(string) bool) string {
|
|
for i := 0; i < len(parts); i++ {
|
|
path := strings.Join(parts[len(parts)-1-i:], sep)
|
|
if matcher(path) {
|
|
return path
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findSymbol(decls []ast.Decl, info *types.Info, symbolMatch func(string) string) []symbolInformation {
|
|
var result []symbolInformation
|
|
for _, decl := range decls {
|
|
switch decl := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
fn := decl.Name.Name
|
|
kind := protocol.Function
|
|
if decl.Recv != nil {
|
|
kind = protocol.Method
|
|
switch typ := decl.Recv.List[0].Type.(type) {
|
|
case *ast.StarExpr:
|
|
fn = typ.X.(*ast.Ident).Name + "." + fn
|
|
case *ast.Ident:
|
|
fn = typ.Name + "." + fn
|
|
}
|
|
}
|
|
if m := symbolMatch(fn); m != "" {
|
|
result = append(result, symbolInformation{
|
|
name: m,
|
|
kind: kind,
|
|
node: decl.Name,
|
|
})
|
|
}
|
|
case *ast.GenDecl:
|
|
for _, spec := range decl.Specs {
|
|
switch spec := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
target := spec.Name.Name
|
|
if m := symbolMatch(target); m != "" {
|
|
result = append(result, symbolInformation{
|
|
name: m,
|
|
kind: typeToKind(info.TypeOf(spec.Type)),
|
|
node: spec.Name,
|
|
})
|
|
}
|
|
switch st := spec.Type.(type) {
|
|
case *ast.StructType:
|
|
for _, field := range st.Fields.List {
|
|
result = append(result, findFieldSymbol(field, protocol.Field, symbolMatch, target)...)
|
|
}
|
|
case *ast.InterfaceType:
|
|
for _, field := range st.Methods.List {
|
|
kind := protocol.Method
|
|
if len(field.Names) == 0 {
|
|
kind = protocol.Interface
|
|
}
|
|
result = append(result, findFieldSymbol(field, kind, symbolMatch, target)...)
|
|
}
|
|
}
|
|
case *ast.ValueSpec:
|
|
for _, name := range spec.Names {
|
|
if m := symbolMatch(name.Name); m != "" {
|
|
kind := protocol.Variable
|
|
if decl.Tok == token.CONST {
|
|
kind = protocol.Constant
|
|
}
|
|
result = append(result, symbolInformation{
|
|
name: m,
|
|
kind: kind,
|
|
node: name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func typeToKind(typ types.Type) protocol.SymbolKind {
|
|
switch typ := typ.Underlying().(type) {
|
|
case *types.Interface:
|
|
return protocol.Interface
|
|
case *types.Struct:
|
|
return protocol.Struct
|
|
case *types.Signature:
|
|
if typ.Recv() != nil {
|
|
return protocol.Method
|
|
}
|
|
return protocol.Function
|
|
case *types.Named:
|
|
return typeToKind(typ.Underlying())
|
|
case *types.Basic:
|
|
i := typ.Info()
|
|
switch {
|
|
case i&types.IsNumeric != 0:
|
|
return protocol.Number
|
|
case i&types.IsBoolean != 0:
|
|
return protocol.Boolean
|
|
case i&types.IsString != 0:
|
|
return protocol.String
|
|
}
|
|
}
|
|
return protocol.Variable
|
|
}
|
|
|
|
func findFieldSymbol(field *ast.Field, kind protocol.SymbolKind, symbolMatch func(string) string, prefix string) []symbolInformation {
|
|
var result []symbolInformation
|
|
|
|
if len(field.Names) == 0 {
|
|
name := types.ExprString(field.Type)
|
|
target := prefix + "." + name
|
|
if m := symbolMatch(target); m != "" {
|
|
result = append(result, symbolInformation{
|
|
name: m,
|
|
kind: kind,
|
|
node: field,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
for _, name := range field.Names {
|
|
target := prefix + "." + name.Name
|
|
if m := symbolMatch(target); m != "" {
|
|
result = append(result, symbolInformation{
|
|
name: m,
|
|
kind: kind,
|
|
node: name,
|
|
})
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|