|
|
// 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 ( "encoding/json" "fmt" "go/ast" "go/build" "go/parser" "go/token" "go/types" "io" "log" "path/filepath" "strings"
"golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/buildutil" "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{})
// A QueryResult is an item of output. Each query produces a stream of
// query results, calling Query.Output for each one.
type QueryResult interface { // JSON returns the QueryResult in JSON form.
JSON(fset *token.FileSet) []byte
// PrintPlain prints the QueryResult in plain text form.
// The implementation calls printfFunc to print each line of output.
PrintPlain(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)) }
// A Query specifies a single guru query.
type Query struct { 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).
// result-printing function
Output func(*token.FileSet, QueryResult) }
// Run runs an guru query and populates its Fset and Result.
func Run(mode string, q *Query) error { switch 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", mode) } }
func setPTAScope(lconf *loader.Config, scope []string) error { pkgs := buildutil.ExpandPatterns(lconf.Build, scope) if len(pkgs) == 0 { return fmt.Errorf("no packages specified for pointer analysis scope") } // The value of each entry in pkgs is true,
// giving ImportWithTests (not Import) semantics.
lconf.ImportPkgs = pkgs 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) { // For each initial package (specified on the command line),
// if it has a main function, analyze that,
// otherwise analyze its tests, if any.
var mains []*ssa.Package for _, info := range lprog.InitialPackages() { p := prog.Package(info.Pkg)
// Add package to the pointer analysis scope.
if p.Pkg.Name() == "main" && p.Func("main") != nil { mains = append(mains, p) } else if main := prog.CreateTestMainPackage(p); main != nil { mains = append(mains, main) } } 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(conf.Build, pos) if err != nil { return "", err // bad query
} filename := fqpos.fset.File(fqpos.start).Name()
_, importPath, err := guessImportPath(filename, conf.Build) if err != nil { // Can't find GOPATH dir.
// Treat the query file as its own package.
importPath = "command-line-arguments" conf.CreateFromFilenames(importPath, filename) } else { // 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 ----------
// loadWithSoftErrors calls lconf.Load, suppressing "soft" errors. (See Go issue 16530.)
// TODO(adonovan): Once the loader has an option to allow soft errors,
// replace calls to loadWithSoftErrors with loader calls with that parameter.
func loadWithSoftErrors(lconf *loader.Config) (*loader.Program, error) { lconf.AllowErrors = true
// Ideally we would just return conf.Load() here, but go/types
// reports certain "soft" errors that gc does not (Go issue 14596).
// As a workaround, we set AllowErrors=true and then duplicate
// the loader's error checking but allow soft errors.
// It would be nice if the loader API permitted "AllowErrors: soft".
prog, err := lconf.Load() if err != nil { return nil, err } var errpkgs []string // Report hard errors in indirectly imported packages.
for _, info := range prog.AllPackages { if containsHardErrors(info.Errors) { errpkgs = append(errpkgs, info.Pkg.Path()) } else { // Enable SSA construction for packages containing only soft errors.
info.TransitivelyErrorFree = true } } if errpkgs != nil { var more string if len(errpkgs) > 3 { more = fmt.Sprintf(" and %d more", len(errpkgs)-3) errpkgs = errpkgs[:3] } return nil, fmt.Errorf("couldn't load packages due to errors: %s%s", strings.Join(errpkgs, ", "), more) } return prog, err }
func containsHardErrors(errors []error) bool { for _, err := range errors { if err, ok := err.(types.Error); ok && err.Soft { continue } return true } return false }
// 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.
//
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 *types.PkgName: // The Pos of most PkgName objects does not coincide with an identifier,
// so we suppress the usual start+len(name) heuristic for types.Objects.
start = pos.Pos() end = start case types.Object: start = pos.Pos() end = start + token.Pos(len(pos.Name())) // heuristic
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") }
func toJSON(x interface{}) []byte { b, err := json.MarshalIndent(x, "", "\t") if err != nil { log.Fatalf("JSON error: %v", err) } return b }
|