gopls/internal/lsp/source/typerefs: reexpress tests wrt ExternalRefs

This change introduces a new API function that (in effect) computes
the path through intra-package edges to imported symbols, and
re-expresses all the tests in terms of it.

A follow-up change will implement SCC-based graph optimizations
within the Refs operation, but this bridge allows us to keep
the tests unchanged during that transition, for increased confidence.

Change-Id: I6735bee2ae8b9b940514bfd7145ad69cd442f117
Reviewed-on: https://go-review.googlesource.com/c/tools/+/481783
Run-TryBot: Alan Donovan <adonovan@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/source/typerefs/pkgrefs.go b/gopls/internal/lsp/source/typerefs/pkgrefs.go
index 0fd8c18..2424f86 100644
--- a/gopls/internal/lsp/source/typerefs/pkgrefs.go
+++ b/gopls/internal/lsp/source/typerefs/pkgrefs.go
@@ -177,6 +177,40 @@
 	return p, nil
 }
 
+// ExternalRefs returns a new map whose keys are the exported symbols
+// of the package (of the specified id, pkgIndex, and refs). The
+// corresponding value of each key is the set of exported symbols
+// indirectly referenced by it.
+//
+// TODO(adonovan): simplify the API once the SCC-based optimization lands.
+func ExternalRefs(pkgIndex *PackageIndex, id source.PackageID, refs map[string][]Ref) map[string]map[Ref]bool {
+	// (This intrapackage recursion will go away in a follow-up CL.)
+	var visit func(name string, res map[Ref]bool, seen map[string]bool)
+	visit = func(name string, res map[Ref]bool, seen map[string]bool) {
+		if !seen[name] {
+			seen[name] = true
+			for _, ref := range refs[name] {
+				if pkgIndex.id(ref.pkg) == id {
+					visit(ref.name, res, seen) // intrapackage recursion
+				} else {
+					res[ref] = true // cross-package ref
+				}
+			}
+		}
+	}
+
+	results := make(map[string]map[Ref]bool)
+	for name := range refs {
+		if token.IsExported(name) {
+			res := make(map[Ref]bool)
+			seen := make(map[string]bool)
+			visit(name, res, seen)
+			results[name] = res
+		}
+	}
+	return results
+}
+
 // reachableByName computes the set of packages that are reachable through
 // references, starting with the declaration for name in package p.
 func (g *PackageGraph) reachableByName(ctx context.Context, p *Package, name string, set *PackageSet, seen map[string]bool) error {
diff --git a/gopls/internal/lsp/source/typerefs/refs_test.go b/gopls/internal/lsp/source/typerefs/refs_test.go
index 6c8c546..fe1f8a9 100644
--- a/gopls/internal/lsp/source/typerefs/refs_test.go
+++ b/gopls/internal/lsp/source/typerefs/refs_test.go
@@ -19,13 +19,16 @@
 	"golang.org/x/tools/internal/testenv"
 )
 
