mirror of
https://github.com/golang/go.git
synced 2025-05-05 15:43:04 +00:00
Fixes golang/go#33950 Change-Id: Iefcb757e773bc052793611c099c25a457fd7e243 Reviewed-on: https://go-review.googlesource.com/c/tools/+/192400 Run-TryBot: Bryan C. Mills <bcmills@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Cottrell <iancottrell@google.com>
227 lines
6.2 KiB
Go
227 lines
6.2 KiB
Go
// 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)
|
|
}
|
|
}
|