gopls/internal/golang: typeDefinition: allow any expression
Previously, typeDefinition required an identifier with a type.
Now it permits any expression.
Also
- fix the bug in the type-switch edge case in the TODO comment.
- fix bug in marker test of @typedef; now it follows the shape
of ordinary @def, and supports multiple locations.
And is documented.
(The real motive for this change was to remove a use of
referencedObject, which needs to be melted down for golang/go#75975.)
+ tests
For golang/go#69058
For golang/go#75975
Change-Id: If8d29d3b920a51f12624ed512c29292bdbb4fbb4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/713600
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Hongxiang Jiang <hxjiang@golang.org>
diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go
index 0a82308..31389a4 100644
--- a/gopls/internal/golang/call_hierarchy.go
+++ b/gopls/internal/golang/call_hierarchy.go
@@ -39,7 +39,7 @@
return nil, err
}
- _, obj, _ := referencedObject(pkg, pgf, pos)
+ _, obj, _ := referencedObject(pkg.TypesInfo(), pgf, pos)
if obj == nil {
return nil, nil
}
@@ -202,7 +202,7 @@
return nil, err
}
- _, obj, _ := referencedObject(pkg, pgf, pos)
+ _, obj, _ := referencedObject(pkg.TypesInfo(), pgf, pos)
if obj == nil {
return nil, nil
}
@@ -271,7 +271,7 @@
outgoingCalls := make(map[protocol.Location]*protocol.CallHierarchyOutgoingCall)
for _, callRange := range callRanges {
- _, obj, _ := referencedObject(declPkg, declPGF, callRange.start)
+ _, obj, _ := referencedObject(declPkg.TypesInfo(), declPGF, callRange.start)
if obj == nil {
continue
}
diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go
index 5c5a076..cace355 100644
--- a/gopls/internal/golang/definition.go
+++ b/gopls/internal/golang/definition.go
@@ -165,7 +165,7 @@
}
// The general case: the cursor is on an identifier.
- _, obj, _ := referencedObject(pkg, pgf, pos)
+ _, obj, _ := referencedObject(pkg.TypesInfo(), pgf, pos)
if obj == nil {
return nil, nil
}
@@ -321,16 +321,13 @@
// TODO(rfindley): this function exists to preserve the pre-existing behavior
// of golang.Identifier. Eliminate this helper in favor of sharing
// functionality with objectsAt, after choosing suitable primitives.
-func referencedObject(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (*ast.Ident, types.Object, types.Type) {
+func referencedObject(info *types.Info, pgf *parsego.File, pos token.Pos) (*ast.Ident, types.Object, types.Type) {
path := pathEnclosingObjNode(pgf.File, pos)
if len(path) == 0 {
return nil, nil, nil
}
- var obj types.Object
- info := pkg.TypesInfo()
- switch n := path[0].(type) {
- case *ast.Ident:
- obj = info.ObjectOf(n)
+ if id, ok := path[0].(*ast.Ident); ok {
+ obj := info.ObjectOf(id)
// If n is the var's declaring ident in a type switch
// [i.e. the x in x := foo.(type)], it will not have an object. In this
// case, set obj to the first implicit object (if any), and return the type
@@ -340,7 +337,7 @@
// implicit objects; this is a type error ("unused x"),
if obj == nil {
if implicits, typ := typeSwitchImplicits(info, path); len(implicits) > 0 {
- return n, implicits[0], typ
+ return id, implicits[0], typ
}
}
@@ -348,11 +345,11 @@
// to the field's type definition, not the field's definition.
if v, ok := obj.(*types.Var); ok && v.Embedded() {
// types.Info.Uses contains the embedded field's *types.TypeName.
- if typeName := info.Uses[n]; typeName != nil {
+ if typeName := info.Uses[id]; typeName != nil {
obj = typeName
}
}
- return n, obj, nil
+ return id, obj, nil
}
return nil, nil, nil
}
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go
index 0620d74..a1ba81f 100644
--- a/gopls/internal/golang/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -301,7 +301,7 @@
// The general case: compute hover information for the object referenced by
// the identifier at pos.
- ident, obj, selectedType := referencedObject(pkg, pgf, pos)
+ ident, obj, selectedType := referencedObject(pkg.TypesInfo(), pgf, pos)
if obj == nil || ident == nil {
return protocol.Range{}, nil, nil // no object to hover
}
diff --git a/gopls/internal/golang/type_definition.go b/gopls/internal/golang/type_definition.go
index 3b6587d..04b32e6 100644
--- a/gopls/internal/golang/type_definition.go
+++ b/gopls/internal/golang/type_definition.go
@@ -7,10 +7,14 @@
import (
"context"
"fmt"
+ "go/ast"
+ "go/types"
+ "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
+ astutil "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/event"
)
@@ -27,18 +31,47 @@
if err != nil {
return nil, err
}
-
- // TODO(rfindley): handle type switch implicits correctly here: if the user
- // jumps to the type definition of x in x := y.(type), it makes sense to jump
- // to the type of y.
- _, obj, _ := referencedObject(pkg, pgf, pos)
- if obj == nil {
- return nil, nil
+ cur, ok := pgf.Cursor.FindByPos(pos, pos)
+ if !ok {
+ return nil, fmt.Errorf("no enclosing syntax") // can't happen
}
- tname := typeToObject(obj.Type())
+ // Find innermost enclosing expression that has a type.
+ // It needn't be an identifier.
+ var (
+ info = pkg.TypesInfo()
+ t types.Type
+ )
+ for cur := range cur.Enclosing() {
+ expr, ok := cur.Node().(ast.Expr)
+ if !ok {
+ continue
+ }
+
+ // edge case: switch id := expr.(type) {}
+ // id has no type; use expr instead.
+ if astutil.IsChildOf(cur, edge.AssignStmt_Lhs) &&
+ astutil.IsChildOf(cur.Parent(), edge.TypeSwitchStmt_Assign) {
+ expr = cur.Parent().Node().(*ast.AssignStmt).Rhs[0].(*ast.TypeAssertExpr).X
+ }
+
+ if id, ok := expr.(*ast.Ident); ok {
+ if obj := info.ObjectOf(id); obj != nil {
+ t = obj.Type()
+ }
+ } else {
+ t = info.TypeOf(expr)
+ }
+ if t != nil {
+ break
+ }
+ }
+ if t == nil {
+ return nil, fmt.Errorf("no enclosing expression has a type")
+ }
+ tname := typeToObject(t)
if tname == nil {
- return nil, fmt.Errorf("no type definition for %s", obj.Name())
+ return nil, fmt.Errorf("cannot find type name from type %s", t)
}
loc, err := ObjectLocation(ctx, pkg.FileSet(), snapshot, tname)
if err != nil {
diff --git a/gopls/internal/golang/type_hierarchy.go b/gopls/internal/golang/type_hierarchy.go
index 6352c45..c07ba21 100644
--- a/gopls/internal/golang/type_hierarchy.go
+++ b/gopls/internal/golang/type_hierarchy.go
@@ -43,7 +43,7 @@
}
// For now, we require that the selection be a type name.
- _, obj, _ := referencedObject(pkg, pgf, pos)
+ _, obj, _ := referencedObject(pkg.TypesInfo(), pgf, pos)
if obj == nil {
return nil, fmt.Errorf("not a symbol")
}
diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go
index 8b4c457..4f7ef28 100644
--- a/gopls/internal/test/marker/doc.go
+++ b/gopls/internal/test/marker/doc.go
@@ -301,6 +301,9 @@
request at the given location, and asserts that the result includes
exactly one token with the given token type and modifier string.
+ - typedef(src, want ...location): performs a textDocument/typeDefinition request
+ at the src location, and checks that the results equals want.
+
- workspacesymbol(query, golden): makes a workspace/symbol request for the
given query, formats the response with one symbol per line, and compares
against the named golden file. As workspace symbols are by definition a
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index 690d1bf..8e1382f 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -1723,11 +1723,16 @@
}
}
-func typedefMarker(mark marker, src, dst protocol.Location) {
- got := mark.run.env.FirstTypeDefinition(src)
- if got != dst {
- mark.errorf("type definition location does not match:\n\tgot: %s\n\twant %s",
- mark.run.fmtLoc(got), mark.run.fmtLoc(dst))
+func typedefMarker(mark marker, loc protocol.Location, want ...protocol.Location) {
+ env := mark.run.env
+ got, err := env.Editor.TypeDefinitions(env.Ctx, loc)
+ if err != nil {
+ mark.errorf("typeDefinition request failed: %v", err)
+ return
+ }
+
+ if err := compareLocations(mark, got, want); err != nil {
+ mark.errorf("typedef failed: %v", err)
}
}
diff --git a/gopls/internal/test/marker/testdata/typedef/typedef.txt b/gopls/internal/test/marker/testdata/typedef/typedef.txt
index 3bc9dab..940b275 100644
--- a/gopls/internal/test/marker/testdata/typedef/typedef.txt
+++ b/gopls/internal/test/marker/testdata/typedef/typedef.txt
@@ -66,3 +66,28 @@
var foo myFunc
_ = foo() //@typedef("foo", myFunc), diag(")", re"not enough arguments")
}
+
+func _() {
+ // Any expression is fair game for typeDefinition;
+ // it needn't be a referring identifier.
+ // In this example it's the r-paren of a call expr.
+ func() Struct {
+ panic(0)
+ }() //@typedef(")", Struct)
+
+ // And in this one, it's the composite literal enclosing the
+ // KeyValueExpr denoted by the colon (which must not be adjacent
+ // to either they key or the value!).
+ _ = Struct{Field : ""} //@typedef(":", Struct)
+}
+
+// edge case: type-switch implicits.
+// y in "switch y" has no type; use x instead.
+func _(x any) {
+ switch y := x.(type) { //@typedef("y", BUILTIN)
+ case Int:
+ _ = y //@typedef("y", Int)
+ case Struct:
+ _ = y //@typedef("y", Struct)
+ }
+}