--- /dev/null
+// 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"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "sort"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/lsp/fuzzy"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/span"
+)
+
+// maxSymbols defines the maximum number of symbol results that should ever be
+// sent in response to a client.
+const maxSymbols = 100
+
+// WorkspaceSymbols matches symbols across all views using the given query,
+// according to the match semantics parameterized by matcherType and style.
+//
+// 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
+ }
+ sc := newSymbolCollector(matcherType, style, query)
+ return sc.walk(ctx, views)
+}
+
+// A matcherFunc determines the matching score of a symbol.
+//
+// See the comment for symbolCollector for more information.
+type matcherFunc func(name string) float64
+
+// A symbolizer returns the best symbol match for name with pkg, according to
+// some heuristic.
+//
+// See the comment for symbolCollector for more information.
+type symbolizer func(name string, pkg Package, m matcherFunc) (string, float64)
+
+func fullyQualifiedSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
+ _, score := dynamicSymbolMatch(name, pkg, matcher)
+ if score > 0 {
+ return pkg.PkgPath() + "." + name, score
+ }
+ return "", 0
+}
+
+func dynamicSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
+ // Prefer any package-qualified match.
+ pkgQualified := pkg.Name() + "." + name
+ if match, score := bestMatch(pkgQualified, matcher); match != "" {
+ return match, score
+ }
+ fullyQualified := pkg.PkgPath() + "." + name
+ if match, score := bestMatch(fullyQualified, matcher); match != "" {
+ return match, score
+ }
+ return "", 0
+}
+
+func packageSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
+ qualified := pkg.Name() + "." + name
+ if matcher(qualified) > 0 {
+ return qualified, 1
+ }
+ return "", 0
+}
+
+// bestMatch returns the highest scoring symbol suffix of fullPath, starting
+// from the right and splitting on selectors and path components.
+//
+// e.g. given a symbol path of the form 'host.com/dir/pkg.type.field', we
+// check the match quality of the following:
+// - field
+// - type.field
+// - pkg.type.field
+// - dir/pkg.type.field
+// - host.com/dir/pkg.type.field
+//
+// and return the best match, along with its score.
+//
+// This is used to implement the 'dynamic' symbol style.
+func bestMatch(fullPath string, matcher matcherFunc) (string, float64) {
+ pathParts := strings.Split(fullPath, "/")
+ dottedParts := strings.Split(pathParts[len(pathParts)-1], ".")
+
+ var best string
+ var score float64
+
+ for i := 0; i < len(dottedParts); i++ {
+ path := strings.Join(dottedParts[len(dottedParts)-1-i:], ".")
+ if match := matcher(path); match > score {
+ best = path
+ score = match
+ }
+ }
+ for i := 0; i < len(pathParts); i++ {
+ path := strings.Join(pathParts[len(pathParts)-1-i:], "/")
+ if match := matcher(path); match > score {
+ best = path
+ score = match
+ }
+ }
+ return best, score
+}
+
+// symbolCollector holds context as we walk Packages, gathering symbols that
+// match a given query.
+//
+// How we match symbols is parameterized by two interfaces:
+// * A matcherFunc determines how well a string symbol matches a query. It
+// returns a non-negative score indicating the quality of the match. A score
+// of zero indicates no match.
+// * A symbolizer determines how we extract the symbol for an object. This
+// enables the 'symbolStyle' configuration option.
+type symbolCollector struct {
+ // These types parameterize the symbol-matching pass.
+ matcher matcherFunc
+ symbolizer symbolizer
+
+ // current holds metadata for the package we are currently walking.
+ current *pkgView
+ curFile *ParsedGoFile
+
+ res [maxSymbols]symbolInformation
+}
+
+func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector {
+ var m matcherFunc
+ switch matcher {
+ case SymbolFuzzy:
+ m = parseQuery(query)
+ case SymbolCaseSensitive:
+ m = func(s string) float64 {
+ if strings.Contains(s, query) {
+ return 1
+ }
+ return 0
+ }
+ case SymbolCaseInsensitive:
+ q := strings.ToLower(query)
+ m = func(s string) float64 {
+ if strings.Contains(strings.ToLower(s), q) {
+ return 1
+ }
+ return 0
+ }
+ default:
+ panic(fmt.Errorf("unknown symbol matcher: %v", matcher))
+ }
+ var s symbolizer
+ switch style {
+ case DynamicSymbols:
+ s = dynamicSymbolMatch
+ case FullyQualifiedSymbols:
+ s = fullyQualifiedSymbolMatch
+ case PackageQualifiedSymbols:
+ s = packageSymbolMatch
+ default:
+ panic(fmt.Errorf("unknown symbol style: %v", style))
+ }
+ return &symbolCollector{
+ matcher: m,
+ symbolizer: s,
+ }
+}
+
+// parseQuery parses a field-separated symbol query, extracting the special
+// characters listed below, and returns a matcherFunc corresponding to the AND
+// of all field queries.
+//
+// Special characters:
+// ^ match exact prefix
+// $ match exact suffix
+// ' match exact
+//
+// In all three of these special queries, matches are 'smart-cased', meaning
+// they are case sensitive if the symbol query contains any upper-case
+// characters, and case insensitive otherwise.
+func parseQuery(q string) matcherFunc {
+ fields := strings.Fields(q)
+ if len(fields) == 0 {
+ return func(string) float64 { return 0 }
+ }
+ var funcs []matcherFunc
+ for _, field := range fields {
+ var f matcherFunc
+ switch {
+ case strings.HasPrefix(field, "^"):
+ prefix := field[1:]
+ f = smartCase(prefix, func(s string) float64 {
+ if strings.HasPrefix(s, prefix) {
+ return 1
+ }
+ return 0
+ })
+ case strings.HasPrefix(field, "'"):
+ exact := field[1:]
+ f = smartCase(exact, func(s string) float64 {
+ if strings.Contains(s, exact) {
+ return 1
+ }
+ return 0
+ })
+ case strings.HasSuffix(field, "$"):
+ suffix := field[0 : len(field)-1]
+ f = smartCase(suffix, func(s string) float64 {
+ if strings.HasSuffix(s, suffix) {
+ return 1
+ }
+ return 0
+ })
+ default:
+ fm := fuzzy.NewMatcher(field)
+ f = func(s string) float64 {
+ return float64(fm.Score(s))
+ }
+ }
+ funcs = append(funcs, f)
+ }
+ return comboMatcher(funcs).match
+}
+
+// smartCase returns a matcherFunc that is case-sensitive if q contains any
+// upper-case characters, and case-insensitive otherwise.
+func smartCase(q string, m matcherFunc) matcherFunc {
+ insensitive := strings.ToLower(q) == q
+ return func(s string) float64 {
+ if insensitive {
+ s = strings.ToLower(s)
+ }
+ return m(s)
+ }
+}
+
+type comboMatcher []matcherFunc
+
+func (c comboMatcher) match(s string) float64 {
+ score := 1.0
+ for _, f := range c {
+ score *= f(s)
+ }
+ return score
+}
+
+// walk walks views, gathers symbols, and returns the results.
+func (sc *symbolCollector) walk(ctx context.Context, views []View) (_ []protocol.SymbolInformation, err error) {
+ toWalk, release, err := sc.collectPackages(ctx, views)
+ defer release()
+ if err != nil {
+ return nil, err
+ }
+ // Make sure we only walk files once (we might see them more than once due to
+ // build constraints).
+ seen := make(map[span.URI]bool)
+ for _, pv := range toWalk {
+ sc.current = pv
+ for _, pgf := range pv.pkg.CompiledGoFiles() {
+ if seen[pgf.URI] {
+ continue
+ }
+ seen[pgf.URI] = true
+ sc.curFile = pgf
+ sc.walkFilesDecls(pgf.File.Decls)
+ }
+ }
+ return sc.results(), nil
+}
+
+func (sc *symbolCollector) results() []protocol.SymbolInformation {
+ var res []protocol.SymbolInformation
+ for _, si := range sc.res {
+ if si.score <= 0 {
+ return res
+ }
+ res = append(res, si.asProtocolSymbolInformation())
+ }
+ return res
+}
+
+// collectPackages gathers the packages we are going to inspect for symbols.
+// This pre-step is required in order to filter out any "duplicate"
+// *types.Package. The duplicates arise for packages that have test variants.
+// For example, if package mod.com/p has test files, then we will visit two
+// packages that have the PkgPath() mod.com/p: the first is the actual package
+// mod.com/p, the second is a special version that includes the non-XTest
+// _test.go files. If we were to walk both of of these packages, then we would
+// get duplicate matching symbols and we would waste effort. Therefore where
+// test variants exist we walk those (because they include any symbols defined
+// in non-XTest _test.go files).
+//
+// One further complication is that even after this filtering, packages between
+// views might not be "identical" because they can be built using different
+// build constraints (via the "env" config option).
+//
+// Therefore on a per view basis we first build up a map of package path ->
+// *types.Package preferring the test variants if they exist. Then we merge the
+// results between views, de-duping by *types.Package.
+func (sc *symbolCollector) collectPackages(ctx context.Context, views []View) ([]*pkgView, func(), error) {
+ gathered := make(map[string]map[*types.Package]*pkgView)
+ var releaseFuncs []func()
+ release := func() {
+ for _, releaseFunc := range releaseFuncs {
+ releaseFunc()
+ }
+ }
+ var toWalk []*pkgView
+ for _, v := range views {
+ seen := make(map[string]*pkgView)
+ snapshot, release := v.Snapshot(ctx)
+ releaseFuncs = append(releaseFuncs, release)
+ knownPkgs, err := snapshot.KnownPackages(ctx)
+ if err != nil {
+ return nil, release, err
+ }
+ workspacePackages, err := snapshot.WorkspacePackages(ctx)
+ if err != nil {
+ return nil, release, err
+ }
+ isWorkspacePkg := make(map[Package]bool)
+ for _, wp := range workspacePackages {
+ isWorkspacePkg[wp] = true
+ }
+ var forTests []*pkgView
+ for _, pkg := range knownPkgs {
+ toAdd := &pkgView{
+ pkg: pkg,
+ snapshot: snapshot,
+ isWorkspace: isWorkspacePkg[pkg],
+ }
+ // Defer test packages, so that they overwrite seen for this package
+ // path.
+ if pkg.ForTest() != "" {
+ forTests = append(forTests, toAdd)
+ } else {
+ seen[pkg.PkgPath()] = toAdd
+ }
+ }
+ for _, pkg := range forTests {
+ seen[pkg.pkg.PkgPath()] = pkg
+ }
+ for _, pkg := range seen {
+ pm, ok := gathered[pkg.pkg.PkgPath()]
+ if !ok {
+ pm = make(map[*types.Package]*pkgView)
+ gathered[pkg.pkg.PkgPath()] = pm
+ }
+ pm[pkg.pkg.GetTypes()] = pkg
+ }
+ }
+ for _, pm := range gathered {
+ for _, pkg := range pm {
+ toWalk = append(toWalk, pkg)
+ }
+ }
+ // Now sort for stability of results. We order by
+ // (pkgView.isWorkspace, pkgView.p.ID())
+ sort.Slice(toWalk, func(i, j int) bool {
+ lhs := toWalk[i]
+ rhs := toWalk[j]
+ switch {
+ case lhs.isWorkspace == rhs.isWorkspace:
+ return lhs.pkg.ID() < rhs.pkg.ID()
+ case lhs.isWorkspace:
+ return true
+ default:
+ return false
+ }
+ })
+ return toWalk, release, nil
+}
+
+func (sc *symbolCollector) walkFilesDecls(decls []ast.Decl) {
+ for _, decl := range decls {
+ switch decl := decl.(type) {
+ case *ast.FuncDecl:
+ kind := protocol.Function
+ var recv *ast.Ident
+ if decl.Recv != nil {
+ kind = protocol.Method
+ switch typ := decl.Recv.List[0].Type.(type) {
+ case *ast.StarExpr:
+ recv = typ.X.(*ast.Ident)
+ case *ast.Ident:
+ recv = typ
+ }
+ }
+ if recv != nil {
+ sc.match(decl.Name.Name, kind, decl.Name, recv)
+ } else {
+ sc.match(decl.Name.Name, kind, decl.Name)
+ }
+ case *ast.GenDecl:
+ for _, spec := range decl.Specs {
+ switch spec := spec.(type) {
+ case *ast.TypeSpec:
+ sc.match(spec.Name.Name, typeToKind(sc.current.pkg.GetTypesInfo().TypeOf(spec.Type)), spec.Name)
+ sc.walkType(spec.Type, spec.Name)
+ case *ast.ValueSpec:
+ for _, name := range spec.Names {
+ kind := protocol.Variable
+ if decl.Tok == token.CONST {
+ kind = protocol.Constant
+ }
+ sc.match(name.Name, kind, name)
+ }
+ }
+ }
+ }
+ }
+}
+
+// walkType processes symbols related to a type expression. path is path of
+// nested type identifiers to the type expression.
+func (sc *symbolCollector) walkType(typ ast.Expr, path ...*ast.Ident) {
+ switch st := typ.(type) {
+ case *ast.StructType:
+ for _, field := range st.Fields.List {
+ sc.walkField(field, protocol.Field, protocol.Field, path...)
+ }
+ case *ast.InterfaceType:
+ for _, field := range st.Methods.List {
+ sc.walkField(field, protocol.Interface, protocol.Method, path...)
+ }
+ }
+}
+
+// walkField processes symbols related to the struct field or interface method.
+//
+// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed
+// or named. path is the path of nested identifiers containing the field.
+func (sc *symbolCollector) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
+ if len(field.Names) == 0 {
+ sc.match(types.ExprString(field.Type), unnamedKind, field, path...)
+ }
+ for _, name := range field.Names {
+ sc.match(name.Name, namedKind, name, path...)
+ sc.walkType(field.Type, append(path, name)...)
+ }
+}
+
+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
+}
+
+// match finds matches and gathers the symbol identified by name, kind and node
+// via the symbolCollector's matcher after first de-duping against previously
+// seen symbols.
+//
+// path specifies the identifier path to a nested field or interface method.
+func (sc *symbolCollector) match(name string, kind protocol.SymbolKind, node ast.Node, path ...*ast.Ident) {
+ if !node.Pos().IsValid() || !node.End().IsValid() {
+ return
+ }
+
+ isExported := isExported(name)
+ if len(path) > 0 {
+ var nameBuilder strings.Builder
+ for _, ident := range path {
+ nameBuilder.WriteString(ident.Name)
+ nameBuilder.WriteString(".")
+ if !ident.IsExported() {
+ isExported = false
+ }
+ }
+ nameBuilder.WriteString(name)
+ name = nameBuilder.String()
+ }
+
+ // Factors to apply to the match score for the purpose of downranking
+ // results.
+ //
+ // These numbers were crudely calibrated based on trial-and-error using a
+ // small number of sample queries. Adjust as necessary.
+ //
+ // All factors are multiplicative, meaning if more than one applies they are
+ // multiplied together.
+ const (
+ // nonWorkspaceFactor is applied to symbols outside of any active
+ // workspace. Developers are less likely to want to jump to code that they
+ // are not actively working on.
+ nonWorkspaceFactor = 0.5
+ // nonWorkspaceUnexportedFactor is applied to unexported symbols outside of
+ // any active workspace. Since one wouldn't usually jump to unexported
+ // symbols to understand a package API, they are particularly irrelevant.
+ nonWorkspaceUnexportedFactor = 0.5
+ // fieldFactor is applied to fields and interface methods. One would
+ // typically jump to the type definition first, so ranking fields highly
+ // can be noisy.
+ fieldFactor = 0.5
+ )
+ symbol, score := sc.symbolizer(name, sc.current.pkg, sc.matcher)
+
+ // Downrank symbols outside of the workspace.
+ if !sc.current.isWorkspace {
+ score *= nonWorkspaceFactor
+ if !isExported {
+ score *= nonWorkspaceUnexportedFactor
+ }
+ }
+
+ // Downrank fields.
+ if len(path) > 0 {
+ score *= fieldFactor
+ }
+
+ // Avoid the work below if we know this score will not be sorted into the
+ // results.
+ if score <= sc.res[len(sc.res)-1].score {
+ return
+ }
+
+ mrng := NewMappedRange(sc.current.snapshot.FileSet(), sc.curFile.Mapper, node.Pos(), node.End())
+ rng, err := mrng.Range()
+ if err != nil {
+ return
+ }
+ si := symbolInformation{
+ score: score,
+ name: name,
+ symbol: symbol,
+ container: sc.current.pkg.PkgPath(),
+ kind: kind,
+ location: protocol.Location{
+ URI: protocol.URIFromSpanURI(mrng.URI()),
+ Range: rng,
+ },
+ }
+ insertAt := sort.Search(len(sc.res), func(i int) bool {
+ return sc.res[i].score < score
+ })
+ if insertAt < len(sc.res)-1 {
+ copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1])
+ }
+ sc.res[insertAt] = si
+}
+
+// isExported reports if a token is exported. Copied from
+// token.IsExported (go1.13+).
+//
+// TODO: replace usage with token.IsExported once go1.12 is no longer
+// supported.
+func isExported(name string) bool {
+ ch, _ := utf8.DecodeRuneInString(name)
+ return unicode.IsUpper(ch)
+}
+
+// pkgView holds information related to a package that we are going to walk.
+type pkgView struct {
+ pkg Package
+ snapshot Snapshot
+ isWorkspace bool
+}
+
+// symbolInformation is a cut-down version of protocol.SymbolInformation that
+// allows struct values of this type to be used as map keys.
+type symbolInformation struct {
+ score float64
+ name string
+ symbol string
+ container string
+ kind protocol.SymbolKind
+ location protocol.Location
+}
+
+// asProtocolSymbolInformation converts s to a protocol.SymbolInformation value.
+//
+// TODO: work out how to handle tags if/when they are needed.
+func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation {
+ return protocol.SymbolInformation{
+ Name: s.symbol,
+ Kind: s.kind,
+ Location: s.location,
+ ContainerName: s.container,
+ }
+}