gopls/internal/lsp/source: referencesV2: support unexported methods
This change adds support for unexported methods to the incremental
implementation of the 'references' operation, which previously
had to fall back to a type-check-the-world strategy for methods.
(Exported methods are still to do.)
The implementation strategy is to record the receiver type R of
the target method M, and search types.Info.Uses for all objects
that are methods of the same name as M, whose receiver is mutually
assignable with R.
Change-Id: I4121f34df822662ccbaac5d53f3e2a5578be5431
Reviewed-on: https://go-review.googlesource.com/c/tools/+/462915
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index 6a6a8ca..eb5baf9 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -867,6 +867,8 @@
want := make(map[protocol.Location]bool)
for i, pos := range itemList {
// We don't want the first result if we aren't including the declaration.
+ // TODO(adonovan): don't assume a single declaration:
+ // there may be >1 if corresponding methods are considered.
if i == 0 && !includeDeclaration {
continue
}
diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go
index 9ebeace..1fc01dc 100644
--- a/gopls/internal/lsp/source/references2.go
+++ b/gopls/internal/lsp/source/references2.go
@@ -30,6 +30,7 @@
"golang.org/x/tools/go/types/objectpath"
"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"
)
@@ -248,18 +249,7 @@
break
}
if obj == nil {
- return nil, ErrNoIdentFound
- }
-
- // If the object is a method, we need to search for all
- // matching implementations and/or interfaces, without
- // type-checking everything.
- //
- // That will require an approach such as the one sketched in
- // go.dev/cl/452060. Until then, we simply fall back to the
- // old implementation for now. TODO(adonovan): fix.
- if fn, ok := obj.(*types.Func); ok && fn.Type().(*types.Signature).Recv() != nil {
- return nil, ErrFallback
+ return nil, ErrNoIdentFound // can't happen
}
// nil, error, iota, or other built-in?
@@ -303,6 +293,18 @@
var exportedObjectPath objectpath.Path
if path, err := objectpath.For(obj); err == nil && obj.Exported() {
exportedObjectPath = path
+
+ // If the object is an exported method, we need to search for
+ // all matching implementations (using the incremental
+ // implementation of 'implementations') and then search for
+ // the set of corresponding methods (requiring the incremental
+ // implementation of 'references' to be generalized to a set
+ // of search objects).
+ // Until then, we simply fall back to the old implementation for now.
+ // TODO(adonovan): fix.
+ if fn, ok := obj.(*types.Func); ok && fn.Type().(*types.Signature).Recv() != nil {
+ return nil, ErrFallback
+ }
}
// If it is exported, how far need we search?
@@ -368,7 +370,7 @@
return refs, nil
}
-// localReferences reports (concurrently) each reference to the object
+// localReferences reports each reference to the object
// declared at the specified URI/offset within its enclosing package m.
func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, declOffset int, m *Metadata, report func(loc protocol.Location, isDecl bool)) error {
pkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, m.ID)
@@ -393,15 +395,57 @@
}
// Report the locations of the declaration(s).
+ // TODO(adonovan): what about for corresponding methods? Add tests.
for _, node := range targets {
report(mustLocation(pgf, node), true)
}
+ // receiver returns the effective receiver type for method-set
+ // comparisons for obj, if it is a method, or nil otherwise.
+ receiver := func(obj types.Object) types.Type {
+ if fn, ok := obj.(*types.Func); ok {
+ if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
+ return methodsets.EnsurePointer(recv.Type())
+ }
+ }
+ return nil
+ }
+
+ // If we're searching for references to a method, broaden the
+ // search to include references to corresponding methods of
+ // mutually assignable receiver types.
+ // (We use a slice, but objectsAt never returns >1 methods.)
+ var methodRecvs []types.Type
+ var methodName string // name of an arbitrary target, iff a method
+ for obj := range targets {
+ if t := receiver(obj); t != nil {
+ methodRecvs = append(methodRecvs, t)
+ methodName = obj.Name()
+ }
+ }
+
+ // matches reports whether obj either is or corresponds to a target.
+ // (Correspondence is defined as usual for interface methods.)
+ matches := func(obj types.Object) bool {
+ if targets[obj] != nil {
+ return true
+ } else if methodRecvs != nil && obj.Name() == methodName {
+ if orecv := receiver(obj); orecv != nil {
+ for _, mrecv := range methodRecvs {
+ if concreteImplementsIntf(orecv, mrecv) {
+ return true
+ }
+ }
+ }
+ }
+ return false
+ }
+
// Scan through syntax looking for uses of one of the target objects.
for _, pgf := range pkg.CompiledGoFiles() {
ast.Inspect(pgf.File, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok {
- if used, ok := pkg.GetTypesInfo().Uses[id]; ok && targets[used] != nil {
+ if obj, ok := pkg.GetTypesInfo().Uses[id]; ok && matches(obj) {
report(mustLocation(pgf, id), false)
}
}
@@ -453,10 +497,14 @@
}
targets[obj] = leaf
}
+
+ if len(targets) == 0 {
+ return nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen
+ }
return targets, nil
}
-// globalReferences reports (concurrently) each cross-package
+// globalReferences reports each cross-package
// reference to the object identified by (pkgPath, objectPath).
func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPath PackagePath, objectPath objectpath.Path, report func(loc protocol.Location, isDecl bool)) error {
// TODO(adonovan): opt: don't actually type-check here,
diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go
index 19704ff..f105f4c 100644
--- a/gopls/internal/regtest/misc/references_test.go
+++ b/gopls/internal/regtest/misc/references_test.go
@@ -52,6 +52,8 @@
// This reproduces and tests golang/go#48400.
func TestReferencesPanicOnError(t *testing.T) {
+ // Ideally this would actually return the correct answer,
+ // instead of merely failing gracefully.
const files = `
-- go.mod --
module mod.com