Skip to content

Commit dc34122

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/golang: fix hovering from the builtin file
The builtin file represents a psuedo package for builtin documentation. When hovering over a builtin declared in the builtin file, its corresponding hover content should be the same as hovering at the call site. Fix this by intercepting hover requests originating from the builtin file. Along the way, fix a bug that jumping to a definition in the builtin file went to the declaring node, not declaring identifier. For golang/go#68026 Change-Id: I070ec701cb7e067dd1cbb14a968c063169bb541c Reviewed-on: https://go-review.googlesource.com/c/tools/+/594795 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Robert Findley <[email protected]>
1 parent 1028e30 commit dc34122

File tree

3 files changed

+153
-25
lines changed

3 files changed

+153
-25
lines changed

gopls/internal/golang/definition.go

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,42 +103,72 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
103103
// builtinDefinition returns the location of the fake source
104104
// declaration of a built-in in {builtin,unsafe}.go.
105105
func builtinDefinition(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) ([]protocol.Location, error) {
106-
pgf, decl, err := builtinDecl(ctx, snapshot, obj)
106+
pgf, ident, err := builtinDecl(ctx, snapshot, obj)
107107
if err != nil {
108108
return nil, err
109109
}
110110

111-
loc, err := pgf.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name())))
111+
loc, err := pgf.NodeLocation(ident)
112112
if err != nil {
113113
return nil, err
114114
}
115115
return []protocol.Location{loc}, nil
116116
}
117117

118118
// builtinDecl returns the parsed Go file and node corresponding to a builtin
119-
// object, which may be a universe object or part of types.Unsafe.
120-
func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*parsego.File, ast.Node, error) {
121-
// getDecl returns the file-level declaration of name
122-
// using legacy (go/ast) object resolution.
123-
getDecl := func(file *ast.File, name string) (ast.Node, error) {
119+
// object, which may be a universe object or part of types.Unsafe, as well as
120+
// its declaring identifier.
121+
func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*parsego.File, *ast.Ident, error) {
122+
// declaringIdent returns the file-level declaration node (as reported by
123+
// ast.Object) and declaring identifier of name using legacy (go/ast) object
124+
// resolution.
125+
declaringIdent := func(file *ast.File, name string) (ast.Node, *ast.Ident, error) {
124126
astObj := file.Scope.Lookup(name)
125127
if astObj == nil {
126128
// Every built-in should have documentation syntax.
127129
// However, it is possible to reach this statement by
128130
// commenting out declarations in {builtin,unsafe}.go.
129-
return nil, fmt.Errorf("internal error: no object for %s", name)
131+
return nil, nil, fmt.Errorf("internal error: no object for %s", name)
130132
}
131133
decl, ok := astObj.Decl.(ast.Node)
132134
if !ok {
133-
return nil, bug.Errorf("internal error: no declaration for %s", obj.Name())
135+
return nil, nil, bug.Errorf("internal error: no declaration for %s", obj.Name())
134136
}
135-
return decl, nil
137+
var ident *ast.Ident
138+
switch node := decl.(type) {
139+
case *ast.Field:
140+
for _, id := range node.Names {
141+
if id.Name == name {
142+
ident = id
143+
}
144+
}
145+
case *ast.ValueSpec:
146+
for _, id := range node.Names {
147+
if id.Name == name {
148+
ident = id
149+
}
150+
}
151+
case *ast.TypeSpec:
152+
ident = node.Name
153+
case *ast.Ident:
154+
ident = node
155+
case *ast.FuncDecl:
156+
ident = node.Name
157+
case *ast.ImportSpec, *ast.LabeledStmt, *ast.AssignStmt:
158+
// Not reachable for imported objects.
159+
default:
160+
return nil, nil, bug.Errorf("internal error: unexpected decl type %T", decl)
161+
}
162+
if ident == nil {
163+
return nil, nil, bug.Errorf("internal error: no declaring identifier for %s", obj.Name())
164+
}
165+
return decl, ident, nil
136166
}
137167

138168
var (
139-
pgf *parsego.File
140-
decl ast.Node
141-
err error
169+
pgf *parsego.File
170+
ident *ast.Ident
171+
err error
142172
)
143173
if obj.Pkg() == types.Unsafe {
144174
// package "unsafe":
@@ -154,11 +184,13 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object
154184
if err != nil {
155185
return nil, nil, err
156186
}
187+
// TODO(rfindley): treat unsafe symmetrically with the builtin file. Either
188+
// pre-parse them both, or look up metadata for both.
157189
pgf, err = snapshot.ParseGo(ctx, fh, parsego.Full&^parser.SkipObjectResolution)
158190
if err != nil {
159191
return nil, nil, err
160192
}
161-
decl, err = getDecl(pgf.File, obj.Name())
193+
_, ident, err = declaringIdent(pgf.File, obj.Name())
162194
if err != nil {
163195
return nil, nil, err
164196
}
@@ -172,23 +204,24 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object
172204

173205
if obj.Parent() == types.Universe {
174206
// built-in function or type
175-
decl, err = getDecl(pgf.File, obj.Name())
207+
_, ident, err = declaringIdent(pgf.File, obj.Name())
176208
if err != nil {
177209
return nil, nil, err
178210
}
179211
} else if obj.Name() == "Error" {
180212
// error.Error method
181-
decl, err = getDecl(pgf.File, "error")
213+
decl, _, err := declaringIdent(pgf.File, "error")
182214
if err != nil {
183215
return nil, nil, err
184216
}
185-
decl = decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List[0]
186-
217+
field := decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List[0]
218+
ident = field.Names[0]
187219
} else {
188220
return nil, nil, bug.Errorf("unknown built-in %v", obj)
189221
}
190222
}
191-
return pgf, decl, nil
223+
224+
return pgf, ident, nil
192225
}
193226

194227
// referencedObject returns the identifier and object referenced at the

gopls/internal/golang/hover.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,41 @@ func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, positi
132132
//
133133
// TODO(adonovan): strength-reduce file.Handle to protocol.DocumentURI.
134134
func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *hoverJSON, error) {
135+
// Check for hover inside the builtin file before attempting type checking
136+
// below. NarrowestPackageForFile may or may not succeed, depending on
137+
// whether this is a GOROOT view, but even if it does succeed the resulting
138+
// package will be command-line-arguments package. The user should get a
139+
// hover for the builtin object, not the object type checked from the
140+
// builtin.go.
141+
if snapshot.IsBuiltin(fh.URI()) {
142+
pgf, err := snapshot.BuiltinFile(ctx)
143+
if err != nil {
144+
return protocol.Range{}, nil, err
145+
}
146+
pos, err := pgf.PositionPos(pp)
147+
if err != nil {
148+
return protocol.Range{}, nil, err
149+
}
150+
path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
151+
if id, ok := path[0].(*ast.Ident); ok {
152+
rng, err := pgf.NodeRange(id)
153+
if err != nil {
154+
return protocol.Range{}, nil, err
155+
}
156+
var obj types.Object
157+
if id.Name == "Error" {
158+
obj = types.Universe.Lookup("error").Type().Underlying().(*types.Interface).Method(0)
159+
} else {
160+
obj = types.Universe.Lookup(id.Name)
161+
}
162+
if obj != nil {
163+
h, err := hoverBuiltin(ctx, snapshot, obj)
164+
return rng, h, err
165+
}
166+
}
167+
return protocol.Range{}, nil, nil // no object to hover
168+
}
169+
135170
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
136171
if err != nil {
137172
return protocol.Range{}, nil, err
@@ -592,31 +627,34 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec
592627
}, nil
593628
}
594629

