1 // Copyright 2020 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
18 "golang.org/x/tools/internal/event"
19 "golang.org/x/tools/internal/lsp/fuzzy"
20 "golang.org/x/tools/internal/lsp/protocol"
21 "golang.org/x/tools/internal/span"
24 // maxSymbols defines the maximum number of symbol results that should ever be
25 // sent in response to a client.
26 const maxSymbols = 100
28 // WorkspaceSymbols matches symbols across all views using the given query,
29 // according to the match semantics parameterized by matcherType and style.
31 // The workspace symbol method is defined in the spec as follows:
33 // The workspace symbol request is sent from the client to the server to
34 // list project-wide symbols matching the query string.
36 // It is unclear what "project-wide" means here, but given the parameters of
37 // workspace/symbol do not include any workspace identifier, then it has to be
38 // assumed that "project-wide" means "across all workspaces". Hence why
39 // WorkspaceSymbols receives the views []View.
41 // However, it then becomes unclear what it would mean to call WorkspaceSymbols
42 // with a different configured SymbolMatcher per View. Therefore we assume that
43 // Session level configuration will define the SymbolMatcher to be used for the
44 // WorkspaceSymbols method.
45 func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) {
46 ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
51 sc := newSymbolCollector(matcherType, style, query)
52 return sc.walk(ctx, views)
55 // A matcherFunc determines the matching score of a symbol.
57 // See the comment for symbolCollector for more information.
58 type matcherFunc func(name string) float64
60 // A symbolizer returns the best symbol match for name with pkg, according to
63 // See the comment for symbolCollector for more information.
64 type symbolizer func(name string, pkg Package, m matcherFunc) (string, float64)
66 func fullyQualifiedSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
67 _, score := dynamicSymbolMatch(name, pkg, matcher)
69 return pkg.PkgPath() + "." + name, score
74 func dynamicSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
75 // Prefer any package-qualified match.
76 pkgQualified := pkg.Name() + "." + name
77 if match, score := bestMatch(pkgQualified, matcher); match != "" {
80 fullyQualified := pkg.PkgPath() + "." + name
81 if match, score := bestMatch(fullyQualified, matcher); match != "" {
87 func packageSymbolMatch(name string, pkg Package, matcher matcherFunc) (string, float64) {
88 qualified := pkg.Name() + "." + name
89 if matcher(qualified) > 0 {
95 // bestMatch returns the highest scoring symbol suffix of fullPath, starting
96 // from the right and splitting on selectors and path components.
98 // e.g. given a symbol path of the form 'host.com/dir/pkg.type.field', we
99 // check the match quality of the following:
103 // - dir/pkg.type.field
104 // - host.com/dir/pkg.type.field
106 // and return the best match, along with its score.
108 // This is used to implement the 'dynamic' symbol style.
109 func bestMatch(fullPath string, matcher matcherFunc) (string, float64) {
110 pathParts := strings.Split(fullPath, "/")
111 dottedParts := strings.Split(pathParts[len(pathParts)-1], ".")
116 for i := 0; i < len(dottedParts); i++ {
117 path := strings.Join(dottedParts[len(dottedParts)-1-i:], ".")
118 if match := matcher(path); match > score {
123 for i := 0; i < len(pathParts); i++ {
124 path := strings.Join(pathParts[len(pathParts)-1-i:], "/")
125 if match := matcher(path); match > score {
133 // symbolCollector holds context as we walk Packages, gathering symbols that
134 // match a given query.
136 // How we match symbols is parameterized by two interfaces:
137 // * A matcherFunc determines how well a string symbol matches a query. It
138 // returns a non-negative score indicating the quality of the match. A score
139 // of zero indicates no match.
140 // * A symbolizer determines how we extract the symbol for an object. This
141 // enables the 'symbolStyle' configuration option.
142 type symbolCollector struct {
143 // These types parameterize the symbol-matching pass.
145 symbolizer symbolizer
147 // current holds metadata for the package we are currently walking.
149 curFile *ParsedGoFile
151 res [maxSymbols]symbolInformation
154 func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector {
158 m = parseQuery(query)
159 case SymbolCaseSensitive:
160 m = func(s string) float64 {
161 if strings.Contains(s, query) {
166 case SymbolCaseInsensitive:
167 q := strings.ToLower(query)
168 m = func(s string) float64 {
169 if strings.Contains(strings.ToLower(s), q) {
175 panic(fmt.Errorf("unknown symbol matcher: %v", matcher))
180 s = dynamicSymbolMatch
181 case FullyQualifiedSymbols:
182 s = fullyQualifiedSymbolMatch
183 case PackageQualifiedSymbols:
184 s = packageSymbolMatch
186 panic(fmt.Errorf("unknown symbol style: %v", style))
188 return &symbolCollector{
194 // parseQuery parses a field-separated symbol query, extracting the special
195 // characters listed below, and returns a matcherFunc corresponding to the AND
196 // of all field queries.
198 // Special characters:
199 // ^ match exact prefix
200 // $ match exact suffix
203 // In all three of these special queries, matches are 'smart-cased', meaning
204 // they are case sensitive if the symbol query contains any upper-case
205 // characters, and case insensitive otherwise.
206 func parseQuery(q string) matcherFunc {
207 fields := strings.Fields(q)
208 if len(fields) == 0 {
209 return func(string) float64 { return 0 }
211 var funcs []matcherFunc
212 for _, field := range fields {
215 case strings.HasPrefix(field, "^"):
217 f = smartCase(prefix, func(s string) float64 {
218 if strings.HasPrefix(s, prefix) {
223 case strings.HasPrefix(field, "'"):
225 f = smartCase(exact, func(s string) float64 {
226 if strings.Contains(s, exact) {
231 case strings.HasSuffix(field, "$"):
232 suffix := field[0 : len(field)-1]
233 f = smartCase(suffix, func(s string) float64 {
234 if strings.HasSuffix(s, suffix) {
240 fm := fuzzy.NewMatcher(field)
241 f = func(s string) float64 {
242 return float64(fm.Score(s))
245 funcs = append(funcs, f)
247 return comboMatcher(funcs).match
250 // smartCase returns a matcherFunc that is case-sensitive if q contains any
251 // upper-case characters, and case-insensitive otherwise.
252 func smartCase(q string, m matcherFunc) matcherFunc {
253 insensitive := strings.ToLower(q) == q
254 return func(s string) float64 {
256 s = strings.ToLower(s)
262 type comboMatcher []matcherFunc
264 func (c comboMatcher) match(s string) float64 {
266 for _, f := range c {
272 // walk walks views, gathers symbols, and returns the results.
273 func (sc *symbolCollector) walk(ctx context.Context, views []View) (_ []protocol.SymbolInformation, err error) {
274 toWalk, release, err := sc.collectPackages(ctx, views)
279 // Make sure we only walk files once (we might see them more than once due to
280 // build constraints).
281 seen := make(map[span.URI]bool)
282 for _, pv := range toWalk {
284 for _, pgf := range pv.pkg.CompiledGoFiles() {
290 sc.walkFilesDecls(pgf.File.Decls)
293 return sc.results(), nil
296 func (sc *symbolCollector) results() []protocol.SymbolInformation {
297 var res []protocol.SymbolInformation
298 for _, si := range sc.res {
302 res = append(res, si.asProtocolSymbolInformation())
307 // collectPackages gathers the packages we are going to inspect for symbols.
308 // This pre-step is required in order to filter out any "duplicate"
309 // *types.Package. The duplicates arise for packages that have test variants.
310 // For example, if package mod.com/p has test files, then we will visit two
311 // packages that have the PkgPath() mod.com/p: the first is the actual package
312 // mod.com/p, the second is a special version that includes the non-XTest
313 // _test.go files. If we were to walk both of of these packages, then we would
314 // get duplicate matching symbols and we would waste effort. Therefore where
315 // test variants exist we walk those (because they include any symbols defined
316 // in non-XTest _test.go files).
318 // One further complication is that even after this filtering, packages between
319 // views might not be "identical" because they can be built using different
320 // build constraints (via the "env" config option).
322 // Therefore on a per view basis we first build up a map of package path ->
323 // *types.Package preferring the test variants if they exist. Then we merge the
324 // results between views, de-duping by *types.Package.
325 func (sc *symbolCollector) collectPackages(ctx context.Context, views []View) ([]*pkgView, func(), error) {
326 gathered := make(map[string]map[*types.Package]*pkgView)
327 var releaseFuncs []func()
329 for _, releaseFunc := range releaseFuncs {
333 var toWalk []*pkgView
334 for _, v := range views {
335 seen := make(map[string]*pkgView)
336 snapshot, release := v.Snapshot(ctx)
337 releaseFuncs = append(releaseFuncs, release)
338 knownPkgs, err := snapshot.KnownPackages(ctx)
340 return nil, release, err
342 workspacePackages, err := snapshot.WorkspacePackages(ctx)
344 return nil, release, err
346 isWorkspacePkg := make(map[Package]bool)
347 for _, wp := range workspacePackages {
348 isWorkspacePkg[wp] = true
350 var forTests []*pkgView
351 for _, pkg := range knownPkgs {
355 isWorkspace: isWorkspacePkg[pkg],
357 // Defer test packages, so that they overwrite seen for this package
359 if pkg.ForTest() != "" {
360 forTests = append(forTests, toAdd)
362 seen[pkg.PkgPath()] = toAdd
365 for _, pkg := range forTests {
366 seen[pkg.pkg.PkgPath()] = pkg
368 for _, pkg := range seen {
369 pm, ok := gathered[pkg.pkg.PkgPath()]
371 pm = make(map[*types.Package]*pkgView)
372 gathered[pkg.pkg.PkgPath()] = pm
374 pm[pkg.pkg.GetTypes()] = pkg
377 for _, pm := range gathered {
378 for _, pkg := range pm {
379 toWalk = append(toWalk, pkg)
382 // Now sort for stability of results. We order by
383 // (pkgView.isWorkspace, pkgView.p.ID())
384 sort.Slice(toWalk, func(i, j int) bool {
388 case lhs.isWorkspace == rhs.isWorkspace:
389 return lhs.pkg.ID() < rhs.pkg.ID()
390 case lhs.isWorkspace:
396 return toWalk, release, nil
399 func (sc *symbolCollector) walkFilesDecls(decls []ast.Decl) {
400 for _, decl := range decls {
401 switch decl := decl.(type) {
403 kind := protocol.Function
405 if decl.Recv != nil {
406 kind = protocol.Method
407 switch typ := decl.Recv.List[0].Type.(type) {
409 recv = typ.X.(*ast.Ident)
415 sc.match(decl.Name.Name, kind, decl.Name, recv)
417 sc.match(decl.Name.Name, kind, decl.Name)
420 for _, spec := range decl.Specs {
421 switch spec := spec.(type) {
423 sc.match(spec.Name.Name, typeToKind(sc.current.pkg.GetTypesInfo().TypeOf(spec.Type)), spec.Name)
424 sc.walkType(spec.Type, spec.Name)
426 for _, name := range spec.Names {
427 kind := protocol.Variable
428 if decl.Tok == token.CONST {
429 kind = protocol.Constant
431 sc.match(name.Name, kind, name)
439 // walkType processes symbols related to a type expression. path is path of
440 // nested type identifiers to the type expression.
441 func (sc *symbolCollector) walkType(typ ast.Expr, path ...*ast.Ident) {
442 switch st := typ.(type) {
443 case *ast.StructType:
444 for _, field := range st.Fields.List {
445 sc.walkField(field, protocol.Field, protocol.Field, path...)
447 case *ast.InterfaceType:
448 for _, field := range st.Methods.List {
449 sc.walkField(field, protocol.Interface, protocol.Method, path...)
454 // walkField processes symbols related to the struct field or interface method.
456 // unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed
457 // or named. path is the path of nested identifiers containing the field.
458 func (sc *symbolCollector) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
459 if len(field.Names) == 0 {
460 sc.match(types.ExprString(field.Type), unnamedKind, field, path...)
462 for _, name := range field.Names {
463 sc.match(name.Name, namedKind, name, path...)
464 sc.walkType(field.Type, append(path, name)...)
468 func typeToKind(typ types.Type) protocol.SymbolKind {
469 switch typ := typ.Underlying().(type) {
470 case *types.Interface:
471 return protocol.Interface
473 return protocol.Struct
474 case *types.Signature:
475 if typ.Recv() != nil {
476 return protocol.Method
478 return protocol.Function
480 return typeToKind(typ.Underlying())
484 case i&types.IsNumeric != 0:
485 return protocol.Number
486 case i&types.IsBoolean != 0:
487 return protocol.Boolean
488 case i&types.IsString != 0:
489 return protocol.String
492 return protocol.Variable
495 // match finds matches and gathers the symbol identified by name, kind and node
496 // via the symbolCollector's matcher after first de-duping against previously
499 // path specifies the identifier path to a nested field or interface method.
500 func (sc *symbolCollector) match(name string, kind protocol.SymbolKind, node ast.Node, path ...*ast.Ident) {
501 if !node.Pos().IsValid() || !node.End().IsValid() {
505 isExported := isExported(name)
507 var nameBuilder strings.Builder
508 for _, ident := range path {
509 nameBuilder.WriteString(ident.Name)
510 nameBuilder.WriteString(".")
511 if !ident.IsExported() {
515 nameBuilder.WriteString(name)
516 name = nameBuilder.String()
519 // Factors to apply to the match score for the purpose of downranking
522 // These numbers were crudely calibrated based on trial-and-error using a
523 // small number of sample queries. Adjust as necessary.
525 // All factors are multiplicative, meaning if more than one applies they are
526 // multiplied together.
528 // nonWorkspaceFactor is applied to symbols outside of any active
529 // workspace. Developers are less likely to want to jump to code that they
530 // are not actively working on.
531 nonWorkspaceFactor = 0.5
532 // nonWorkspaceUnexportedFactor is applied to unexported symbols outside of
533 // any active workspace. Since one wouldn't usually jump to unexported
534 // symbols to understand a package API, they are particularly irrelevant.
535 nonWorkspaceUnexportedFactor = 0.5
536 // fieldFactor is applied to fields and interface methods. One would
537 // typically jump to the type definition first, so ranking fields highly
541 symbol, score := sc.symbolizer(name, sc.current.pkg, sc.matcher)
543 // Downrank symbols outside of the workspace.
544 if !sc.current.isWorkspace {
545 score *= nonWorkspaceFactor
547 score *= nonWorkspaceUnexportedFactor
556 // Avoid the work below if we know this score will not be sorted into the
558 if score <= sc.res[len(sc.res)-1].score {
562 mrng := NewMappedRange(sc.current.snapshot.FileSet(), sc.curFile.Mapper, node.Pos(), node.End())
563 rng, err := mrng.Range()
567 si := symbolInformation{
571 container: sc.current.pkg.PkgPath(),
573 location: protocol.Location{
574 URI: protocol.URIFromSpanURI(mrng.URI()),
578 insertAt := sort.Search(len(sc.res), func(i int) bool {
579 return sc.res[i].score < score
581 if insertAt < len(sc.res)-1 {
582 copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1])
584 sc.res[insertAt] = si
587 // isExported reports if a token is exported. Copied from
588 // token.IsExported (go1.13+).
590 // TODO: replace usage with token.IsExported once go1.12 is no longer
592 func isExported(name string) bool {
593 ch, _ := utf8.DecodeRuneInString(name)
594 return unicode.IsUpper(ch)
597 // pkgView holds information related to a package that we are going to walk.
598 type pkgView struct {
604 // symbolInformation is a cut-down version of protocol.SymbolInformation that
605 // allows struct values of this type to be used as map keys.
606 type symbolInformation struct {
611 kind protocol.SymbolKind
612 location protocol.Location
615 // asProtocolSymbolInformation converts s to a protocol.SymbolInformation value.
617 // TODO: work out how to handle tags if/when they are needed.
618 func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation {
619 return protocol.SymbolInformation{
622 Location: s.location,
623 ContainerName: s.container,