golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/go/callgraph/cha/cha_test.go (about) 1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // No testdata on Android. 6 7 //go:build !android 8 // +build !android 9 10 package cha_test 11 12 import ( 13 "bytes" 14 "fmt" 15 "go/ast" 16 "go/build" 17 "go/parser" 18 "go/token" 19 "go/types" 20 "os" 21 "sort" 22 "strings" 23 "testing" 24 25 "golang.org/x/tools/go/buildutil" 26 "golang.org/x/tools/go/callgraph" 27 "golang.org/x/tools/go/callgraph/cha" 28 "golang.org/x/tools/go/loader" 29 "golang.org/x/tools/go/ssa" 30 "golang.org/x/tools/go/ssa/ssautil" 31 ) 32 33 var inputs = []string{ 34 "testdata/func.go", 35 "testdata/iface.go", 36 "testdata/recv.go", 37 "testdata/issue23925.go", 38 } 39 40 func expectation(f *ast.File) (string, token.Pos) { 41 for _, c := range f.Comments { 42 text := strings.TrimSpace(c.Text()) 43 if t := strings.TrimPrefix(text, "WANT:\n"); t != text { 44 return t, c.Pos() 45 } 46 } 47 return "", token.NoPos 48 } 49 50 // TestCHA runs CHA on each file in inputs, prints the dynamic edges of 51 // the call graph, and compares it with the golden results embedded in 52 // the WANT comment at the end of the file. 53 func TestCHA(t *testing.T) { 54 for _, filename := range inputs { 55 prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics) 56 if err != nil { 57 t.Error(err) 58 continue 59 } 60 61 want, pos := expectation(f) 62 if pos == token.NoPos { 63 t.Error(fmt.Errorf("No WANT: comment in %s", filename)) 64 continue 65 } 66 67 cg := cha.CallGraph(prog) 68 69 if got := printGraph(cg, mainPkg.Pkg, "dynamic", "Dynamic calls"); got != want { 70 t.Errorf("%s: got:\n%s\nwant:\n%s", 71 prog.Fset.Position(pos), got, want) 72 } 73 } 74 } 75 76 // TestCHAGenerics is TestCHA tailored for testing generics, 77 func TestCHAGenerics(t *testing.T) { 78 filename := "testdata/generics.go" 79 prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics) 80 if err != nil { 81 t.Fatal(err) 82 } 83 84 want, pos := expectation(f) 85 if pos == token.NoPos { 86 t.Fatal(fmt.Errorf("No WANT: comment in %s", filename)) 87 } 88 89 cg := cha.CallGraph(prog) 90 91 if got := printGraph(cg, mainPkg.Pkg, "", "All calls"); got != want { 92 t.Errorf("%s: got:\n%s\nwant:\n%s", 93 prog.Fset.Position(pos), got, want) 94 } 95 } 96 97 // TestCHAUnexported tests call resolution for unexported methods. 98 func TestCHAUnexported(t *testing.T) { 99 // The two packages below each have types with methods called "m". 100 // Each of these methods should only be callable by functions in their 101 // own package, because they are unexported. 102 // 103 // In particular: 104 // - main.main can call (main.S1).m 105 // - p2.Foo can call (p2.S2).m 106 // - main.main cannot call (p2.S2).m 107 // - p2.Foo cannot call (main.S1).m 108 // 109 // We use CHA to build a callgraph, then check that it has the 110 // appropriate set of edges. 111 112 main := `package main 113 import "p2" 114 type I1 interface { m() } 115 type S1 struct { p2.I2 } 116 func (s S1) m() { } 117 func main() { 118 var s S1 119 var o I1 = s 120 o.m() 121 p2.Foo(s) 122 }` 123 124 p2 := `package p2 125 type I2 interface { m() } 126 type S2 struct { } 127 func (s S2) m() { } 128 func Foo(i I2) { i.m() }` 129 130 want := `All calls 131 main.init --> p2.init 132 main.main --> (main.S1).m 133 main.main --> p2.Foo 134 p2.Foo --> (p2.S2).m` 135 136 conf := loader.Config{ 137 Build: fakeContext(map[string]string{"main": main, "p2": p2}), 138 } 139 conf.Import("main") 140 iprog, err := conf.Load() 141 if err != nil { 142 t.Fatalf("Load failed: %v", err) 143 } 144 prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) 145 prog.Build() 146 147 cg := cha.CallGraph(prog) 148 149 // The graph is easier to read without synthetic nodes. 150 cg.DeleteSyntheticNodes() 151 152 if got := printGraph(cg, nil, "", "All calls"); got != want { 153 t.Errorf("cha.CallGraph: got:\n%s\nwant:\n%s", got, want) 154 } 155 } 156 157 // Simplifying wrapper around buildutil.FakeContext for single-file packages. 158 func fakeContext(pkgs map[string]string) *build.Context { 159 pkgs2 := make(map[string]map[string]string) 160 for path, content := range pkgs { 161 pkgs2[path] = map[string]string{"x.go": content} 162 } 163 return buildutil.FakeContext(pkgs2) 164 } 165 166 func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) { 167 content, err := os.ReadFile(filename) 168 if err != nil { 169 return nil, nil, nil, fmt.Errorf("couldn't read file '%s': %s", filename, err) 170 } 171 172 conf := loader.Config{ 173 ParserMode: parser.ParseComments, 174 } 175 f, err := conf.ParseFile(filename, content) 176 if err != nil { 177 return nil, nil, nil, err 178 } 179 180 conf.CreateFromFiles("main", f) 181 iprog, err := conf.Load() 182 if err != nil { 183 return nil, nil, nil, err 184 } 185 186 prog := ssautil.CreateProgram(iprog, mode) 187 prog.Build() 188 189 return prog, f, prog.Package(iprog.Created[0].Pkg), nil 190 } 191 192 // printGraph returns a string representation of cg involving only edges 193 // whose description contains edgeMatch. The string representation is 194 // prefixed with a desc line. 195 func printGraph(cg *callgraph.Graph, from *types.Package, edgeMatch string, desc string) string { 196 var edges []string 197 callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error { 198 if strings.Contains(e.Description(), edgeMatch) { 199 edges = append(edges, fmt.Sprintf("%s --> %s", 200 e.Caller.Func.RelString(from), 201 e.Callee.Func.RelString(from))) 202 } 203 return nil 204 }) 205 sort.Strings(edges) 206 207 var buf bytes.Buffer 208 buf.WriteString(desc + "\n") 209 for _, edge := range edges { 210 fmt.Fprintf(&buf, " %s\n", edge) 211 } 212 return strings.TrimSpace(buf.String()) 213 }