gopls/internal/lsp/source: references: support exported methods

This change extends referencesV2 to support exported methods,
avoiding the need to fall back to the old type-check-the-world
implementation. (The latter is not quite dead code yet.)

A references query on a method (C).F will compute all methods
I.F of interfaces that correspond to it, by doing a lookup
in the method set index used by the global "implementations"
operation. The found methods are then used to augment the
set of targets in the global search.

The methodsets index has been extended to save the (PkgPath,
objectpath) pair for each method, since these are the needed
inputs to the global references query. (The method's package
is not necessarily the same as that of the enclosing type's
package due to embedding.) The current encoding increases the
index size by about 10%. I suspect we could do better by
exploiting redundancy between the objectpath and fingerprint,
but there's no urgency.

Also:
- typeDeclPosition no longer returns methodID, since
  it can be derived later.
- ensure that pkg.xrefs is always set by doTypeCheck.

Change-Id: I4037dcfc1a2a710f0cb7a2f5268527e77ebfcd79
Reviewed-on: https://go-review.googlesource.com/c/tools/+/463141
Reviewed-by: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index d7b0685..722691d 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -432,9 +432,6 @@
 	}
 	pkg.diagnostics = append(pkg.diagnostics, depsErrors...)
 
-	// Build index of outbound cross-references.
-	pkg.xrefs = xrefs.Index(pkg)
-
 	return pkg, nil
 }
 
@@ -488,8 +485,10 @@
 	if m.PkgPath == "unsafe" {
 		// Don't type check Unsafe: it's unnecessary, and doing so exposes a data
 		// race to Unsafe.completed.
+		// TODO(adonovan): factor (tail-merge) with the normal control path.
 		pkg.types = types.Unsafe
 		pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types)
+		pkg.xrefs = xrefs.Index(pkg)
 		return pkg, nil
 	}
 
@@ -574,6 +573,9 @@
 	// Build global index of method sets for 'implementations' queries.
 	pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types)
 
