blob: daebea2ff59d9848b9c527a695660c1d5370b6ac [file] [log] [blame]
// 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/aliases"
"golang.org/x/tools/internal/facts"
"golang.org/x/tools/internal/testenv"
)
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()
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(pkgPath string) ([]byte, error) { return factmap[pkgPath], 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 := aliases.Unalias(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(pkgPath 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)
}
}
// 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) {
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{
// Notation: myFact(a.[N]) means: package a has members {N}.
{
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": "no fact", "b": "myFact(b.[B])", "c": "myFact(c.[C])"},
// package fact myFact(a.[N]) not reexported
},
},
},
}
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(pkgPath string) ([]byte, error) { return factmap[pkgPath], 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 }