go/callgraph/cha: adds tests for (instantiated) generics

Updates golang/go#48525

Change-Id: Id568e0b188dd045356f556cb9d759a775c8c5a04
Reviewed-on: https://go-review.googlesource.com/c/tools/+/402474
Reviewed-by: Tim King <taking@google.com>
diff --git a/go/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go
index 8777ce4..a12b3d0 100644
--- a/go/callgraph/cha/cha_test.go
+++ b/go/callgraph/cha/cha_test.go
@@ -24,7 +24,9 @@
 	"golang.org/x/tools/go/callgraph"
 	"golang.org/x/tools/go/callgraph/cha"
 	"golang.org/x/tools/go/loader"
+	"golang.org/x/tools/go/ssa"
 	"golang.org/x/tools/go/ssa/ssautil"
+	"golang.org/x/tools/internal/typeparams"
 )
 
 var inputs = []string{
@@ -49,16 +51,7 @@
 // the WANT comment at the end of the file.
 func TestCHA(t *testing.T) {
 	for _, filename := range inputs {
-		content, err := ioutil.ReadFile(filename)
-		if err != nil {
-			t.Errorf("couldn't read file '%s': %s", filename, err)
-			continue
-		}
-
-		conf := loader.Config{
-			ParserMode: parser.ParseComments,
-		}
-		f, err := conf.ParseFile(filename, content)
+		prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics)
 		if err != nil {
 			t.Error(err)
 			continue
@@ -66,34 +59,77 @@
 
 		want, pos := expectation(f)
 		if pos == token.NoPos {
-			t.Errorf("No WANT: comment in %s", filename)
+			t.Error(fmt.Errorf("No WANT: comment in %s", filename))
 			continue
 		}
 
-		conf.CreateFromFiles("main", f)
-		iprog, err := conf.Load()
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		prog := ssautil.CreateProgram(iprog, 0)
-		mainPkg := prog.Package(iprog.Created[0].Pkg)
-		prog.Build()
-
 		cg := cha.CallGraph(prog)
 
-		if got := printGraph(cg, mainPkg.Pkg); got != want {
+		if got := printGraph(cg, mainPkg.Pkg, "dynamic", "Dynamic calls"); got != want {
 			t.Errorf("%s: got:\n%s\nwant:\n%s",
 				prog.Fset.Position(pos), got, want)
 		}
 	}
 }
 
-func printGraph(cg *callgraph.Graph, from *types.Package) string {
+// TestCHAGenerics is TestCHA tailored for testing generics,
+func TestCHAGenerics(t *testing.T) {
+	if !typeparams.Enabled {
+		t.Skip("TestCHAGenerics requires type parameters")
+	}
+
+	filename := "testdata/generics.go"
+	prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want, pos := expectation(f)
+	if pos == token.NoPos {
+		t.Fatal(fmt.Errorf("No WANT: comment in %s", filename))
+	}
+
+	cg := cha.CallGraph(prog)
+
+	if got := printGraph(cg, mainPkg.Pkg, "", "All calls"); got != want {
+		t.Errorf("%s: got:\n%s\nwant:\n%s",
+			prog.Fset.Position(pos), got, want)
+	}
+}
+
+func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) {
+	content, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("couldn't read file '%s': %s", filename, err)
+	}
+
+	conf := loader.Config{
+		ParserMode: parser.ParseComments,
+	}
+	f, err := conf.ParseFile(filename, content)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	conf.CreateFromFiles("main", f)
+	iprog, err := conf.Load()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	prog := ssautil.CreateProgram(iprog, mode)
+	prog.Build()
+
+	return prog, f, prog.Package(iprog.Created[0].Pkg), nil
+}
+
+// printGraph returns a string representation of cg involving only edges
+// whose description contains edgeMatch. The string representation is
+// prefixed with a desc line.
+func printGraph(cg *callgraph.Graph, from *types.Package, edgeMatch string, desc string) string {
 	var edges []string
 	callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error {
-		if strings.Contains(e.Description(), "dynamic") {
+		if strings.Contains(e.Description(), edgeMatch) {
 			edges = append(edges, fmt.Sprintf("%s --> %s",
 				e.Caller.Func.RelString(from),
 				e.Callee.Func.RelString(from)))
@@ -103,7 +139,7 @@
 	sort.Strings(edges)
 
 	var buf bytes.Buffer
-	buf.WriteString("Dynamic calls\n")
+	buf.WriteString(desc + "\n")
 	for _, edge := range edges {
 		fmt.Fprintf(&buf, "  %s\n", edge)
 	}
diff --git a/go/callgraph/cha/testdata/generics.go b/go/callgraph/cha/testdata/generics.go
new file mode 100644
index 0000000..2711dc0
--- /dev/null
+++ b/go/callgraph/cha/testdata/generics.go
@@ -0,0 +1,45 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+// Test of generic function calls.
+
+type I interface {
+	Foo()
+}
+
+type A struct{}
+
+func (a A) Foo() {}
+
+type B struct{}
+
+func (b B) Foo() {}
+
+func instantiated[X I](x X) {
+	x.Foo()
+}
+
+func Bar() {}
+
+func f(h func(), g func(I), k func(A), a A, b B) {
+	h()
+
+	k(a)
+	g(b) // g:func(I) is not matched by instantiated[[B]]:func(B)
+
+	instantiated[A](a) // static call
+	instantiated[B](b) // static call
+}
+
+// WANT:
+// All calls
+//   (*A).Foo --> (A).Foo
+//   (*B).Foo --> (B).Foo
+//   f --> Bar
+//   f --> instantiated[[main.A]]
+//   f --> instantiated[[main.A]]
+//   f --> instantiated[[main.B]]
+//   instantiated[[main.A]] --> (A).Foo
+//   instantiated[[main.B]] --> (B).Foo