golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/internal/facts/facts_test.go (about) 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/ast" 11 "go/parser" 12 "go/token" 13 "go/types" 14 "os" 15 "reflect" 16 "strings" 17 "testing" 18 19 "golang.org/x/tools/go/analysis/analysistest" 20 "golang.org/x/tools/go/packages" 21 "golang.org/x/tools/internal/aliases" 22 "golang.org/x/tools/internal/facts" 23 "golang.org/x/tools/internal/testenv" 24 ) 25 26 type myFact struct { 27 S string 28 } 29 30 func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) } 31 func (f *myFact) AFact() {} 32 33 func init() { 34 gob.Register(new(myFact)) 35 } 36 37 func TestEncodeDecode(t *testing.T) { 38 tests := []struct { 39 name string 40 typeparams bool // requires typeparams to be enabled 41 files map[string]string 42 plookups []pkgLookups // see testEncodeDecode for details 43 }{ 44 { 45 name: "loading-order", 46 // c -> b -> a, a2 47 // c does not directly depend on a, but it indirectly uses a.T. 48 // 49 // Package a2 is never loaded directly so it is incomplete. 50 // 51 // We use only types in this example because we rely on 52 // types.Eval to resolve the lookup expressions, and it only 53 // works for types. This is a definite gap in the typechecker API. 54 files: map[string]string{ 55 "a/a.go": `package a; type A int; type T int`, 56 "a2/a.go": `package a2; type A2 int; type Unneeded int`, 57 "b/b.go": `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`, 58 "c/c.go": `package c; import "b"; type C []b.B`, 59 }, 60 // In the following table, we analyze packages (a, b, c) in order, 61 // look up various objects accessible within each package, 62 // and see if they have a fact. The "analysis" exports a fact 63 // for every object at package level. 64 // 65 // Note: Loop iterations are not independent test cases; 66 // order matters, as we populate factmap. 67 plookups: []pkgLookups{ 68 {"a", []lookup{ 69 {"A", "myFact(a.A)"}, 70 }}, 71 {"b", []lookup{ 72 {"a.A", "myFact(a.A)"}, 73 {"a.T", "myFact(a.T)"}, 74 {"B", "myFact(b.B)"}, 75 {"F", "myFact(b.F)"}, 76 {"F(nil)()", "myFact(a.T)"}, // (result type of b.F) 77 }}, 78 {"c", []lookup{ 79 {"b.B", "myFact(b.B)"}, 80 {"b.F", "myFact(b.F)"}, 81 {"b.F(nil)()", "myFact(a.T)"}, 82 {"C", "myFact(c.C)"}, 83 {"C{}[0]", "myFact(b.B)"}, 84 {"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2) 85 }}, 86 }, 87 }, 88 { 89 name: "underlying", 90 // c->b->a 91 // c does not import a directly or use any of its types, but it does use 92 // the types within a indirectly. c.q has the type a.a so package a should 93 // be included by importMap. 94 files: map[string]string{ 95 "a/a.go": `package a; type a int; type T *a`, 96 "b/b.go": `package b; import "a"; type B a.T`, 97 "c/c.go": `package c; import "b"; type C b.B; var q = *C(nil)`, 98 }, 99 plookups: []pkgLookups{ 100 {"a", []lookup{ 101 {"a", "myFact(a.a)"}, 102 {"T", "myFact(a.T)"}, 103 }}, 104 {"b", []lookup{ 105 {"B", "myFact(b.B)"}, 106 {"B(nil)", "myFact(b.B)"}, 107 {"*(B(nil))", "myFact(a.a)"}, 108 }}, 109 {"c", []lookup{ 110 {"C", "myFact(c.C)"}, 111 {"C(nil)", "myFact(c.C)"}, 112 {"*C(nil)", "myFact(a.a)"}, 113 {"q", "myFact(a.a)"}, 114 }}, 115 }, 116 }, 117 { 118 name: "methods", 119 // c->b->a 120 // c does not import a directly or use any of its types, but it does use 121 // the types within a indirectly via a method. 122 files: map[string]string{ 123 "a/a.go": `package a; type T int`, 124 "b/b.go": `package b; import "a"; type B struct{}; func (_ B) M() a.T { return 0 }`, 125 "c/c.go": `package c; import "b"; var C b.B`, 126 }, 127 plookups: []pkgLookups{ 128 {"a", []lookup{ 129 {"T", "myFact(a.T)"}, 130 }}, 131 {"b", []lookup{ 132 {"B{}", "myFact(b.B)"}, 133 {"B{}.M()", "myFact(a.T)"}, 134 }}, 135 {"c", []lookup{ 136 {"C", "myFact(b.B)"}, 137 {"C.M()", "myFact(a.T)"}, 138 }}, 139 }, 140 }, 141 { 142 name: "globals", 143 files: map[string]string{ 144 "a/a.go": `package a; 145 type T1 int 146 type T2 int 147 type T3 int 148 type T4 int 149 type T5 int 150 type K int; type V string 151 `, 152 "b/b.go": `package b 153 import "a" 154 var ( 155 G1 []a.T1 156 G2 [7]a.T2 157 G3 chan a.T3 158 G4 *a.T4 159 G5 struct{ F a.T5 } 160 G6 map[a.K]a.V 161 ) 162 `, 163 "c/c.go": `package c; import "b"; 164 var ( 165 v1 = b.G1 166 v2 = b.G2 167 v3 = b.G3 168 v4 = b.G4 169 v5 = b.G5 170 v6 = b.G6 171 ) 172 `, 173 }, 174 plookups: []pkgLookups{ 175 {"a", []lookup{}}, 176 {"b", []lookup{}}, 177 {"c", []lookup{ 178 {"v1[0]", "myFact(a.T1)"}, 179 {"v2[0]", "myFact(a.T2)"}, 180 {"<-v3", "myFact(a.T3)"}, 181 {"*v4", "myFact(a.T4)"}, 182 {"v5.F", "myFact(a.T5)"}, 183 {"v6[0]", "myFact(a.V)"}, 184 }}, 185 }, 186 }, 187 { 188 name: "typeparams", 189 typeparams: true, 190 files: map[string]string{ 191 "a/a.go": `package a 192 type T1 int 193 type T2 int 194 type T3 interface{Foo()} 195 type T4 int 196 type T5 int 197 type T6 interface{Foo()} 198 `, 199 "b/b.go": `package b 200 import "a" 201 type N1[T a.T1|int8] func() T 202 type N2[T any] struct{ F T } 203 type N3[T a.T3] func() T 204 type N4[T a.T4|int8] func() T 205 type N5[T interface{Bar() a.T5} ] func() T 206 207 type t5 struct{}; func (t5) Bar() a.T5 { return 0 } 208 209 var G1 N1[a.T1] 210 var G2 func() N2[a.T2] 211 var G3 N3[a.T3] 212 var G4 N4[a.T4] 213 var G5 N5[t5] 214 215 func F6[T a.T6]() T { var x T; return x } 216 `, 217 "c/c.go": `package c; import "b"; 218 var ( 219 v1 = b.G1 220 v2 = b.G2 221 v3 = b.G3 222 v4 = b.G4 223 v5 = b.G5 224 v6 = b.F6[t6] 225 ) 226 227 type t6 struct{}; func (t6) Foo() {} 228 `, 229 }, 230 plookups: []pkgLookups{ 231 {"a", []lookup{}}, 232 {"b", []lookup{}}, 233 {"c", []lookup{ 234 {"v1", "myFact(b.N1)"}, 235 {"v1()", "myFact(a.T1)"}, 236 {"v2()", "myFact(b.N2)"}, 237 {"v2().F", "myFact(a.T2)"}, 238 {"v3", "myFact(b.N3)"}, 239 {"v4", "myFact(b.N4)"}, 240 {"v4()", "myFact(a.T4)"}, 241 {"v5", "myFact(b.N5)"}, 242 {"v5()", "myFact(b.t5)"}, 243 {"v6()", "myFact(c.t6)"}, 244 }}, 245 }, 246 }, 247 } 248 249 for i := range tests { 250 test := tests[i] 251 t.Run(test.name, func(t *testing.T) { 252 t.Parallel() 253 testEncodeDecode(t, test.files, test.plookups) 254 }) 255 } 256 } 257 258 type lookup struct { 259 objexpr string 260 want string 261 } 262 263 type pkgLookups struct { 264 path string 265 lookups []lookup 266 } 267 268 // testEncodeDecode tests fact encoding and decoding and simulates how package facts 269 // are passed during analysis. It operates on a group of Go file contents. Then 270 // for each <package, []lookup> in tests it does the following: 271 // 1. loads and type checks the package, 272 // 2. calls (*facts.Decoder).Decode to load the facts exported by its imports, 273 // 3. exports a myFact Fact for all of package level objects, 274 // 4. For each lookup for the current package: 275 // 4.a) lookup the types.Object for a Go source expression in the current package 276 // (or confirms one is not expected want=="no object"), 277 // 4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"), 278 // 4.c) compares the content of the Fact to want. 279 // 5. encodes the Facts of the package. 280 // 281 // Note: tests are not independent test cases; order matters (as does a package being 282 // skipped). It changes what Facts can be imported. 283 // 284 // Failures are reported on t. 285 func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) { 286 dir, cleanup, err := analysistest.WriteFiles(files) 287 if err != nil { 288 t.Fatal(err) 289 } 290 defer cleanup() 291 292 // factmap represents the passing of encoded facts from one 293 // package to another. In practice one would use the file system. 294 factmap := make(map[string][]byte) 295 read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil } 296 297 // Analyze packages in order, look up various objects accessible within 298 // each package, and see if they have a fact. The "analysis" exports a 299 // fact for every object at package level. 300 // 301 // Note: Loop iterations are not independent test cases; 302 // order matters, as we populate factmap. 303 for _, test := range tests { 304 // load package 305 pkg, err := load(t, dir, test.path) 306 if err != nil { 307 t.Fatal(err) 308 } 309 310 // decode 311 facts, err := facts.NewDecoder(pkg).Decode(read) 312 if err != nil { 313 t.Fatalf("Decode failed: %v", err) 314 } 315 t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts 316 317 // export 318 // (one fact for each package-level object) 319 for _, name := range pkg.Scope().Names() { 320 obj := pkg.Scope().Lookup(name) 321 fact := &myFact{obj.Pkg().Name() + "." + obj.Name()} 322 facts.ExportObjectFact(obj, fact) 323 } 324 t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts 325 326 // import 327 // (after export, because an analyzer may import its own facts) 328 for _, lookup := range test.lookups { 329 fact := new(myFact) 330 var got string 331 if obj := find(pkg, lookup.objexpr); obj == nil { 332 got = "no object" 333 } else if facts.ImportObjectFact(obj, fact) { 334 got = fact.String() 335 } else { 336 got = "no fact" 337 } 338 if got != lookup.want { 339 t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s", 340 pkg.Path(), lookup.objexpr, fact, got, lookup.want) 341 } 342 } 343 344 // encode 345 factmap[pkg.Path()] = facts.Encode() 346 } 347 } 348 349 func find(p *types.Package, expr string) types.Object { 350 // types.Eval only allows us to compute a TypeName object for an expression. 351 // TODO(adonovan): support other expressions that denote an object: 352 // - an identifier (or qualified ident) for a func, const, or var 353 // - new(T).f for a field or method 354 // I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677. 355 // If that becomes available, use it. 356 357 // Choose an arbitrary position within the (single-file) package 358 // so that we are within the scope of its import declarations. 359 somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos() 360 tv, err := types.Eval(token.NewFileSet(), p, somepos, expr) 361 if err != nil { 362 return nil 363 } 364 if n, ok := aliases.Unalias(tv.Type).(*types.Named); ok { 365 return n.Obj() 366 } 367 return nil 368 } 369 370 func load(t *testing.T, dir string, path string) (*types.Package, error) { 371 cfg := &packages.Config{ 372 Mode: packages.LoadSyntax, 373 Dir: dir, 374 Env: append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"), 375 } 376 testenv.NeedsGoPackagesEnv(t, cfg.Env) 377 pkgs, err := packages.Load(cfg, path) 378 if err != nil { 379 return nil, err 380 } 381 if packages.PrintErrors(pkgs) > 0 { 382 return nil, fmt.Errorf("packages had errors") 383 } 384 if len(pkgs) == 0 { 385 return nil, fmt.Errorf("no package matched %s", path) 386 } 387 return pkgs[0].Types, nil 388 } 389 390 type otherFact struct { 391 S string 392 } 393 394 func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) } 395 func (f *otherFact) AFact() {} 396 397 func TestFactFilter(t *testing.T) { 398 files := map[string]string{ 399 "a/a.go": `package a; type A int`, 400 } 401 dir, cleanup, err := analysistest.WriteFiles(files) 402 if err != nil { 403 t.Fatal(err) 404 } 405 defer cleanup() 406 407 pkg, err := load(t, dir, "a") 408 if err != nil { 409 t.Fatal(err) 410 } 411 412 obj := pkg.Scope().Lookup("A") 413 s, err := facts.NewDecoder(pkg).Decode(func(pkgPath string) ([]byte, error) { return nil, nil }) 414 if err != nil { 415 t.Fatal(err) 416 } 417 s.ExportObjectFact(obj, &myFact{"good object fact"}) 418 s.ExportPackageFact(&myFact{"good package fact"}) 419 s.ExportObjectFact(obj, &otherFact{"bad object fact"}) 420 s.ExportPackageFact(&otherFact{"bad package fact"}) 421 422 filter := map[reflect.Type]bool{ 423 reflect.TypeOf(&myFact{}): true, 424 } 425 426 pkgFacts := s.AllPackageFacts(filter) 427 wantPkgFacts := `[{package a ("a") myFact(good package fact)}]` 428 if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts { 429 t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts) 430 } 431 432 objFacts := s.AllObjectFacts(filter) 433 wantObjFacts := "[{type a.A int myFact(good object fact)}]" 434 if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts { 435 t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts) 436 } 437 } 438 439 // TestMalformed checks that facts can be encoded and decoded *despite* 440 // types.Config.Check returning an error. Importing facts is expected to 441 // happen when Analyzers have RunDespiteErrors set to true. So this 442 // needs to robust, e.g. no infinite loops. 443 func TestMalformed(t *testing.T) { 444 var findPkg func(*types.Package, string) *types.Package 445 findPkg = func(p *types.Package, name string) *types.Package { 446 if p.Name() == name { 447 return p 448 } 449 for _, o := range p.Imports() { 450 if f := findPkg(o, name); f != nil { 451 return f 452 } 453 } 454 return nil 455 } 456 457 type pkgTest struct { 458 content string 459 err string // if non-empty, expected substring of err.Error() from conf.Check(). 460 wants map[string]string // package path to expected name 461 } 462 tests := []struct { 463 name string 464 pkgs []pkgTest 465 }{ 466 { 467 name: "initialization-cycle", 468 pkgs: []pkgTest{ 469 // Notation: myFact(a.[N]) means: package a has members {N}. 470 { 471 content: `package a; type N[T any] struct { F *N[N[T]] }`, 472 err: "instantiation cycle:", 473 wants: map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"}, 474 }, 475 { 476 content: `package b; import "a"; type B a.N[int]`, 477 wants: map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"}, 478 }, 479 { 480 content: `package c; import "b"; var C b.B`, 481 wants: map[string]string{"a": "no fact", "b": "myFact(b.[B])", "c": "myFact(c.[C])"}, 482 // package fact myFact(a.[N]) not reexported 483 }, 484 }, 485 }, 486 } 487 488 for i := range tests { 489 test := tests[i] 490 t.Run(test.name, func(t *testing.T) { 491 t.Parallel() 492 493 // setup for test wide variables. 494 packages := make(map[string]*types.Package) 495 conf := types.Config{ 496 Importer: closure(packages), 497 Error: func(err error) {}, // do not stop on first type checking error 498 } 499 fset := token.NewFileSet() 500 factmap := make(map[string][]byte) 501 read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil } 502 503 // Processes the pkgs in order. For package, export a package fact, 504 // and use this fact to verify which package facts are reachable via Decode. 505 // We allow for packages to have type checking errors. 506 for i, pkgTest := range test.pkgs { 507 // parse 508 f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0) 509 if err != nil { 510 t.Fatal(err) 511 } 512 513 // typecheck 514 pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil) 515 var got string 516 if err != nil { 517 got = err.Error() 518 } 519 if !strings.Contains(got, pkgTest.err) { 520 t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err) 521 } 522 packages[pkg.Path()] = pkg 523 524 // decode facts 525 facts, err := facts.NewDecoder(pkg).Decode(read) 526 if err != nil { 527 t.Fatalf("Decode failed: %v", err) 528 } 529 530 // export facts 531 fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())} 532 facts.ExportPackageFact(fact) 533 534 // import facts 535 for other, want := range pkgTest.wants { 536 fact := new(myFact) 537 var got string 538 if found := findPkg(pkg, other); found == nil { 539 got = "no package" 540 } else if facts.ImportPackageFact(found, fact) { 541 got = fact.String() 542 } else { 543 got = "no fact" 544 } 545 if got != want { 546 t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s", 547 pkg.Path(), other, fact, got, want) 548 } 549 } 550 551 // encode facts 552 factmap[pkg.Path()] = facts.Encode() 553 } 554 }) 555 } 556 } 557 558 type closure map[string]*types.Package 559 560 func (c closure) Import(path string) (*types.Package, error) { return c[path], nil }