aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/source/implementation.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/source/implementation.go')
-rw-r--r--gopls/internal/lsp/source/implementation.go482
1 files changed, 482 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go
new file mode 100644
index 000000000..72ec90d28
--- /dev/null
+++ b/gopls/internal/lsp/source/implementation.go
@@ -0,0 +1,482 @@
+// 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
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "reflect"
+ "sort"
+ "strings"
+ "sync"
+
+ "golang.org/x/sync/errgroup"
+ "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/event"
+)
+
+// This file defines the new implementation of the 'implementation'
+// operator that does not require type-checker data structures for an
+// unbounded number of packages.
+//
+// TODO(adonovan):
+// - Audit to ensure robustness in face of type errors.
+// - Support 'error' and 'error.Error', which were also lacking from the old implementation.
+// - Eliminate false positives due to 'tricky' cases of the global algorithm.
+// - Ensure we have test coverage of:
+// type aliases
+// nil, PkgName, Builtin (all errors)
+// any (empty result)
+// method of unnamed interface type (e.g. var x interface { f() })
+// (the global algorithm may find implementations of this type
+// but will not include it in the index.)
+
+// Implementation returns a new sorted array of locations of
+// declarations of types that implement (or are implemented by) the
+// type referred to at the given position.
+//
+// If the position denotes a method, the computation is applied to its
+// receiver type and then its corresponding methods are returned.
+func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) {
+ ctx, done := event.Start(ctx, "source.Implementation")
+ defer done()
+
+ locs, err := implementations2(ctx, snapshot, f, pp)
+ if err != nil {
+ return nil, err
+ }
+
+ // Sort and de-duplicate locations.
+ sort.Slice(locs, func(i, j int) bool {
+ return protocol.CompareLocation(locs[i], locs[j]) < 0
+ })
+ out := locs[:0]
+ for _, loc := range locs {
+ if len(out) == 0 || out[len(out)-1] != loc {
+ out = append(out, loc)
+ }
+ }
+ locs = out
+
+ return locs, nil
+}
+
+func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.Location, error) {
+
+ // Type-check the query package, find the query identifier,
+ // and locate the type or method declaration it refers to.
+ declPosn, err := typeDeclPosition(ctx, snapshot, fh.URI(), pp)
+ if err != nil {
+ return nil, err
+ }
+
+ // Type-check the declaring package (incl. variants) for use
+ // by the "local" search, which uses type information to
+ // enumerate all types within the package that satisfy the
+ // query type, even those defined local to a function.
+ declURI := span.URIFromPath(declPosn.Filename)
+ declMetas, err := snapshot.MetadataForFile(ctx, declURI)
+ if err != nil {
+ return nil, err
+ }
+ if len(declMetas) == 0 {
+ return nil, fmt.Errorf("no packages for file %s", declURI)
+ }
+ ids := make([]PackageID, len(declMetas))
+ for i, m := range declMetas {
+ ids[i] = m.ID
+ }
+ localPkgs, err := snapshot.TypeCheck(ctx, ids...)
+ if err != nil {
+ return nil, err
+ }
+ // The narrowest package will do, since the local search is based
+ // on position and the global search is based on fingerprint.
+ // (Neither is based on object identity.)
+ declPkg := localPkgs[0]
+ declFile, err := declPkg.File(declURI)
+ if err != nil {
+ return nil, err // "can't happen"
+ }
+
+ // Find declaration of corresponding object
+ // in this package based on (URI, offset).
+ pos, err := safetoken.Pos(declFile.Tok, declPosn.Offset)
+ if err != nil {
+ return nil, err
+ }
+ // TODO(adonovan): simplify: use objectsAt?
+ path := pathEnclosingObjNode(declFile.File, pos)
+ if path == nil {
+ return nil, ErrNoIdentFound // checked earlier
+ }
+ id, ok := path[0].(*ast.Ident)
+ if !ok {
+ return nil, ErrNoIdentFound // checked earlier
+ }
+ obj := declPkg.GetTypesInfo().ObjectOf(id) // may be nil
+
+ // Is the selected identifier a type name or method?
+ // (For methods, report the corresponding method names.)
+ var queryType types.Type
+ var queryMethodID string
+ switch obj := obj.(type) {
+ case *types.TypeName:
+ queryType = obj.Type()
+ case *types.Func:
+ // For methods, use the receiver type, which may be anonymous.
+ if recv := obj.Type().(*types.Signature).Recv(); recv != nil {
+ queryType = recv.Type()
+ queryMethodID = obj.Id()
+ }
+ }
+ if queryType == nil {
+ return nil, fmt.Errorf("%s is not a type or method", id.Name)
+ }
+
+ // Compute the method-set fingerprint used as a key to the global search.
+ key, hasMethods := methodsets.KeyOf(queryType)
+ if !hasMethods {
+ // A type with no methods yields an empty result.
+ // (No point reporting that every type satisfies 'any'.)
+ return nil, nil
+ }
+
+ // The global search needs to look at every package in the workspace;
+ // see package ./methodsets.
+ //
+ // For now we do all the type checking before beginning the search.
+ // TODO(adonovan): opt: search in parallel topological order
+ // so that we can overlap index lookup with typechecking.
+ // I suspect a number of algorithms on the result of TypeCheck could
+ // be optimized by being applied as soon as each package is available.
+ globalMetas, err := snapshot.AllMetadata(ctx)
+ if err != nil {
+ return nil, err
+ }
+ globalIDs := make([]PackageID, 0, len(globalMetas))
+ for _, m := range globalMetas {
+ if m.PkgPath == declPkg.Metadata().PkgPath {
+ continue // declaring package is handled by local implementation
+ }
+ globalIDs = append(globalIDs, m.ID)
+ }
+ indexes, err := snapshot.MethodSets(ctx, globalIDs...)
+ if err != nil {
+ return nil, err
+ }
+
+ // Search local and global packages in parallel.
+ var (
+ group errgroup.Group
+ locsMu sync.Mutex
+ locs []protocol.Location
+ )
+ // local search
+ for _, localPkg := range localPkgs {
+ localPkg := localPkg
+ group.Go(func() error {
+ localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, queryMethodID)
+ if err != nil {
+ return err
+ }
+ locsMu.Lock()
+ locs = append(locs, localLocs...)
+ locsMu.Unlock()
+ return nil
+ })
+ }
+ // global search
+ for _, index := range indexes {
+ index := index
+ group.Go(func() error {
+ for _, res := range index.Search(key, queryMethodID) {
+ loc := res.Location
+ // Map offsets to protocol.Locations in parallel (may involve I/O).
+ group.Go(func() error {
+ ploc, err := offsetToLocation(ctx, snapshot, loc.Filename, loc.Start, loc.End)
+ if err != nil {
+ return err
+ }
+ locsMu.Lock()
+ locs = append(locs, ploc)
+ locsMu.Unlock()
+ return nil
+ })
+ }
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return nil, err
+ }
+
+ return locs, nil
+}
+
+// offsetToLocation converts an offset-based position to a protocol.Location,
+// which requires reading the file.
+func offsetToLocation(ctx context.Context, snapshot Snapshot, filename string, start, end int) (protocol.Location, error) {
+ uri := span.URIFromPath(filename)
+ fh, err := snapshot.GetFile(ctx, uri)
+ if err != nil {
+ return protocol.Location{}, err // cancelled, perhaps
+ }
+ content, err := fh.Read()
+ if err != nil {
+ return protocol.Location{}, err // nonexistent or deleted ("can't happen")
+ }
+ m := protocol.NewMapper(uri, content)
+ return m.OffsetLocation(start, end)
+}
+
+// typeDeclPosition returns the position of the declaration of the
+// type (or one of its methods) referred to at (uri, ppos).
+func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (token.Position, error) {
+ var noPosn token.Position
+
+ pkg, pgf, err := PackageForFile(ctx, snapshot, uri, WidestPackage)
+ if err != nil {
+ return noPosn, err
+ }
+ pos, err := pgf.PositionPos(ppos)
+ if err != nil {
+ return noPosn, err
+ }
+
+ // This function inherits the limitation of its predecessor in
+ // requiring the selection to be an identifier (of a type or
+ // method). But there's no fundamental reason why one could
+ // not pose this query about any selected piece of syntax that
+ // has a type and thus a method set.
+ // (If LSP was more thorough about passing text selections as
+ // intervals to queries, you could ask about the method set of a
+ // subexpression such as x.f().)
+
+ // TODO(adonovan): simplify: use objectsAt?
+ path := pathEnclosingObjNode(pgf.File, pos)
+ if path == nil {
+ return noPosn, ErrNoIdentFound
+ }
+ id, ok := path[0].(*ast.Ident)
+ if !ok {
+ return noPosn, ErrNoIdentFound
+ }
+
+ // Is the object a type or method? Reject other kinds.
+ obj := pkg.GetTypesInfo().Uses[id]
+ if obj == nil {
+ // Check uses first (unlike ObjectOf) so that T in
+ // struct{T} is treated as a reference to a type,
+ // not a declaration of a field.
+ obj = pkg.GetTypesInfo().Defs[id]
+ }
+ switch obj := obj.(type) {
+ case *types.TypeName:
+ // ok
+ case *types.Func:
+ if obj.Type().(*types.Signature).Recv() == nil {
+ return noPosn, fmt.Errorf("%s is a function, not a method", id.Name)
+ }
+ case nil:
+ return noPosn, fmt.Errorf("%s denotes unknown object", id.Name)
+ default:
+ // e.g. *types.Var -> "var".
+ kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types."))
+ return noPosn, fmt.Errorf("%s is a %s, not a type", id.Name, kind)
+ }
+
+ declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
+ return declPosn, nil
+}
+
+// localImplementations searches within pkg for declarations of all
+// types that are assignable to/from the query type, and returns a new
+// unordered array of their locations.
+//
+// If methodID is non-empty, the function instead returns the location
+// of each type's method (if any) of that ID.
+//
+// ("Local" refers to the search within the same package, but this
+// function's results may include type declarations that are local to
+// a function body. The global search index excludes such types
+// because reliably naming such types is hard.)
+func localImplementations(ctx context.Context, snapshot Snapshot, pkg Package, queryType types.Type, methodID string) ([]protocol.Location, error) {
+ queryType = methodsets.EnsurePointer(queryType)
+
+ // Scan through all type declarations in the syntax.
+ var locs []protocol.Location
+ var methodLocs []methodsets.Location
+ for _, pgf := range pkg.CompiledGoFiles() {
+ ast.Inspect(pgf.File, func(n ast.Node) bool {
+ spec, ok := n.(*ast.TypeSpec)
+ if !ok {
+ return true // not a type declaration
+ }
+ def := pkg.GetTypesInfo().Defs[spec.Name]
+ if def == nil {
+ return true // "can't happen" for types
+ }
+ if def.(*types.TypeName).IsAlias() {
+ return true // skip type aliases to avoid duplicate reporting
+ }
+ candidateType := methodsets.EnsurePointer(def.Type())
+
+ // The historical behavior enshrined by this
+ // function rejects cases where both are
+ // (nontrivial) interface types?
+ // That seems like useful information.
+ // TODO(adonovan): UX: report I/I pairs too?
+ // The same question appears in the global algorithm (methodsets).
+ if !concreteImplementsIntf(candidateType, queryType) {
+ return true // not assignable
+ }
+
+ // Ignore types with empty method sets.
+ // (No point reporting that every type satisfies 'any'.)
+ mset := types.NewMethodSet(candidateType)
+ if mset.Len() == 0 {
+ return true
+ }
+
+ if methodID == "" {
+ // Found matching type.
+ locs = append(locs, mustLocation(pgf, spec.Name))
+ return true
+ }
+
+ // Find corresponding method.
+ //
+ // We can't use LookupFieldOrMethod because it requires
+ // the methodID's types.Package, which we don't know.
+ // We could recursively search pkg.Imports for it,
+ // but it's easier to walk the method set.
+ for i := 0; i < mset.Len(); i++ {
+ method := mset.At(i).Obj()
+ if method.Id() == methodID {
+ posn := safetoken.StartPosition(pkg.FileSet(), method.Pos())
+ methodLocs = append(methodLocs, methodsets.Location{
+ Filename: posn.Filename,
+ Start: posn.Offset,
+ End: posn.Offset + len(method.Name()),
+ })
+ break
+ }
+ }
+ return true
+ })
+ }
+
+ // Finally convert method positions to protocol form by reading the files.
+ for _, mloc := range methodLocs {
+ loc, err := offsetToLocation(ctx, snapshot, mloc.Filename, mloc.Start, mloc.End)
+ if err != nil {
+ return nil, err
+ }
+ locs = append(locs, loc)
+ }
+
+ return locs, nil
+}
+
+// concreteImplementsIntf returns true if a is an interface type implemented by
+// concrete type b, or vice versa.
+func concreteImplementsIntf(a, b types.Type) bool {
+ aIsIntf, bIsIntf := types.IsInterface(a), types.IsInterface(b)
+
+ // Make sure exactly one is an interface type.
+ if aIsIntf == bIsIntf {
+ return false
+ }
+
+ // Rearrange if needed so "a" is the concrete type.
+ if aIsIntf {
+ a, b = b, a
+ }
+
+ // TODO(adonovan): this should really use GenericAssignableTo
+ // to report (e.g.) "ArrayList[T] implements List[T]", but
+ // GenericAssignableTo doesn't work correctly on pointers to
+ // generic named types. Thus the legacy implementation and the
+ // "local" part of implementation2 fail to report generics.
+ // The global algorithm based on subsets does the right thing.
+ return types.AssignableTo(a, b)
+}
+
+var (
+ // TODO(adonovan): why do various RPC handlers related to
+ // IncomingCalls return (nil, nil) on the protocol in response
+ // to this error? That seems like a violation of the protocol.
+ // Is it perhaps a workaround for VSCode behavior?
+ errNoObjectFound = errors.New("no object found")
+)
+
+// pathEnclosingObjNode returns the AST path to the object-defining
+// node associated with pos. "Object-defining" means either an
+// *ast.Ident mapped directly to a types.Object or an ast.Node mapped
+// implicitly to a types.Object.
+func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node {
+ var (
+ path []ast.Node
+ found bool
+ )
+
+ ast.Inspect(f, func(n ast.Node) bool {
+ if found {
+ return false
+ }
+
+ if n == nil {
+ path = path[:len(path)-1]
+ return false
+ }
+
+ path = append(path, n)
+
+ switch n := n.(type) {
+ case *ast.Ident:
+ // Include the position directly after identifier. This handles
+ // the common case where the cursor is right after the
+ // identifier the user is currently typing. Previously we
+ // handled this by calling astutil.PathEnclosingInterval twice,
+ // once for "pos" and once for "pos-1".
+ found = n.Pos() <= pos && pos <= n.End()
+ case *ast.ImportSpec:
+ if n.Path.Pos() <= pos && pos < n.Path.End() {
+ found = true
+ // If import spec has a name, add name to path even though
+ // position isn't in the name.
+ if n.Name != nil {
+ path = append(path, n.Name)
+ }
+ }
+ case *ast.StarExpr:
+ // Follow star expressions to the inner identifier.
+ if pos == n.Star {
+ pos = n.X.Pos()
+ }
+ }
+
+ return !found
+ })
+
+ if len(path) == 0 {
+ return nil
+ }
+
+ // Reverse path so leaf is first element.
+ for i := 0; i < len(path)/2; i++ {
+ path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i]
+ }
+
+ return path
+}