1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package facts_test
6
7import (
8	"encoding/gob"
9	"fmt"
10	"go/token"
11	"go/types"
12	"os"
13	"testing"
14
15	"golang.org/x/tools/go/analysis/analysistest"
16	"golang.org/x/tools/go/analysis/internal/facts"
17	"golang.org/x/tools/go/packages"
18)
19
20type myFact struct {
21	S string
22}
23
24func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) }
25func (f *myFact) AFact()         {}
26
27func TestEncodeDecode(t *testing.T) {
28	gob.Register(new(myFact))
29
30	// c -> b -> a, a2
31	// c does not directly depend on a, but it indirectly uses a.T.
32	//
33	// Package a2 is never loaded directly so it is incomplete.
34	//
35	// We use only types in this example because we rely on
36	// types.Eval to resolve the lookup expressions, and it only
37	// works for types. This is a definite gap in the typechecker API.
38	files := map[string]string{
39		"a/a.go":  `package a; type A int; type T int`,
40		"a2/a.go": `package a2; type A2 int; type Unneeded int`,
41		"b/b.go":  `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`,
42		"c/c.go":  `package c; import "b"; type C []b.B`,
43	}
44	dir, cleanup, err := analysistest.WriteFiles(files)
45	if err != nil {
46		t.Fatal(err)
47	}
48	defer cleanup()
49
50	// factmap represents the passing of encoded facts from one
51	// package to another. In practice one would use the file system.
52	factmap := make(map[string][]byte)
53	read := func(path string) ([]byte, error) { return factmap[path], nil }
54
55	// In the following table, we analyze packages (a, b, c) in order,
56	// look up various objects accessible within each package,
57	// and see if they have a fact.  The "analysis" exports a fact
58	// for every object at package level.
59	//
60	// Note: Loop iterations are not independent test cases;
61	// order matters, as we populate factmap.
62	type lookups []struct {
63		objexpr string
64		want    string
65	}
66	for _, test := range []struct {
67		path    string
68		lookups lookups
69	}{
70		{"a", lookups{
71			{"A", "myFact(a.A)"},
72		}},
73		{"b", lookups{
74			{"a.A", "myFact(a.A)"},
75			{"a.T", "myFact(a.T)"},
76			{"B", "myFact(b.B)"},
77			{"F", "myFact(b.F)"},
78			{"F(nil)()", "myFact(a.T)"}, // (result type of b.F)
79		}},
80		{"c", lookups{
81			{"b.B", "myFact(b.B)"},
82			{"b.F", "myFact(b.F)"},
83			//{"b.F(nil)()", "myFact(a.T)"}, // no fact; TODO(adonovan): investigate
84			{"C", "myFact(c.C)"},
85			{"C{}[0]", "myFact(b.B)"},
86			{"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2)
87		}},
88	} {
89		// load package
90		pkg, err := load(dir, test.path)
91		if err != nil {
92			t.Fatal(err)
93		}
94
95		// decode
96		facts, err := facts.Decode(pkg, read)
97		if err != nil {
98			t.Fatalf("Decode failed: %v", err)
99		}
100		if true {
101			t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts
102		}
103
104		// export
105		// (one fact for each package-level object)
106		scope := pkg.Scope()
107		for _, name := range scope.Names() {
108			obj := scope.Lookup(name)
109			fact := &myFact{obj.Pkg().Name() + "." + obj.Name()}
110			facts.ExportObjectFact(obj, fact)
111		}
112
113		// import
114		// (after export, because an analyzer may import its own facts)
115		for _, lookup := range test.lookups {
116			fact := new(myFact)
117			var got string
118			if obj := find(pkg, lookup.objexpr); obj == nil {
119				got = "no object"
120			} else if facts.ImportObjectFact(obj, fact) {
121				got = fact.String()
122			} else {
123				got = "no fact"
124			}
125			if got != lookup.want {
126				t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s",
127					pkg.Path(), lookup.objexpr, fact, got, lookup.want)
128			}
129		}
130
131		// encode
132		factmap[pkg.Path()] = facts.Encode()
133	}
134}
135
136func find(p *types.Package, expr string) types.Object {
137	// types.Eval only allows us to compute a TypeName object for an expression.
138	// TODO(adonovan): support other expressions that denote an object:
139	// - an identifier (or qualified ident) for a func, const, or var
140	// - new(T).f for a field or method
141	// I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677.
142	// If that becomes available, use it.
143
144	// Choose an arbitrary position within the (single-file) package
145	// so that we are within the scope of its import declarations.
146	somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos()
147	tv, err := types.Eval(token.NewFileSet(), p, somepos, expr)
148	if err != nil {
149		return nil
150	}
151	if n, ok := tv.Type.(*types.Named); ok {
152		return n.Obj()
153	}
154	return nil
155}
156
157func load(dir string, path string) (*types.Package, error) {
158	cfg := &packages.Config{
159		Mode: packages.LoadSyntax,
160		Dir:  dir,
161		Env:  append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
162	}
163	pkgs, err := packages.Load(cfg, path)
164	if err != nil {
165		return nil, err
166	}
167	if packages.PrintErrors(pkgs) > 0 {
168		return nil, fmt.Errorf("packages had errors")
169	}
170	if len(pkgs) == 0 {
171		return nil, fmt.Errorf("no package matched %s", path)
172	}
173	return pkgs[0].Types, nil
174}
175