gopls/internal/lsp/source: avoid unnecessary transitive rdeps

Most callers of ReverseDependencies need only the direct importers.
The new 'transitive bool' flag lets them express that and do less
work.

Change-Id: Ib0a6b1efc892424dde0e4d2c30ad67c91212fc56
Reviewed-on: https://go-review.googlesource.com/c/tools/+/458855
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go
index 5df0234..aaabbdf 100644
--- a/gopls/internal/lsp/cache/graph.go
+++ b/gopls/internal/lsp/cache/graph.go
@@ -103,10 +103,10 @@
 	}
 }
 
-// reverseTransitiveClosure returns a new mapping containing the
+// reverseReflexiveTransitiveClosure returns a new mapping containing the
 // metadata for the specified packages along with any package that
-// transitively imports one of them, keyed by ID.
-func (g *metadataGraph) reverseTransitiveClosure(ids ...PackageID) map[PackageID]*source.Metadata {
+// transitively imports one of them, keyed by ID, including all the initial packages.
+func (g *metadataGraph) reverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageID]*source.Metadata {
 	seen := make(map[PackageID]*source.Metadata)
 	var visitAll func([]PackageID)
 	visitAll = func(ids []PackageID) {
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index ea57d57..bda3690 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -819,19 +819,32 @@
 	return metas, nil
 }
 
-func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID) (map[PackageID]*source.Metadata, error) {
+func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*source.Metadata, error) {
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
 	}
 	s.mu.Lock()
 	meta := s.meta
 	s.mu.Unlock()
-	rdeps := meta.reverseTransitiveClosure(id)
 
-	// Remove the original package ID from the map.
-	// TODO(adonovan): we should make ReverseDependencies and
-	// reverseTransitiveClosure consistent wrt reflexiveness.
-	delete(rdeps, id)
+	var rdeps map[PackageID]*source.Metadata
+	if transitive {
+		rdeps = meta.reverseReflexiveTransitiveClosure(id)
+
+		// Remove the original package ID from the map.
+		// (Callers all want irreflexivity but it's easier
+		// to compute reflexively then subtract.)
+		delete(rdeps, id)
+
+	} else {
+		// direct reverse dependencies
+		rdeps = make(map[PackageID]*source.Metadata)
+		for _, rdepID := range meta.importedBy[id] {
+			if rdep := meta.metadata[rdepID]; rdep != nil {
+				rdeps[rdepID] = rdep
+			}
+		}
+	}
 
 	return rdeps, nil
 }
diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go
index b8e11bc..076aed0 100644
--- a/gopls/internal/lsp/source/call_hierarchy.go
+++ b/gopls/internal/lsp/source/call_hierarchy.go
@@ -96,19 +96,17 @@
 			event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name))
 			continue
 		}
+
 		loc := protocol.Location{
 			URI:   callItem.URI,
 			Range: callItem.Range,
 		}
-
-		if incomingCall, ok := incomingCalls[loc]; ok {
-			incomingCall.FromRanges = append(incomingCall.FromRanges, refRange)
-			continue
+		call, ok := incomingCalls[loc]
+		if !ok {
+			call = &protocol.CallHierarchyIncomingCall{From: callItem}
+			incomingCalls[loc] = call
 		}
-		incomingCalls[loc] = &protocol.CallHierarchyIncomingCall{
-			From:       callItem,
-			FromRanges: []protocol.Range{refRange},
-		}
+		call.FromRanges = append(call.FromRanges, refRange)
 	}
 
 	incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls))
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
index 33ecdc3..f51f7f1 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/lsp/source/references.go
@@ -52,29 +52,14 @@
 		}
 		targetPkg := metas[len(metas)-1] // widest package
 
