aboutsummaryrefslogtreecommitdiff
path: root/internal/facts/facts_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/facts/facts_test.go')
-rw-r--r--internal/facts/facts_test.go564
1 files changed, 564 insertions, 0 deletions
diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go
new file mode 100644
index 000000000..ad8751539
--- /dev/null
+++ b/internal/facts/facts_test.go
@@ -0,0 +1,564 @@
+// 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/ast"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "os"
+ "reflect"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/facts"
+ "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)"},
+ {"C", "myFact(c.C)"},
+ {"C{}[0]", "myFact(b.B)"},
+ {"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2)
+ }},
+ },
+ },
+ {
+ name: "underlying",
+ // c->b->a
+ // c does not import a directly or use any of its types, but it does use
+ // the types within a indirectly. c.q has the type a.a so package a should
+ // be included by importMap.
+ files: map[string]string{
+ "a/a.go": `package a; type a int; type T *a`,
+ "b/b.go": `package b; import "a"; type B a.T`,
+ "c/c.go": `package c; import "b"; type C b.B; var q = *C(nil)`,
+ },
+ plookups: []pkgLookups{
+ {"a", []lookup{
+ {"a", "myFact(a.a)"},
+ {"T", "myFact(a.T)"},
+ }},
+ {"b", []lookup{
+ {"B", "myFact(b.B)"},
+ {"B(nil)", "myFact(b.B)"},
+ {"*(B(nil))", "myFact(a.a)"},
+ }},
+ {"c", []lookup{
+ {"C", "myFact(c.C)"},
+ {"C(nil)", "myFact(c.C)"},
+ {"*C(nil)", "myFact(a.a)"},
+ {"q", "myFact(a.a)"},
+ }},
+ },
+ },
+ {
+ name: "methods",
+ // c->b->a
+ // c does not import a directly or use any of its types, but it does use
+ // the types within a indirectly via a method.
+ files: map[string]string{
+ "a/a.go": `package a; type T int`,
+ "b/b.go": `package b; import "a"; type B struct{}; func (_ B) M() a.T { return 0 }`,
+ "c/c.go": `package c; import "b"; var C b.B`,
+ },
+ plookups: []pkgLookups{
+ {"a", []lookup{
+ {"T", "myFact(a.T)"},
+ }},
+ {"b", []lookup{
+ {"B{}", "myFact(b.B)"},
+ {"B{}.M()", "myFact(a.T)"},
+ }},
+ {"c", []lookup{
+ {"C", "myFact(b.B)"},
+ {"C.M()", "myFact(a.T)"},
+ }},
+ },
+ },
+ {
+ 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 { return 0 }
+
+ 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.Decoder).Decode to load 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(imp *types.Package) ([]byte, error) { return factmap[imp.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.NewDecoder(pkg).Decode(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.NewDecoder(pkg).Decode(func(*types.Package) ([]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)
+ }
+}
+
+// TestMalformed checks that facts can be encoded and decoded *despite*
+// types.Config.Check returning an error. Importing facts is expected to
+// happen when Analyzers have RunDespiteErrors set to true. So this
+// needs to robust, e.g. no infinite loops.
+func TestMalformed(t *testing.T) {
+ if !typeparams.Enabled {
+ t.Skip("type parameters are not enabled")
+ }
+ var findPkg func(*types.Package, string) *types.Package
+ findPkg = func(p *types.Package, name string) *types.Package {
+ if p.Name() == name {
+ return p
+ }
+ for _, o := range p.Imports() {
+ if f := findPkg(o, name); f != nil {
+ return f
+ }
+ }
+ return nil
+ }
+
+ type pkgTest struct {
+ content string
+ err string // if non-empty, expected substring of err.Error() from conf.Check().
+ wants map[string]string // package path to expected name
+ }
+ tests := []struct {
+ name string
+ pkgs []pkgTest
+ }{
+ {
+ name: "initialization-cycle",
+ pkgs: []pkgTest{
+ {
+ content: `package a; type N[T any] struct { F *N[N[T]] }`,
+ err: "instantiation cycle:",
+ wants: map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"},
+ },
+ {
+ content: `package b; import "a"; type B a.N[int]`,
+ wants: map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"},
+ },
+ {
+ content: `package c; import "b"; var C b.B`,
+ wants: map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "myFact(c.[C])"},
+ },
+ },
+ },
+ }
+
+ for i := range tests {
+ test := tests[i]
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ // setup for test wide variables.
+ packages := make(map[string]*types.Package)
+ conf := types.Config{
+ Importer: closure(packages),
+ Error: func(err error) {}, // do not stop on first type checking error
+ }
+ fset := token.NewFileSet()
+ factmap := make(map[string][]byte)
+ read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil }
+
+ // Processes the pkgs in order. For package, export a package fact,
+ // and use this fact to verify which package facts are reachable via Decode.
+ // We allow for packages to have type checking errors.
+ for i, pkgTest := range test.pkgs {
+ // parse
+ f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // typecheck
+ pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil)
+ var got string
+ if err != nil {
+ got = err.Error()
+ }
+ if !strings.Contains(got, pkgTest.err) {
+ t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err)
+ }
+ packages[pkg.Path()] = pkg
+
+ // decode facts
+ facts, err := facts.NewDecoder(pkg).Decode(read)
+ if err != nil {
+ t.Fatalf("Decode failed: %v", err)
+ }
+
+ // export facts
+ fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())}
+ facts.ExportPackageFact(fact)
+
+ // import facts
+ for other, want := range pkgTest.wants {
+ fact := new(myFact)
+ var got string
+ if found := findPkg(pkg, other); found == nil {
+ got = "no package"
+ } else if facts.ImportPackageFact(found, fact) {
+ got = fact.String()
+ } else {
+ got = "no fact"
+ }
+ if got != want {
+ t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s",
+ pkg.Path(), other, fact, got, want)
+ }
+ }
+
+ // encode facts
+ factmap[pkg.Path()] = facts.Encode()
+ }
+ })
+ }
+}
+
+type closure map[string]*types.Package
+
+func (c closure) Import(path string) (*types.Package, error) { return c[path], nil }