1// Copyright 2014 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 main
6
7// TODO(adonovan): new queries
8// - show all statements that may update the selected lvalue
9//   (local, global, field, etc).
10// - show all places where an object of type T is created
11//   (&T{}, var t T, new(T), new(struct{array [3]T}), etc.
12
13import (
14	"encoding/json"
15	"fmt"
16	"go/ast"
17	"go/build"
18	"go/parser"
19	"go/token"
20	"go/types"
21	"io"
22	"log"
23	"path/filepath"
24	"strings"
25
26	"golang.org/x/tools/go/ast/astutil"
27	"golang.org/x/tools/go/buildutil"
28	"golang.org/x/tools/go/loader"
29	"golang.org/x/tools/go/pointer"
30	"golang.org/x/tools/go/ssa"
31)
32
33type printfFunc func(pos interface{}, format string, args ...interface{})
34
35// A QueryResult is an item of output.  Each query produces a stream of
36// query results, calling Query.Output for each one.
37type QueryResult interface {
38	// JSON returns the QueryResult in JSON form.
39	JSON(fset *token.FileSet) []byte
40
41	// PrintPlain prints the QueryResult in plain text form.
42	// The implementation calls printfFunc to print each line of output.
43	PrintPlain(printf printfFunc)
44}
45
46// A QueryPos represents the position provided as input to a query:
47// a textual extent in the program's source code, the AST node it
48// corresponds to, and the package to which it belongs.
49// Instances are created by parseQueryPos.
50type queryPos struct {
51	fset       *token.FileSet
52	start, end token.Pos           // source extent of query
53	path       []ast.Node          // AST path from query node to root of ast.File
54	exact      bool                // 2nd result of PathEnclosingInterval
55	info       *loader.PackageInfo // type info for the queried package (nil for fastQueryPos)
56}
57
58// TypeString prints type T relative to the query position.
59func (qpos *queryPos) typeString(T types.Type) string {
60	return types.TypeString(T, types.RelativeTo(qpos.info.Pkg))
61}
62
63// ObjectString prints object obj relative to the query position.
64func (qpos *queryPos) objectString(obj types.Object) string {
65	return types.ObjectString(obj, types.RelativeTo(qpos.info.Pkg))
66}
67
68// A Query specifies a single guru query.
69type Query struct {
70	Pos   string         // query position
71	Build *build.Context // package loading configuration
72
73	// pointer analysis options
74	Scope      []string  // main packages in (*loader.Config).FromArgs syntax
75	PTALog     io.Writer // (optional) pointer-analysis log file
76	Reflection bool      // model reflection soundly (currently slow).
77
78	// result-printing function, safe for concurrent use
79	Output func(*token.FileSet, QueryResult)
80}
81
82// Run runs an guru query and populates its Fset and Result.
83func Run(mode string, q *Query) error {
84	switch mode {
85	case "callees":
86		return callees(q)
87	case "callers":
88		return callers(q)
89	case "callstack":
90		return callstack(q)
91	case "peers":
92		return peers(q)
93	case "pointsto":
94		return pointsto(q)
95	case "whicherrs":
96		return whicherrs(q)
97	case "definition":
98		return definition(q)
99	case "describe":
100		return describe(q)
101	case "freevars":
102		return freevars(q)
103	case "implements":
104		return implements(q)
105	case "referrers":
106		return referrers(q)
107	case "what":
108		return what(q)
109	default:
110		return fmt.Errorf("invalid mode: %q", mode)
111	}
112}
113
114func setPTAScope(lconf *loader.Config, scope []string) error {
115	pkgs := buildutil.ExpandPatterns(lconf.Build, scope)
116	if len(pkgs) == 0 {
117		return fmt.Errorf("no packages specified for pointer analysis scope")
118	}
119	// The value of each entry in pkgs is true,
120	// giving ImportWithTests (not Import) semantics.
121	lconf.ImportPkgs = pkgs
122	return nil
123}
124
125// Create a pointer.Config whose scope is the initial packages of lprog
126// and their dependencies.
127func setupPTA(prog *ssa.Program, lprog *loader.Program, ptaLog io.Writer, reflection bool) (*pointer.Config, error) {
128	// For each initial package (specified on the command line),
129	// if it has a main function, analyze that,
130	// otherwise analyze its tests, if any.
131	var mains []*ssa.Package
132	for _, info := range lprog.InitialPackages() {
133		p := prog.Package(info.Pkg)
134
135		// Add package to the pointer analysis scope.
136		if p.Pkg.Name() == "main" && p.Func("main") != nil {
137			mains = append(mains, p)
138		} else if main := prog.CreateTestMainPackage(p); main != nil {
139			mains = append(mains, main)
140		}
141	}
142	if mains == nil {
143		return nil, fmt.Errorf("analysis scope has no main and no tests")
144	}
145	return &pointer.Config{
146		Log:        ptaLog,
147		Reflection: reflection,
148		Mains:      mains,
149	}, nil
150}
151
152// importQueryPackage finds the package P containing the
153// query position and tells conf to import it.
154// It returns the package's path.
155func importQueryPackage(pos string, conf *loader.Config) (string, error) {
156	fqpos, err := fastQueryPos(conf.Build, pos)
157	if err != nil {
158		return "", err // bad query
159	}
160	filename := fqpos.fset.File(fqpos.start).Name()
161
162	_, importPath, err := guessImportPath(filename, conf.Build)
163	if err != nil {
164		// Can't find GOPATH dir.
165		// Treat the query file as its own package.
166		importPath = "command-line-arguments"
167		conf.CreateFromFilenames(importPath, filename)
168	} else {
169		// Check that it's possible to load the queried package.
170		// (e.g. guru tests contain different 'package' decls in same dir.)
171		// Keep consistent with logic in loader/util.go!
172		cfg2 := *conf.Build
173		cfg2.CgoEnabled = false
174		bp, err := cfg2.Import(importPath, "", 0)
175		if err != nil {
176			return "", err // no files for package
177		}
178
179		switch pkgContainsFile(bp, filename) {
180		case 'T':
181			conf.ImportWithTests(importPath)
182		case 'X':
183			conf.ImportWithTests(importPath)
184			importPath += "_test" // for TypeCheckFuncBodies
185		case 'G':
186			conf.Import(importPath)
187		default:
188			// This happens for ad-hoc packages like
189			// $GOROOT/src/net/http/triv.go.
190			return "", fmt.Errorf("package %q doesn't contain file %s",
191				importPath, filename)
192		}
193	}
194
195	conf.TypeCheckFuncBodies = func(p string) bool { return p == importPath }
196
197	return importPath, nil
198}
199
200// pkgContainsFile reports whether file was among the packages Go
201// files, Test files, eXternal test files, or not found.
202func pkgContainsFile(bp *build.Package, filename string) byte {
203	for i, files := range [][]string{bp.GoFiles, bp.TestGoFiles, bp.XTestGoFiles} {
204		for _, file := range files {
205			if sameFile(filepath.Join(bp.Dir, file), filename) {
206				return "GTX"[i]
207			}
208		}
209	}
210	return 0 // not found
211}
212
213// ParseQueryPos parses the source query position pos and returns the
214// AST node of the loaded program lprog that it identifies.
215// If needExact, it must identify a single AST subtree;
216// this is appropriate for queries that allow fairly arbitrary syntax,
217// e.g. "describe".
218//
219func parseQueryPos(lprog *loader.Program, pos string, needExact bool) (*queryPos, error) {
220	filename, startOffset, endOffset, err := parsePos(pos)
221	if err != nil {
222		return nil, err
223	}
224
225	// Find the named file among those in the loaded program.
226	var file *token.File
227	lprog.Fset.Iterate(func(f *token.File) bool {
228		if sameFile(filename, f.Name()) {
229			file = f
230			return false // done
231		}
232		return true // continue
233	})
234	if file == nil {
235		return nil, fmt.Errorf("file %s not found in loaded program", filename)
236	}
237
238	start, end, err := fileOffsetToPos(file, startOffset, endOffset)
239	if err != nil {
240		return nil, err
241	}
242	info, path, exact := lprog.PathEnclosingInterval(start, end)
243	if path == nil {
244		return nil, fmt.Errorf("no syntax here")
245	}
246	if needExact && !exact {
247		return nil, fmt.Errorf("ambiguous selection within %s", astutil.NodeDescription(path[0]))
248	}
249	return &queryPos{lprog.Fset, start, end, path, exact, info}, nil
250}
251
252// ---------- Utilities ----------
253
254// loadWithSoftErrors calls lconf.Load, suppressing "soft" errors.  (See Go issue 16530.)
255// TODO(adonovan): Once the loader has an option to allow soft errors,
256// replace calls to loadWithSoftErrors with loader calls with that parameter.
257func loadWithSoftErrors(lconf *loader.Config) (*loader.Program, error) {
258	lconf.AllowErrors = true
259
260	// Ideally we would just return conf.Load() here, but go/types
261	// reports certain "soft" errors that gc does not (Go issue 14596).
262	// As a workaround, we set AllowErrors=true and then duplicate
263	// the loader's error checking but allow soft errors.
264	// It would be nice if the loader API permitted "AllowErrors: soft".
265	prog, err := lconf.Load()
266	if err != nil {
267		return nil, err
268	}
269	var errpkgs []string
270	// Report hard errors in indirectly imported packages.
271	for _, info := range prog.AllPackages {
272		if containsHardErrors(info.Errors) {
273			errpkgs = append(errpkgs, info.Pkg.Path())
274		} else {
275			// Enable SSA construction for packages containing only soft errors.
276			info.TransitivelyErrorFree = true
277		}
278	}
279	if errpkgs != nil {
280		var more string
281		if len(errpkgs) > 3 {
282			more = fmt.Sprintf(" and %d more", len(errpkgs)-3)
283			errpkgs = errpkgs[:3]
284		}
285		return nil, fmt.Errorf("couldn't load packages due to errors: %s%s",
286			strings.Join(errpkgs, ", "), more)
287	}
288	return prog, err
289}
290
291func containsHardErrors(errors []error) bool {
292	for _, err := range errors {
293		if err, ok := err.(types.Error); ok && err.Soft {
294			continue
295		}
296		return true
297	}
298	return false
299}
300
301// allowErrors causes type errors to be silently ignored.
302// (Not suitable if SSA construction follows.)
303func allowErrors(lconf *loader.Config) {
304	ctxt := *lconf.Build // copy
305	ctxt.CgoEnabled = false
306	lconf.Build = &ctxt
307	lconf.AllowErrors = true
308	// AllErrors makes the parser always return an AST instead of
309	// bailing out after 10 errors and returning an empty ast.File.
310	lconf.ParserMode = parser.AllErrors
311	lconf.TypeChecker.Error = func(err error) {}
312}
313
314// ptrAnalysis runs the pointer analysis and returns its result.
315func ptrAnalysis(conf *pointer.Config) *pointer.Result {
316	result, err := pointer.Analyze(conf)
317	if err != nil {
318		panic(err) // pointer analysis internal error
319	}
320	return result
321}
322
323func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) }
324
325// deref returns a pointer's element type; otherwise it returns typ.
326func deref(typ types.Type) types.Type {
327	if p, ok := typ.Underlying().(*types.Pointer); ok {
328		return p.Elem()
329	}
330	return typ
331}
332
333// fprintf prints to w a message of the form "location: message\n"
334// where location is derived from pos.
335//
336// pos must be one of:
337//    - a token.Pos, denoting a position
338//    - an ast.Node, denoting an interval
339//    - anything with a Pos() method:
340//         ssa.Member, ssa.Value, ssa.Instruction, types.Object, pointer.Label, etc.
341//    - a QueryPos, denoting the extent of the user's query.
342//    - nil, meaning no position at all.
343//
344// The output format is is compatible with the 'gnu'
345// compilation-error-regexp in Emacs' compilation mode.
346//
347func fprintf(w io.Writer, fset *token.FileSet, pos interface{}, format string, args ...interface{}) {
348	var start, end token.Pos
349	switch pos := pos.(type) {
350	case ast.Node:
351		start = pos.Pos()
352		end = pos.End()
353	case token.Pos:
354		start = pos
355		end = start
356	case *types.PkgName:
357		// The Pos of most PkgName objects does not coincide with an identifier,
358		// so we suppress the usual start+len(name) heuristic for types.Objects.
359		start = pos.Pos()
360		end = start
361	case types.Object:
362		start = pos.Pos()
363		end = start + token.Pos(len(pos.Name())) // heuristic
364	case interface {
365		Pos() token.Pos
366	}:
367		start = pos.Pos()
368		end = start
369	case *queryPos:
370		start = pos.start
371		end = pos.end
372	case nil:
373		// no-op
374	default:
375		panic(fmt.Sprintf("invalid pos: %T", pos))
376	}
377
378	if sp := fset.Position(start); start == end {
379		// (prints "-: " for token.NoPos)
380		fmt.Fprintf(w, "%s: ", sp)
381	} else {
382		ep := fset.Position(end)
383		// The -1 below is a concession to Emacs's broken use of
384		// inclusive (not half-open) intervals.
385		// Other editors may not want it.
386		// TODO(adonovan): add an -editor=vim|emacs|acme|auto
387		// flag; auto uses EMACS=t / VIM=... / etc env vars.
388		fmt.Fprintf(w, "%s:%d.%d-%d.%d: ",
389			sp.Filename, sp.Line, sp.Column, ep.Line, ep.Column-1)
390	}
391	fmt.Fprintf(w, format, args...)
392	io.WriteString(w, "\n")
393}
394
395func toJSON(x interface{}) []byte {
396	b, err := json.MarshalIndent(x, "", "\t")
397	if err != nil {
398		log.Fatalf("JSON error: %v", err)
399	}
400	return b
401}
402