aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/source/references.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/source/references.go')
-rw-r--r--gopls/internal/lsp/source/references.go582
1 files changed, 582 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
new file mode 100644
index 000000000..3f8960180
--- /dev/null
+++ b/gopls/internal/lsp/source/references.go
@@ -0,0 +1,582 @@
+// Copyright 2019 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
+
+// This file defines the 'references' query based on a serializable
+// index constructed during type checking, thus avoiding the need to
+// type-check packages at search time.
+//
+// See the ./xrefs/ subpackage for the index construction and lookup.
+//
+// This implementation does not intermingle objects from distinct
+// calls to TypeCheck.
+
+import (
+ "context"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "sort"
+ "strings"
+ "sync"
+
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/tools/go/types/objectpath"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
+ "golang.org/x/tools/gopls/internal/lsp/source/methodsets"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/bug"
+ "golang.org/x/tools/internal/event"
+)
+
+// References returns a list of all references (sorted with
+// definitions before uses) to the object denoted by the identifier at
+// the given file/position, searching the entire workspace.
+func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) {
+ references, err := references(ctx, snapshot, fh, pp, includeDeclaration)
+ if err != nil {
+ return nil, err
+ }
+ locations := make([]protocol.Location, len(references))
+ for i, ref := range references {
+ locations[i] = ref.location
+ }
+ return locations, nil
+}
+
+// A reference describes an identifier that refers to the same
+// object as the subject of a References query.
+type reference struct {
+ isDeclaration bool
+ location protocol.Location
+ pkgPath PackagePath // of declaring package (same for all elements of the slice)
+}
+
+// references returns a list of all references (sorted with
+// definitions before uses) to the object denoted by the identifier at
+// the given file/position, searching the entire workspace.
+func references(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]reference, error) {
+ ctx, done := event.Start(ctx, "source.References2")
+ defer done()
+
+ // Is the cursor within the package name declaration?
+ _, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp)
+ if err != nil {
+ return nil, err
+ }
+
+ var refs []reference
+ if inPackageName {
+ refs, err = packageReferences(ctx, snapshot, f.URI())
+ } else {
+ refs, err = ordinaryReferences(ctx, snapshot, f.URI(), pp)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ sort.Slice(refs, func(i, j int) bool {
+ x, y := refs[i], refs[j]
+ if x.isDeclaration != y.isDeclaration {
+ return x.isDeclaration // decls < refs
+ }
+ return protocol.CompareLocation(x.location, y.location) < 0
+ })
+
+ // De-duplicate by location, and optionally remove declarations.
+ out := refs[:0]
+ for _, ref := range refs {
+ if !includeDeclaration && ref.isDeclaration {
+ continue
+ }
+ if len(out) == 0 || out[len(out)-1].location != ref.location {
+ out = append(out, ref)
+ }
+ }
+ refs = out
+
+ return refs, nil
+}
+
+// packageReferences returns a list of references to the package
+// declaration of the specified name and uri by searching among the
+// import declarations of all packages that directly import the target
+// package.
+func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI) ([]reference, error) {
+ metas, err := snapshot.MetadataForFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ if len(metas) == 0 {
+ return nil, fmt.Errorf("found no package containing %s", uri)
+ }
+
+ var refs []reference
+
+ // Find external references to the package declaration
+ // from each direct import of the package.
+ //
+ // The narrowest package is the most broadly imported,
+ // so we choose it for the external references.
+ //
+ // But if the file ends with _test.go then we need to
+ // find the package it is testing; there's no direct way
+ // to do that, so pick a file from the same package that
+ // doesn't end in _test.go and start over.
+ narrowest := metas[0]
+ if narrowest.ForTest != "" && strings.HasSuffix(string(uri), "_test.go") {
+ for _, f := range narrowest.CompiledGoFiles {
+ if !strings.HasSuffix(string(f), "_test.go") {
+ return packageReferences(ctx, snapshot, f)
+ }
+ }
+ // This package has no non-test files.
+ // Skip the search for external references.
+ // (Conceivably one could blank-import an empty package, but why?)
+ } else {
+ rdeps, err := snapshot.ReverseDependencies(ctx, narrowest.ID, false) // direct
+ if err != nil {
+ return nil, err
+ }
+ for _, rdep := range rdeps {
+ for _, uri := range rdep.CompiledGoFiles {
+ fh, err := snapshot.GetFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ f, err := snapshot.ParseGo(ctx, fh, ParseHeader)
+ if err != nil {
+ return nil, err
+ }
+ for _, imp := range f.File.Imports {
+ if rdep.DepsByImpPath[UnquoteImportPath(imp)] == narrowest.ID {
+ refs = append(refs, reference{
+ isDeclaration: false,
+ location: mustLocation(f, imp),
+ pkgPath: narrowest.PkgPath,
+ })
+ }
+ }
+ }
+ }
+ }
+
+ // Find internal "references" to the package from
+ // of each package declaration in the target package itself.
+ //
+ // The widest package (possibly a test variant) has the
+ // greatest number of files and thus we choose it for the
+ // "internal" references.
+ widest := metas[len(metas)-1]
+ for _, uri := range widest.CompiledGoFiles {
+ fh, err := snapshot.GetFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ f, err := snapshot.ParseGo(ctx, fh, ParseHeader)
+ if err != nil {
+ return nil, err
+ }
+ refs = append(refs, reference{
+ isDeclaration: true, // (one of many)
+ location: mustLocation(f, f.File.Name),
+ pkgPath: widest.PkgPath,
+ })
+ }
+
+ return refs, nil
+}
+
+// ordinaryReferences computes references for all ordinary objects (not package declarations).
+func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp protocol.Position) ([]reference, error) {
+ // Strategy: use the reference information computed by the
+ // type checker to find the declaration. First type-check this
+ // package to find the declaration, then type check the
+ // declaring package (which may be different), plus variants,
+ // to find local (in-package) references.
+ // Global references are satisfied by the index.
+
+ // Strictly speaking, a wider package could provide a different
+ // declaration (e.g. because the _test.go files can change the
+ // meaning of a field or method selection), but the narrower
+ // package reports the more broadly referenced object.
+ pkg, pgf, err := PackageForFile(ctx, snapshot, uri, NarrowestPackage)
+ if err != nil {
+ return nil, err
+ }
+
+ // Find the selected object (declaration or reference).
+ pos, err := pgf.PositionPos(pp)
+ if err != nil {
+ return nil, err
+ }
+ candidates, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos)
+ if err != nil {
+ return nil, err
+ }
+
+ // Pick first object arbitrarily.
+ // The case variables of a type switch have different
+ // types but that difference is immaterial here.
+ var obj types.Object
+ for obj = range candidates {
+ break
+ }
+ if obj == nil {
+ return nil, ErrNoIdentFound // can't happen
+ }
+
+ // nil, error, error.Error, iota, or other built-in?
+ if obj.Pkg() == nil {
+ // For some reason, existing tests require that iota has no references,
+ // nor an error. TODO(adonovan): do something more principled.
+ if obj.Name() == "iota" {
+ return nil, nil
+ }
+
+ return nil, fmt.Errorf("references to builtin %q are not supported", obj.Name())
+ }
+
+ // Find metadata of all packages containing the object's defining file.
+ // This may include the query pkg, and possibly other variants.
+ declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
+ declURI := span.URIFromPath(declPosn.Filename)
+ variants, err := snapshot.MetadataForFile(ctx, declURI)
+ if err != nil {
+ return nil, err
+ }
+ if len(variants) == 0 {
+ return nil, fmt.Errorf("no packages for file %q", declURI) // can't happen
+ }
+
+ // Is object exported?
+ // If so, compute scope and targets of the global search.
+ var (
+ globalScope = make(map[PackageID]*Metadata)
+ globalTargets map[PackagePath]map[objectpath.Path]unit
+ )
+ // TODO(adonovan): what about generic functions. Need to consider both
+ // uninstantiated and instantiated. The latter have no objectpath. Use Origin?
+ if path, err := objectpath.For(obj); err == nil && obj.Exported() {
+ pkgPath := variants[0].PkgPath // (all variants have same package path)
+ globalTargets = map[PackagePath]map[objectpath.Path]unit{
+ pkgPath: {path: {}}, // primary target
+ }
+
+ // How far need we search?
+ // For package-level objects, we need only search the direct importers.
+ // For fields and methods, we must search transitively.
+ transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj
+
+ // The scope is the union of rdeps of each variant.
+ // (Each set is disjoint so there's no benefit to
+ // to combining the metadata graph traversals.)
+ for _, m := range variants {
+ rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive)
+ if err != nil {
+ return nil, err
+ }
+ for id, rdep := range rdeps {
+ globalScope[id] = rdep
+ }
+ }
+
+ // Is object a method?
+ //
+ // If so, expand the search so that the targets include
+ // all methods that correspond to it through interface
+ // satisfaction, and the scope includes the rdeps of
+ // the package that declares each corresponding type.
+ if recv := effectiveReceiver(obj); recv != nil {
+ if err := expandMethodSearch(ctx, snapshot, obj.(*types.Func), recv, globalScope, globalTargets); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ // The search functions will call report(loc) for each hit.
+ var (
+ refsMu sync.Mutex
+ refs []reference
+ )
+ report := func(loc protocol.Location, isDecl bool) {
+ ref := reference{
+ isDeclaration: isDecl,
+ location: loc,
+ pkgPath: pkg.Metadata().PkgPath,
+ }
+ refsMu.Lock()
+ refs = append(refs, ref)
+ refsMu.Unlock()
+ }
+
+ // Loop over the variants of the declaring package,
+ // and perform both the local (in-package) and global
+ // (cross-package) searches, in parallel.
+ //
+ // TODO(adonovan): opt: support LSP reference streaming. See:
+ // - https://github.com/microsoft/vscode-languageserver-node/pull/164
+ // - https://github.com/microsoft/language-server-protocol/pull/182
+ //
+ // Careful: this goroutine must not return before group.Wait.
+ var group errgroup.Group
+
+ // Compute local references for each variant.
+ for _, m := range variants {
+ // We want the ordinary importable package,
+ // plus any test-augmented variants, since
+ // declarations in _test.go files may change
+ // the reference of a selection, or even a
+ // field into a method or vice versa.
+ //
+ // But we don't need intermediate test variants,
+ // as their local references will be covered
+ // already by other variants.
+ if m.IsIntermediateTestVariant() {
+ continue
+ }
+ m := m
+ group.Go(func() error {
+ return localReferences(ctx, snapshot, declURI, declPosn.Offset, m, report)
+ })
+ }
+
+ // Compute global references for selected reverse dependencies.
+ group.Go(func() error {
+ var globalIDs []PackageID
+ for id := range globalScope {
+ globalIDs = append(globalIDs, id)
+ }
+ indexes, err := snapshot.References(ctx, globalIDs...)
+ if err != nil {
+ return err
+ }
+ for _, index := range indexes {
+ for _, loc := range index.Lookup(globalTargets) {
+ report(loc, false)
+ }
+ }
+ return nil
+ })
+
+ if err := group.Wait(); err != nil {
+ return nil, err
+ }
+ return refs, nil
+}
+
+// expandMethodSearch expands the scope and targets of a global search
+// for an exported method to include all methods that correspond to
+// it through interface satisfaction.
+//
+// recv is the method's effective receiver type, for method-set computations.
+func expandMethodSearch(ctx context.Context, snapshot Snapshot, method *types.Func, recv types.Type, scope map[PackageID]*Metadata, targets map[PackagePath]map[objectpath.Path]unit) error {
+ // Compute the method-set fingerprint used as a key to the global search.
+ key, hasMethods := methodsets.KeyOf(recv)
+ if !hasMethods {
+ return bug.Errorf("KeyOf(%s)={} yet %s is a method", recv, method)
+ }
+ metas, err := snapshot.AllMetadata(ctx)
+ if err != nil {
+ return err
+ }
+ allIDs := make([]PackageID, 0, len(metas))
+ for _, m := range metas {
+ allIDs = append(allIDs, m.ID)
+ }
+ // Search the methodset index of each package in the workspace.
+ indexes, err := snapshot.MethodSets(ctx, allIDs...)
+ if err != nil {
+ return err
+ }
+ var mu sync.Mutex // guards scope and targets
+ var group errgroup.Group
+ for i, index := range indexes {
+ i := i
+ index := index
+ group.Go(func() error {
+ // Consult index for matching methods.
+ results := index.Search(key, method.Name())
+ if len(results) == 0 {
+ return nil
+ }
+
+ // Expand global search scope to include rdeps of this pkg.
+ rdeps, err := snapshot.ReverseDependencies(ctx, allIDs[i], true)
+ if err != nil {
+ return err
+ }
+ mu.Lock()
+ defer mu.Unlock()
+ for _, rdep := range rdeps {
+ scope[rdep.ID] = rdep
+ }
+
+ // Add each corresponding method the to set of global search targets.
+ for _, res := range results {
+ methodPkg := PackagePath(res.PkgPath)
+ opaths, ok := targets[methodPkg]
+ if !ok {
+ opaths = make(map[objectpath.Path]unit)
+ targets[methodPkg] = opaths
+ }
+ opaths[res.ObjectPath] = unit{}
+ }
+ return nil
+ })
+ }
+ return group.Wait()
+}
+
+// localReferences reports each reference to the object
+// declared at the specified URI/offset within its enclosing package m.
+func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, declOffset int, m *Metadata, report func(loc protocol.Location, isDecl bool)) error {
+ pkgs, err := snapshot.TypeCheck(ctx, m.ID)
+ if err != nil {
+ return err
+ }
+ pkg := pkgs[0] // narrowest
+
+ // Find declaration of corresponding object
+ // in this package based on (URI, offset).
+ pgf, err := pkg.File(declURI)
+ if err != nil {
+ return err
+ }
+ pos, err := safetoken.Pos(pgf.Tok, declOffset)
+ if err != nil {
+ return err
+ }
+ targets, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos)
+ if err != nil {
+ return err // unreachable? (probably caught earlier)
+ }
+
+ // Report the locations of the declaration(s).
+ // TODO(adonovan): what about for corresponding methods? Add tests.
+ for _, node := range targets {
+ report(mustLocation(pgf, node), true)
+ }
+
+ // If we're searching for references to a method, broaden the
+ // search to include references to corresponding methods of
+ // mutually assignable receiver types.
+ // (We use a slice, but objectsAt never returns >1 methods.)
+ var methodRecvs []types.Type
+ var methodName string // name of an arbitrary target, iff a method
+ for obj := range targets {
+ if t := effectiveReceiver(obj); t != nil {
+ methodRecvs = append(methodRecvs, t)
+ methodName = obj.Name()
+ }
+ }
+
+ // matches reports whether obj either is or corresponds to a target.
+ // (Correspondence is defined as usual for interface methods.)
+ matches := func(obj types.Object) bool {
+ if targets[obj] != nil {
+ return true
+ } else if methodRecvs != nil && obj.Name() == methodName {
+ if orecv := effectiveReceiver(obj); orecv != nil {
+ for _, mrecv := range methodRecvs {
+ if concreteImplementsIntf(orecv, mrecv) {
+ return true
+ }
+ }
+ }
+ }
+ return false
+ }
+
+ // Scan through syntax looking for uses of one of the target objects.
+ for _, pgf := range pkg.CompiledGoFiles() {
+ ast.Inspect(pgf.File, func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok {
+ if obj, ok := pkg.GetTypesInfo().Uses[id]; ok && matches(obj) {
+ report(mustLocation(pgf, id), false)
+ }
+ }
+ return true
+ })
+ }
+ return nil
+}
+
+// effectiveReceiver returns the effective receiver type for method-set
+// comparisons for obj, if it is a method, or nil otherwise.
+func effectiveReceiver(obj types.Object) types.Type {
+ if fn, ok := obj.(*types.Func); ok {
+ if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
+ return methodsets.EnsurePointer(recv.Type())
+ }
+ }
+ return nil
+}
+
+// objectsAt returns the non-empty set of objects denoted (def or use)
+// by the specified position within a file syntax tree, or an error if
+// none were found.
+//
+// The result may contain more than one element because all case
+// variables of a type switch appear to be declared at the same
+// position.
+//
+// Each object is mapped to the syntax node that was treated as an
+// identifier, which is not always an ast.Ident. The second component
+// of the result is the innermost node enclosing pos.
+//
+// TODO(adonovan): factor in common with referencedObject.
+func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Object]ast.Node, ast.Node, error) {
+ path := pathEnclosingObjNode(file, pos)
+ if path == nil {
+ return nil, nil, ErrNoIdentFound
+ }
+
+ targets := make(map[types.Object]ast.Node)
+
+ switch leaf := path[0].(type) {
+ case *ast.Ident:
+ // If leaf represents an implicit type switch object or the type
+ // switch "assign" variable, expand to all of the type switch's
+ // implicit objects.
+ if implicits, _ := typeSwitchImplicits(info, path); len(implicits) > 0 {
+ for _, obj := range implicits {
+ targets[obj] = leaf
+ }
+ } else {
+ obj := info.ObjectOf(leaf)
+ if obj == nil {
+ return nil, nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name)
+ }
+ targets[obj] = leaf
+ }
+ case *ast.ImportSpec:
+ // Look up the implicit *types.PkgName.
+ obj := info.Implicits[leaf]
+ if obj == nil {
+ return nil, nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf))
+ }
+ targets[obj] = leaf
+ }
+
+ if len(targets) == 0 {
+ return nil, nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen
+ }
+ return targets, path[0], nil
+}
+
+// mustLocation reports the location interval a syntax node,
+// which must belong to m.File.
+//
+// Safe for use only by references2 and implementations2.
+func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location {
+ loc, err := pgf.NodeLocation(n)
+ if err != nil {
+ panic(err) // can't happen in references2 or implementations2
+ }
+ return loc
+}