+	// Build global index of outbound cross-references.
+	pkg.xrefs = xrefs.Index(pkg)
+
 	// If the context was cancelled, we may have returned a ton of transient
 	// errors to the type checker. Swallow them.
 	if ctx.Err() != nil {
diff --git a/gopls/internal/lsp/source/implementation2.go b/gopls/internal/lsp/source/implementation2.go
index 7ca2e08..09659cc 100644
--- a/gopls/internal/lsp/source/implementation2.go
+++ b/gopls/internal/lsp/source/implementation2.go
@@ -72,8 +72,8 @@
 func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.Location, error) {
 
 	// Type-check the query package, find the query identifier,
-	// and locate the type/method declaration it refers to.
-	declPosn, methodID, err := typeDeclPosition(ctx, snapshot, fh.URI(), pp)
+	// and locate the type or method declaration it refers to.
+	declPosn, err := typeDeclPosition(ctx, snapshot, fh.URI(), pp)
 	if err != nil {
 		return nil, err
 	}
@@ -127,6 +127,7 @@
 	// Is the selected identifier a type name or method?
 	// (For methods, report the corresponding method names.)
 	var queryType types.Type
+	var queryMethodID string
 	switch obj := obj.(type) {
 	case *types.TypeName:
 		queryType = obj.Type()
@@ -134,6 +135,7 @@
 		// For methods, use the receiver type, which may be anonymous.
 		if recv := obj.Type().(*types.Signature).Recv(); recv != nil {
 			queryType = recv.Type()
+			queryMethodID = obj.Id()
 		}
 	default:
 		return nil, fmt.Errorf("%s is not a type or method", id.Name)
@@ -184,7 +186,7 @@
 	for _, localPkg := range localPkgs {
 		localPkg := localPkg
 		group.Go(func() error {
-			localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, methodID)
+			localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, queryMethodID)
 			if err != nil {
 				return err
 			}
@@ -198,8 +200,8 @@
 	for _, globalPkg := range globalPkgs {
 		globalPkg := globalPkg
 		group.Go(func() error {
-			for _, loc := range globalPkg.MethodSetsIndex().Search(key, methodID) {
-				loc := loc
+			for _, res := range globalPkg.MethodSetsIndex().Search(key, queryMethodID) {
+				loc := res.Location
 				// Map offsets to protocol.Locations in parallel (may involve I/O).
 				group.Go(func() error {
 					ploc, err := offsetToLocation(ctx, snapshot, loc.Filename, loc.Start, loc.End)
@@ -239,18 +241,17 @@
 }
 
 // typeDeclPosition returns the position of the declaration of the
-// type referred to at (uri, ppos).  If it refers to a method, the
-// function returns the method's receiver type and ID.
-func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (token.Position, string, error) {
+// type (or one of its methods) referred to at (uri, ppos).
+func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (token.Position, error) {
 	var noPosn token.Position
 
 	pkg, pgf, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, WidestPackage)
 	if err != nil {
-		return noPosn, "", err
+		return noPosn, err
 	}
 	pos, err := pgf.PositionPos(ppos)
 	if err != nil {
-		return noPosn, "", err
+		return noPosn, err
 	}
 
 	// This function inherits the limitation of its predecessor in
@@ -265,15 +266,14 @@
 	// TODO(adonovan): simplify: use objectsAt?
 	path := pathEnclosingObjNode(pgf.File, pos)
 	if path == nil {
-		return noPosn, "", ErrNoIdentFound
+		return noPosn, ErrNoIdentFound
 	}
 	id, ok := path[0].(*ast.Ident)
 	if !ok {
-		return noPosn, "", ErrNoIdentFound
+		return noPosn, ErrNoIdentFound
 	}
 
 	// Is the object a type or method? Reject other kinds.
-	var methodID string
 	obj := pkg.GetTypesInfo().Uses[id]
 	if obj == nil {
 		// Check uses first (unlike ObjectOf) so that T in
@@ -286,19 +286,18 @@
 		// ok
 	case *types.Func:
 		if obj.Type().(*types.Signature).Recv() == nil {
-			return noPosn, "", fmt.Errorf("%s is a function, not a method", id.Name)
+			return noPosn, fmt.Errorf("%s is a function, not a method", id.Name)
 		}
-		methodID = obj.Id()
 	case nil:
-		return noPosn, "", fmt.Errorf("%s denotes unknown object", id.Name)
+		return noPosn, fmt.Errorf("%s denotes unknown object", id.Name)
 	default:
 		// e.g. *types.Var -> "var".
 		kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types."))
-		return noPosn, "", fmt.Errorf("%s is a %s, not a type", id.Name, kind)
+		return noPosn, fmt.Errorf("%s is a %s, not a type", id.Name, kind)
 	}
 
 	declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
-	return declPosn, methodID, nil
+	return declPosn, nil
 }
 
 // localImplementations searches within pkg for declarations of all
diff --git a/gopls/internal/lsp/source/methodsets/methodsets.go b/gopls/internal/lsp/source/methodsets/methodsets.go
index 81d5d00..2c02555 100644
--- a/gopls/internal/lsp/source/methodsets/methodsets.go
+++ b/gopls/internal/lsp/source/methodsets/methodsets.go
@@ -51,6 +51,7 @@
 	"strconv"
 	"strings"
 
+	"golang.org/x/tools/go/types/objectpath"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/typeparams"
 )
@@ -72,13 +73,6 @@
 //
 // Conversion to protocol (UTF-16) form is done by the caller after a
 // search, not during index construction.
-// TODO(adonovan): opt: reconsider this choice, if FileHandles, not
-// ParsedGoFiles were to provide ColumnMapper-like functionality.
-// (Column mapping is currently associated with parsing,
-// but non-parsed and even non-Go files need it too.)
-// Since type checking requires reading (but not parsing) all
-// dependencies' Go files, we could do the conversion at type-checking
-// time at little extra cost in that case.
 type Location struct {
 	Filename   string
 	Start, End int // byte offsets
@@ -93,19 +87,30 @@
 // KeyOf returns the search key for the method sets of a given type.
 // It returns false if the type has no methods.
 func KeyOf(t types.Type) (Key, bool) {
-	mset := methodSetInfo(func(types.Object) (_ gobPosition) { return }, t, gobPosition{})
+	mset := methodSetInfo(t, nil)
 	if mset.Mask == 0 {
 		return Key{}, false // no methods
 	}
 	return Key{mset}, true
 }
 
+// A Result reports a matching type or method in a method-set search.
+type Result struct {
+	Location Location // location of the type or method
+
+	// methods only:
+	PkgPath    string          // path of declaring package (may differ due to embedding)
+	ObjectPath objectpath.Path // path of method within declaring package
+}
+
 // Search reports each type that implements (or is implemented by) the
 // type that produced the search key. If methodID is nonempty, only
 // that method of each type is reported.
-// The result is the location of each type or method.
-func (index *Index) Search(key Key, methodID string) []Location {
-	var locs []Location
+//
+// The result does not include the error.Error method.
+// TODO(adonovan): give this special case a more systematic treatment.
+func (index *Index) Search(key Key, methodID string) []Result {
+	var results []Result
 	for _, candidate := range index.pkg.MethodSets {
 		// Traditionally this feature doesn't report
 		// interface/interface elements of the relation.
@@ -125,19 +130,34 @@
 		}
 
 		if methodID == "" {
-			locs = append(locs, index.location(candidate.Posn))
+			results = append(results, Result{Location: index.location(candidate.Posn)})
 		} else {
 			for _, m := range candidate.Methods {
 				// Here we exploit knowledge of the shape of the fingerprint string.
 				if strings.HasPrefix(m.Fingerprint, methodID) &&
 					m.Fingerprint[len(methodID)] == '(' {
-					locs = append(locs, index.location(m.Posn))
+
+					// Don't report error.Error among the results:
+					// it has no true source location, no package,
+					// and is excluded from the xrefs index.
+					if m.PkgPath == 0 || m.ObjectPath == 0 {
+						if methodID != "Error" {
+							panic("missing info for" + methodID)
+						}
+						continue
+					}
+
+					results = append(results, Result{
+						Location:   index.location(m.Posn),
+						PkgPath:    index.pkg.Strings[m.PkgPath],
+						ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]),
+					})
 					break
 				}
 			}
 		}
 	}
-	return locs
+	return results
 }
 
 // satisfies does a fast check for whether x satisfies y.
@@ -161,7 +181,7 @@
 
 func (index *Index) location(posn gobPosition) Location {
 	return Location{
-		Filename: index.pkg.Filenames[posn.File],
+		Filename: index.pkg.Strings[posn.File],
 		Start:    posn.Offset,
 		End:      posn.Offset + posn.Len,
 	}
@@ -170,54 +190,73 @@
 // An indexBuilder builds an index for a single package.
 type indexBuilder struct {
 	gobPackage
-	filenameIndex map[string]int
+	stringIndex map[string]int
 }
 
 // build adds to the index all package-level named types of the specified package.
 func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index {
+	_ = b.string("") // 0 => ""
+
+	objectPos := func(obj types.Object) gobPosition {
+		posn := safetoken.StartPosition(fset, obj.Pos())
+		return gobPosition{b.string(posn.Filename), posn.Offset, len(obj.Name())}
+	}
+
+	// setindexInfo sets the (Posn, PkgPath, ObjectPath) fields for each method declaration.
+	setIndexInfo := func(m *gobMethod, method *types.Func) {
+		// error.Error has empty Position, PkgPath, and ObjectPath.
+		if method.Pkg() == nil {
+			return
+		}
+
+		m.Posn = objectPos(method)
+		m.PkgPath = b.string(method.Pkg().Path())
+
+		// Instantiations of generic methods don't have an
+		// object path, so we use the generic.
+		if p, err := objectpath.For(typeparams.OriginMethod(method)); err != nil {
+			panic(err) // can't happen for a method of a package-level type
+		} else {
+			m.ObjectPath = b.string(string(p))
+		}
+	}
+
 	// We ignore aliases, though in principle they could define a
 	// struct{...}  or interface{...} type, or an instantiation of
 	// a generic, that has a novel method set.
 	scope := pkg.Scope()
 	for _, name := range scope.Names() {
 		if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() {
-			b.add(fset, tname)
+			if mset := methodSetInfo(tname.Type(), setIndexInfo); mset.Mask != 0 {
+				mset.Posn = objectPos(tname)
+				// Only record types with non-trivial method sets.
+				b.MethodSets = append(b.MethodSets, mset)
+			}
 		}
 	}
 
 	return &Index{pkg: b.gobPackage}
 }
 
-func (b *indexBuilder) add(fset *token.FileSet, tname *types.TypeName) {
-	objectPos := func(obj types.Object) gobPosition {
-		posn := safetoken.StartPosition(fset, obj.Pos())
-		return gobPosition{b.fileIndex(posn.Filename), posn.Offset, len(obj.Name())}
-	}
-	if mset := methodSetInfo(objectPos, tname.Type(), objectPos(tname)); mset.Mask != 0 {
-		// Only record types with non-trivial method sets.
-		b.MethodSets = append(b.MethodSets, mset)
-	}
-}
-
-// fileIndex returns a small integer that encodes the file name.
-func (b *indexBuilder) fileIndex(filename string) int {
-	i, ok := b.filenameIndex[filename]
+// string returns a small integer that encodes the string.
+func (b *indexBuilder) string(s string) int {
+	i, ok := b.stringIndex[s]
 	if !ok {
-		i = len(b.Filenames)
-		if b.filenameIndex == nil {
-			b.filenameIndex = make(map[string]int)
+		i = len(b.Strings)
+		if b.stringIndex == nil {
+			b.stringIndex = make(map[string]int)
 		}
-		b.filenameIndex[filename] = i
-		b.Filenames = append(b.Filenames, filename)
+		b.stringIndex[s] = i
+		b.Strings = append(b.Strings, s)
 	}
 	return i
 }
 
-// methodSetInfo returns the method-set fingerprint
-// of a type and records its position (typePosn)
-// and the position of each of its methods m,
-// as provided by objectPos(m).
-func methodSetInfo(objectPos func(types.Object) gobPosition, t types.Type, typePosn gobPosition) gobMethodSet {
+// methodSetInfo returns the method-set fingerprint of a type.
+// It calls the optional setIndexInfo function for each gobMethod.
+// This is used during index construction, but not search (KeyOf),
+// to store extra information.
+func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gobMethodSet {
 	// For non-interface types, use *T
 	// (if T is not already a pointer)
 	// since it may have more methods.
@@ -234,10 +273,18 @@
 			tricky = true
 		}
 		sum := crc32.ChecksumIEEE([]byte(fp))
-		methods[i] = gobMethod{fp, sum, objectPos(m)}
+		methods[i] = gobMethod{Fingerprint: fp, Sum: sum}
+		if setIndexInfo != nil {
+			setIndexInfo(&methods[i], m) // set Position, PkgPath, ObjectPath
+		}
 		mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f)
 	}
-	return gobMethodSet{typePosn, types.IsInterface(t), tricky, mask, methods}
+	return gobMethodSet{
+		IsInterface: types.IsInterface(t),
+		Tricky:      tricky,
+		Mask:        mask,
+		Methods:     methods,
+	}
 }
 
 // EnsurePointer wraps T in a types.Pointer if T is a named, non-interface type.
@@ -398,7 +445,7 @@
 
 // A gobPackage records the method set of each package-level type for a single package.
 type gobPackage struct {
-	Filenames  []string // see gobPosition.File
+	Strings    []string // index of strings used by gobPosition.File, gobMethod.{Pkg,Object}Path
 	MethodSets []gobMethodSet
 }
 
@@ -413,13 +460,17 @@
 
 // A gobMethod records the name, type, and position of a single method.
 type gobMethod struct {
-	Fingerprint string      // string of form "methodID(params...)(results)"
-	Sum         uint32      // checksum of fingerprint
-	Posn        gobPosition // location of method declaration
+	Fingerprint string // string of form "methodID(params...)(results)"
+	Sum         uint32 // checksum of fingerprint
+
+	// index records only (zero in KeyOf; also for index of error.Error).
+	Posn       gobPosition // location of method declaration
+	PkgPath    int         // path of package containing method declaration
+	ObjectPath int         // object path of method relative to PkgPath
 }
 
 // A gobPosition records the file, offset, and length of an identifier.
 type gobPosition struct {
-	File        int // index into Index.filenames
+	File        int // index into gopPackage.Strings
 	Offset, Len int // in bytes
 }
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
index 25d4a99..afe03f1 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/lsp/source/references.go
@@ -34,8 +34,8 @@
 // referencesV1 returns a list of references for a given identifier within the packages
 // containing pp. Declarations appear first in the result.
 //
-// Currently used by Server.{incomingCalls,rename}.
-// TODO(adonovan): switch each over to referencesV2 in turn.
+// Currently called only from Server.incomingCalls.
+// TODO(adonovan): switch over to referencesV2.
 func referencesV1(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) {
 	ctx, done := event.Start(ctx, "source.References")
 	defer done()
diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go
index 538639a..2a40065 100644
--- a/gopls/internal/lsp/source/references2.go
+++ b/gopls/internal/lsp/source/references2.go
@@ -17,7 +17,6 @@
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"go/ast"
 	"go/token"
@@ -32,6 +31,7 @@
 	"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/bug"
 	"golang.org/x/tools/internal/event"
 )
 
@@ -43,27 +43,12 @@
 	Location      protocol.Location
 }
 
-var ErrFallback = errors.New("fallback")
-
 // References returns a list of all references (sorted with
 // definitions before uses) to the object denoted by the identifier at
 // the given file/position, searching the entire workspace.
 func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) {
 	references, err := referencesV2(ctx, snapshot, fh, pp, includeDeclaration)
 	if err != nil {
-		if err == ErrFallback {
-			// Fall back to old implementation.
-			// TODO(adonovan): support methods in V2 and eliminate referencesV1.
-			references, err := referencesV1(ctx, snapshot, fh, pp, includeDeclaration)
-			if err != nil {
-				return nil, err
-			}
-			var locations []protocol.Location
-			for _, ref := range references {
-				locations = append(locations, ref.MappedRange.Location())
-			}
-			return locations, nil
-		}
 		return nil, err
 	}
 	// TODO(adonovan): eliminate references[i].Name field?
@@ -78,9 +63,6 @@
 // referencesV2 returns a list of all references (sorted with
 // definitions before uses) to the object denoted by the identifier at
 // the given file/position, searching the entire workspace.
-//
-// Returns ErrFallback if it can't yet handle the case, indicating
-// that we should call the old implementation.
 func referencesV2(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfoV2, error) {
 	ctx, done := event.Start(ctx, "source.References2")
 	defer done()
@@ -252,7 +234,7 @@
 		return nil, ErrNoIdentFound // can't happen
 	}
 
-	// nil, error, iota, or other built-in?
+	// nil, error, error.Error, iota, or other built-in?
 	if obj.Pkg() == nil {
 		// For some reason, existing tests require that iota has no references,
 		// nor an error. TODO(adonovan): do something more principled.
@@ -267,10 +249,58 @@
 	// This may include the query pkg, and possibly other variants.
 	declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
 	declURI := span.URIFromPath(declPosn.Filename)
-	metas, err := snapshot.MetadataForFile(ctx, declURI)
+	variants, err := snapshot.MetadataForFile(ctx, declURI)
 	if err != nil {
 		return nil, err
 	}
+	if len(variants) == 0 {
+		return nil, fmt.Errorf("no packages for file %q", declURI) // can't happen
+	}
+
+	// Is object exported?
+	// If so, compute scope and targets of the global search.
+	var (
+		globalScope   = make(map[PackageID]*Metadata)
+		globalTargets map[PackagePath]map[objectpath.Path]unit
+	)
+	// TODO(adonovan): what about generic functions. Need to consider both
+	// uninstantiated and instantiated. The latter have no objectpath. Use Origin?
+	if path, err := objectpath.For(obj); err == nil && obj.Exported() {
+		pkgPath := variants[0].PkgPath // (all variants have same package path)
+		globalTargets = map[PackagePath]map[objectpath.Path]unit{
+			pkgPath: {path: {}}, // primary target
+		}
+
+		// How far need we search?
+		// For package-level objects, we need only search the direct importers.
+		// For fields and methods, we must search transitively.
+		transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj
+
+		// The scope is the union of rdeps of each variant.
+		// (Each set is disjoint so there's no benefit to
+		// to combining the metadata graph traversals.)
+		for _, m := range variants {
+			rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive)
+			if err != nil {
+				return nil, err
+			}
+			for id, rdep := range rdeps {
+				globalScope[id] = rdep
+			}
+		}
+
+		// Is object a method?
+		//
+		// If so, expand the search so that the targets include
+		// all methods that correspond to it through interface
+		// satisfaction, and the scope includes the rdeps of
+		// the package that declares each corresponding type.
+		if recv := effectiveReceiver(obj); recv != nil {
+			if err := expandMethodSearch(ctx, snapshot, obj.(*types.Func), recv, globalScope, globalTargets); err != nil {
+				return nil, err
+			}
+		}
+	}
 
 	// The search functions will call report(loc) for each hit.
 	var (
@@ -288,30 +318,6 @@
 		refsMu.Unlock()
 	}
 
-	// Is the object exported?
-	// (objectpath succeeds for lowercase names, arguably a bug.)
-	var exportedObjectPaths map[objectpath.Path]unit
-	if path, err := objectpath.For(obj); err == nil && obj.Exported() {
-		exportedObjectPaths = map[objectpath.Path]unit{path: unit{}}
-
-		// 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?
-	// For package-level objects, we need only search the direct importers.
-	// For fields and methods, we must search transitively.
-	transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj
-
 	// Loop over the variants of the declaring package,
 	// and perform both the local (in-package) and global
 	// (cross-package) searches, in parallel.
@@ -322,56 +328,99 @@
 	//
 	// Careful: this goroutine must not return before group.Wait.
 	var group errgroup.Group
-	for _, m := range metas { // for each variant
-		m := m
 
-		// local
+	// Compute local references for each variant.
+	for _, m := range variants {
+		// We want the ordinary importable package,
+		// plus any test-augmented variants, since
+		// declarations in _test.go files may change
+		// the reference of a selection, or even a
+		// field into a method or vice versa.
+		//
+		// But we don't need intermediate test variants,
+		// as their local references will be covered
+		// already by other variants.
+		if m.IsIntermediateTestVariant() {
+			continue
+		}
+		m := m
 		group.Go(func() error {
-			// We want the ordinary importable package,
-			// plus any test-augmented variants, since
-			// declarations in _test.go files may change
-			// the reference of a selection, or even a
-			// field into a method or vice versa.
-			//
-			// But we don't need intermediate test variants,
-			// as their local references will be covered
-			// already by other variants.
-			if m.IsIntermediateTestVariant() {
-				return nil
-			}
 			return localReferences(ctx, snapshot, declURI, declPosn.Offset, m, report)
 		})
+	}
 
-		if exportedObjectPaths == nil {
-			continue // non-exported
-		}
-
-		targets := map[PackagePath]map[objectpath.Path]struct{}{m.PkgPath: exportedObjectPaths}
-
-		// global
+	// Compute global references for selected reverse dependencies.
+	for _, m := range globalScope {
+		m := m
 		group.Go(func() error {
-			// Compute the global-scope query for every variant
-			// of the declaring package in parallel,
-			// as the rdeps of each variant are disjoint.
-			rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive)
-			if err != nil {
-				return err
-			}
-			for _, rdep := range rdeps {
-				rdep := rdep
-				group.Go(func() error {
-					return globalReferences(ctx, snapshot, rdep, targets, report)
-				})
-			}
-			return nil
+			return globalReferences(ctx, snapshot, m, globalTargets, report)
 		})
 	}
+
 	if err := group.Wait(); err != nil {
 		return nil, err
 	}
 	return refs, nil
 }
 
+// expandMethodSearch expands the scope and targets of a global search
+// for an exported method to include all methods that correspond to
+// it through interface satisfaction.
+//
+// recv is the method's effective receiver type, for method-set computations.
+func expandMethodSearch(ctx context.Context, snapshot Snapshot, method *types.Func, recv types.Type, scope map[PackageID]*Metadata, targets map[PackagePath]map[objectpath.Path]unit) error {
+	// Compute the method-set fingerprint used as a key to the global search.
+	key, hasMethods := methodsets.KeyOf(recv)
+	if !hasMethods {
+		return bug.Errorf("KeyOf(%s)={} yet %s is a method", recv, method)
+	}
+	metas, err := snapshot.AllMetadata(ctx)
+	if err != nil {
+		return err
+	}
+	allIDs := make([]PackageID, 0, len(metas))
+	for _, m := range metas {
+		allIDs = append(allIDs, m.ID)
+	}
+	// Search the methodset index of each package in the workspace.
+	allPkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, allIDs...)
+	if err != nil {
+		return err
+	}
+	var group errgroup.Group
+	for _, pkg := range allPkgs {
+		pkg := pkg
+		group.Go(func() error {
+			// Consult index for matching methods.
+			results := pkg.MethodSetsIndex().Search(key, method.Name())
+
+			// Expand global search scope to include rdeps of this pkg.
+			if len(results) > 0 {
+				rdeps, err := snapshot.ReverseDependencies(ctx, pkg.ID(), true)
+				if err != nil {
+					return err
+				}
+				for _, rdep := range rdeps {
+					scope[rdep.ID] = rdep
+				}
+			}
+
+			// Add each corresponding method the to set of global search targets.
+			for _, res := range results {
+				methodPkg := PackagePath(res.PkgPath)
+				opaths, ok := targets[methodPkg]
+				if !ok {
+					opaths = make(map[objectpath.Path]unit)
+					targets[methodPkg] = opaths
+				}
+				opaths[res.ObjectPath] = unit{}
+			}
+			return nil
+		})
+	}
+	return group.Wait()
+}
+
 // 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 {
@@ -402,17 +451,6 @@
 		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.
@@ -420,7 +458,7 @@
 	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 {
+		if t := effectiveReceiver(obj); t != nil {
 			methodRecvs = append(methodRecvs, t)
 			methodName = obj.Name()
 		}
@@ -432,7 +470,7 @@
 		if targets[obj] != nil {
 			return true
 		} else if methodRecvs != nil && obj.Name() == methodName {
-			if orecv := receiver(obj); orecv != nil {
+			if orecv := effectiveReceiver(obj); orecv != nil {
 				for _, mrecv := range methodRecvs {
 					if concreteImplementsIntf(orecv, mrecv) {
 						return true
@@ -457,6 +495,17 @@
 	return nil
 }
 
+// effectiveReceiver returns the effective receiver type for method-set
+// comparisons for obj, if it is a method, or nil otherwise.
+func effectiveReceiver(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
+}
+
 // objectsAt returns the non-empty set of objects denoted (def or use)
 // by the specified position within a file syntax tree, or an error if
 // none were found.
diff --git a/gopls/internal/lsp/source/xrefs/xrefs.go b/gopls/internal/lsp/source/xrefs/xrefs.go
index b0d2207..5f781f7 100644
--- a/gopls/internal/lsp/source/xrefs/xrefs.go
+++ b/gopls/internal/lsp/source/xrefs/xrefs.go
@@ -176,6 +176,7 @@
 // TODO(adonovan): opt: choose a more compact encoding. Gzip reduces
 // the gob output to about one third its size, so clearly there's room
 // to improve. The gobRef.Range field is the obvious place to begin.
+// Even a zero-length slice gob-encodes to ~285 bytes.
 
 // A gobPackage records the set of outgoing references from the index
 // package to symbols defined in a dependency package.
diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go
index f105f4c..8e99a1c 100644
--- a/gopls/internal/regtest/misc/references_test.go
+++ b/gopls/internal/regtest/misc/references_test.go
@@ -50,8 +50,8 @@
 	})
 }
 
-// This reproduces and tests golang/go#48400.
-func TestReferencesPanicOnError(t *testing.T) {
+// This is a regression test for golang/go#48400 (a panic).
+func TestReferencesOnErrorMethod(t *testing.T) {
 	// Ideally this would actually return the correct answer,
 	// instead of merely failing gracefully.
 	const files = `
@@ -81,12 +81,19 @@
 		env.OpenFile("main.go")
 		file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `Error`))
 		refs, err := env.Editor.References(env.Ctx, file, pos)
-		if err == nil {
-			t.Fatalf("expected error for references, instead got %v", refs)
+		if err != nil {
+			t.Fatalf("references on (*s).Error failed: %v", err)
 		}
-		wantErr := "no position for func (error).Error() string"
-		if err.Error() != wantErr {
-			t.Fatalf("expected error with message %s, instead got %s", wantErr, err.Error())
+		// TODO(adonovan): this test is crying out for marker support in regtests.
+		var buf strings.Builder
+		for _, ref := range refs {
+			fmt.Fprintf(&buf, "%s %s\n", env.Sandbox.Workdir.URIToPath(ref.URI), ref.Range)
+		}
+		got := buf.String()
+		want := "main.go 8:10-8:15\n" + // (*s).Error decl
+			"main.go 14:7-14:12\n" // s.Error() call
+		if diff := cmp.Diff(want, got); diff != "" {
+			t.Errorf("unexpected references on (*s).Error (-want +got):\n%s", diff)
 		}
 	})
 }