go/callgraph/rta: adds tests for (instantiated) generics
Updates golang/go#48525
Change-Id: I7e25ab136dd69ebd50b12894bc893986fc59999b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/402994
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Tim King <taking@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go
index 5dfb441..2e80415 100644
--- a/go/callgraph/rta/rta.go
+++ b/go/callgraph/rta/rta.go
@@ -45,6 +45,10 @@
// replacing all "unreachable" functions by a special intrinsic, and
// ensure that that intrinsic is never called.
+// TODO(zpavlinovic): decide if the clients must use ssa.InstantiateGenerics
+// mode when building programs with generics. It might be possible to
+// extend rta to accurately support generics with just ssa.BuilderMode(0).
+
import (
"fmt"
"go/types"
diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go
index 89ade37..67d05d6 100644
--- a/go/callgraph/rta/rta_test.go
+++ b/go/callgraph/rta/rta_test.go
@@ -16,7 +16,7 @@
"go/parser"
"go/token"
"go/types"
- "io/ioutil"
+ "os"
"sort"
"strings"
"testing"
@@ -26,6 +26,7 @@
"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{
@@ -53,16 +54,7 @@
// one per line. Each set is sorted.
func TestRTA(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.BuilderMode(0))
if err != nil {
t.Error(err)
continue
@@ -74,30 +66,77 @@
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()
-
res := rta.Analyze([]*ssa.Function{
mainPkg.Func("main"),
mainPkg.Func("init"),
}, true)
- if got := printResult(res, mainPkg.Pkg); got != want {
+ if got := printResult(res, mainPkg.Pkg, "dynamic", "Dynamic calls"); got != want {
t.Errorf("%s: got:\n%s\nwant:\n%s",
prog.Fset.Position(pos), got, want)
}
}
}
-func printResult(res *rta.Result, from *types.Package) string {
+// TestRTAGenerics is TestRTA specialized for testing generics.
+func TestRTAGenerics(t *testing.T) {
+ if !typeparams.Enabled {
+ t.Skip("TestRTAGenerics 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.Fatalf("No WANT: comment in %s", filename)
+ }
+
+ res := rta.Analyze([]*ssa.Function{
+ mainPkg.Func("main"),
+ mainPkg.Func("init"),
+ }, true)
+
+ if got := printResult(res, 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 := os.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
+}
+
+// printResult returns a string representation of res, i.e., call graph,
+// reachable functions, and reflect types. For call graph, only edges
+// whose description contains edgeMatch are returned and their string
+// representation is prefixed with a desc line.
+func printResult(res *rta.Result, from *types.Package, edgeMatch, desc string) string {
var buf bytes.Buffer
writeSorted := func(ss []string) {
@@ -107,10 +146,10 @@
}
}
- buf.WriteString("Dynamic calls\n")
+ buf.WriteString(desc + "\n")
var edges []string
callgraph.GraphVisitEdges(res.CallGraph, 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)))
diff --git a/go/callgraph/rta/testdata/generics.go b/go/callgraph/rta/testdata/generics.go
new file mode 100644
index 0000000..012a64c
--- /dev/null
+++ b/go/callgraph/rta/testdata/generics.go
@@ -0,0 +1,79 @@
+//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()
+}
+
+var a A
+var b B
+
+func main() {
+ instantiated[A](a) // static call
+ instantiated[B](b) // static call
+
+ local[C]().Foo()
+
+ lambda[A]()()()
+}
+
+func local[X I]() I {
+ var x X
+ return x
+}
+
+type C struct{}
+
+func (c C) Foo() {}
+
+func lambda[X I]() func() func() {
+ return func() func() {
+ var x X
+ return x.Foo
+ }
+}
+
+// WANT:
+// All calls
+// (*C).Foo --> (C).Foo
+// (A).Foo$bound --> (A).Foo
+// instantiated[[main.A]] --> (A).Foo
+// instantiated[[main.B]] --> (B).Foo
+// main --> (*C).Foo
+// main --> (A).Foo$bound
+// main --> (C).Foo
+// main --> instantiated[[main.A]]
+// main --> instantiated[[main.B]]
+// main --> lambda[[main.A]]
+// main --> lambda[[main.A]]$1
+// main --> local[[main.C]]
+// Reachable functions
+// (*C).Foo
+// (A).Foo
+// (A).Foo$bound
+// (B).Foo
+// (C).Foo
+// instantiated[[main.A]]
+// instantiated[[main.B]]
+// lambda[[main.A]]
+// lambda[[main.A]]$1
+// local[[main.C]]
+// Reflect types
+// *C
+// C