595-
pgf, node, err := builtinDecl(ctx, snapshot, obj)
630+
pgf, ident, err := builtinDecl(ctx, snapshot, obj)
596631
if err != nil {
597632
return nil, err
598633
}
599634

600-
var comment *ast.CommentGroup
601-
path, _ := astutil.PathEnclosingInterval(pgf.File, node.Pos(), node.End())
635+
var (
636+
comment *ast.CommentGroup
637+
decl ast.Decl
638+
)
639+
path, _ := astutil.PathEnclosingInterval(pgf.File, ident.Pos(), ident.Pos())
602640
for _, n := range path {
603641
switch n := n.(type) {
604642
case *ast.GenDecl:
605643
// Separate documentation and signature.
606644
comment = n.Doc
607645
node2 := *n
608646
node2.Doc = nil
609-
node = &node2
647+
decl = &node2
610648
case *ast.FuncDecl:
611649
// Ditto.
612650
comment = n.Doc
613651
node2 := *n
614652
node2.Doc = nil
615-
node = &node2
653+
decl = &node2
616654
}
617655
}
618656

619-
signature := formatNodeFile(pgf.Tok, node)
657+
signature := formatNodeFile(pgf.Tok, decl)
620658
// Replace fake types with their common equivalent.
621659
// TODO(rfindley): we should instead use obj.Type(), which would have the
622660
// *actual* types of the builtin call.

gopls/internal/test/integration/misc/hover_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"testing"
1212

13+
"github.com/google/go-cmp/cmp"
1314
"golang.org/x/tools/gopls/internal/protocol"
1415
. "golang.org/x/tools/gopls/internal/test/integration"
1516
"golang.org/x/tools/gopls/internal/test/integration/fake"
@@ -603,3 +604,59 @@ func main() {
603604
}
604605
})
605606
}
607+
608+
func TestHoverBuiltinFile(t *testing.T) {
609+
testenv.NeedsGo1Point(t, 21) // uses 'min'
610+
611+
// This test verifies that hovering in the builtin file provides the same
612+
// hover content as hovering over a use of a builtin.
613+
614+
const src = `
615+
-- p.go --
616+
package p
617+
618+
func _() {
619+
const (
620+
_ = iota
621+
_ = true
622+
)
623+
var (
624+
_ any
625+
err error = e{} // avoid nil deref warning
626+
)
627+
_ = err.Error
628+
println("Hello")
629+
_ = min(1, 2)
630+
}
631+
632+
// e implements Error, for use above.
633+
type e struct{}
634+
func (e) Error() string
635+
`
636+
637+
// Test hovering over various builtins with different kinds of declarations.
638+
tests := []string{
639+
"iota",
640+
"true",
641+
"any",
642+
"error",
643+
"Error",
644+
"println",
645+
"min",
646+
}
647+
648+
Run(t, src, func(t *testing.T, env *Env) {
649+
env.OpenFile("p.go")
650+
env.AfterChange(NoDiagnostics()) // avoid accidental compiler errors
651+
652+
for _, builtin := range tests {
653+
useLocation := env.RegexpSearch("p.go", builtin)
654+
calleeHover, _ := env.Hover(useLocation)
655+
declLocation := env.GoToDefinition(useLocation)
656+
declHover, _ := env.Hover(declLocation)
657+
if diff := cmp.Diff(calleeHover, declHover); diff != "" {
658+
t.Errorf("Hover mismatch (-callee hover +decl hover):\n%s", diff)
659+
}
660+
}
661+
})
662+
}

0 commit comments

Comments
 (0)