| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package facts_test |
| |
| import ( |
| "encoding/gob" |
| "fmt" |
| "go/token" |
| "go/types" |
| "os" |
| "reflect" |
| "testing" |
| |
| "golang.org/x/tools/go/analysis/analysistest" |
| "golang.org/x/tools/go/analysis/internal/facts" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/internal/testenv" |
| "golang.org/x/tools/internal/typeparams" |
| ) |
| |
| type myFact struct { |
| S string |
| } |
| |
| func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) } |
| func (f *myFact) AFact() {} |
| |
| func init() { |
| gob.Register(new(myFact)) |
| } |
| |
| func TestEncodeDecode(t *testing.T) { |
| tests := []struct { |
| name string |
| typeparams bool // requires typeparams to be enabled |
| files map[string]string |
| plookups []pkgLookups // see testEncodeDecode for details |
| }{ |
| { |
| name: "loading-order", |
| // c -> b -> a, a2 |
| // c does not directly depend on a, but it indirectly uses a.T. |
| // |
| // Package a2 is never loaded directly so it is incomplete. |
| // |
| // We use only types in this example because we rely on |
| // types.Eval to resolve the lookup expressions, and it only |
| // works for types. This is a definite gap in the typechecker API. |
| files: map[string]string{ |
| "a/a.go": `package a; type A int; type T int`, |
| "a2/a.go": `package a2; type A2 int; type Unneeded int`, |
| "b/b.go": `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`, |
| "c/c.go": `package c; import "b"; type C []b.B`, |
| }, |
| // In the following table, we analyze packages (a, b, c) in order, |
| // look up various objects accessible within each package, |
| // and see if they have a fact. The "analysis" exports a fact |
| // for every object at package level. |
| // |
| // Note: Loop iterations are not independent test cases; |
| // order matters, as we populate factmap. |
| plookups: []pkgLookups{ |
| {"a", []lookup{ |
| {"A", "myFact(a.A)"}, |
| }}, |
| {"b", []lookup{ |
| {"a.A", "myFact(a.A)"}, |
| {"a.T", "myFact(a.T)"}, |
| {"B", "myFact(b.B)"}, |
| {"F", "myFact(b.F)"}, |
| {"F(nil)()", "myFact(a.T)"}, // (result type of b.F) |
| }}, |
| {"c", []lookup{ |
| {"b.B", "myFact(b.B)"}, |
| {"b.F", "myFact(b.F)"}, |
| //{"b.F(nil)()", "myFact(a.T)"}, // no fact; TODO(adonovan): investigate |
| {"C", "myFact(c.C)"}, |
| {"C{}[0]", "myFact(b.B)"}, |
| {"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2) |
| }}, |
| }, |
| }, |
| { |
| name: "globals", |
| files: map[string]string{ |
| "a/a.go": `package a; |
| type T1 int |
| type T2 int |
| type T3 int |
| type T4 int |
| type T5 int |
| type K int; type V string |
| `, |
| "b/b.go": `package b |
| import "a" |
| var ( |
| G1 []a.T1 |
| G2 [7]a.T2 |
| G3 chan a.T3 |
| G4 *a.T4 |
| G5 struct{ F a.T5 } |
| G6 map[a.K]a.V |
| ) |
| `, |
| "c/c.go": `package c; import "b"; |
| var ( |
| v1 = b.G1 |
| v2 = b.G2 |
| v3 = b.G3 |
| v4 = b.G4 |
| v5 = b.G5 |
| v6 = b.G6 |
| ) |
| `, |
| }, |
| plookups: []pkgLookups{ |
| {"a", []lookup{}}, |
| {"b", []lookup{}}, |
| {"c", []lookup{ |
| {"v1[0]", "myFact(a.T1)"}, |
| {"v2[0]", "myFact(a.T2)"}, |
| {"<-v3", "myFact(a.T3)"}, |
| {"*v4", "myFact(a.T4)"}, |
| {"v5.F", "myFact(a.T5)"}, |
| {"v6[0]", "myFact(a.V)"}, |
| }}, |
| }, |
| }, |
| { |
| name: "typeparams", |
| typeparams: true, |
| files: map[string]string{ |
| "a/a.go": `package a |
| type T1 int |
| type T2 int |
| type T3 interface{Foo()} |
| type T4 int |
| type T5 int |
| type T6 interface{Foo()} |
| `, |
| "b/b.go": `package b |
| import "a" |
| type N1[T a.T1|int8] func() T |
| type N2[T any] struct{ F T } |
| type N3[T a.T3] func() T |
| type N4[T a.T4|int8] func() T |
| type N5[T interface{Bar() a.T5} ] func() T |
| |
| type t5 struct{}; func (t5) Bar() a.T5 |
| |
| var G1 N1[a.T1] |
| var G2 func() N2[a.T2] |
| var G3 N3[a.T3] |
| var G4 N4[a.T4] |
| var G5 N5[t5] |
| |
| func F6[T a.T6]() T { var x T; return x } |
| `, |
| "c/c.go": `package c; import "b"; |
| var ( |
| v1 = b.G1 |
| v2 = b.G2 |
| v3 = b.G3 |
| v4 = b.G4 |
| v5 = b.G5 |
| v6 = b.F6[t6] |
| ) |
| |
| type t6 struct{}; func (t6) Foo() {} |
| `, |
| }, |
| plookups: []pkgLookups{ |
| {"a", []lookup{}}, |
| {"b", []lookup{}}, |
| {"c", []lookup{ |
| {"v1", "myFact(b.N1)"}, |
| {"v1()", "myFact(a.T1)"}, |
| {"v2()", "myFact(b.N2)"}, |
| {"v2().F", "myFact(a.T2)"}, |
| {"v3", "myFact(b.N3)"}, |
| {"v4", "myFact(b.N4)"}, |
| {"v4()", "myFact(a.T4)"}, |
| {"v5", "myFact(b.N5)"}, |
| {"v5()", "myFact(b.t5)"}, |
| {"v6()", "myFact(c.t6)"}, |
| }}, |
| }, |
| }, |
| } |
| |
| for i := range tests { |
| test := tests[i] |
| t.Run(test.name, func(t *testing.T) { |
| t.Parallel() |
| if test.typeparams && !typeparams.Enabled { |
| t.Skip("type parameters are not enabled") |
| } |
| testEncodeDecode(t, test.files, test.plookups) |
| }) |
| } |
| } |
| |
| type lookup struct { |
| objexpr string |
| want string |
| } |
| |
| type pkgLookups struct { |
| path string |
| lookups []lookup |
| } |
| |
| // testEncodeDecode tests fact encoding and decoding and simulates how package facts |
| // are passed during analysis. It operates on a group of Go file contents. Then |
| // for each <package, []lookup> in tests it does the following: |
| // 1. loads and type checks the package, |
| // 2. calls facts.Decode to loads the facts exported by its imports, |
| // 3. exports a myFact Fact for all of package level objects, |
| // 4. For each lookup for the current package: |
| // 4.a) lookup the types.Object for an Go source expression in the curent package |
| // (or confirms one is not expected want=="no object"), |
| // 4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"), |
| // 4.c) compares the content of the Fact to want. |
| // 5. encodes the Facts of the package. |
| // |
| // Note: tests are not independent test cases; order matters (as does a package being |
| // skipped). It changes what Facts can be imported. |
| // |
| // Failures are reported on t. |
| func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) { |
| dir, cleanup, err := analysistest.WriteFiles(files) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer cleanup() |
| |
| // factmap represents the passing of encoded facts from one |
| // package to another. In practice one would use the file system. |
| factmap := make(map[string][]byte) |
| read := func(path string) ([]byte, error) { return factmap[path], nil } |
| |
| // Analyze packages in order, look up various objects accessible within |
| // each package, and see if they have a fact. The "analysis" exports a |
| // fact for every object at package level. |
| // |
| // Note: Loop iterations are not independent test cases; |
| // order matters, as we populate factmap. |
| for _, test := range tests { |
| // load package |
| pkg, err := load(t, dir, test.path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // decode |
| facts, err := facts.Decode(pkg, read) |
| if err != nil { |
| t.Fatalf("Decode failed: %v", err) |
| } |
| t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts |
| |
| // export |
| // (one fact for each package-level object) |
| for _, name := range pkg.Scope().Names() { |
| obj := pkg.Scope().Lookup(name) |
| fact := &myFact{obj.Pkg().Name() + "." + obj.Name()} |
| facts.ExportObjectFact(obj, fact) |
| } |
| t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts |
| |
| // import |
| // (after export, because an analyzer may import its own facts) |
| for _, lookup := range test.lookups { |
| fact := new(myFact) |
| var got string |
| if obj := find(pkg, lookup.objexpr); obj == nil { |
| got = "no object" |
| } else if facts.ImportObjectFact(obj, fact) { |
| got = fact.String() |
| } else { |
| got = "no fact" |
| } |
| if got != lookup.want { |
| t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s", |
| pkg.Path(), lookup.objexpr, fact, got, lookup.want) |
| } |
| } |
| |
| // encode |
| factmap[pkg.Path()] = facts.Encode() |
| } |
| } |
| |
| func find(p *types.Package, expr string) types.Object { |
| // types.Eval only allows us to compute a TypeName object for an expression. |
| // TODO(adonovan): support other expressions that denote an object: |
| // - an identifier (or qualified ident) for a func, const, or var |
| // - new(T).f for a field or method |
| // I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677. |
| // If that becomes available, use it. |
| |
| // Choose an arbitrary position within the (single-file) package |
| // so that we are within the scope of its import declarations. |
| somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos() |
| tv, err := types.Eval(token.NewFileSet(), p, somepos, expr) |
| if err != nil { |
| return nil |
| } |
| if n, ok := tv.Type.(*types.Named); ok { |
| return n.Obj() |
| } |
| return nil |
| } |
| |
| func load(t *testing.T, dir string, path string) (*types.Package, error) { |
| cfg := &packages.Config{ |
| Mode: packages.LoadSyntax, |
| Dir: dir, |
| Env: append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"), |
| } |
| testenv.NeedsGoPackagesEnv(t, cfg.Env) |
| pkgs, err := packages.Load(cfg, path) |
| if err != nil { |
| return nil, err |
| } |
| if packages.PrintErrors(pkgs) > 0 { |
| return nil, fmt.Errorf("packages had errors") |
| } |
| if len(pkgs) == 0 { |
| return nil, fmt.Errorf("no package matched %s", path) |
| } |
| return pkgs[0].Types, nil |
| } |
| |
| type otherFact struct { |
| S string |
| } |
| |
| func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) } |
| func (f *otherFact) AFact() {} |
| |
| func TestFactFilter(t *testing.T) { |
| files := map[string]string{ |
| "a/a.go": `package a; type A int`, |
| } |
| dir, cleanup, err := analysistest.WriteFiles(files) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer cleanup() |
| |
| pkg, err := load(t, dir, "a") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| obj := pkg.Scope().Lookup("A") |
| s, err := facts.Decode(pkg, func(string) ([]byte, error) { return nil, nil }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| s.ExportObjectFact(obj, &myFact{"good object fact"}) |
| s.ExportPackageFact(&myFact{"good package fact"}) |
| s.ExportObjectFact(obj, &otherFact{"bad object fact"}) |
| s.ExportPackageFact(&otherFact{"bad package fact"}) |
| |
| filter := map[reflect.Type]bool{ |
| reflect.TypeOf(&myFact{}): true, |
| } |
| |
| pkgFacts := s.AllPackageFacts(filter) |
| wantPkgFacts := `[{package a ("a") myFact(good package fact)}]` |
| if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts { |
| t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts) |
| } |
| |
| objFacts := s.AllObjectFacts(filter) |
| wantObjFacts := "[{type a.A int myFact(good object fact)}]" |
| if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts { |
| t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts) |
| } |
| } |