| // 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/loader" |
| ) |
| |
| 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 |
| |
| // 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 "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) |
| case "callees", "callers", "pointsto", "whicherrs", "callstack", "peers": |
| return fmt.Errorf("mode %q is no longer supported (see Go issue #59676)", mode) |
| default: |
| return fmt.Errorf("invalid mode: %q", mode) |
| } |
| } |
| |
| // 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) {} |
| } |
| |
| 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, etc. |
| // - a QueryPos, denoting the extent of the user's query. |
| // - nil, meaning no position at all. |
| // |
| // The output format 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 |
| } |