// 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" ) type myFact struct { S string } func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) } func (f *myFact) AFact() {} func TestEncodeDecode(t *testing.T) { gob.Register(new(myFact)) // 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`, } 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 } // 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. type lookups []struct { objexpr string want string } for _, test := range []struct { path string lookups lookups }{ {"a", lookups{ {"A", "myFact(a.A)"}, }}, {"b", lookups{ {"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", lookups{ {"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) }}, } { // 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) } if true { t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts } // export // (one fact for each package-level object) scope := pkg.Scope() for _, name := range scope.Names() { obj := scope.Lookup(name) fact := &myFact{obj.Pkg().Name() + "." + obj.Name()} facts.ExportObjectFact(obj, fact) } // 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) } }