+// TestRefs checks that the analysis reports, for each exported member
+// of the test package ("p"), its correct dependencies on exported
+// members of its direct imports (e.g. "ext").
 func TestRefs(t *testing.T) {
 	ctx := context.Background()
 
 	tests := []struct {
 		label     string
 		srcs      []string            // source for the local package; package name must be p
-		imports   map[string]string   // for simplicity: importPath -> pkgID/pkgName (we set pkgName == pkgID)
+		imports   map[string]string   // for simplicity: importPath -> pkgID/pkgName (we set pkgName == pkgID); 'ext' is always available.
 		want      map[string][]string // decl name -> id.<decl name>
 		go118     bool                // test uses generics
 		allowErrs bool                // whether we expect parsing errors
@@ -39,19 +42,23 @@
 			srcs: []string{`
 package p
 
+import "ext"
+
 type A struct{ b B }
 type B func(c C) (d D)
-type C int
-type D int
+type C ext.C
+type D ext.D
 
 // Should not be referenced by field names.
-type b int
-type c int
-type d int
+type b ext.B_
+type c int.C_
+type d ext.D_
 `},
 			want: map[string][]string{
-				"A": {"p.B"},
-				"B": {"p.C", "p.D"},
+				"A": {"ext.C", "ext.D"},
+				"B": {"ext.C", "ext.D"},
+				"C": {"ext.C"},
+				"D": {"ext.D"},
 			},
 		},
 		{
@@ -59,6 +66,8 @@
 			srcs: []string{`
 package p
 
+import "ext"
+
 type A struct{
 	B
 	_ struct {
@@ -66,15 +75,17 @@
 	}
 	D
 }
-type B int
-type C int
+type B ext.B
+type C ext.C
 type D interface{
 	B
 }
 `},
 			want: map[string][]string{
-				"A": {"p.B", "p.C", "p.D"},
-				"D": {"p.B"},
+				"A": {"ext.B", "ext.C"},
+				"B": {"ext.B"},
+				"C": {"ext.C"},
+				"D": {"ext.B"},
 			},
 		},
 		{
@@ -82,17 +93,22 @@
 			srcs: []string{`
 package p
 
+import "ext"
+
 type A interface{
 	int | B | ~C
 	struct{D}
 }
 
-type B int
-type C int
-type D int
+type B ext.B
+type C ext.C
+type D ext.D
 `},
 			want: map[string][]string{
-				"A": {"p.B", "p.C", "p.D"},
+				"A": {"ext.B", "ext.C", "ext.D"},
+				"B": {"ext.B"},
+				"C": {"ext.C"},
+				"D": {"ext.D"},
 			},
 			go118: true,
 		},
@@ -101,8 +117,10 @@
 			srcs: []string{`
 package p
 
-type A int
-type B int
+import "ext"
+
+type A ext.A
+type B ext.B
 const C B = 2
 func F(A) B {
 	return C
@@ -111,26 +129,33 @@
 var W A
 `},
 			want: map[string][]string{
-				"C": {"p.B"},
-				"F": {"p.A", "p.B"},
-				"V": {"p.F", "p.W"}, // p.W edge can't be eliminated: F could be builtin or generic
-				"W": {"p.A"},
+				"A": {"ext.A"},
+				"B": {"ext.B"},
+				"C": {"ext.B"},
+				"F": {"ext.A", "ext.B"},
+				"V": {
+					"ext.A", // via F
+					"ext.B", // via W: can't be eliminated: F could be builtin or generic
+				},
+				"W": {"ext.A"},
 			},
 		},
 		{
 			label: "methods",
 			srcs: []string{`package p
 
-type A int
-type B int
+import "ext"
+
+type A ext.A
+type B ext.B
 `, `package p
 
 func (A) M(B)
 func (*B) M(A)
 `},
 			want: map[string][]string{
-				"A": {"p.B"},
-				"B": {"p.A"},
+				"A": {"ext.A", "ext.B"},
+				"B": {"ext.A", "ext.B"},
 			},
 		},
 		{
@@ -138,134 +163,144 @@
 			srcs: []string{`
 package p
 
-var a b = c // type does not depend on c
-type b int
-var c = d // type does depend on d
+import "ext"
+
+var A b = C // type does not depend on C
+type b ext.B
+var C = d // type does depend on D
 var d b
 
 var e = d + a
 
-var f = func() b { return e }
+var F = func() B { return E }
 
-var g = struct{
-	a b
-	_ [unsafe.Sizeof(g)]int
+var G = struct{
+	A b
+	_ [unsafe.Sizeof(ext.V)]int // array size + Sizeof creates edge to a var
+	_ [unsafe.Sizeof(G)]int // creates a self edge; doesn't affect output though
 }{}
 
-var h = (d + a + c*c)
+var H = (D + A + C*C)
 
-var i = (a+c).f
+var I = (A+C).F
 `},
 			want: map[string][]string{
-				"a": {"p.b"},
-				"c": {"p.d"},
-				"d": {"p.b"},
-				"e": {"p.a", "p.d"},
-				"f": {"p.b"},
-				"g": {"p.b", "p.g"}, // p.g edge could be skipped
-				"h": {"p.a", "p.c", "p.d"},
-				"i": {"p.a", "p.c"},
+				"A": {"ext.B"},
+				"C": {"ext.B"},          // via d
+				"G": {"ext.B", "ext.V"}, // via b,C
+				"H": {"ext.B"},          // via d,A,C
+				"I": {"ext.B"},
 			},
 		},
 		{
 			label: "builtins",
 			srcs: []string{`package p
 
-var A = new(B)
-type B struct{}
+import "ext"
 
-type C chan D
-type D int
+var A = new(b)
+type b struct{ ext.B }
 
-type S []T
-type T int
-var U = append(([]*S)(nil), new(T))
+type C chan d
+type d ext.D
 
-type X map[K]V
-type K int
-type V int
+type S []ext.S
+type t ext.T
+var U = append(([]*S)(nil), new(t))
 
-var Z = make(map[K]A)
+type X map[k]v
+type k ext.K
+type v ext.V
+
+var Z = make(map[k]A)
 
 // close, delete, and panic cannot occur outside of statements
 `},
 			want: map[string][]string{
-				"A": {"p.B"},
-				"C": {"p.D"},
-				"S": {"p.T"},
-				"U": {"p.S", "p.T"}, // p.T edge could be eliminated
-				"X": {"p.K", "p.V"},
-				"Z": {"p.A", "p.K"},
+				"A": {"ext.B"},
+				"C": {"ext.D"},
+				"S": {"ext.S"},
+				"U": {"ext.S", "ext.T"}, // ext.T edge could be eliminated
+				"X": {"ext.K", "ext.V"},
+				"Z": {"ext.B", "ext.K"},
 			},
 		},
 		{
 			label: "builtin shadowing",
 			srcs: []string{`package p
 
-var A = new(B)
+import "ext"
+
+var A = new(ext.B)
 func new() c
-type c int
+type c ext.C
 `},
 			want: map[string][]string{
-				"A":   {"p.new"},
-				"new": {"p.c"},
+				"A": {"ext.B", "ext.C"},
 			},
 		},
 		{
 			label: "named forwarding",
 			srcs: []string{`package p
 
+import "ext"
+
 type A B
-type B C
-type C int
+type B c
+type c ext.C
 `},
 			want: map[string][]string{
-				"A": {"p.B"},
-				"B": {"p.C"},
+				"A": {"ext.C"},
+				"B": {"ext.C"},
 			},
 		},
 		{
 			label: "aliases",
 			srcs: []string{`package p
 
+import "ext"
+
 type A = B
 type B = C
-type C = int
+type C = ext.C
 `},
 			want: map[string][]string{
-				"A": {"p.B"},
-				"B": {"p.C"},
+				"A": {"ext.C"},
+				"B": {"ext.C"},
+				"C": {"ext.C"},
 			},
 		},
 		{
 			label: "array length",
 			srcs: []string{`package p
 
+import "ext"
 import "unsafe"
 
-type A [unsafe.Sizeof(B{C})]int
-type A2 [unsafe.Sizeof(B{f:C})]int // use a KeyValueExpr
-type B struct{ f int }
-var C = 0
+type A [unsafe.Sizeof(ext.B{ext.C})]int
+type A2 [unsafe.Sizeof(ext.B{f:ext.C})]int // use a KeyValueExpr
 
 type D [unsafe.Sizeof(struct{ f E })]int
-type E int
+type E ext.E
 
 type F [3]G
-type G [C]int
+type G [ext.C]int
 `},
 			want: map[string][]string{
-				"A":  {"p.B"},
-				"A2": {"p.B"},
-				"D":  {"p.E"},
-				"F":  {"p.G"},
-				"G":  {"p.C"},
+				"A":  {"ext.B"}, // no ext.C: doesn't enter CompLit
+				"A2": {"ext.B"}, // ditto
+				"D":  {"ext.E"},
+				"E":  {"ext.E"},
+				"F":  {"ext.C"},
+				"G":  {"ext.C"},
 			},
 		},
 		{
 			label: "imports",
 			srcs: []string{`package p
 
+import "ext"
+
 import (
 	"q"
 	r2 "r"
@@ -277,7 +312,7 @@
 	q.Q
 	r2.R
 	s.S // invalid ref
-	z.Z // note: shadowed below
+	z.Z // references both external z.Z as well as package-level type z
 }
 
 type B struct {
@@ -285,16 +320,16 @@
 	t.T
 }
 
-var x int = q.V
-var y = q.V.W
+var X int = q.V // X={}: no descent into RHS of 'var v T = rhs'
+var Y = q.V.W
 
-type z interface{}
+type z ext.Z
 `},
 			imports: map[string]string{"q": "q", "r": "r", "s": "t", "z": "z"},
 			want: map[string][]string{
-				"A": {"p.z", "q.Q", "r.R", "z.Z"},
+				"A": {"ext.Z", "q.Q", "r.R", "z.Z"},
 				"B": {"t.T"},
-				"y": {"q.V"},
+				"Y": {"q.V"},
 			},
 		},
 		{
@@ -326,11 +361,15 @@
 var G = Field.X
 `, `package p
 
-type D interface{}
+import "ext"
+import "q"
+
+type D ext.D
 `},
 			imports: map[string]string{"q": "q"},
 			want: map[string][]string{
-				"B": {"p.D", "q.C"},
+				"B": {"ext.D", "q.C"},
+				"D": {"ext.D"},
 				"F": {"q.Field"},
 				"G": {"q.Field"},
 			},
@@ -339,31 +378,37 @@
 			label: "typeparams",
 			srcs: []string{`package p
 
+import "ext"
+
 type A[T any] struct {
 	t T
 	b B
 }
 
-type B int
+type B ext.B
 
 func F1[T any](T, B)
 func F2[T C]()(T, B)
 
-type T int
+type T ext.T
 
-type C any
+type C ext.C
 
 func F3[T1 ~[]T2, T2 ~[]T3](t1 T1, t2 T2)
-type T3 any
+type T3 ext.T3
 `, `package p
 
 func (A[B]) M(C) {}
 `},
 			want: map[string][]string{
-				"A":  {"p.B", "p.C"},
-				"F1": {"p.B"},
-				"F2": {"p.B", "p.C"},
-				"F3": {"p.T3"},
+				"A":  {"ext.B", "ext.C"},
+				"B":  {"ext.B"},
+				"C":  {"ext.C"},
+				"F1": {"ext.B"},
+				"F2": {"ext.B", "ext.C"},
+				"F3": {"ext.T3"},
+				"T":  {"ext.T"},
+				"T3": {"ext.T3"},
 			},
 			go118: true,
 		},
@@ -371,16 +416,21 @@
 			label: "instances",
 			srcs: []string{`package p
 
-type A[T any] struct{}
-type B[T1, T2 any] struct{}
+import "ext"
+
+type A[T any] ext.A
+type B[T1, T2 any] ext.B
 
 type C A[int]
 type D B[int, A[E]]
-type E int
+type E ext.E
 `},
 			want: map[string][]string{
-				"C": {"p.A"},
-				"D": {"p.A", "p.B", "p.E"},
+				"A": {"ext.A"},
+				"B": {"ext.B"},
+				"C": {"ext.A"},
+				"D": {"ext.A", "ext.B", "ext.E"},
+				"E": {"ext.E"},
 			},
 			go118: true,
 		},
@@ -389,35 +439,41 @@
 			srcs: []string{`package p
 
 import "a"
+import "ext"
 
 type a a.A
-type b int
+type A a
+type b ext.B
 type C a.A
 func (C) Foo(x) {} // invalid parameter, but that does not matter
 type C b
 func (C) Bar(y) {} // invalid parameter, but that does not matter
 
-var x, y int
+var x ext.X
+var y ext.Y
 `},
 			imports: map[string]string{"a": "a", "b": "b"}, // "b" import should not matter, since it isn't in this file
 			want: map[string][]string{
-				"a": {"a.A", "p.a"},
-				"C": {"a.A", "p.a", "p.b", "p.x", "p.y"},
+				"A": {"a.A"},
+				"C": {"a.A", "ext.B", "ext.X", "ext.Y"},
 			},
 		},
 		{
 			label: "invalid decls",
 			srcs: []string{`package p
 
+import "ext"
+
 type A B
 
 func () Foo(B){}
 
-var B
+var B struct{ ext.B
 `},
 			want: map[string][]string{
-				"A":   {"p.B"},
-				"Foo": {"p.B"},
+				"A":   {"ext.B"},
+				"B":   {"ext.B"},
+				"Foo": {"ext.B"},
 			},
 			allowErrs: true,
 		},
@@ -439,7 +495,9 @@
 				pgfs = append(pgfs, pgf)
 			}
 
-			imports := make(map[source.ImportPath]*source.Metadata)
+			imports := map[source.ImportPath]*source.Metadata{
+				"ext": {ID: "ext", Name: "ext"}, // this one comes for free
+			}
 			for path, m := range test.imports {
 				imports[source.ImportPath(path)] = &source.Metadata{
 					ID:   source.PackageID(m),
@@ -450,10 +508,14 @@
 			index := typerefs.NewPackageIndex()
 			refs := typerefs.Refs(pgfs, "p", imports, index)
 
+			// TODO(adonovan): simplify the API once the
+			// new SCC-based optimization lands.
+			extrefs := typerefs.ExternalRefs(index, "p", refs)
+
 			got := make(map[string][]string)
-			for name, refs := range refs {
+			for name, refs := range extrefs {
 				var srefs []string
-				for _, ref := range refs {
+				for ref := range refs {
 					id, name := ref.Unpack(index)
 					srefs = append(srefs, fmt.Sprintf("%s.%s", id, name))
 				}