-		// Find external references to the package.
-		rdeps, err := snapshot.ReverseDependencies(ctx, targetPkg.ID)
+		// Find external direct references to the package (imports).
+		rdeps, err := snapshot.ReverseDependencies(ctx, targetPkg.ID, false)
 		if err != nil {
 			return nil, err
 		}
 
 		var refs []*ReferenceInfo
 		for _, rdep := range rdeps {
-			// Optimization: skip this loop (parsing) if rdep
-			// doesn't directly import the target package.
-			// (This logic also appears in rename; perhaps we need
-			// a ReverseDirectDependencies method?)
-			direct := false
-			for _, importedID := range rdep.DepsByImpPath {
-				if importedID == targetPkg.ID {
-					direct = true
-					break
-				}
-			}
-			if !direct {
-				continue
-			}
-
 			for _, uri := range rdep.CompiledGoFiles {
 				fh, err := snapshot.GetFile(ctx, uri)
 				if err != nil {
@@ -199,12 +184,16 @@
 
 		// Only search dependents if the object is exported.
 		if qo.obj.Exported() {
-			rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.ID())
+			// If obj is a package-level object, we need only search
+			// among direct reverse dependencies.
+			// TODO(adonovan): opt: this will still spuriously search
+			// transitively for (e.g.) capitalized local variables.
+			// We could do better by checking for an objectpath.
+			transitive := qo.obj.Pkg().Scope().Lookup(qo.obj.Name()) != qo.obj
+			rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.ID(), transitive)
 			if err != nil {
 				return nil, err
 			}
-			// TODO(adonovan): opt: if obj is a package-level object, we need
-			// only search among direct reverse dependencies.
 			ids := make([]PackageID, 0, len(rdeps))
 			for _, rdep := range rdeps {
 				ids = append(ids, rdep.ID)
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go
index 93048e0..fa98f46 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/lsp/source/rename.go
@@ -268,6 +268,7 @@
 		}
 
 		if m.Module == nil {
+			// This check will always fail under Bazel.
 			return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PkgPath)
 		}
 
@@ -364,7 +365,7 @@
 //
 // Edits are written into the edits map.
 func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error {
-	rdeps, err := snapshot.ReverseDependencies(ctx, m.ID)
+	rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, false) // find direct importers
 	if err != nil {
 		return err
 	}
@@ -376,20 +377,6 @@
 			continue // for renaming, these variants are redundant
 		}
 
-		// Optimization: skip packages that don't directly import m.
-		// (This logic also appears in 'references'; perhaps we need
-		// Snapshot.ReverseDirectDependencies?)
-		direct := false
-		for _, importedID := range rdep.DepsByImpPath {
-			if importedID == m.ID {
-				direct = true
-				break
-			}
-		}
-		if !direct {
-			continue
-		}
-
 		for _, uri := range rdep.CompiledGoFiles {
 			if seen.add(uri, m.PkgPath) {
 				continue
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index b6bab12..0aeac54 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -177,8 +177,9 @@
 
 	// ReverseDependencies returns a new mapping whose entries are
 	// the ID and Metadata of each package in the workspace that
-	// transitively depends on the package denoted by id, excluding id itself.
-	ReverseDependencies(ctx context.Context, id PackageID) (map[PackageID]*Metadata, error)
+	// directly or transitively depend on the package denoted by id,
+	// excluding id itself.
+	ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*Metadata, error)
 
 	// CachedImportPaths returns all the imported packages loaded in this
 	// snapshot, indexed by their package path (not import path, despite the name)
diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go
index 7cff19c..0e8ff45 100644
--- a/gopls/internal/regtest/misc/references_test.go
+++ b/gopls/internal/regtest/misc/references_test.go
@@ -40,6 +40,7 @@
 			t.Fatal(err)
 		}
 		if len(refs) != 2 {
+			// TODO(adonovan): make this assertion less maintainer-hostile.
 			t.Fatalf("got %v reference(s), want 2", len(refs))
 		}
 		// The first reference is guaranteed to be the definition.
@@ -152,6 +153,7 @@
 			pos := env.RegexpSearch(f, test.packageName)
 			refs := env.References(fmt.Sprintf("%s/a.go", test.packageName), pos)
 			if len(refs) != test.wantRefCount {
+				// TODO(adonovan): make this assertion less maintainer-hostile.
 				t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount)
 			}
 			var refURIs []string