whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
// 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 defines a serializable set of analysis.Fact.
|
||||
//
|
||||
// It provides a partial implementation of the Fact-related parts of the
|
||||
// analysis.Pass interface for use in analysis drivers such as "go vet"
|
||||
// and other build systems.
|
||||
//
|
||||
// The serial format is unspecified and may change, so the same version
|
||||
// of this package must be used for reading and writing serialized facts.
|
||||
//
|
||||
// The handling of facts in the analysis system parallels the handling
|
||||
// of type information in the compiler: during compilation of package P,
|
||||
// the compiler emits an export data file that describes the type of
|
||||
// every object (named thing) defined in package P, plus every object
|
||||
// indirectly reachable from one of those objects. Thus the downstream
|
||||
// compiler of package Q need only load one export data file per direct
|
||||
// import of Q, and it will learn everything about the API of package P
|
||||
// and everything it needs to know about the API of P's dependencies.
|
||||
//
|
||||
// Similarly, analysis of package P emits a fact set containing facts
|
||||
// about all objects exported from P, plus additional facts about only
|
||||
// those objects of P's dependencies that are reachable from the API of
|
||||
// package P; the downstream analysis of Q need only load one fact set
|
||||
// per direct import of Q.
|
||||
//
|
||||
// The notion of "exportedness" that matters here is that of the
|
||||
// compiler. According to the language spec, a method pkg.T.f is
|
||||
// unexported simply because its name starts with lowercase. But the
|
||||
// compiler must nonetheless export f so that downstream compilations can
|
||||
// accurately ascertain whether pkg.T implements an interface pkg.I
|
||||
// defined as interface{f()}. Exported thus means "described in export
|
||||
// data".
|
||||
package facts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/go/types/objectpath"
|
||||
)
|
||||
|
||||
const debug = false
|
||||
|
||||
// A Set is a set of analysis.Facts.
|
||||
//
|
||||
// Decode creates a Set of facts by reading from the imports of a given
|
||||
// package, and Encode writes out the set. Between these operation,
|
||||
// the Import and Export methods will query and update the set.
|
||||
//
|
||||
// All of Set's methods except String are safe to call concurrently.
|
||||
type Set struct {
|
||||
pkg *types.Package
|
||||
mu sync.Mutex
|
||||
m map[key]analysis.Fact
|
||||
}
|
||||
|
||||
type key struct {
|
||||
pkg *types.Package
|
||||
obj types.Object // (object facts only)
|
||||
t reflect.Type
|
||||
}
|
||||
|
||||
// ImportObjectFact implements analysis.Pass.ImportObjectFact.
|
||||
func (s *Set) ImportObjectFact(obj types.Object, ptr analysis.Fact) bool {
|
||||
if obj == nil {
|
||||
panic("nil object")
|
||||
}
|
||||
key := key{pkg: obj.Pkg(), obj: obj, t: reflect.TypeOf(ptr)}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if v, ok := s.m[key]; ok {
|
||||
reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExportObjectFact implements analysis.Pass.ExportObjectFact.
|
||||
func (s *Set) ExportObjectFact(obj types.Object, fact analysis.Fact) {
|
||||
if obj.Pkg() != s.pkg {
|
||||
log.Panicf("in package %s: ExportObjectFact(%s, %T): can't set fact on object belonging another package",
|
||||
s.pkg, obj, fact)
|
||||
}
|
||||
key := key{pkg: obj.Pkg(), obj: obj, t: reflect.TypeOf(fact)}
|
||||
s.mu.Lock()
|
||||
s.m[key] = fact // clobber any existing entry
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Set) AllObjectFacts(filter map[reflect.Type]bool) []analysis.ObjectFact {
|
||||
var facts []analysis.ObjectFact
|
||||
s.mu.Lock()
|
||||
for k, v := range s.m {
|
||||
if k.obj != nil && filter[k.t] {
|
||||
facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: v})
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return facts
|
||||
}
|
||||
|
||||
// ImportPackageFact implements analysis.Pass.ImportPackageFact.
|
||||
func (s *Set) ImportPackageFact(pkg *types.Package, ptr analysis.Fact) bool {
|
||||
if pkg == nil {
|
||||
panic("nil package")
|
||||
}
|
||||
key := key{pkg: pkg, t: reflect.TypeOf(ptr)}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if v, ok := s.m[key]; ok {
|
||||
reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExportPackageFact implements analysis.Pass.ExportPackageFact.
|
||||
func (s *Set) ExportPackageFact(fact analysis.Fact) {
|
||||
key := key{pkg: s.pkg, t: reflect.TypeOf(fact)}
|
||||
s.mu.Lock()
|
||||
s.m[key] = fact // clobber any existing entry
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Set) AllPackageFacts(filter map[reflect.Type]bool) []analysis.PackageFact {
|
||||
var facts []analysis.PackageFact
|
||||
s.mu.Lock()
|
||||
for k, v := range s.m {
|
||||
if k.obj == nil && filter[k.t] {
|
||||
facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: v})
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return facts
|
||||
}
|
||||
|
||||
// gobFact is the Gob declaration of a serialized fact.
|
||||
type gobFact struct {
|
||||
PkgPath string // path of package
|
||||
Object objectpath.Path // optional path of object relative to package itself
|
||||
Fact analysis.Fact // type and value of user-defined Fact
|
||||
}
|
||||
|
||||
// A Decoder decodes the facts from the direct imports of the package
|
||||
// provided to NewEncoder. A single decoder may be used to decode
|
||||
// multiple fact sets (e.g. each for a different set of fact types)
|
||||
// for the same package. Each call to Decode returns an independent
|
||||
// fact set.
|
||||
type Decoder struct {
|
||||
pkg *types.Package
|
||||
getPackage GetPackageFunc
|
||||
}
|
||||
|
||||
// NewDecoder returns a fact decoder for the specified package.
|
||||
//
|
||||
// It uses a brute-force recursive approach to enumerate all objects
|
||||
// defined by dependencies of pkg, so that it can learn the set of
|
||||
// package paths that may be mentioned in the fact encoding. This does
|
||||
// not scale well; use [NewDecoderFunc] where possible.
|
||||
func NewDecoder(pkg *types.Package) *Decoder {
|
||||
// Compute the import map for this package.
|
||||
// See the package doc comment.
|
||||
m := importMap(pkg.Imports())
|
||||
getPackageFunc := func(path string) *types.Package { return m[path] }
|
||||
return NewDecoderFunc(pkg, getPackageFunc)
|
||||
}
|
||||
|
||||
// NewDecoderFunc returns a fact decoder for the specified package.
|
||||
//
|
||||
// It calls the getPackage function for the package path string of
|
||||
// each dependency (perhaps indirect) that it encounters in the
|
||||
// encoding. If the function returns nil, the fact is discarded.
|
||||
//
|
||||
// This function is preferred over [NewDecoder] when the client is
|
||||
// capable of efficient look-up of packages by package path.
|
||||
func NewDecoderFunc(pkg *types.Package, getPackage GetPackageFunc) *Decoder {
|
||||
return &Decoder{
|
||||
pkg: pkg,
|
||||
getPackage: getPackage,
|
||||
}
|
||||
}
|
||||
|
||||
// A GetPackageFunc function returns the package denoted by a package path.
|
||||
type GetPackageFunc = func(pkgPath string) *types.Package
|
||||
|
||||
// Decode decodes all the facts relevant to the analysis of package
|
||||
// pkgPath. The read function reads serialized fact data from an external
|
||||
// source for one of pkg's direct imports, identified by package path.
|
||||
// The empty file is a valid encoding of an empty fact set.
|
||||
//
|
||||
// It is the caller's responsibility to call gob.Register on all
|
||||
// necessary fact types.
|
||||
//
|
||||
// Concurrent calls to Decode are safe, so long as the
|
||||
// [GetPackageFunc] (if any) is also concurrency-safe.
|
||||
func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error) {
|
||||
// Read facts from imported packages.
|
||||
// Facts may describe indirectly imported packages, or their objects.
|
||||
m := make(map[key]analysis.Fact) // one big bucket
|
||||
for _, imp := range d.pkg.Imports() {
|
||||
logf := func(format string, args ...interface{}) {
|
||||
if debug {
|
||||
prefix := fmt.Sprintf("in %s, importing %s: ",
|
||||
d.pkg.Path(), imp.Path())
|
||||
log.Print(prefix, fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// Read the gob-encoded facts.
|
||||
data, err := read(imp.Path())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in %s, can't import facts for package %q: %v",
|
||||
d.pkg.Path(), imp.Path(), err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue // no facts
|
||||
}
|
||||
var gobFacts []gobFact
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&gobFacts); err != nil {
|
||||
return nil, fmt.Errorf("decoding facts for %q: %v", imp.Path(), err)
|
||||
}
|
||||
logf("decoded %d facts: %v", len(gobFacts), gobFacts)
|
||||
|
||||
// Parse each one into a key and a Fact.
|
||||
for _, f := range gobFacts {
|
||||
factPkg := d.getPackage(f.PkgPath) // possibly an indirect dependency
|
||||
if factPkg == nil {
|
||||
// Fact relates to a dependency that was
|
||||
// unused in this translation unit. Skip.
|
||||
logf("no package %q; discarding %v", f.PkgPath, f.Fact)
|
||||
continue
|
||||
}
|
||||
key := key{pkg: factPkg, t: reflect.TypeOf(f.Fact)}
|
||||
if f.Object != "" {
|
||||
// object fact
|
||||
obj, err := objectpath.Object(factPkg, f.Object)
|
||||
if err != nil {
|
||||
// (most likely due to unexported object)
|
||||
// TODO(adonovan): audit for other possibilities.
|
||||
logf("no object for path: %v; discarding %s", err, f.Fact)
|
||||
continue
|
||||
}
|
||||
key.obj = obj
|
||||
logf("read %T fact %s for %v", f.Fact, f.Fact, key.obj)
|
||||
} else {
|
||||
// package fact
|
||||
logf("read %T fact %s for %v", f.Fact, f.Fact, factPkg)
|
||||
}
|
||||
m[key] = f.Fact
|
||||
}
|
||||
}
|
||||
|
||||
return &Set{pkg: d.pkg, m: m}, nil
|
||||
}
|
||||
|
||||
// Encode encodes a set of facts to a memory buffer.
|
||||
//
|
||||
// It may fail if one of the Facts could not be gob-encoded, but this is
|
||||
// a sign of a bug in an Analyzer.
|
||||
func (s *Set) Encode() []byte {
|
||||
encoder := new(objectpath.Encoder)
|
||||
|
||||
// TODO(adonovan): opt: use a more efficient encoding
|
||||
// that avoids repeating PkgPath for each fact.
|
||||
|
||||
// Gather all facts, including those from imported packages.
|
||||
var gobFacts []gobFact
|
||||
|
||||
s.mu.Lock()
|
||||
for k, fact := range s.m {
|
||||
if debug {
|
||||
log.Printf("%v => %s\n", k, fact)
|
||||
}
|
||||
|
||||
// Don't export facts that we imported from another
|
||||
// package, unless they represent fields or methods,
|
||||
// or package-level types.
|
||||
// (Facts about packages, and other package-level
|
||||
// objects, are only obtained from direct imports so
|
||||
// they needn't be reexported.)
|
||||
//
|
||||
// This is analogous to the pruning done by "deep"
|
||||
// export data for types, but not as precise because
|
||||
// we aren't careful about which structs or methods
|
||||
// we rexport: it should be only those referenced
|
||||
// from the API of s.pkg.
|
||||
// TODO(adonovan): opt: be more precise. e.g.
|
||||
// intersect with the set of objects computed by
|
||||
// importMap(s.pkg.Imports()).
|
||||
// TODO(adonovan): opt: implement "shallow" facts.
|
||||
if k.pkg != s.pkg {
|
||||
if k.obj == nil {
|
||||
continue // imported package fact
|
||||
}
|
||||
if _, isType := k.obj.(*types.TypeName); !isType &&
|
||||
k.obj.Parent() == k.obj.Pkg().Scope() {
|
||||
continue // imported fact about package-level non-type object
|
||||
}
|
||||
}
|
||||
|
||||
var object objectpath.Path
|
||||
if k.obj != nil {
|
||||
path, err := encoder.For(k.obj)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Printf("discarding fact %s about %s\n", fact, k.obj)
|
||||
}
|
||||
continue // object not accessible from package API; discard fact
|
||||
}
|
||||
object = path
|
||||
}
|
||||
gobFacts = append(gobFacts, gobFact{
|
||||
PkgPath: k.pkg.Path(),
|
||||
Object: object,
|
||||
Fact: fact,
|
||||
})
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// Sort facts by (package, object, type) for determinism.
|
||||
sort.Slice(gobFacts, func(i, j int) bool {
|
||||
x, y := gobFacts[i], gobFacts[j]
|
||||
if x.PkgPath != y.PkgPath {
|
||||
return x.PkgPath < y.PkgPath
|
||||
}
|
||||
if x.Object != y.Object {
|
||||
return x.Object < y.Object
|
||||
}
|
||||
tx := reflect.TypeOf(x.Fact)
|
||||
ty := reflect.TypeOf(y.Fact)
|
||||
if tx != ty {
|
||||
return tx.String() < ty.String()
|
||||
}
|
||||
return false // equal
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
if len(gobFacts) > 0 {
|
||||
if err := gob.NewEncoder(&buf).Encode(gobFacts); err != nil {
|
||||
// Fact encoding should never fail. Identify the culprit.
|
||||
for _, gf := range gobFacts {
|
||||
if err := gob.NewEncoder(io.Discard).Encode(gf); err != nil {
|
||||
fact := gf.Fact
|
||||
pkgpath := reflect.TypeOf(fact).Elem().PkgPath()
|
||||
log.Panicf("internal error: gob encoding of analysis fact %s failed: %v; please report a bug against fact %T in package %q",
|
||||
fact, err, fact, pkgpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("package %q: encode %d facts, %d bytes\n",
|
||||
s.pkg.Path(), len(gobFacts), buf.Len())
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// String is provided only for debugging, and must not be called
|
||||
// concurrent with any Import/Export method.
|
||||
func (s *Set) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("{")
|
||||
for k, f := range s.m {
|
||||
if buf.Len() > 1 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
if k.obj != nil {
|
||||
buf.WriteString(k.obj.String())
|
||||
} else {
|
||||
buf.WriteString(k.pkg.Path())
|
||||
}
|
||||
fmt.Fprintf(&buf, ": %v", f)
|
||||
}
|
||||
buf.WriteString("}")
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
// 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 a Go source expression in the current 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 }
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
// importMap computes the import map for a package by traversing the
|
||||
// entire exported API each of its imports.
|
||||
//
|
||||
// This is a workaround for the fact that we cannot access the map used
|
||||
// internally by the types.Importer returned by go/importer. The entries
|
||||
// in this map are the packages and objects that may be relevant to the
|
||||
// current analysis unit.
|
||||
//
|
||||
// Packages in the map that are only indirectly imported may be
|
||||
// incomplete (!pkg.Complete()).
|
||||
//
|
||||
// This function scales very poorly with packages' transitive object
|
||||
// references, which can be more than a million for each package near
|
||||
// the top of a large project. (This was a significant contributor to
|
||||
// #60621.)
|
||||
// TODO(adonovan): opt: compute this information more efficiently
|
||||
// by obtaining it from the internals of the gcexportdata decoder.
|
||||
func importMap(imports []*types.Package) map[string]*types.Package {
|
||||
objects := make(map[types.Object]bool)
|
||||
typs := make(map[types.Type]bool) // Named and TypeParam
|
||||
packages := make(map[string]*types.Package)
|
||||
|
||||
var addObj func(obj types.Object)
|
||||
var addType func(T types.Type)
|
||||
|
||||
addObj = func(obj types.Object) {
|
||||
if !objects[obj] {
|
||||
objects[obj] = true
|
||||
addType(obj.Type())
|
||||
if pkg := obj.Pkg(); pkg != nil {
|
||||
packages[pkg.Path()] = pkg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addType = func(T types.Type) {
|
||||
switch T := T.(type) {
|
||||
case *aliases.Alias:
|
||||
addType(aliases.Unalias(T))
|
||||
case *types.Basic:
|
||||
// nop
|
||||
case *types.Named:
|
||||
// Remove infinite expansions of *types.Named by always looking at the origin.
|
||||
// Some named types with type parameters [that will not type check] have
|
||||
// infinite expansions:
|
||||
// type N[T any] struct { F *N[N[T]] }
|
||||
// importMap() is called on such types when Analyzer.RunDespiteErrors is true.
|
||||
T = T.Origin()
|
||||
if !typs[T] {
|
||||
typs[T] = true
|
||||
addObj(T.Obj())
|
||||
addType(T.Underlying())
|
||||
for i := 0; i < T.NumMethods(); i++ {
|
||||
addObj(T.Method(i))
|
||||
}
|
||||
if tparams := T.TypeParams(); tparams != nil {
|
||||
for i := 0; i < tparams.Len(); i++ {
|
||||
addType(tparams.At(i))
|
||||
}
|
||||
}
|
||||
if targs := T.TypeArgs(); targs != nil {
|
||||
for i := 0; i < targs.Len(); i++ {
|
||||
addType(targs.At(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
case *types.Pointer:
|
||||
addType(T.Elem())
|
||||
case *types.Slice:
|
||||
addType(T.Elem())
|
||||
case *types.Array:
|
||||
addType(T.Elem())
|
||||
case *types.Chan:
|
||||
addType(T.Elem())
|
||||
case *types.Map:
|
||||
addType(T.Key())
|
||||
addType(T.Elem())
|
||||
case *types.Signature:
|
||||
addType(T.Params())
|
||||
addType(T.Results())
|
||||
if tparams := T.TypeParams(); tparams != nil {
|
||||
for i := 0; i < tparams.Len(); i++ {
|
||||
addType(tparams.At(i))
|
||||
}
|
||||
}
|
||||
case *types.Struct:
|
||||
for i := 0; i < T.NumFields(); i++ {
|
||||
addObj(T.Field(i))
|
||||
}
|
||||
case *types.Tuple:
|
||||
for i := 0; i < T.Len(); i++ {
|
||||
addObj(T.At(i))
|
||||
}
|
||||
case *types.Interface:
|
||||
for i := 0; i < T.NumMethods(); i++ {
|
||||
addObj(T.Method(i))
|
||||
}
|
||||
for i := 0; i < T.NumEmbeddeds(); i++ {
|
||||
addType(T.EmbeddedType(i)) // walk Embedded for implicits
|
||||
}
|
||||
case *types.Union:
|
||||
for i := 0; i < T.Len(); i++ {
|
||||
addType(T.Term(i).Type())
|
||||
}
|
||||
case *types.TypeParam:
|
||||
if !typs[T] {
|
||||
typs[T] = true
|
||||
addObj(T.Obj())
|
||||
addType(T.Constraint())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, imp := range imports {
|
||||
packages[imp.Path()] = imp
|
||||
|
||||
scope := imp.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
addObj(scope.Lookup(name))
|
||||
}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
Reference in New Issue
Block a user