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>
This commit is contained in:
Zvonimir Pavlinovic 2022-04-26 15:46:41 -07:00
parent dcaea06afc
commit a37ba1418f
2 changed files with 107 additions and 26 deletions

View File

@ -24,7 +24,9 @@ import (
"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 @@ func expectation(f *ast.File) (string, token.Pos) {
// 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 @@ func TestCHA(t *testing.T) {
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 @@ func printGraph(cg *callgraph.Graph, from *types.Package) string {
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)
}

45
go/callgraph/cha/testdata/generics.go vendored Normal file
View File

@ -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