1 | // Copyright 2018 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 | package facts_test |
6 | |
7 | import ( |
8 | "encoding/gob" |
9 | "fmt" |
10 | "go/token" |
11 | "go/types" |
12 | "os" |
13 | "reflect" |
14 | "testing" |
15 | |
16 | "golang.org/x/tools/go/analysis/analysistest" |
17 | "golang.org/x/tools/go/packages" |
18 | "golang.org/x/tools/internal/facts" |
19 | "golang.org/x/tools/internal/testenv" |
20 | "golang.org/x/tools/internal/typeparams" |
21 | ) |
22 | |
23 | type myFact struct { |
24 | S string |
25 | } |
26 | |
27 | func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) } |
28 | func (f *myFact) AFact() {} |
29 | |
30 | func init() { |
31 | gob.Register(new(myFact)) |
32 | } |
33 | |
34 | func TestEncodeDecode(t *testing.T) { |
35 | tests := []struct { |
36 | name string |
37 | typeparams bool // requires typeparams to be enabled |
38 | files map[string]string |
39 | plookups []pkgLookups // see testEncodeDecode for details |
40 | }{ |
41 | { |
42 | name: "loading-order", |
43 | // c -> b -> a, a2 |
44 | // c does not directly depend on a, but it indirectly uses a.T. |
45 | // |
46 | // Package a2 is never loaded directly so it is incomplete. |
47 | // |
48 | // We use only types in this example because we rely on |
49 | // types.Eval to resolve the lookup expressions, and it only |
50 | // works for types. This is a definite gap in the typechecker API. |
51 | files: map[string]string{ |
52 | "a/a.go": `package a; type A int; type T int`, |
53 | "a2/a.go": `package a2; type A2 int; type Unneeded int`, |
54 | "b/b.go": `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`, |
55 | "c/c.go": `package c; import "b"; type C []b.B`, |
56 | }, |
57 | // In the following table, we analyze packages (a, b, c) in order, |
58 | // look up various objects accessible within each package, |
59 | // and see if they have a fact. The "analysis" exports a fact |
60 | // for every object at package level. |
61 | // |
62 | // Note: Loop iterations are not independent test cases; |
63 | // order matters, as we populate factmap. |
64 | plookups: []pkgLookups{ |
65 | {"a", []lookup{ |
66 | {"A", "myFact(a.A)"}, |
67 | }}, |
68 | {"b", []lookup{ |
69 | {"a.A", "myFact(a.A)"}, |
70 | {"a.T", "myFact(a.T)"}, |
71 | {"B", "myFact(b.B)"}, |
72 | {"F", "myFact(b.F)"}, |
73 | {"F(nil)()", "myFact(a.T)"}, // (result type of b.F) |
74 | }}, |
75 | {"c", []lookup{ |
76 | {"b.B", "myFact(b.B)"}, |
77 | {"b.F", "myFact(b.F)"}, |
78 | //{"b.F(nil)()", "myFact(a.T)"}, // no fact; TODO(adonovan): investigate |
79 | {"C", "myFact(c.C)"}, |
80 | {"C{}[0]", "myFact(b.B)"}, |
81 | {"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2) |
82 | }}, |
83 | }, |
84 | }, |
85 | { |
86 | name: "globals", |
87 | files: map[string]string{ |
88 | "a/a.go": `package a; |
89 | type T1 int |
90 | type T2 int |
91 | type T3 int |
92 | type T4 int |
93 | type T5 int |
94 | type K int; type V string |
95 | `, |
96 | "b/b.go": `package b |
97 | import "a" |
98 | var ( |
99 | G1 []a.T1 |
100 | G2 [7]a.T2 |
101 | G3 chan a.T3 |
102 | G4 *a.T4 |
103 | G5 struct{ F a.T5 } |
104 | G6 map[a.K]a.V |
105 | ) |
106 | `, |
107 | "c/c.go": `package c; import "b"; |
108 | var ( |
109 | v1 = b.G1 |
110 | v2 = b.G2 |
111 | v3 = b.G3 |
112 | v4 = b.G4 |
113 | v5 = b.G5 |
114 | v6 = b.G6 |
115 | ) |
116 | `, |
117 | }, |
118 | plookups: []pkgLookups{ |
119 | {"a", []lookup{}}, |
120 | {"b", []lookup{}}, |
121 | {"c", []lookup{ |
122 | {"v1[0]", "myFact(a.T1)"}, |
123 | {"v2[0]", "myFact(a.T2)"}, |
124 | {"<-v3", "myFact(a.T3)"}, |
125 | {"*v4", "myFact(a.T4)"}, |
126 | {"v5.F", "myFact(a.T5)"}, |
127 | {"v6[0]", "myFact(a.V)"}, |
128 | }}, |
129 | }, |
130 | }, |
131 | { |
132 | name: "typeparams", |
133 | typeparams: true, |
134 | files: map[string]string{ |
135 | "a/a.go": `package a |
136 | type T1 int |
137 | type T2 int |
138 | type T3 interface{Foo()} |
139 | type T4 int |
140 | type T5 int |
141 | type T6 interface{Foo()} |
142 | `, |
143 | "b/b.go": `package b |
144 | import "a" |
145 | type N1[T a.T1|int8] func() T |
146 | type N2[T any] struct{ F T } |
147 | type N3[T a.T3] func() T |
148 | type N4[T a.T4|int8] func() T |
149 | type N5[T interface{Bar() a.T5} ] func() T |
150 | |
151 | type t5 struct{}; func (t5) Bar() a.T5 { return 0 } |
152 | |
153 | var G1 N1[a.T1] |
154 | var G2 func() N2[a.T2] |
155 | var G3 N3[a.T3] |
156 | var G4 N4[a.T4] |
157 | var G5 N5[t5] |
158 | |
159 | func F6[T a.T6]() T { var x T; return x } |
160 | `, |
161 | "c/c.go": `package c; import "b"; |
162 | var ( |
163 | v1 = b.G1 |
164 | v2 = b.G2 |
165 | v3 = b.G3 |
166 | v4 = b.G4 |
167 | v5 = b.G5 |
168 | v6 = b.F6[t6] |
169 | ) |
170 | |
171 | type t6 struct{}; func (t6) Foo() {} |
172 | `, |
173 | }, |
174 | plookups: []pkgLookups{ |
175 | {"a", []lookup{}}, |
176 | {"b", []lookup{}}, |
177 | {"c", []lookup{ |
178 | {"v1", "myFact(b.N1)"}, |
179 | {"v1()", "myFact(a.T1)"}, |
180 | {"v2()", "myFact(b.N2)"}, |
181 | {"v2().F", "myFact(a.T2)"}, |
182 | {"v3", "myFact(b.N3)"}, |
183 | {"v4", "myFact(b.N4)"}, |
184 | {"v4()", "myFact(a.T4)"}, |
185 | {"v5", "myFact(b.N5)"}, |
186 | {"v5()", "myFact(b.t5)"}, |
187 | {"v6()", "myFact(c.t6)"}, |
188 | }}, |
189 | }, |
190 | }, |
191 | } |
192 | |
193 | for i := range tests { |
194 | test := tests[i] |
195 | t.Run(test.name, func(t *testing.T) { |
196 | t.Parallel() |
197 | if test.typeparams && !typeparams.Enabled { |
198 | t.Skip("type parameters are not enabled") |
199 | } |
200 | testEncodeDecode(t, test.files, test.plookups) |
201 | }) |
202 | } |
203 | } |
204 | |
205 | type lookup struct { |
206 | objexpr string |
207 | want string |
208 | } |
209 | |
210 | type pkgLookups struct { |
211 | path string |
212 | lookups []lookup |
213 | } |
214 | |
215 | // testEncodeDecode tests fact encoding and decoding and simulates how package facts |
216 | // are passed during analysis. It operates on a group of Go file contents. Then |
217 | // for each <package, []lookup> in tests it does the following: |
218 | // 1. loads and type checks the package, |
219 | // 2. calls (*facts.Decoder).Decode to load the facts exported by its imports, |
220 | // 3. exports a myFact Fact for all of package level objects, |
221 | // 4. For each lookup for the current package: |
222 | // 4.a) lookup the types.Object for an Go source expression in the curent package |
223 | // (or confirms one is not expected want=="no object"), |
224 | // 4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"), |
225 | // 4.c) compares the content of the Fact to want. |
226 | // 5. encodes the Facts of the package. |
227 | // |
228 | // Note: tests are not independent test cases; order matters (as does a package being |
229 | // skipped). It changes what Facts can be imported. |
230 | // |
231 | // Failures are reported on t. |
232 | func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) { |
233 | dir, cleanup, err := analysistest.WriteFiles(files) |
234 | if err != nil { |
235 | t.Fatal(err) |
236 | } |
237 | defer cleanup() |
238 | |
239 | // factmap represents the passing of encoded facts from one |
240 | // package to another. In practice one would use the file system. |
241 | factmap := make(map[string][]byte) |
242 | read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil } |
243 | |
244 | // Analyze packages in order, look up various objects accessible within |
245 | // each package, and see if they have a fact. The "analysis" exports a |
246 | // fact for every object at package level. |
247 | // |
248 | // Note: Loop iterations are not independent test cases; |
249 | // order matters, as we populate factmap. |
250 | for _, test := range tests { |
251 | // load package |
252 | pkg, err := load(t, dir, test.path) |
253 | if err != nil { |
254 | t.Fatal(err) |
255 | } |
256 | |
257 | // decode |
258 | facts, err := facts.NewDecoder(pkg).Decode(read) |
259 | if err != nil { |
260 | t.Fatalf("Decode failed: %v", err) |
261 | } |
262 | t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts |
263 | |
264 | // export |
265 | // (one fact for each package-level object) |
266 | for _, name := range pkg.Scope().Names() { |
267 | obj := pkg.Scope().Lookup(name) |
268 | fact := &myFact{obj.Pkg().Name() + "." + obj.Name()} |
269 | facts.ExportObjectFact(obj, fact) |
270 | } |
271 | t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts |
272 | |
273 | // import |
274 | // (after export, because an analyzer may import its own facts) |
275 | for _, lookup := range test.lookups { |
276 | fact := new(myFact) |
277 | var got string |
278 | if obj := find(pkg, lookup.objexpr); obj == nil { |
279 | got = "no object" |
280 | } else if facts.ImportObjectFact(obj, fact) { |
281 | got = fact.String() |
282 | } else { |
283 | got = "no fact" |
284 | } |
285 | if got != lookup.want { |
286 | t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s", |
287 | pkg.Path(), lookup.objexpr, fact, got, lookup.want) |
288 | } |
289 | } |
290 | |
291 | // encode |
292 | factmap[pkg.Path()] = facts.Encode() |
293 | } |
294 | } |
295 | |
296 | func find(p *types.Package, expr string) types.Object { |
297 | // types.Eval only allows us to compute a TypeName object for an expression. |
298 | // TODO(adonovan): support other expressions that denote an object: |
299 | // - an identifier (or qualified ident) for a func, const, or var |
300 | // - new(T).f for a field or method |
301 | // I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677. |
302 | // If that becomes available, use it. |
303 | |
304 | // Choose an arbitrary position within the (single-file) package |
305 | // so that we are within the scope of its import declarations. |
306 | somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos() |
307 | tv, err := types.Eval(token.NewFileSet(), p, somepos, expr) |
308 | if err != nil { |
309 | return nil |
310 | } |
311 | if n, ok := tv.Type.(*types.Named); ok { |
312 | return n.Obj() |
313 | } |
314 | return nil |
315 | } |
316 | |
317 | func load(t *testing.T, dir string, path string) (*types.Package, error) { |
318 | cfg := &packages.Config{ |
319 | Mode: packages.LoadSyntax, |
320 | Dir: dir, |
321 | Env: append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"), |
322 | } |
323 | testenv.NeedsGoPackagesEnv(t, cfg.Env) |
324 | pkgs, err := packages.Load(cfg, path) |
325 | if err != nil { |
326 | return nil, err |
327 | } |
328 | if packages.PrintErrors(pkgs) > 0 { |
329 | return nil, fmt.Errorf("packages had errors") |
330 | } |
331 | if len(pkgs) == 0 { |
332 | return nil, fmt.Errorf("no package matched %s", path) |
333 | } |
334 | return pkgs[0].Types, nil |
335 | } |
336 | |
337 | type otherFact struct { |
338 | S string |
339 | } |
340 | |
341 | func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) } |
342 | func (f *otherFact) AFact() {} |
343 | |
344 | func TestFactFilter(t *testing.T) { |
345 | files := map[string]string{ |
346 | "a/a.go": `package a; type A int`, |
347 | } |
348 | dir, cleanup, err := analysistest.WriteFiles(files) |
349 | if err != nil { |
350 | t.Fatal(err) |
351 | } |
352 | defer cleanup() |
353 | |
354 | pkg, err := load(t, dir, "a") |
355 | if err != nil { |
356 | t.Fatal(err) |
357 | } |
358 | |
359 | obj := pkg.Scope().Lookup("A") |
360 | s, err := facts.NewDecoder(pkg).Decode(func(*types.Package) ([]byte, error) { return nil, nil }) |
361 | if err != nil { |
362 | t.Fatal(err) |
363 | } |
364 | s.ExportObjectFact(obj, &myFact{"good object fact"}) |
365 | s.ExportPackageFact(&myFact{"good package fact"}) |
366 | s.ExportObjectFact(obj, &otherFact{"bad object fact"}) |
367 | s.ExportPackageFact(&otherFact{"bad package fact"}) |
368 | |
369 | filter := map[reflect.Type]bool{ |
370 | reflect.TypeOf(&myFact{}): true, |
371 | } |
372 | |
373 | pkgFacts := s.AllPackageFacts(filter) |
374 | wantPkgFacts := `[{package a ("a") myFact(good package fact)}]` |
375 | if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts { |
376 | t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts) |
377 | } |
378 | |
379 | objFacts := s.AllObjectFacts(filter) |
380 | wantObjFacts := "[{type a.A int myFact(good object fact)}]" |
381 | if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts { |
382 | t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts) |
383 | } |
384 | } |
385 |
Members