mirror of
https://github.com/golang/go
synced 2024-11-19 00:44:40 -07:00
b28839e4bd
Features: More robust: silently ignore type errors in modes that don't need SSA form: describe, referrers, implements, freevars, description. This makes the tool much more robust for everyday queries. Less configuration: don't require a scope argument for all queries. Only queries that do pointer analysis need it. For the rest, the initial position is enough for importQueryPackage to deduce the scope. It now works for queries in GoFiles, TestGoFiles, or XTestGoFiles. (It no longer works for ad-hoc main packages like $GOROOT/src/net/http/triv.go) More complete: "referrers" computes the scope automatically by scanning the import graph of the entire workspace, using gorename's refactor/importgraph package. This requires two passes at loading. Faster: simplified start-up logic avoids unnecessary package loading and SSA construction (a consequence of bad abstraction) in many cases. "callgraph": remove it. Unlike all the other commands it isn't related to the current selection, and we have golang.org/x/tools/cmdcallgraph now. Internals: Drop support for long-running clients (i.e., Pythia), since godoc -analysis supports all the same features except "pointsto", and precomputes all the results so latency is much lower. Get rid of various unhelpful abstractions introduced to support long-running clients. Expand out the set-up logic for each subcommand. This is simpler, easier to read, and gives us more control, at a small cost in duplication---the familiar story of abstractions. Discard PTA warnings. We weren't showing them (nor should we). Split tests into separate directories (so that importgraph works). Change-Id: I55d46b3ab33cdf7ac22436fcc2148fe04c901237 Reviewed-on: https://go-review.googlesource.com/8243 Reviewed-by: David Crawshaw <crawshaw@golang.org>
342 lines
10 KiB
Go
342 lines
10 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 oracle contains the implementation of the oracle tool whose
|
|
// command-line is provided by golang.org/x/tools/cmd/oracle.
|
|
//
|
|
// http://golang.org/s/oracle-design
|
|
// http://golang.org/s/oracle-user-manual
|
|
//
|
|
package oracle // import "golang.org/x/tools/oracle"
|
|
|
|
// This file defines oracle.Query, the entry point for the oracle tool.
|
|
// The actual executable is defined in cmd/oracle.
|
|
|
|
// 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/token"
|
|
"io"
|
|
"path/filepath"
|
|
|
|
"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"
|
|
"golang.org/x/tools/go/types"
|
|
"golang.org/x/tools/oracle/serial"
|
|
)
|
|
|
|
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(qpos.info.Pkg, T)
|
|
}
|
|
|
|
// ObjectString prints object obj relative to the query position.
|
|
func (qpos *queryPos) objectString(obj types.Object) string {
|
|
return types.ObjectString(qpos.info.Pkg, obj)
|
|
}
|
|
|
|
// SelectionString prints selection sel relative to the query position.
|
|
func (qpos *queryPos) selectionString(sel *types.Selection) string {
|
|
return types.SelectionString(qpos.info.Pkg, sel)
|
|
}
|
|
|
|
// A Query specifies a single oracle 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 package 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 oracle 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 oracle query and populates its Fset and Result.
|
|
func Run(conf *Query) error {
|
|
switch conf.Mode {
|
|
case "callees":
|
|
return callees(conf)
|
|
case "callers":
|
|
return callers(conf)
|
|
case "callstack":
|
|
return callstack(conf)
|
|
case "peers":
|
|
return peers(conf)
|
|
case "pointsto":
|
|
return pointsto(conf)
|
|
case "whicherrs":
|
|
return whicherrs(conf)
|
|
case "definition":
|
|
return definition(conf)
|
|
case "describe":
|
|
return describe(conf)
|
|
case "freevars":
|
|
return freevars(conf)
|
|
case "implements":
|
|
return implements(conf)
|
|
case "referrers":
|
|
return referrers(conf)
|
|
case "what":
|
|
return what(conf)
|
|
default:
|
|
return fmt.Errorf("invalid mode: %q", conf.Mode)
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func importQueryPackage(pos string, conf *loader.Config) error {
|
|
fqpos, err := fastQueryPos(pos)
|
|
if err != nil {
|
|
return err // bad query
|
|
}
|
|
filename := fqpos.fset.File(fqpos.start).Name()
|
|
|
|
// This will not work for ad-hoc packages
|
|
// such as $GOROOT/src/net/http/triv.go.
|
|
// TODO(adonovan): ensure we report a clear error.
|
|
_, importPath, err := guessImportPath(filename, conf.Build)
|
|
if err != nil {
|
|
return err // can't find GOPATH dir
|
|
}
|
|
if importPath == "" {
|
|
return fmt.Errorf("can't guess import path from %s", filename)
|
|
}
|
|
|
|
// Check that it's possible to load the queried package.
|
|
// (e.g. oracle 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:
|
|
return fmt.Errorf("package %q doesn't contain file %s",
|
|
importPath, filename)
|
|
}
|
|
|
|
conf.TypeCheckFuncBodies = func(p string) bool { return p == importPath }
|
|
|
|
return 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, posFlag string, needExact bool) (*queryPos, error) {
|
|
filename, startOffset, endOffset, err := parsePosFlag(posFlag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
start, end, err := findQueryPos(lprog.Fset, filename, 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) {
|
|
lconf.AllowErrors = true
|
|
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")
|
|
}
|