| 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