1
0
mirror of https://github.com/golang/go synced 2024-10-01 04:18:33 -06:00
go/internal/lsp/source/identifier.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

359 lines
9.6 KiB
Go

// Copyright 2018 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"
"strconv"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
// IdentifierInfo holds information about an identifier in Go source.
type IdentifierInfo struct {
Name string
Snapshot Snapshot
mappedRange
Type struct {
mappedRange
Object types.Object
}
Declaration Declaration
ident *ast.Ident
// enclosing is an expression used to determine the link anchor for an identifier.
enclosing types.Type
pkg Package
qf types.Qualifier
}
type Declaration struct {
mappedRange
node ast.Node
obj types.Object
}
// Identifier returns identifier information for a position
// in a file, accounting for a potentially incomplete selector.
func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position, selectPackage PackagePolicy) (*IdentifierInfo, error) {
ctx, done := trace.StartSpan(ctx, "source.Identifier")
defer done()
pkg, pgh, err := getParsedFile(ctx, snapshot, fh, selectPackage)
if err != nil {
return nil, fmt.Errorf("getting file for Identifier: %v", err)
}
file, m, _, err := pgh.Cached()
if err != nil {
return nil, err
}
spn, err := m.PointSpan(pos)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
return findIdentifier(ctx, snapshot, pkg, file, rng.Start)
}
var ErrNoIdentFound = errors.New("no identifier found")
func findIdentifier(ctx context.Context, s Snapshot, pkg Package, file *ast.File, pos token.Pos) (*IdentifierInfo, error) {
// Handle import specs separately, as there is no formal position for a package declaration.
if result, err := importSpec(s, pkg, file, pos); result != nil || err != nil {
return result, err
}
path := pathEnclosingObjNode(file, pos)
if path == nil {
return nil, ErrNoIdentFound
}
view := s.View()
ident, _ := path[0].(*ast.Ident)
if ident == nil {
return nil, ErrNoIdentFound
}
result := &IdentifierInfo{
Snapshot: s,
qf: qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()),
pkg: pkg,
ident: ident,
enclosing: searchForEnclosing(pkg, path),
}
var wasEmbeddedField bool
for _, n := range path[1:] {
if field, ok := n.(*ast.Field); ok {
wasEmbeddedField = len(field.Names) == 0
break
}
}
result.Name = result.ident.Name
var err error
if result.mappedRange, err = posToMappedRange(view, pkg, result.ident.Pos(), result.ident.End()); err != nil {
return nil, err
}
result.Declaration.obj = pkg.GetTypesInfo().ObjectOf(result.ident)
if result.Declaration.obj == nil {
// If there was no types.Object for the declaration, there might be an implicit local variable
// declaration in a type switch.
if objs := typeSwitchImplicits(pkg, path); len(objs) > 0 {
// There is no types.Object for the declaration of an implicit local variable,
// but all of the types.Objects associated with the usages of this variable can be
// used to connect it back to the declaration.
// Preserve the first of these objects and treat it as if it were the declaring object.
result.Declaration.obj = objs[0]
} else {
// Probably a type error.
return nil, errors.Errorf("no object for ident %v", result.Name)
}
}
// Handle builtins separately.
if result.Declaration.obj.Parent() == types.Universe {
astObj, err := view.LookupBuiltin(ctx, result.Name)
if err != nil {
return nil, err
}
decl, ok := astObj.Decl.(ast.Node)
if !ok {
return nil, errors.Errorf("no declaration for %s", result.Name)
}
result.Declaration.node = decl
if result.Declaration.mappedRange, err = nameToMappedRange(view, pkg, decl.Pos(), result.Name); err != nil {
return nil, err
}
return result, nil
}
if wasEmbeddedField {
// The original position was on the embedded field declaration, so we
// try to dig out the type and jump to that instead.
if v, ok := result.Declaration.obj.(*types.Var); ok {
if typObj := typeToObject(v.Type()); typObj != nil {
result.Declaration.obj = typObj
}
}
}
if result.Declaration.mappedRange, err = objToMappedRange(view, pkg, result.Declaration.obj); err != nil {
return nil, err
}
if result.Declaration.node, err = objToNode(s.View(), pkg, result.Declaration.obj); err != nil {
return nil, err
}
typ := pkg.GetTypesInfo().TypeOf(result.ident)
if typ == nil {
return result, nil
}
result.Type.Object = typeToObject(typ)
if result.Type.Object != nil {
// Identifiers with the type "error" are a special case with no position.
if hasErrorType(result.Type.Object) {
return result, nil
}
if result.Type.mappedRange, err = objToMappedRange(view, pkg, result.Type.Object); err != nil {
return nil, err
}
}
return result, nil
}
func searchForEnclosing(pkg Package, path []ast.Node) types.Type {
for _, n := range path {
switch n := n.(type) {
case *ast.SelectorExpr:
if selection, ok := pkg.GetTypesInfo().Selections[n]; ok {
return deref(selection.Recv())
}
case *ast.CompositeLit:
if t, ok := pkg.GetTypesInfo().Types[n]; ok {
return t.Type
}
case *ast.TypeSpec:
if _, ok := n.Type.(*ast.StructType); ok {
if t, ok := pkg.GetTypesInfo().Defs[n.Name]; ok {
return t.Type()
}
}
}
}
return nil
}
func typeToObject(typ types.Type) types.Object {
switch typ := typ.(type) {
case *types.Named:
return typ.Obj()
case *types.Pointer:
return typeToObject(typ.Elem())
default:
return nil
}
}
func hasErrorType(obj types.Object) bool {
return types.IsInterface(obj.Type()) && obj.Pkg() == nil && obj.Name() == "error"
}
func objToNode(v View, pkg Package, obj types.Object) (ast.Decl, error) {
declAST, _, err := findPosInPackage(v, pkg, obj.Pos())
if err != nil {
return nil, err
}
path, _ := astutil.PathEnclosingInterval(declAST, obj.Pos(), obj.Pos())
if path == nil {
return nil, errors.Errorf("no path for object %v", obj.Name())
}
for _, node := range path {
switch node := node.(type) {
case *ast.GenDecl:
// Type names, fields, and methods.
switch obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func:
return node, nil
}
case *ast.FuncDecl:
// Function signatures.
if _, ok := obj.(*types.Func); ok {
return node, nil
}
}
}
return nil, nil // didn't find a node, but don't fail
}
// importSpec handles positions inside of an *ast.ImportSpec.
func importSpec(s Snapshot, pkg Package, file *ast.File, pos token.Pos) (*IdentifierInfo, error) {
var imp *ast.ImportSpec
for _, spec := range file.Imports {
if spec.Path.Pos() <= pos && pos < spec.Path.End() {
imp = spec
}
}
if imp == nil {
return nil, nil
}
importPath, err := strconv.Unquote(imp.Path.Value)
if err != nil {
return nil, errors.Errorf("import path not quoted: %s (%v)", imp.Path.Value, err)
}
result := &IdentifierInfo{
Snapshot: s,
Name: importPath,
pkg: pkg,
}
if result.mappedRange, err = posToMappedRange(s.View(), pkg, imp.Path.Pos(), imp.Path.End()); err != nil {
return nil, err
}
// Consider the "declaration" of an import spec to be the imported package.
importedPkg, err := pkg.GetImport(importPath)
if err != nil {
return nil, err
}
if importedPkg.GetSyntax() == nil {
return nil, errors.Errorf("no syntax for for %q", importPath)
}
// Heuristic: Jump to the longest (most "interesting") file of the package.
var dest *ast.File
for _, f := range importedPkg.GetSyntax() {
if dest == nil || f.End()-f.Pos() > dest.End()-dest.Pos() {
dest = f
}
}
if dest == nil {
return nil, errors.Errorf("package %q has no files", importPath)
}
if result.Declaration.mappedRange, err = posToMappedRange(s.View(), pkg, dest.Pos(), dest.End()); err != nil {
return nil, err
}
result.Declaration.node = imp
return result, nil
}
// typeSwitchImplicits returns all the implicit type switch objects
// that correspond to the leaf *ast.Ident.
func typeSwitchImplicits(pkg Package, path []ast.Node) []types.Object {
ident, _ := path[0].(*ast.Ident)
if ident == nil {
return nil
}
var (
ts *ast.TypeSwitchStmt
assign *ast.AssignStmt
cc *ast.CaseClause
obj = pkg.GetTypesInfo().ObjectOf(ident)
)
// Walk our ancestors to determine if our leaf ident refers to a
// type switch variable, e.g. the "a" from "switch a := b.(type)".
Outer:
for i := 1; i < len(path); i++ {
switch n := path[i].(type) {
case *ast.AssignStmt:
// Check if ident is the "a" in "a := foo.(type)". The "a" in
// this case has no types.Object, so check for ident equality.
if len(n.Lhs) == 1 && n.Lhs[0] == ident {
assign = n
}
case *ast.CaseClause:
// Check if ident is a use of "a" within a case clause. Each
// case clause implicitly maps "a" to a different types.Object,
// so check if ident's object is the case clause's implicit
// object.
if obj != nil && pkg.GetTypesInfo().Implicits[n] == obj {
cc = n
}
case *ast.TypeSwitchStmt:
// Look for the type switch that owns our previously found
// *ast.AssignStmt or *ast.CaseClause.
if n.Assign == assign {
ts = n
break Outer
}
for _, stmt := range n.Body.List {
if stmt == cc {
ts = n
break Outer
}
}
}
}
if ts == nil {
return nil
}
// Our leaf ident refers to a type switch variable. Fan out to the
// type switch's implicit case clause objects.
var objs []types.Object
for _, cc := range ts.Body.List {
if ccObj := pkg.GetTypesInfo().Implicits[cc]; ccObj != nil {
objs = append(objs, ccObj)
}
}
return objs
}