+++ /dev/null
-// 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, safe for concurrent use
- 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
-}