| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // No testdata on Android. |
| |
| //go:build !android |
| // +build !android |
| |
| package cha_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/build" |
| "go/parser" |
| "go/token" |
| "go/types" |
| "os" |
| "sort" |
| "strings" |
| "testing" |
| |
| "golang.org/x/tools/go/buildutil" |
| "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" |
| ) |
| |
| var inputs = []string{ |
| "testdata/func.go", |
| "testdata/iface.go", |
| "testdata/recv.go", |
| "testdata/issue23925.go", |
| } |
| |
| func expectation(f *ast.File) (string, token.Pos) { |
| for _, c := range f.Comments { |
| text := strings.TrimSpace(c.Text()) |
| if t := strings.TrimPrefix(text, "WANT:\n"); t != text { |
| return t, c.Pos() |
| } |
| } |
| return "", token.NoPos |
| } |
| |
| // TestCHA runs CHA on each file in inputs, prints the dynamic edges of |
| // the call graph, and compares it with the golden results embedded in |
| // the WANT comment at the end of the file. |
| func TestCHA(t *testing.T) { |
| for _, filename := range inputs { |
| prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| |
| want, pos := expectation(f) |
| if pos == token.NoPos { |
| t.Error(fmt.Errorf("No WANT: comment in %s", filename)) |
| continue |
| } |
| |
| cg := cha.CallGraph(prog) |
| |
| 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) |
| } |
| } |
| } |
| |
| // TestCHAGenerics is TestCHA tailored for testing generics, |
| func TestCHAGenerics(t *testing.T) { |
| 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) |
| } |
| } |
| |
| // TestCHAUnexported tests call resolution for unexported methods. |
| func TestCHAUnexported(t *testing.T) { |
| // The two packages below each have types with methods called "m". |
| // Each of these methods should only be callable by functions in their |
| // own package, because they are unexported. |
| // |
| // In particular: |
| // - main.main can call (main.S1).m |
| // - p2.Foo can call (p2.S2).m |
| // - main.main cannot call (p2.S2).m |
| // - p2.Foo cannot call (main.S1).m |
| // |
| // We use CHA to build a callgraph, then check that it has the |
| // appropriate set of edges. |
| |
| main := `package main |
| import "p2" |
| type I1 interface { m() } |
| type S1 struct { p2.I2 } |
| func (s S1) m() { } |
| func main() { |
| var s S1 |
| var o I1 = s |
| o.m() |
| p2.Foo(s) |
| }` |
| |
| p2 := `package p2 |
| type I2 interface { m() } |
| type S2 struct { } |
| func (s S2) m() { } |
| func Foo(i I2) { i.m() }` |
| |
| want := `All calls |
| main.init --> p2.init |
| main.main --> (main.S1).m |
| main.main --> p2.Foo |
| p2.Foo --> (p2.S2).m` |
| |
| conf := loader.Config{ |
| Build: fakeContext(map[string]string{"main": main, "p2": p2}), |
| } |
| conf.Import("main") |
| iprog, err := conf.Load() |
| if err != nil { |
| t.Fatalf("Load failed: %v", err) |
| } |
| prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) |
| prog.Build() |
| |
| cg := cha.CallGraph(prog) |
| |
| // The graph is easier to read without synthetic nodes. |
| cg.DeleteSyntheticNodes() |
| |
| if got := printGraph(cg, nil, "", "All calls"); got != want { |
| t.Errorf("cha.CallGraph: got:\n%s\nwant:\n%s", got, want) |
| } |
| } |
| |
| // Simplifying wrapper around buildutil.FakeContext for single-file packages. |
| func fakeContext(pkgs map[string]string) *build.Context { |
| pkgs2 := make(map[string]map[string]string) |
| for path, content := range pkgs { |
| pkgs2[path] = map[string]string{"x.go": content} |
| } |
| return buildutil.FakeContext(pkgs2) |
| } |
| |
| 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 |
| } |
| |
| // 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(), edgeMatch) { |
| edges = append(edges, fmt.Sprintf("%s --> %s", |
| e.Caller.Func.RelString(from), |
| e.Callee.Func.RelString(from))) |
| } |
| return nil |
| }) |
| sort.Strings(edges) |
| |
| var buf bytes.Buffer |
| buf.WriteString(desc + "\n") |
| for _, edge := range edges { |
| fmt.Fprintf(&buf, " %s\n", edge) |
| } |
| return strings.TrimSpace(buf.String()) |
| } |