1
0
mirror of https://github.com/golang/go synced 2024-09-30 22:48:32 -06:00
go/cmd/guru/guru.go
Alan Donovan fb3df4a89d cmd/guru: definition: try parser-based resolution first
The parser does intra-file resolution.  It's only a best-effort but
it's extremely fast, so we try it first and fall back to the loader-based
implementation if it fails.

Also:
- factor definitionResult so it doesn't depend on either go/types.Object
  or go/ast.Object.
- guessImportPath: make importPath=="" result an error,
  since you can't import the empty string.  Simplify importQueryPackage.
- Make fastQueryPos continue in face of minor parse errors.
- describe: eliminate old TODO; suppress log statement.

Change-Id: I30a32487e9fd9d5308b1580ec12220cad2c16299
Reviewed-on: https://go-review.googlesource.com/19507
Reviewed-by: Daniel Morsing <daniel.morsing@gmail.com>
2016-02-16 00:29:23 +00:00

367 lines
11 KiB
Go

// Copyright 2014 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 main
// TODO(adonovan): new queries
// - show all statements that may update the selected lvalue
// (local, global, field, etc).
// - show all places where an object of type T is created
// (&T{}, var t T, new(T), new(struct{array [3]T}), etc.
import (
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"io"
"path/filepath"
"golang.org/x/tools/cmd/guru/serial"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/pointer"
"golang.org/x/tools/go/ssa"
)
type printfFunc func(pos interface{}, format string, args ...interface{})
// queryResult is the interface of each query-specific result type.
type queryResult interface {
toSerial(res *serial.Result, fset *token.FileSet)
display(printf printfFunc)
}
// A QueryPos represents the position provided as input to a query:
// a textual extent in the program's source code, the AST node it
// corresponds to, and the package to which it belongs.
// Instances are created by parseQueryPos.
type queryPos struct {
fset *token.FileSet
start, end token.Pos // source extent of query
path []ast.Node // AST path from query node to root of ast.File
exact bool // 2nd result of PathEnclosingInterval
info *loader.PackageInfo // type info for the queried package (nil for fastQueryPos)
}
// TypeString prints type T relative to the query position.
func (qpos *queryPos) typeString(T types.Type) string {
return types.TypeString(T, types.RelativeTo(qpos.info.Pkg))
}
// ObjectString prints object obj relative to the query position.
func (qpos *queryPos) objectString(obj types.Object) string {
return types.ObjectString(obj, types.RelativeTo(qpos.info.Pkg))
}
// SelectionString prints selection sel relative to the query position.
func (qpos *queryPos) selectionString(sel *types.Selection) string {
return types.SelectionString(sel, types.RelativeTo(qpos.info.Pkg))
}
// A Query specifies a single guru query.
type Query struct {
Mode string // query mode ("callers", etc)
Pos string // query position
Build *build.Context // package loading configuration
// pointer analysis options
Scope []string // main packages in (*loader.Config).FromArgs syntax
PTALog io.Writer // (optional) pointer-analysis log file
Reflection bool // model reflection soundly (currently slow).
// Populated during Run()
Fset *token.FileSet
result queryResult
}
// Serial returns an instance of serial.Result, which implements the
// {xml,json}.Marshaler interfaces so that query results can be
// serialized as JSON or XML.
//
func (q *Query) Serial() *serial.Result {
resj := &serial.Result{Mode: q.Mode}
q.result.toSerial(resj, q.Fset)
return resj
}
// WriteTo writes the guru query result res to out in a compiler diagnostic format.
func (q *Query) WriteTo(out io.Writer) {
printf := func(pos interface{}, format string, args ...interface{}) {
fprintf(out, q.Fset, pos, format, args...)
}
q.result.display(printf)
}
// Run runs an guru query and populates its Fset and Result.
func Run(q *Query) error {
switch q.Mode {
case "callees":
return callees(q)
case "callers":
return callers(q)
case "callstack":
return callstack(q)
case "peers":
return peers(q)
case "pointsto":
return pointsto(q)
case "whicherrs":
return whicherrs(q)
case "definition":
return definition(q)
case "describe":
return describe(q)
case "freevars":
return freevars(q)
case "implements":
return implements(q)
case "referrers":
return referrers(q)
case "what":
return what(q)
default:
return fmt.Errorf("invalid mode: %q", q.Mode)
}
}
func setPTAScope(lconf *loader.Config, scope []string) error {
if len(scope) == 0 {
return fmt.Errorf("no packages specified for pointer analysis scope")
}
// Determine initial packages for PTA.
args, err := lconf.FromArgs(scope, true)
if err != nil {
return err
}
if len(args) > 0 {
return fmt.Errorf("surplus arguments: %q", args)
}
return nil
}
// Create a pointer.Config whose scope is the initial packages of lprog
// and their dependencies.
func setupPTA(prog *ssa.Program, lprog *loader.Program, ptaLog io.Writer, reflection bool) (*pointer.Config, error) {
// TODO(adonovan): the body of this function is essentially
// duplicated in all go/pointer clients. Refactor.
// For each initial package (specified on the command line),
// if it has a main function, analyze that,
// otherwise analyze its tests, if any.
var testPkgs, mains []*ssa.Package
for _, info := range lprog.InitialPackages() {
initialPkg := prog.Package(info.Pkg)
// Add package to the pointer analysis scope.
if initialPkg.Func("main") != nil {
mains = append(mains, initialPkg)
} else {
testPkgs = append(testPkgs, initialPkg)
}
}
if testPkgs != nil {
if p := prog.CreateTestMainPackage(testPkgs...); p != nil {
mains = append(mains, p)
}
}
if mains == nil {
return nil, fmt.Errorf("analysis scope has no main and no tests")
}
return &pointer.Config{
Log: ptaLog,
Reflection: reflection,
Mains: mains,
}, nil
}
// importQueryPackage finds the package P containing the
// query position and tells conf to import it.
// It returns the package's path.
func importQueryPackage(pos string, conf *loader.Config) (string, error) {
fqpos, err := fastQueryPos(pos)
if err != nil {
return "", err // bad query
}
filename := fqpos.fset.File(fqpos.start).Name()
_, importPath, err := guessImportPath(filename, conf.Build)
if err != nil {
return "", err // can't find GOPATH dir
}
// Check that it's possible to load the queried package.
// (e.g. guru tests contain different 'package' decls in same dir.)
// Keep consistent with logic in loader/util.go!
cfg2 := *conf.Build
cfg2.CgoEnabled = false
bp, err := cfg2.Import(importPath, "", 0)
if err != nil {
return "", err // no files for package
}
switch pkgContainsFile(bp, filename) {
case 'T':
conf.ImportWithTests(importPath)
case 'X':
conf.ImportWithTests(importPath)
importPath += "_test" // for TypeCheckFuncBodies
case 'G':
conf.Import(importPath)
default:
// This happens for ad-hoc packages like
// $GOROOT/src/net/http/triv.go.
return "", fmt.Errorf("package %q doesn't contain file %s",
importPath, filename)
}
conf.TypeCheckFuncBodies = func(p string) bool { return p == importPath }
return importPath, nil
}
// pkgContainsFile reports whether file was among the packages Go
// files, Test files, eXternal test files, or not found.
func pkgContainsFile(bp *build.Package, filename string) byte {
for i, files := range [][]string{bp.GoFiles, bp.TestGoFiles, bp.XTestGoFiles} {
for _, file := range files {
if sameFile(filepath.Join(bp.Dir, file), filename) {
return "GTX"[i]
}
}
}
return 0 // not found
}
// ParseQueryPos parses the source query position pos and returns the
// AST node of the loaded program lprog that it identifies.
// If needExact, it must identify a single AST subtree;
// this is appropriate for queries that allow fairly arbitrary syntax,
// e.g. "describe".
//
func parseQueryPos(lprog *loader.Program, pos string, needExact bool) (*queryPos, error) {
filename, startOffset, endOffset, err := parsePos(pos)
if err != nil {
return nil, err
}
// Find the named file among those in the loaded program.
var file *token.File
lprog.Fset.Iterate(func(f *token.File) bool {
if sameFile(filename, f.Name()) {
file = f
return false // done
}
return true // continue
})
if file == nil {
return nil, fmt.Errorf("file %s not found in loaded program", filename)
}
start, end, err := fileOffsetToPos(file, startOffset, endOffset)
if err != nil {
return nil, err
}
info, path, exact := lprog.PathEnclosingInterval(start, end)
if path == nil {
return nil, fmt.Errorf("no syntax here")
}
if needExact && !exact {
return nil, fmt.Errorf("ambiguous selection within %s", astutil.NodeDescription(path[0]))
}
return &queryPos{lprog.Fset, start, end, path, exact, info}, nil
}
// ---------- Utilities ----------
// allowErrors causes type errors to be silently ignored.
// (Not suitable if SSA construction follows.)
func allowErrors(lconf *loader.Config) {
ctxt := *lconf.Build // copy
ctxt.CgoEnabled = false
lconf.Build = &ctxt
lconf.AllowErrors = true
// AllErrors makes the parser always return an AST instead of
// bailing out after 10 errors and returning an empty ast.File.
lconf.ParserMode = parser.AllErrors
lconf.TypeChecker.Error = func(err error) {}
}
// ptrAnalysis runs the pointer analysis and returns its result.
func ptrAnalysis(conf *pointer.Config) *pointer.Result {
result, err := pointer.Analyze(conf)
if err != nil {
panic(err) // pointer analysis internal error
}
return result
}
func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) }
// deref returns a pointer's element type; otherwise it returns typ.
func deref(typ types.Type) types.Type {
if p, ok := typ.Underlying().(*types.Pointer); ok {
return p.Elem()
}
return typ
}
// fprintf prints to w a message of the form "location: message\n"
// where location is derived from pos.
//
// pos must be one of:
// - a token.Pos, denoting a position
// - an ast.Node, denoting an interval
// - anything with a Pos() method:
// ssa.Member, ssa.Value, ssa.Instruction, types.Object, pointer.Label, etc.
// - a QueryPos, denoting the extent of the user's query.
// - nil, meaning no position at all.
//
// The output format is is compatible with the 'gnu'
// compilation-error-regexp in Emacs' compilation mode.
// TODO(adonovan): support other editors.
//
func fprintf(w io.Writer, fset *token.FileSet, pos interface{}, format string, args ...interface{}) {
var start, end token.Pos
switch pos := pos.(type) {
case ast.Node:
start = pos.Pos()
end = pos.End()
case token.Pos:
start = pos
end = start
case interface {
Pos() token.Pos
}:
start = pos.Pos()
end = start
case *queryPos:
start = pos.start
end = pos.end
case nil:
// no-op
default:
panic(fmt.Sprintf("invalid pos: %T", pos))
}
if sp := fset.Position(start); start == end {
// (prints "-: " for token.NoPos)
fmt.Fprintf(w, "%s: ", sp)
} else {
ep := fset.Position(end)
// The -1 below is a concession to Emacs's broken use of
// inclusive (not half-open) intervals.
// Other editors may not want it.
// TODO(adonovan): add an -editor=vim|emacs|acme|auto
// flag; auto uses EMACS=t / VIM=... / etc env vars.
fmt.Fprintf(w, "%s:%d.%d-%d.%d: ",
sp.Filename, sp.Line, sp.Column, ep.Line, ep.Column-1)
}
fmt.Fprintf(w, format, args...)
io.WriteString(w, "\n")
}