1
0
mirror of https://github.com/golang/go synced 2024-11-19 00:44:40 -07:00
go/oracle/oracle.go
Alan Donovan b28839e4bd oracle: several major improvements
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>
2015-03-30 19:21:37 +00:00

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")
}