github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/go/analysis/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/token" 11 "go/types" 12 "os" 13 "reflect" 14 "testing" 15 16 "github.com/powerman/golang-tools/go/analysis/analysistest" 17 "github.com/powerman/golang-tools/go/analysis/internal/facts" 18 "github.com/powerman/golang-tools/go/packages" 19 "github.com/powerman/golang-tools/internal/testenv" 20 "github.com/powerman/golang-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 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.Decode to loads 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(path string) ([]byte, error) { return factmap[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.Decode(pkg, 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.Decode(pkg, func(string) ([]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 }