1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
// 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"
"testing"
"golang.org/x/tools/go/analysis/analysistest"
"golang.org/x/tools/go/analysis/internal/facts"
"golang.org/x/tools/go/packages"
)
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(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(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"),
}
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
}
|