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 rta_test |
11 | |
12 | import ( |
13 | "bytes" |
14 | "fmt" |
15 | "go/ast" |
16 | "go/parser" |
17 | "go/token" |
18 | "go/types" |
19 | "os" |
20 | "sort" |
21 | "strings" |
22 | "testing" |
23 | |
24 | "golang.org/x/tools/go/callgraph" |
25 | "golang.org/x/tools/go/callgraph/rta" |
26 | "golang.org/x/tools/go/loader" |
27 | "golang.org/x/tools/go/ssa" |
28 | "golang.org/x/tools/go/ssa/ssautil" |
29 | "golang.org/x/tools/internal/typeparams" |
30 | ) |
31 | |
32 | var inputs = []string{ |
33 | "testdata/func.go", |
34 | "testdata/rtype.go", |
35 | "testdata/iface.go", |
36 | } |
37 | |
38 | func expectation(f *ast.File) (string, token.Pos) { |
39 | for _, c := range f.Comments { |
40 | text := strings.TrimSpace(c.Text()) |
41 | if t := strings.TrimPrefix(text, "WANT:\n"); t != text { |
42 | return t, c.Pos() |
43 | } |
44 | } |
45 | return "", token.NoPos |
46 | } |
47 | |
48 | // TestRTA runs RTA on each file in inputs, prints the results, and |
49 | // compares it with the golden results embedded in the WANT comment at |
50 | // the end of the file. |
51 | // |
52 | // The results string consists of two parts: the set of dynamic call |
53 | // edges, "f --> g", one per line, and the set of reachable functions, |
54 | // one per line. Each set is sorted. |
55 | func TestRTA(t *testing.T) { |
56 | for _, filename := range inputs { |
57 | prog, f, mainPkg, err := loadProgInfo(filename, ssa.BuilderMode(0)) |
58 | if err != nil { |
59 | t.Error(err) |
60 | continue |
61 | } |
62 | |
63 | want, pos := expectation(f) |
64 | if pos == token.NoPos { |
65 | t.Errorf("No WANT: comment in %s", filename) |
66 | continue |
67 | } |
68 | |
69 | res := rta.Analyze([]*ssa.Function{ |
70 | mainPkg.Func("main"), |
71 | mainPkg.Func("init"), |
72 | }, true) |
73 | |
74 | if got := printResult(res, mainPkg.Pkg, "dynamic", "Dynamic calls"); got != want { |
75 | t.Errorf("%s: got:\n%s\nwant:\n%s", |
76 | prog.Fset.Position(pos), got, want) |
77 | } |
78 | } |
79 | } |
80 | |
81 | // TestRTAGenerics is TestRTA specialized for testing generics. |
82 | func TestRTAGenerics(t *testing.T) { |
83 | if !typeparams.Enabled { |
84 | t.Skip("TestRTAGenerics requires type parameters") |
85 | } |
86 | |
87 | filename := "testdata/generics.go" |
88 | prog, f, mainPkg, err := loadProgInfo(filename, ssa.InstantiateGenerics) |
89 | if err != nil { |
90 | t.Fatal(err) |
91 | } |
92 | |
93 | want, pos := expectation(f) |
94 | if pos == token.NoPos { |
95 | t.Fatalf("No WANT: comment in %s", filename) |
96 | } |
97 | |
98 | res := rta.Analyze([]*ssa.Function{ |
99 | mainPkg.Func("main"), |
100 | mainPkg.Func("init"), |
101 | }, true) |
102 | |
103 | if got := printResult(res, mainPkg.Pkg, "", "All calls"); got != want { |
104 | t.Errorf("%s: got:\n%s\nwant:\n%s", |
105 | prog.Fset.Position(pos), got, want) |
106 | } |
107 | } |
108 | |
109 | func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) { |
110 | content, err := os.ReadFile(filename) |
111 | if err != nil { |
112 | return nil, nil, nil, fmt.Errorf("couldn't read file '%s': %s", filename, err) |
113 | } |
114 | |
115 | conf := loader.Config{ |
116 | ParserMode: parser.ParseComments, |
117 | } |
118 | f, err := conf.ParseFile(filename, content) |
119 | if err != nil { |
120 | return nil, nil, nil, err |
121 | } |
122 | |
123 | conf.CreateFromFiles("main", f) |
124 | iprog, err := conf.Load() |
125 | if err != nil { |
126 | return nil, nil, nil, err |
127 | } |
128 | |
129 | prog := ssautil.CreateProgram(iprog, mode) |
130 | prog.Build() |
131 | |
132 | return prog, f, prog.Package(iprog.Created[0].Pkg), nil |
133 | } |
134 | |
135 | // printResult returns a string representation of res, i.e., call graph, |
136 | // reachable functions, and reflect types. For call graph, only edges |
137 | // whose description contains edgeMatch are returned and their string |
138 | // representation is prefixed with a desc line. |
139 | func printResult(res *rta.Result, from *types.Package, edgeMatch, desc string) string { |
140 | var buf bytes.Buffer |
141 | |
142 | writeSorted := func(ss []string) { |
143 | sort.Strings(ss) |
144 | for _, s := range ss { |
145 | fmt.Fprintf(&buf, " %s\n", s) |
146 | } |
147 | } |
148 | |
149 | buf.WriteString(desc + "\n") |
150 | var edges []string |
151 | callgraph.GraphVisitEdges(res.CallGraph, func(e *callgraph.Edge) error { |
152 | if strings.Contains(e.Description(), edgeMatch) { |
153 | edges = append(edges, fmt.Sprintf("%s --> %s", |
154 | e.Caller.Func.RelString(from), |
155 | e.Callee.Func.RelString(from))) |
156 | } |
157 | return nil |
158 | }) |
159 | writeSorted(edges) |
160 | |
161 | buf.WriteString("Reachable functions\n") |
162 | var reachable []string |
163 | for f := range res.Reachable { |
164 | reachable = append(reachable, f.RelString(from)) |
165 | } |
166 | writeSorted(reachable) |
167 | |
168 | buf.WriteString("Reflect types\n") |
169 | var rtypes []string |
170 | res.RuntimeTypes.Iterate(func(key types.Type, value interface{}) { |
171 | if value == false { // accessible to reflection |
172 | rtypes = append(rtypes, types.TypeString(key, types.RelativeTo(from))) |
173 | } |
174 | }) |
175 | writeSorted(rtypes) |
176 | |
177 | return strings.TrimSpace(buf.String()) |
178 | } |
179 |
Members