1
0
mirror of https://github.com/golang/go synced 2024-11-18 21:34:46 -07:00
go/internal/lsp/source/implementation.go
Muir Manders f80fb1dfa1 internal/lsp: refactor find-references and rename
The main goal is to push the package variant logic from internal/lsp
into internal/lsp/source so all users of internal/lsp/source benefit.

"references" and "rename" now have top-level source.References() and
source.Rename() entry points (as opposed to hanging off
source.Identifier()). I expanded objectsAtProtocolPos() to know about
implicit objects (type switch and import spec), and to
handle *ast.ImportSpec generically. This gets rid of special case
handling of *types.PkgName in various places.

The biggest practical benefit, though, is that "references" no longer
needs to compute the objectpath for every types.Object comparison it
does, instead using direct types.Object equality. This speeds up
"references" and "rename" a lot.

Two other notable improvements that fell out of not using
source.Identifier()'s logic:

- Finding references on an embedded field now shows references to the
  field, not the type being embedded.
- Finding references on an imported object now works
  correctly (previously it searched the importing package's dependents
  rather than the imported package's dependents).

Finally, I refactored findIdentifier() to use pathEnclosingObjNode()
instead of astutil.PathEnclosingInterval. Now we only need a single
call to get the path because pathEnclosingObjNode() has the
"try pos || try pos-1" logic built in.

Change-Id: I667be9bed6ad83912404b90257c5c1485b3a7025
Reviewed-on: https://go-review.googlesource.com/c/tools/+/211999
Run-TryBot: Muir Manders <muir@mnd.rs>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-01-16 06:24:15 +00:00

327 lines
7.6 KiB
Go

// 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"
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
func Implementation(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) {
ctx, done := trace.StartSpan(ctx, "source.Implementation")
defer done()
impls, err := implementations(ctx, s, f, pp)
if err != nil {
return nil, err
}
var locations []protocol.Location
for _, impl := range impls {
if impl.pkg == nil || len(impl.pkg.CompiledGoFiles()) == 0 {
continue
}
rng, err := objToMappedRange(s.View(), impl.pkg, impl.obj)
if err != nil {
return nil, err
}
pr, err := rng.Range()
if err != nil {
return nil, err
}
locations = append(locations, protocol.Location{
URI: protocol.NewURI(rng.URI()),
Range: pr,
})
}
return locations, nil
}
var ErrNotAnInterface = errors.New("not an interface or interface method")
func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]qualifiedObject, error) {
var (
impls []qualifiedObject
seen = make(map[token.Position]bool)
fset = s.View().Session().Cache().FileSet()
)
qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
if err != nil {
return nil, err
}
for _, qo := range qos {
var (
T *types.Interface
method *types.Func
)
switch obj := qo.obj.(type) {
case *types.Func:
method = obj
if recv := obj.Type().(*types.Signature).Recv(); recv != nil {
T, _ = recv.Type().Underlying().(*types.Interface)
}
case *types.TypeName:
T, _ = obj.Type().Underlying().(*types.Interface)
}
if T == nil {
return nil, ErrNotAnInterface
}
if T.NumMethods() == 0 {
return nil, nil
}
// Find all named types, even local types (which can have methods
// due to promotion).
var (
allNamed []*types.Named
pkgs = make(map[*types.Package]Package)
)
knownPkgs, err := s.KnownPackages(ctx)
if err != nil {
return nil, err
}
for _, ph := range knownPkgs {
pkg, err := ph.Check(ctx)
if err != nil {
return nil, err
}
pkgs[pkg.GetTypes()] = pkg
info := pkg.GetTypesInfo()
for _, obj := range info.Defs {
obj, ok := obj.(*types.TypeName)
// We ignore aliases 'type M = N' to avoid duplicate reporting
// of the Named type N.
if !ok || obj.IsAlias() {
continue
}
named, ok := obj.Type().(*types.Named)
// We skip interface types since we only want concrete
// implementations.
if !ok || isInterface(named) {
continue
}
allNamed = append(allNamed, named)
}
}
// Find all the named types that implement our interface.
for _, U := range allNamed {
var concrete types.Type = U
if !types.AssignableTo(concrete, T) {
// We also accept T if *T implements our interface.
concrete = types.NewPointer(concrete)
if !types.AssignableTo(concrete, T) {
continue
}
}
var obj types.Object = U.Obj()
if method != nil {
obj = types.NewMethodSet(concrete).Lookup(method.Pkg(), method.Name()).Obj()
}
pos := fset.Position(obj.Pos())
if obj == method || seen[pos] {
continue
}
seen[pos] = true
impls = append(impls, qualifiedObject{
obj: obj,
pkg: pkgs[obj.Pkg()],
})
}
}
return impls, nil
}
type qualifiedObject struct {
obj types.Object
// pkg is the Package that contains obj's definition.
pkg Package
// node is the *ast.Ident or *ast.ImportSpec we followed to find obj, if any.
node ast.Node
// sourcePkg is the Package that contains node, if any.
sourcePkg Package
}
// qualifiedObjsAtProtocolPos returns info for all the type.Objects
// referenced at the given position. An object will be returned for
// every package that the file belongs to.
func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]qualifiedObject, error) {
phs, err := s.PackageHandles(ctx, f)
if err != nil {
return nil, err
}
var qualifiedObjs []qualifiedObject
// Check all the packages that the file belongs to.
for _, ph := range phs {
pkg, err := ph.Check(ctx)
if err != nil {
return nil, err
}
astFile, pos, err := getASTFile(pkg, f, pp)
if err != nil {
return nil, err
}
path := pathEnclosingObjNode(astFile, pos)
if path == nil {
return nil, ErrNoIdentFound
}
var objs []types.Object
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(pkg, path); len(implicits) > 0 {
objs = append(objs, implicits...)
} else {
obj := pkg.GetTypesInfo().ObjectOf(leaf)
if obj == nil {
return nil, fmt.Errorf("no object for %q", leaf.Name)
}
objs = append(objs, obj)
}
case *ast.ImportSpec:
// Look up the implicit *types.PkgName.
obj := pkg.GetTypesInfo().Implicits[leaf]
if obj == nil {
return nil, fmt.Errorf("no object for import %q", importPath(leaf))
}
objs = append(objs, obj)
}
pkgs := make(map[*types.Package]Package)
pkgs[pkg.GetTypes()] = pkg
for _, imp := range pkg.Imports() {
pkgs[imp.GetTypes()] = imp
}
for _, obj := range objs {
qualifiedObjs = append(qualifiedObjs, qualifiedObject{
obj: obj,
pkg: pkgs[obj.Pkg()],
sourcePkg: pkg,
node: path[0],
})
}
}
return qualifiedObjs, nil
}
func getASTFile(pkg Package, f FileHandle, pos protocol.Position) (*ast.File, token.Pos, error) {
pgh, err := pkg.File(f.Identity().URI)
if err != nil {
return nil, 0, err
}
file, m, _, err := pgh.Cached()
if err != nil {
return nil, 0, err
}
spn, err := m.PointSpan(pos)
if err != nil {
return nil, 0, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, 0, err
}
return file, rng.Start, nil
}
// 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 identifer.
if pos == n.Star {
pos = n.X.Pos()
}
case *ast.SelectorExpr:
// If pos is on the ".", move it into the selector.
if pos == n.X.End() {
pos = n.Sel.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
}