go.tools/oracle: improvements to command set and performance.

Command set:
- what: an extremely fast query that parses a single
  file and returns the AST stack, package name and the
  set of query modes that apply to the current selection.
  Intended for GUI tools that need to grey out UI elements.
- definition: shows the definition of an identifier.
- pointsto: the PTA features of 'describe' have been split
  out into their own command.
- describe: with PTA stripped out, the cost is now bounded by
  type checking.

Performance:
- The importer.Config.TypeCheckFuncBodies predicate supports
  setting the 'IgnoreFuncBodies' typechecker flag on a
  per-package basis.  This means we can load dependencies from
  source more quickly if we only need exported types.
  (We avoid gcimport data because it may be absent or stale.)
  This also means we can run type-based queries on packages
  that aren't part of the pointer analysis scope. (Yay.)
- Modes that require only type analysis of the query package
  run a "what" query first, and restrict their analysis scope
  to just that package and its dependencies (sans func
  bodies), making them much faster.
- We call newOracle not oracle.New in Query, so that the
  'needs' bitset isn't ignored (oops!).  This makes the
  non-PTA queries faster.

Also:
- removed vestigial timers junk.
- pos.go: existing position utilties split out into own file.
  Added parsePosFlag utility.
- numerous cosmetic tweaks.

+ very basic tests.

To do in follow-ups:
- sophisticated editor integration of "what".
- better tests.
- refactoring of control flow as described in comment.
- changes to "implements", "describe" commands.
- update design doc + user manual.

R=crawshaw, dominik.honnef
CC=golang-dev, gri
https://golang.org/cl/40630043
This commit is contained in:
Alan Donovan 2013-12-13 10:04:55 -05:00
parent 3df3227c35
commit f119874203
43 changed files with 1518 additions and 647 deletions

View File

@ -576,7 +576,7 @@ func NodeDescription(n ast.Node) string {
if n.Tok == token.INC {
return "increment statement"
}
return "derement statement"
return "decrement statement"
case *ast.IndexExpr:
return "index expression"
case *ast.InterfaceType:

View File

@ -122,7 +122,7 @@ func main() {
os.Exit(2)
}
if len(args) == 0 {
if len(args) == 0 && mode != "what" {
fmt.Fprint(os.Stderr, "Error: no package arguments.\n"+useHelp)
os.Exit(2)
}

View File

@ -36,14 +36,18 @@
nil
"History of values supplied to `go-oracle-set-scope'.")
;; TODO(adonovan): I'd like to get rid of this separate mode since it
;; makes it harder to use the oracle.
(defvar go-oracle-mode-map
(let ((m (make-sparse-keymap)))
(define-key m (kbd "C-c C-o d") #'go-oracle-describe)
(define-key m (kbd "C-c C-o t") #'go-oracle-describe) ; t for type
(define-key m (kbd "C-c C-o f") #'go-oracle-freevars)
(define-key m (kbd "C-c C-o g") #'go-oracle-callgraph)
(define-key m (kbd "C-c C-o i") #'go-oracle-implements)
(define-key m (kbd "C-c C-o p") #'go-oracle-peers)
(define-key m (kbd "C-c C-o c") #'go-oracle-peers) ; c for channel
(define-key m (kbd "C-c C-o r") #'go-oracle-referrers)
(define-key m (kbd "C-c C-o d") #'go-oracle-definition)
(define-key m (kbd "C-c C-o p") #'go-oracle-pointsto)
(define-key m (kbd "C-c C-o s") #'go-oracle-callstack)
(define-key m (kbd "C-c C-o <") #'go-oracle-callers)
(define-key m (kbd "C-c C-o >") #'go-oracle-callees)
@ -128,7 +132,7 @@ result."
;; Hide the file/line info to save space.
;; Replace each with a little widget.
;; compilation-mode + this loop = slooow.
;; TODO(adonovan): have oracle give us an S-expression
;; TODO(adonovan): have oracle give us JSON
;; and we'll do the markup directly.
(let ((buffer-read-only nil)
(p 1))
@ -171,11 +175,21 @@ function containing the current point."
(interactive)
(go-oracle--run "callstack"))
(defun go-oracle-definition ()
"Show the definition of the selected identifier."
(interactive)
(go-oracle--run "definition"))
(defun go-oracle-describe ()
"Describe the expression at the current point."
"Describe the selected syntax, its kind, type and methods."
(interactive)
(go-oracle--run "describe"))
(defun go-oracle-pointsto ()
"Show what the selected expression points to."
(interactive)
(go-oracle--run "pointsto"))
(defun go-oracle-implements ()
"Describe the 'implements' relation for types in the package
containing the current point."

View File

@ -61,7 +61,7 @@ import (
// An Importer's exported methods are not thread-safe.
type Importer struct {
Fset *token.FileSet // position info for all files seen
config Config // the client configuration, modified by us
config *Config // the client configuration, unmodified
importfn types.Importer // client's type import function
augment map[string]bool // packages to be augmented by TestFiles when imported
allPackagesMu sync.Mutex // guards 'allPackages' during internal concurrency
@ -81,9 +81,24 @@ type importInfo struct {
// Config specifies the configuration for the importer.
type Config struct {
// TypeChecker contains options relating to the type checker.
//
// The supplied IgnoreFuncBodies is not used; the effective
// value comes from the TypeCheckFuncBodies func below.
//
// All callbacks must be thread-safe.
TypeChecker types.Config
// TypeCheckFuncBodies is a predicate over package import
// paths. A package for which the predicate is false will
// have its package-level declarations type checked, but not
// its function bodies; this can be used to quickly load
// dependencies from source. If nil, all func bodies are type
// checked.
//
// Must be thread-safe.
//
TypeCheckFuncBodies func(string) bool
// If Build is non-nil, it is used to satisfy imports.
//
// If it is nil, binary object files produced by the gc
@ -100,6 +115,13 @@ type Config struct {
// specified by config.
//
func New(config *Config) *Importer {
// Initialize by mutating the caller's copy,
// so all copies agree on the identity of the map.
if config.TypeChecker.Packages == nil {
config.TypeChecker.Packages = make(map[string]*types.Package)
}
// Save the caller's effective Import funcion.
importfn := config.TypeChecker.Import
if importfn == nil {
importfn = gcimporter.Import
@ -107,18 +129,11 @@ func New(config *Config) *Importer {
imp := &Importer{
Fset: token.NewFileSet(),
config: *config, // copy (don't clobber client input)
config: config,
importfn: importfn,
augment: make(map[string]bool),
imported: make(map[string]*importInfo),
}
// TODO(adonovan): get typechecker to supply us with a source
// position, then pass errors back to the application
// (e.g. oracle).
if imp.config.TypeChecker.Error == nil {
imp.config.TypeChecker.Error = func(e error) { fmt.Fprintln(os.Stderr, e) }
}
imp.config.TypeChecker.Import = imp.doImport // wraps importfn, effectively
return imp
}
@ -318,7 +333,18 @@ func (imp *Importer) CreatePackage(path string, files ...*ast.File) *PackageInfo
Selections: make(map[*ast.SelectorExpr]*types.Selection),
},
}
info.Pkg, info.Err = imp.config.TypeChecker.Check(path, imp.Fset, files, &info.Info)
// Use a copy of the types.Config so we can vary IgnoreFuncBodies.
tc := imp.config.TypeChecker
tc.IgnoreFuncBodies = false
if f := imp.config.TypeCheckFuncBodies; f != nil {
tc.IgnoreFuncBodies = !f(path)
}
if tc.Error == nil {
tc.Error = func(e error) { fmt.Fprintln(os.Stderr, e) }
}
tc.Import = imp.doImport // doImport wraps the user's importfn, effectively
info.Pkg, info.Err = tc.Check(path, imp.Fset, files, &info.Info)
imp.addPackage(info)
return info
}

View File

@ -4,8 +4,6 @@
package importer
// TODO(gri): absorb this into go/types.
import (
"fmt"
"go/ast"
@ -95,3 +93,17 @@ func (info *PackageInfo) TypeCaseVar(cc *ast.CaseClause) *types.Var {
}
return nil
}
// ImportSpecPkg returns the PkgName for a given ImportSpec, possibly
// an implicit one for a dot-import or an import-without-rename.
// It returns nil if not found.
//
func (info *PackageInfo) ImportSpecPkg(spec *ast.ImportSpec) *types.PkgName {
if spec.Name != nil {
return info.ObjectOf(spec.Name).(*types.PkgName)
}
if p := info.Implicits[spec]; p != nil {
return p.(*types.PkgName)
}
return nil
}

99
oracle/TODO Normal file
View File

@ -0,0 +1,99 @@
ORACLE TODO
===========
General
=======
Refactor control flow so that each mode has a "one-shot setup" function.
Use a fault-tolerant parser that can recover from bad parses.
Save unsaved editor buffers into an archive and provide that to the
tools, which should act as if they were saved.
Fix: make the guessImportPath hack work with external _test.go files too.
Allow the analysis scope to include multiple test packages at once.
Include complete pos/end information Serial output.
But beware that sometimes a single token (e.g. +) is more helpful
than the pos/end of the containing expression (e.g. x \n + \n y).
Remove pointer analysis context information when printing results as
it tends to be unhelpful.
Specific queries
================
callers, callees
Use a type-based (e.g. RTA) callgraph when a callers/callees query is
outside the analysis scope.
implements
Make it require that the selection is a type, and show only the
implements relation as it applies to that type.
definition, referrers
Use the parser's resolver information to answer the query
for local names. Only run the type checker if that fails.
(NB: gri's new parser won't do any resolution.)
referrers: Show the text of the matching line of code, like grep.
definition: Make it work with qualified identifiers (SelectorExpr) too.
references: Make it work on things that are implicit idents, like
import specs, perhaps?
what
Report def/ref info if available.
Editors could use it to highlight all idents of the same local var.
Fix: support it in (*Oracle).Query (long-running tools).
More tests.
pointsto
When invoked on a function Ident, we get an error.
When invoked on a named return parameter, we get an error.
describe
When invoked on a var, we want to see the type and its methods.
Split "show type" and "describe syntax" into separate commands?
peers
Permit querying from a makechan, close(), for...range, or reflective op.
Report aliasing reflect.{Send,Recv,Close} and close() operations.
New queries
"updaters": show all statements that may update the selected lvalue
(local, global, field, etc).
"creators": show all places where an object of type T is created
(&T{}, var t T, new(T), new(struct{array [3]T}), etc.
(Useful for datatypes whose zero value is not safe)
Editor-specific
===============
Add support for "what" to .el; clean up.
Emacs: use JSON to get the raw information from the oracle. Don't
open an editor buffer for simpler queries, just jump to the result
and/or display it in the modeline.
Support other editors: vim, Eclipse, Sublime, etc.

View File

@ -102,7 +102,7 @@ func findCallees(o *Oracle, site ssa.CallInstruction) ([]*ssa.Function, error) {
}
// Dynamic call: use pointer analysis.
o.config.BuildCallGraph = true
o.ptaConfig.BuildCallGraph = true
callgraph := ptrAnalysis(o).CallGraph
// Find all call edges from the site.

View File

@ -36,7 +36,7 @@ func callers(o *Oracle, qpos *QueryPos) (queryResult, error) {
// Run the pointer analysis, recording each
// call found to originate from target.
o.config.BuildCallGraph = true
o.ptaConfig.BuildCallGraph = true
callgraph := ptrAnalysis(o).CallGraph
var edges []call.Edge
call.GraphVisitEdges(callgraph, func(edge call.Edge) error {

View File

@ -32,7 +32,7 @@ func callgraph(o *Oracle, _ *QueryPos) (queryResult, error) {
buildSSA(o)
// Run the pointer analysis and build the complete callgraph.
o.config.BuildCallGraph = true
o.ptaConfig.BuildCallGraph = true
ptares := ptrAnalysis(o)
return &callgraphResult{

View File

@ -41,7 +41,7 @@ func callstack(o *Oracle, qpos *QueryPos) (queryResult, error) {
}
// Run the pointer analysis and build the complete call graph.
o.config.BuildCallGraph = true
o.ptaConfig.BuildCallGraph = true
callgraph := ptrAnalysis(o).CallGraph
// Search for an arbitrary path from a root to the target function.

53
oracle/definition.go Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2013 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
import (
"fmt"
"go/ast"
"go/token"
"code.google.com/p/go.tools/go/types"
"code.google.com/p/go.tools/oracle/serial"
)
// definition reports the location of the definition of an identifier.
//
// TODO(adonovan): opt: for intra-file references, the parser's
// resolution might be enough; we should start with that.
//
func definition(o *Oracle, qpos *QueryPos) (queryResult, error) {
id, _ := qpos.path[0].(*ast.Ident)
if id == nil {
return nil, fmt.Errorf("no identifier here")
}
obj := qpos.info.ObjectOf(id)
if obj == nil {
// Happens for y in "switch y := x.(type)", but I think that's all.
return nil, fmt.Errorf("no object for identifier")
}
return &definitionResult{qpos, obj}, nil
}
type definitionResult struct {
qpos *QueryPos
obj types.Object // object it denotes
}
func (r *definitionResult) display(printf printfFunc) {
printf(r.obj, "defined here as %s", r.qpos.ObjectString(r.obj))
}
func (r *definitionResult) toSerial(res *serial.Result, fset *token.FileSet) {
definition := &serial.Definition{
Desc: r.obj.String(),
}
if pos := r.obj.Pos(); pos != token.NoPos { // Package objects have no Pos()
definition.ObjPos = fset.Position(pos).String()
}
res.Definition = definition
}

View File

@ -10,8 +10,6 @@ import (
"go/ast"
"go/token"
"os"
"sort"
"strconv"
"strings"
"code.google.com/p/go.tools/astutil"
@ -19,25 +17,19 @@ import (
"code.google.com/p/go.tools/go/types"
"code.google.com/p/go.tools/importer"
"code.google.com/p/go.tools/oracle/serial"
"code.google.com/p/go.tools/pointer"
"code.google.com/p/go.tools/ssa"
)
// describe describes the syntax node denoted by the query position,
// including:
// - its syntactic category
// - the location of the definition of its referent (for identifiers)
// - the definition of its referent (for identifiers) [now redundant]
// - its type and method set (for an expression or type expression)
// - its points-to set (for a pointer-like expression)
// - its dynamic types (for an interface, reflect.Value, or
// reflect.Type expression) and their points-to sets.
//
// All printed sets are sorted to ensure determinism.
//
func describe(o *Oracle, qpos *QueryPos) (queryResult, error) {
if false { // debugging
o.fprintf(os.Stderr, qpos.path[0], "you selected: %s %s",
astutil.NodeDescription(qpos.path[0]), pathToString2(qpos.path))
fprintf(os.Stderr, o.fset, qpos.path[0], "you selected: %s %s",
astutil.NodeDescription(qpos.path[0]), pathToString(qpos.path))
}
path, action := findInterestingNode(qpos.info, qpos.path)
@ -189,7 +181,7 @@ func findInterestingNode(pkginfo *importer.PackageInfo, path []ast.Node) ([]ast.
case *ast.SelectorExpr:
if pkginfo.ObjectOf(n.Sel) == nil {
// Is this reachable?
// TODO(adonovan): is this reachable?
return path, actionUnknown
}
// Descend to .Sel child.
@ -234,6 +226,9 @@ func findInterestingNode(pkginfo *importer.PackageInfo, path []ast.Node) ([]ast.
// For reference to built-in function, return enclosing call.
path = path[1:] // ascend to enclosing function call
continue
case *types.Nil:
return path, actionExpr
}
// No object.
@ -274,6 +269,7 @@ func findInterestingNode(pkginfo *importer.PackageInfo, path []ast.Node) ([]ast.
default:
// e.g. blank identifier (go/types bug?)
// or y in "switch y := x.(type)" (go/types bug?)
// or code in a _test.go file that's not part of the package.
fmt.Printf("unknown reference %s in %T\n", n, path[1])
return path, actionUnknown
}
@ -297,54 +293,6 @@ func findInterestingNode(pkginfo *importer.PackageInfo, path []ast.Node) ([]ast.
return nil, actionUnknown // unreachable
}
// ---- VALUE ------------------------------------------------------------
// ssaValueForIdent returns the ssa.Value for the ast.Ident whose path
// to the root of the AST is path. isAddr reports whether the
// ssa.Value is the address denoted by the ast.Ident, not its value.
// ssaValueForIdent may return a nil Value without an error to
// indicate the pointer analysis is not appropriate.
//
func ssaValueForIdent(prog *ssa.Program, qinfo *importer.PackageInfo, obj types.Object, path []ast.Node) (value ssa.Value, isAddr bool, err error) {
if obj, ok := obj.(*types.Var); ok {
pkg := prog.Package(qinfo.Pkg)
pkg.Build()
if v, addr := prog.VarValue(obj, pkg, path); v != nil {
// Don't run pointer analysis on a ref to a const expression.
if _, ok := v.(*ssa.Const); ok {
return
}
return v, addr, nil
}
return nil, false, fmt.Errorf("can't locate SSA Value for var %s", obj.Name())
}
// Don't run pointer analysis on const/func objects.
return
}
// ssaValueForExpr returns the ssa.Value of the non-ast.Ident
// expression whose path to the root of the AST is path. It may
// return a nil Value without an error to indicate the pointer
// analysis is not appropriate.
//
func ssaValueForExpr(prog *ssa.Program, qinfo *importer.PackageInfo, path []ast.Node) (value ssa.Value, isAddr bool, err error) {
pkg := prog.Package(qinfo.Pkg)
pkg.SetDebugMode(true)
pkg.Build()
fn := ssa.EnclosingFunction(pkg, path)
if fn == nil {
return nil, false, fmt.Errorf("no SSA function built for this location (dead code?)")
}
if v, addr := fn.ValueForExpr(path[0].(ast.Expr)); v != nil {
return v, addr, nil
}
return nil, false, fmt.Errorf("can't locate SSA Value for expression in %s", fn)
}
func describeValue(o *Oracle, qpos *QueryPos, path []ast.Node) (*describeValueResult, error) {
var expr ast.Expr
var obj types.Object
@ -358,114 +306,28 @@ func describeValue(o *Oracle, qpos *QueryPos, path []ast.Node) (*describeValueRe
case ast.Expr:
expr = n
default:
// Is this reachable?
// TODO(adonovan): is this reachable?
return nil, fmt.Errorf("unexpected AST for expr: %T", n)
}
typ := qpos.info.TypeOf(expr)
constVal := qpos.info.ValueOf(expr)
// From this point on, we cannot fail with an error.
// Failure to run the pointer analysis will be reported later.
//
// Our disposition to pointer analysis may be one of the following:
// - ok: ssa.Value was const or func.
// - error: no ssa.Value for expr (e.g. trivially dead code)
// - ok: ssa.Value is non-pointerlike
// - error: no Pointer for ssa.Value (e.g. analytically unreachable)
// - ok: Pointer has empty points-to set
// - ok: Pointer has non-empty points-to set
// ptaErr is non-nil only in the "error:" cases.
var ptaErr error
var ptrs []pointerResult
// Only run pointer analysis on pointerlike expression types.
if pointer.CanPoint(typ) {
// Determine the ssa.Value for the expression.
var value ssa.Value
var isAddr bool
if obj != nil {
// def/ref of func/var/const object
value, isAddr, ptaErr = ssaValueForIdent(o.prog, qpos.info, obj, path)
} else {
// any other expression
if qpos.info.ValueOf(path[0].(ast.Expr)) == nil { // non-constant?
value, isAddr, ptaErr = ssaValueForExpr(o.prog, qpos.info, path)
}
}
if value != nil {
ptrs, ptaErr = describePointer(o, value, isAddr)
}
}
return &describeValueResult{
qpos: qpos,
expr: expr,
typ: typ,
constVal: constVal,
obj: obj,
ptaErr: ptaErr,
ptrs: ptrs,
}, nil
}
// describePointer runs the pointer analysis of the selected SSA value or address.
func describePointer(o *Oracle, v ssa.Value, isAddr bool) (ptrs []pointerResult, err error) {
buildSSA(o)
if isAddr {
o.config.AddIndirectQuery(v)
} else {
o.config.AddQuery(v)
}
ptares := ptrAnalysis(o)
// Combine the PT sets from all contexts.
var pointers []pointer.Pointer
if isAddr {
pointers = ptares.IndirectQueries[v]
} else {
pointers = ptares.Queries[v]
}
if pointers == nil {
return nil, fmt.Errorf("PTA did not encounter this expression (dead code?)")
}
pts := pointer.PointsToCombined(pointers)
if pointer.CanHaveDynamicTypes(v.Type()) {
// Show concrete types for interface/reflect.Value expression.
if concs := pts.DynamicTypes(); concs.Len() > 0 {
concs.Iterate(func(conc types.Type, pta interface{}) {
combined := pointer.PointsToCombined(pta.([]pointer.Pointer))
labels := combined.Labels()
sort.Sort(byPosAndString(labels)) // to ensure determinism
ptrs = append(ptrs, pointerResult{conc, labels})
})
}
} else {
// Show labels for other expressions.
labels := pts.Labels()
sort.Sort(byPosAndString(labels)) // to ensure determinism
ptrs = append(ptrs, pointerResult{v.Type(), labels})
}
sort.Sort(byTypeString(ptrs)) // to ensure determinism
return ptrs, nil
}
type pointerResult struct {
typ types.Type // type of the pointer (always concrete)
labels []*pointer.Label
}
type describeValueResult struct {
qpos *QueryPos
expr ast.Expr // query node
typ types.Type // type of expression
constVal exact.Value // value of expression, if constant
obj types.Object // var/func/const object, if expr was Ident
ptaErr error // reason why pointer analysis couldn't be run, or failed
ptrs []pointerResult // pointer info (typ is concrete => len==1)
expr ast.Expr // query node
typ types.Type // type of expression
constVal exact.Value // value of expression, if constant
obj types.Object // var/func/const object, if expr was Ident
}
func (r *describeValueResult) display(printf printfFunc) {
@ -506,81 +368,16 @@ func (r *describeValueResult) display(printf printfFunc) {
printf(r.expr, "%s of type %s", desc, r.qpos.TypeString(r.typ))
}
}
// pointer analysis could not be run
if r.ptaErr != nil {
printf(r.expr, "no points-to information: %s", r.ptaErr)
return
}
if r.ptrs == nil {
return // PTA was not invoked (not an error)
}
// Display the results of pointer analysis.
if pointer.CanHaveDynamicTypes(r.typ) {
// Show concrete types for interface, reflect.Type or
// reflect.Value expression.
if len(r.ptrs) > 0 {
printf(r.qpos, "this %s may contain these dynamic types:", r.qpos.TypeString(r.typ))
for _, ptr := range r.ptrs {
var obj types.Object
if nt, ok := deref(ptr.typ).(*types.Named); ok {
obj = nt.Obj()
}
if len(ptr.labels) > 0 {
printf(obj, "\t%s, may point to:", r.qpos.TypeString(ptr.typ))
printLabels(printf, ptr.labels, "\t\t")
} else {
printf(obj, "\t%s", r.qpos.TypeString(ptr.typ))
}
}
} else {
printf(r.qpos, "this %s cannot contain any dynamic types.", r.typ)
}
} else {
// Show labels for other expressions.
if ptr := r.ptrs[0]; len(ptr.labels) > 0 {
printf(r.qpos, "value may point to these labels:")
printLabels(printf, ptr.labels, "\t")
} else {
printf(r.qpos, "value cannot point to anything.")
}
}
}
func (r *describeValueResult) toSerial(res *serial.Result, fset *token.FileSet) {
var value, objpos, ptaerr string
var value, objpos string
if r.constVal != nil {
value = r.constVal.String()
}
if r.obj != nil {
objpos = fset.Position(r.obj.Pos()).String()
}
if r.ptaErr != nil {
ptaerr = r.ptaErr.Error()
}
var pts []*serial.DescribePointer
for _, ptr := range r.ptrs {
var namePos string
if nt, ok := deref(ptr.typ).(*types.Named); ok {
namePos = fset.Position(nt.Obj().Pos()).String()
}
var labels []serial.DescribePTALabel
for _, l := range ptr.labels {
labels = append(labels, serial.DescribePTALabel{
Pos: fset.Position(l.Pos()).String(),
Desc: l.String(),
})
}
pts = append(pts, &serial.DescribePointer{
Type: r.qpos.TypeString(ptr.typ),
NamePos: namePos,
Labels: labels,
})
}
res.Describe = &serial.Describe{
Desc: astutil.NodeDescription(r.expr),
@ -590,35 +387,10 @@ func (r *describeValueResult) toSerial(res *serial.Result, fset *token.FileSet)
Type: r.qpos.TypeString(r.typ),
Value: value,
ObjPos: objpos,
PTAErr: ptaerr,
PTS: pts,
},
}
}
type byTypeString []pointerResult
func (a byTypeString) Len() int { return len(a) }
func (a byTypeString) Less(i, j int) bool { return a[i].typ.String() < a[j].typ.String() }
func (a byTypeString) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type byPosAndString []*pointer.Label
func (a byPosAndString) Len() int { return len(a) }
func (a byPosAndString) Less(i, j int) bool {
cmp := a[i].Pos() - a[j].Pos()
return cmp < 0 || (cmp == 0 && a[i].String() < a[j].String())
}
func (a byPosAndString) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func printLabels(printf printfFunc, labels []*pointer.Label, prefix string) {
// TODO(adonovan): due to context-sensitivity, many of these
// labels may differ only by context, which isn't apparent.
for _, label := range labels {
printf(label, "%s%s", prefix, label)
}
}
// ---- TYPE ------------------------------------------------------------
func describeType(o *Oracle, qpos *QueryPos, path []ast.Node) (*describeTypeResult, error) {
@ -725,13 +497,9 @@ func describePackage(o *Oracle, qpos *QueryPos, path []ast.Node) (*describePacka
var pkg *types.Package
switch n := path[0].(type) {
case *ast.ImportSpec:
// Most ImportSpecs have no .Name Ident so we can't
// use ObjectOf.
// We could use the types.Info.Implicits mechanism,
// but it's easier just to look it up by name.
description = "import of package " + n.Path.Value
importPath, _ := strconv.Unquote(n.Path.Value)
pkg = o.prog.ImportedPackage(importPath).Object
pkgname := qpos.info.ImportSpecPkg(n)
description = fmt.Sprintf("import of package %q", pkgname.Name())
pkg = pkgname.Pkg()
case *ast.Ident:
if _, isDef := path[1].(*ast.File); isDef {
@ -771,7 +539,7 @@ func describePackage(o *Oracle, qpos *QueryPos, path []ast.Node) (*describePacka
}
}
return &describePackageResult{o.prog.Fset, path[0], description, pkg, members}, nil
return &describePackageResult{o.fset, path[0], description, pkg, members}, nil
}
type describePackageResult struct {
@ -801,7 +569,7 @@ func (r *describePackageResult) display(printf printfFunc) {
for _, mem := range r.members {
printf(mem.obj, "\t%s", formatMember(mem.obj, maxname))
for _, meth := range mem.methods {
printf(meth.Obj(), "\t\t%s", meth)
printf(meth.Obj(), "\t\t%s", types.SelectionString(r.pkg, meth))
}
}
}
@ -904,7 +672,7 @@ func describeStmt(o *Oracle, qpos *QueryPos, path []ast.Node) (*describeStmtResu
// Nothing much to say about statements.
description = astutil.NodeDescription(n)
}
return &describeStmtResult{o.prog.Fset, path[0], description}, nil
return &describeStmtResult{o.fset, path[0], description}, nil
}
type describeStmtResult struct {
@ -929,7 +697,7 @@ func (r *describeStmtResult) toSerial(res *serial.Result, fset *token.FileSet) {
// pathToString returns a string containing the concrete types of the
// nodes in path.
func pathToString2(path []ast.Node) string {
func pathToString(path []ast.Node) string {
var buf bytes.Buffer
fmt.Fprint(&buf, "[")
for i, n := range path {

View File

@ -5,7 +5,9 @@
package oracle
import (
"bytes"
"go/ast"
"go/printer"
"go/token"
"sort"
@ -120,7 +122,7 @@ func freevars(o *Oracle, qpos *QueryPos) (queryResult, error) {
}
typ := qpos.info.TypeOf(n.(ast.Expr))
ref := freevarsRef{kind, o.printNode(n), typ, obj}
ref := freevarsRef{kind, printNode(o.fset, n), typ, obj}
refsMap[ref.ref] = ref
if prune {
@ -140,14 +142,12 @@ func freevars(o *Oracle, qpos *QueryPos) (queryResult, error) {
return &freevarsResult{
qpos: qpos,
fset: o.prog.Fset,
refs: refs,
}, nil
}
type freevarsResult struct {
qpos *QueryPos
fset *token.FileSet
refs []freevarsRef
}
@ -164,7 +164,12 @@ func (r *freevarsResult) display(printf printfFunc) {
} else {
printf(r.qpos, "Free identifiers:")
for _, ref := range r.refs {
printf(ref.obj, "%s %s %s", ref.kind, ref.ref, ref.typ)
// Avoid printing "type T T".
var typstr string
if ref.kind != "type" {
typstr = " " + types.TypeString(r.qpos.info.Pkg, ref.typ)
}
printf(ref.obj, "%s %s%s", ref.kind, ref.ref, typstr)
}
}
}
@ -190,3 +195,10 @@ type byRef []freevarsRef
func (p byRef) Len() int { return len(p) }
func (p byRef) Less(i, j int) bool { return p[i].ref < p[j].ref }
func (p byRef) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// printNode returns the pretty-printed syntax of n.
func printNode(fset *token.FileSet, n ast.Node) string {
var buf bytes.Buffer
printer.Fprint(&buf, fset, n)
return buf.String()
}

View File

@ -11,7 +11,7 @@ import (
"code.google.com/p/go.tools/oracle/serial"
)
// Implements displays the 'implements" relation among all
// Implements displays the "implements" relation among all
// package-level named types in the package containing the query
// position.
//
@ -65,7 +65,7 @@ func implements(o *Oracle, qpos *QueryPos) (queryResult, error) {
}
// TODO(adonovan): sort facts to ensure test nondeterminism.
return &implementsResult{o.prog.Fset, facts}, nil
return &implementsResult{o.fset, facts}, nil
}
type implementsFact struct {

View File

@ -13,22 +13,48 @@ package oracle
// This file defines oracle.Query, the entry point for the oracle tool.
// The actual executable is defined in cmd/oracle.
// TODO(adonovan): new query: show all statements that may update the
// selected lvalue (local, global, field, etc).
// 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.
// ORACLE CONTROL FLOW
//
// The Oracle is somewhat convoluted due to the need to support two
// very different use-cases, "one-shot" and "long running", and to do
// so quickly.
//
// The cmd/oracle tool issues "one-shot" queries via the exported
// Query function, which creates an Oracle to answer a single query.
// newOracle consults the 'needs' flags of the query mode and the
// package containing the query to avoid doing more work than it needs
// (loading, parsing, type checking, SSA construction).
//
// The Pythia tool (github.com/fzipp/pythia) is an example of a "long
// running" tool. It calls New() and then loops, calling
// ParseQueryPos and (*Oracle).Query to handle each incoming HTTP
// query. Since New cannot see which queries will follow, it must
// load, parse, type-check and SSA-build the entire transitive closure
// of the analysis scope, retaining full debug information and all
// typed ASTs.
//
// TODO(adonovan): experiment with inverting the control flow by
// making each mode consist of two functions: a "one-shot setup"
// function and the existing "impl" function. The one-shot setup
// function would do all of the work of Query and newOracle,
// specialized to each mode, calling library utilities for the common
// things. This would give it more control over "scope reduction".
// Long running tools would not call the one-shot setup function but
// would have their own setup function equivalent to the existing
// 'needsAll' flow path.
import (
"bytes"
"fmt"
"go/ast"
"go/build"
"go/printer"
"go/token"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"code.google.com/p/go.tools/astutil"
"code.google.com/p/go.tools/go/types"
@ -40,29 +66,25 @@ import (
// An Oracle holds the program state required for one or more queries.
type Oracle struct {
out io.Writer // standard output
prog *ssa.Program // the SSA program [only populated if need&SSA]
config pointer.Config // pointer analysis configuration [TODO rename ptaConfig]
// need&AllTypeInfo
typeInfo map[*types.Package]*importer.PackageInfo // type info for all ASTs in the program
timers map[string]time.Duration // phase timing information
fset *token.FileSet // file set [all queries]
prog *ssa.Program // the SSA program [needSSA]
ptaConfig pointer.Config // pointer analysis configuration [needPTA]
typeInfo map[*types.Package]*importer.PackageInfo // type info for all ASTs in the program [needRetainTypeInfo]
}
// A set of bits indicating the analytical requirements of each mode.
//
// Typed ASTs for the whole program are always constructed
// transiently; they are retained only for the queried package unless
// needAllTypeInfo is set.
// needRetainTypeInfo is set.
const (
needPos = 1 << iota // needs a position
needExactPos // needs an exact AST selection; implies needPos
needAllTypeInfo // needs to retain type info for all ASTs in the program
needSSA // needs ssa.Packages for whole program
needSSADebug // needs debug info for ssa.Packages
needPTA = needSSA // needs pointer analysis
needAll = -1 // needs everything (e.g. a sequence of queries)
needPos = 1 << iota // needs a position
needExactPos // needs an exact AST selection; implies needPos
needRetainTypeInfo // needs to retain type info for all ASTs in the program
needSSA // needs ssa.Packages for whole program
needSSADebug // needs debug info for ssa.Packages
needPTA = needSSA // needs pointer analysis
needAll = -1 // needs everything (e.g. a sequence of queries)
)
type modeInfo struct {
@ -72,15 +94,20 @@ type modeInfo struct {
}
var modes = []*modeInfo{
// Pointer analyses: (whole program)
{"callees", needPTA | needExactPos, callees},
{"callers", needPTA | needPos, callers},
{"callgraph", needPTA, callgraph},
{"callstack", needPTA | needPos, callstack},
{"describe", needPTA | needSSADebug | needExactPos, describe},
{"peers", needPTA | needSSADebug | needPos, peers},
{"pointsto", needPTA | needSSADebug | needExactPos, pointsto},
// Type-based analyses: (modular, mostly)
{"definition", needPos, definition},
{"describe", needExactPos, describe},
{"freevars", needPos, freevars},
{"implements", needPos, implements},
{"peers", needPTA | needSSADebug | needPos, peers},
{"referrers", needAllTypeInfo | needPos, referrers},
{"referrers", needRetainTypeInfo | needPos, referrers}, // (whole-program)
}
func findMode(mode string) *modeInfo {
@ -106,9 +133,11 @@ type queryResult interface {
// Instances are created by ParseQueryPos.
//
type QueryPos struct {
fset *token.FileSet
start, end token.Pos // source extent of query
info *importer.PackageInfo // type info for the queried package
path []ast.Node // AST path from query node to root of ast.File
exact bool // 2nd result of PathEnclosingInterval
info *importer.PackageInfo // type info for the queried package (nil for fastQueryPos)
}
// TypeString prints type T relative to the query position.
@ -128,9 +157,7 @@ func (qpos *QueryPos) SelectionString(sel *types.Selection) string {
// A Result encapsulates the result of an oracle.Query.
type Result struct {
fset *token.FileSet
// fprintf is a closure over the oracle's fileset and start/end position.
fprintf func(w io.Writer, pos interface{}, format string, args ...interface{})
fset *token.FileSet
q queryResult // the query-specific result
mode string // query mode
warnings []pointer.Warning // pointer analysis warnings
@ -180,31 +207,38 @@ func (res *Result) Serial() *serial.Result {
// depends on the query mode; how should we expose this?
//
func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *build.Context, reflection bool) (*Result, error) {
if mode == "what" {
// Bypass package loading, type checking, SSA construction.
return what(pos, buildContext)
}
minfo := findMode(mode)
if minfo == nil {
return nil, fmt.Errorf("invalid mode type: %q", mode)
}
imp := importer.New(&importer.Config{Build: buildContext})
o, err := New(imp, args, ptalog, reflection)
impcfg := importer.Config{Build: buildContext}
// For queries needing only a single typed package,
// reduce the analysis scope to that package.
if minfo.needs&(needSSA|needRetainTypeInfo) == 0 {
reduceScope(pos, &impcfg, &args)
}
// TODO(adonovan): report type errors to the user via Serial
// types, not stderr?
// impcfg.TypeChecker.Error = func(err error) {
// E := err.(types.Error)
// fmt.Fprintf(os.Stderr, "%s: %s\n", E.Fset.Position(E.Pos), E.Msg)
// }
imp := importer.New(&impcfg)
o, err := newOracle(imp, args, ptalog, minfo.needs, reflection)
if err != nil {
return nil, err
}
// Phase timing diagnostics.
// TODO(adonovan): needs more work.
// if false {
// defer func() {
// fmt.Println()
// for name, duration := range o.timers {
// fmt.Printf("# %-30s %s\n", name, duration)
// }
// }()
// }
var qpos *QueryPos
if minfo.needs&(needPos|needExactPos) != 0 {
var err error
qpos, err = ParseQueryPos(imp, pos, minfo.needs&needExactPos != 0)
if err != nil {
return nil, err
@ -218,6 +252,58 @@ func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *buil
return o.query(minfo, qpos)
}
// reduceScope is called for one-shot queries that need only a single
// typed package. It attempts to guess the query package from pos and
// reduce the analysis scope (set of loaded packages) to just that one
// plus (the exported parts of) its dependencies. It leaves its
// arguments unchanged on failure.
//
// TODO(adonovan): this is a real mess... but it's fast.
//
func reduceScope(pos string, impcfg *importer.Config, args *[]string) {
// TODO(adonovan): make the 'args' argument of
// (*Importer).LoadInitialPackages part of the
// importer.Config, and inline LoadInitialPackages into
// NewImporter. Then we won't need the 'args' argument.
fqpos, err := fastQueryPos(pos)
if err != nil {
return // bad query
}
// TODO(adonovan): fix: this gives the wrong results for files
// in non-importable packages such as tests and ad-hoc packages
// specified as a list of files (incl. the oracle's tests).
_, importPath, err := guessImportPath(fqpos.fset.File(fqpos.start).Name(), impcfg.Build)
if err != nil {
return // can't find GOPATH dir
}
if importPath == "" {
return
}
// 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 importer/util.go!
ctxt2 := *impcfg.Build
ctxt2.CgoEnabled = false
bp, err := ctxt2.Import(importPath, "", 0)
if err != nil {
return // no files for package
}
_ = bp
// TODO(adonovan): fix: also check that the queried file appears in the package.
// for _, f := range bp.GoFiles, bp.TestGoFiles, bp.XTestGoFiles {
// if sameFile(f, fqpos.filename) { goto found }
// }
// return // not found
// found:
impcfg.TypeCheckFuncBodies = func(p string) bool { return p == importPath }
*args = []string{importPath}
}
// New constructs a new Oracle that can be used for a sequence of queries.
//
// imp will be used to load source code for imported packages.
@ -233,15 +319,9 @@ func New(imp *importer.Importer, args []string, ptalog io.Writer, reflection boo
}
func newOracle(imp *importer.Importer, args []string, ptalog io.Writer, needs int, reflection bool) (*Oracle, error) {
o := &Oracle{
prog: ssa.NewProgram(imp.Fset, 0),
timers: make(map[string]time.Duration),
}
o.config.Log = ptalog
o.config.Reflection = reflection
o := &Oracle{fset: imp.Fset}
// Load/parse/type-check program from args.
start := time.Now()
initialPkgInfos, args, err := imp.LoadInitialPackages(args)
if err != nil {
return nil, err // I/O or parser error
@ -249,10 +329,9 @@ func newOracle(imp *importer.Importer, args []string, ptalog io.Writer, needs in
if len(args) > 0 {
return nil, fmt.Errorf("surplus arguments: %q", args)
}
o.timers["load/parse/type"] = time.Since(start)
// Retain type info for all ASTs in the program.
if needs&needAllTypeInfo != 0 {
if needs&needRetainTypeInfo != 0 {
m := make(map[*types.Package]*importer.PackageInfo)
for _, p := range imp.AllPackages() {
m[p.Pkg] = p
@ -262,49 +341,56 @@ func newOracle(imp *importer.Importer, args []string, ptalog io.Writer, needs in
// Create SSA package for the initial packages and their dependencies.
if needs&needSSA != 0 {
start = time.Now()
prog := ssa.NewProgram(o.fset, 0)
// Create SSA packages.
if err := o.prog.CreatePackages(imp); err != nil {
if err := prog.CreatePackages(imp); err != nil {
return nil, err
}
// 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 []*ssa.Package
var testPkgs, mains []*ssa.Package
for _, info := range initialPkgInfos {
initialPkg := o.prog.Package(info.Pkg)
initialPkg := prog.Package(info.Pkg)
// Add package to the pointer analysis scope.
if initialPkg.Func("main") != nil {
o.config.Mains = append(o.config.Mains, initialPkg)
mains = append(mains, initialPkg)
} else {
testPkgs = append(testPkgs, initialPkg)
}
}
if testPkgs != nil {
if p := o.prog.CreateTestMainPackage(testPkgs...); p != nil {
o.config.Mains = append(o.config.Mains, p)
if p := prog.CreateTestMainPackage(testPkgs...); p != nil {
mains = append(mains, p)
}
}
if o.config.Mains == nil {
if mains == nil {
return nil, fmt.Errorf("analysis scope has no main and no tests")
}
o.ptaConfig.Log = ptalog
o.ptaConfig.Reflection = reflection
o.ptaConfig.Mains = mains
if needs&needSSADebug != 0 {
for _, pkg := range o.prog.AllPackages() {
for _, pkg := range prog.AllPackages() {
pkg.SetDebugMode(true)
}
}
o.timers["SSA-create"] = time.Since(start)
o.prog = prog
}
return o, nil
}
// Query runs the query of the specified mode and selection.
//
// TODO(adonovan): fix: this function does not currently support the
// "what" query, which needs to access the go/build.Context.
//
func (o *Oracle) Query(mode string, qpos *QueryPos) (*Result, error) {
minfo := findMode(mode)
if minfo == nil {
@ -314,10 +400,13 @@ func (o *Oracle) Query(mode string, qpos *QueryPos) (*Result, error) {
}
func (o *Oracle) query(minfo *modeInfo, qpos *QueryPos) (*Result, error) {
// Clear out residue of previous query (for long-running clients).
o.ptaConfig.Queries = nil
o.ptaConfig.IndirectQueries = nil
res := &Result{
mode: minfo.name,
fset: o.prog.Fset,
fprintf: o.fprintf, // captures o.prog, o.{start,end}Pos for later printing
mode: minfo.name,
fset: o.fset,
}
var err error
res.q, err = minfo.impl(o, qpos)
@ -328,10 +417,16 @@ func (o *Oracle) query(minfo *modeInfo, qpos *QueryPos) (*Result, error) {
}
// ParseQueryPos parses the source query position pos.
// If needExact, it must identify a single AST subtree.
// If needExact, it must identify a single AST subtree;
// this is appropriate for queries that allow fairly arbitrary syntax,
// e.g. "describe".
//
func ParseQueryPos(imp *importer.Importer, pos string, needExact bool) (*QueryPos, error) {
start, end, err := parseQueryPos(imp.Fset, pos)
func ParseQueryPos(imp *importer.Importer, posFlag string, needExact bool) (*QueryPos, error) {
filename, startOffset, endOffset, err := parsePosFlag(posFlag)
if err != nil {
return nil, err
}
start, end, err := findQueryPos(imp.Fset, filename, startOffset, endOffset)
if err != nil {
return nil, err
}
@ -342,13 +437,13 @@ func ParseQueryPos(imp *importer.Importer, pos string, needExact bool) (*QueryPo
if needExact && !exact {
return nil, fmt.Errorf("ambiguous selection within %s", astutil.NodeDescription(path[0]))
}
return &QueryPos{start, end, info, path}, nil
return &QueryPos{imp.Fset, start, end, path, exact, info}, nil
}
// WriteTo writes the oracle query result res to out in a compiler diagnostic format.
func (res *Result) WriteTo(out io.Writer) {
printf := func(pos interface{}, format string, args ...interface{}) {
res.fprintf(out, pos, format, args...)
fprintf(out, res.fset, pos, format, args...)
}
res.q.display(printf)
@ -367,110 +462,12 @@ func (res *Result) WriteTo(out io.Writer) {
// Not needed in simpler modes, e.g. freevars.
//
func buildSSA(o *Oracle) {
start := time.Now()
o.prog.BuildAll()
o.timers["SSA-build"] = time.Since(start)
}
// ptrAnalysis runs the pointer analysis and returns its result.
func ptrAnalysis(o *Oracle) *pointer.Result {
start := time.Now()
result := pointer.Analyze(&o.config)
o.timers["pointer analysis"] = time.Since(start)
return result
}
// parseOctothorpDecimal returns the numeric value if s matches "#%d",
// otherwise -1.
func parseOctothorpDecimal(s string) int {
if s != "" && s[0] == '#' {
if s, err := strconv.ParseInt(s[1:], 10, 32); err == nil {
return int(s)
}
}
return -1
}
// parseQueryPos parses a string of the form "file:pos" or
// file:start,end" where pos, start, end match #%d and represent byte
// offsets, and returns the extent to which it refers.
//
// (Numbers without a '#' prefix are reserved for future use,
// e.g. to indicate line/column positions.)
//
func parseQueryPos(fset *token.FileSet, queryPos string) (start, end token.Pos, err error) {
if queryPos == "" {
err = fmt.Errorf("no source position specified (-pos flag)")
return
}
colon := strings.LastIndex(queryPos, ":")
if colon < 0 {
err = fmt.Errorf("invalid source position -pos=%q", queryPos)
return
}
filename, offset := queryPos[:colon], queryPos[colon+1:]
startOffset := -1
endOffset := -1
if hyphen := strings.Index(offset, ","); hyphen < 0 {
// e.g. "foo.go:#123"
startOffset = parseOctothorpDecimal(offset)
endOffset = startOffset
} else {
// e.g. "foo.go:#123,#456"
startOffset = parseOctothorpDecimal(offset[:hyphen])
endOffset = parseOctothorpDecimal(offset[hyphen+1:])
}
if startOffset < 0 || endOffset < 0 {
err = fmt.Errorf("invalid -pos offset %q", offset)
return
}
var file *token.File
fset.Iterate(func(f *token.File) bool {
if sameFile(filename, f.Name()) {
// (f.Name() is absolute)
file = f
return false // done
}
return true // continue
})
if file == nil {
err = fmt.Errorf("couldn't find file containing position -pos=%q", queryPos)
return
}
// Range check [start..end], inclusive of both end-points.
if 0 <= startOffset && startOffset <= file.Size() {
start = file.Pos(int(startOffset))
} else {
err = fmt.Errorf("start position is beyond end of file -pos=%q", queryPos)
return
}
if 0 <= endOffset && endOffset <= file.Size() {
end = file.Pos(int(endOffset))
} else {
err = fmt.Errorf("end position is beyond end of file -pos=%q", queryPos)
return
}
return
}
// sameFile returns true if x and y have the same basename and denote
// the same file.
//
func sameFile(x, y string) bool {
if filepath.Base(x) == filepath.Base(y) { // (optimisation)
if xi, err := os.Stat(x); err == nil {
if yi, err := os.Stat(y); err == nil {
return os.SameFile(xi, yi)
}
}
}
return false
return pointer.Analyze(&o.ptaConfig)
}
// unparen returns e with any enclosing parentheses stripped.
@ -508,7 +505,7 @@ func deref(typ types.Type) types.Type {
// compilation-error-regexp in Emacs' compilation mode.
// TODO(adonovan): support other editors.
//
func (o *Oracle) fprintf(w io.Writer, pos interface{}, format string, args ...interface{}) {
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:
@ -531,11 +528,11 @@ func (o *Oracle) fprintf(w io.Writer, pos interface{}, format string, args ...in
panic(fmt.Sprintf("invalid pos: %T", pos))
}
if sp := o.prog.Fset.Position(start); start == end {
if sp := fset.Position(start); start == end {
// (prints "-: " for token.NoPos)
fmt.Fprintf(w, "%s: ", sp)
} else {
ep := o.prog.Fset.Position(end)
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.
@ -547,10 +544,3 @@ func (o *Oracle) fprintf(w io.Writer, pos interface{}, format string, args ...in
fmt.Fprintf(w, format, args...)
io.WriteString(w, "\n")
}
// printNode returns the pretty-printed syntax of n.
func (o *Oracle) printNode(n ast.Node) string {
var buf bytes.Buffer
printer.Fprint(&buf, o.prog.Fset, n)
return buf.String()
}

View File

@ -206,13 +206,17 @@ func TestOracle(t *testing.T) {
"testdata/src/main/implements.go",
"testdata/src/main/imports.go",
"testdata/src/main/peers.go",
"testdata/src/main/pointsto.go",
"testdata/src/main/reflection.go",
"testdata/src/main/what.go",
// JSON:
"testdata/src/main/callgraph-json.go",
"testdata/src/main/calls-json.go",
"testdata/src/main/peers-json.go",
"testdata/src/main/describe-json.go",
"testdata/src/main/pointsto-json.go",
"testdata/src/main/referrers-json.go",
"testdata/src/main/what-json.go",
} {
useJson := strings.HasSuffix(filename, "-json.go")
queries := parseQueries(t, filename)

View File

@ -60,11 +60,11 @@ func peers(o *Oracle, qpos *QueryPos) (queryResult, error) {
// ignore both directionality and type names.
queryType := queryOp.ch.Type()
queryElemType := queryType.Underlying().(*types.Chan).Elem()
o.config.AddQuery(queryOp.ch)
o.ptaConfig.AddQuery(queryOp.ch)
i := 0
for _, op := range ops {
if types.IsIdentical(op.ch.Type().Underlying().(*types.Chan).Elem(), queryElemType) {
o.config.AddQuery(op.ch)
o.ptaConfig.AddQuery(op.ch)
ops[i] = op
i++
}

257
oracle/pointsto.go Normal file
View File

@ -0,0 +1,257 @@
// Copyright 2013 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
import (
"fmt"
"go/ast"
"go/token"
"sort"
"code.google.com/p/go.tools/astutil"
"code.google.com/p/go.tools/go/types"
"code.google.com/p/go.tools/importer"
"code.google.com/p/go.tools/oracle/serial"
"code.google.com/p/go.tools/pointer"
"code.google.com/p/go.tools/ssa"
)
// pointsto runs the pointer analysis on the selected expression,
// and reports its points-to set (for a pointer-like expression)
// or its dynamic types (for an interface, reflect.Value, or
// reflect.Type expression) and their points-to sets.
//
// All printed sets are sorted to ensure determinism.
//
func pointsto(o *Oracle, qpos *QueryPos) (queryResult, error) {
path, action := findInterestingNode(qpos.info, qpos.path)
if action != actionExpr {
return nil, fmt.Errorf("pointer analysis wants an expression; got %s",
astutil.NodeDescription(qpos.path[0]))
}
var expr ast.Expr
var obj types.Object
switch n := path[0].(type) {
case *ast.ValueSpec:
// ambiguous ValueSpec containing multiple names
return nil, fmt.Errorf("multiple value specification")
case *ast.Ident:
obj = qpos.info.ObjectOf(n)
expr = n
case ast.Expr:
expr = n
default:
// TODO(adonovan): is this reachable?
return nil, fmt.Errorf("unexpected AST for expr: %T", n)
}
// Reject non-pointerlike types (includes all constants).
typ := qpos.info.TypeOf(expr)
if !pointer.CanPoint(typ) {
return nil, fmt.Errorf("pointer analysis wants an expression of reference type; got %s", typ)
}
// Determine the ssa.Value for the expression.
var value ssa.Value
var isAddr bool
var err error
if obj != nil {
// def/ref of func/var object
value, isAddr, err = ssaValueForIdent(o.prog, qpos.info, obj, path)
} else {
value, isAddr, err = ssaValueForExpr(o.prog, qpos.info, path)
}
if err != nil {
return nil, err // e.g. trivially dead code
}
// Run the pointer analysis.
ptrs, err := runPTA(o, value, isAddr)
if err != nil {
return nil, err // e.g. analytically unreachable
}
return &pointstoResult{
qpos: qpos,
typ: typ,
ptrs: ptrs,
}, nil
}
// ssaValueForIdent returns the ssa.Value for the ast.Ident whose path
// to the root of the AST is path. isAddr reports whether the
// ssa.Value is the address denoted by the ast.Ident, not its value.
//
func ssaValueForIdent(prog *ssa.Program, qinfo *importer.PackageInfo, obj types.Object, path []ast.Node) (value ssa.Value, isAddr bool, err error) {
switch obj := obj.(type) {
case *types.Var:
pkg := prog.Package(qinfo.Pkg)
pkg.Build()
if v, addr := prog.VarValue(obj, pkg, path); v != nil {
return v, addr, nil
}
return nil, false, fmt.Errorf("can't locate SSA Value for var %s", obj.Name())
case *types.Func:
return prog.FuncValue(obj), false, nil
}
panic(obj)
}
// ssaValueForExpr returns the ssa.Value of the non-ast.Ident
// expression whose path to the root of the AST is path.
//
func ssaValueForExpr(prog *ssa.Program, qinfo *importer.PackageInfo, path []ast.Node) (value ssa.Value, isAddr bool, err error) {
pkg := prog.Package(qinfo.Pkg)
pkg.SetDebugMode(true)
pkg.Build()
fn := ssa.EnclosingFunction(pkg, path)
if fn == nil {
return nil, false, fmt.Errorf("no SSA function built for this location (dead code?)")
}
if v, addr := fn.ValueForExpr(path[0].(ast.Expr)); v != nil {
return v, addr, nil
}
return nil, false, fmt.Errorf("can't locate SSA Value for expression in %s", fn)
}
// runPTA runs the pointer analysis of the selected SSA value or address.
func runPTA(o *Oracle, v ssa.Value, isAddr bool) (ptrs []pointerResult, err error) {
buildSSA(o)
if isAddr {
o.ptaConfig.AddIndirectQuery(v)
} else {
o.ptaConfig.AddQuery(v)
}
ptares := ptrAnalysis(o)
// Combine the PT sets from all contexts.
var pointers []pointer.Pointer
if isAddr {
pointers = ptares.IndirectQueries[v]
} else {
pointers = ptares.Queries[v]
}
if pointers == nil {
return nil, fmt.Errorf("pointer analysis did not find expression (dead code?)")
}
pts := pointer.PointsToCombined(pointers)
if pointer.CanHaveDynamicTypes(v.Type()) {
// Show concrete types for interface/reflect.Value expression.
if concs := pts.DynamicTypes(); concs.Len() > 0 {
concs.Iterate(func(conc types.Type, pta interface{}) {
combined := pointer.PointsToCombined(pta.([]pointer.Pointer))
labels := combined.Labels()
sort.Sort(byPosAndString(labels)) // to ensure determinism
ptrs = append(ptrs, pointerResult{conc, labels})
})
}
} else {
// Show labels for other expressions.
labels := pts.Labels()
sort.Sort(byPosAndString(labels)) // to ensure determinism
ptrs = append(ptrs, pointerResult{v.Type(), labels})
}
sort.Sort(byTypeString(ptrs)) // to ensure determinism
return ptrs, nil
}
type pointerResult struct {
typ types.Type // type of the pointer (always concrete)
labels []*pointer.Label // set of labels
}
type pointstoResult struct {
qpos *QueryPos
typ types.Type // type of expression
ptrs []pointerResult // pointer info (typ is concrete => len==1)
}
func (r *pointstoResult) display(printf printfFunc) {
if pointer.CanHaveDynamicTypes(r.typ) {
// Show concrete types for interface, reflect.Type or
// reflect.Value expression.
if len(r.ptrs) > 0 {
printf(r.qpos, "this %s may contain these dynamic types:", r.qpos.TypeString(r.typ))
for _, ptr := range r.ptrs {
var obj types.Object
if nt, ok := deref(ptr.typ).(*types.Named); ok {
obj = nt.Obj()
}
if len(ptr.labels) > 0 {
printf(obj, "\t%s, may point to:", r.qpos.TypeString(ptr.typ))
printLabels(printf, ptr.labels, "\t\t")
} else {
printf(obj, "\t%s", r.qpos.TypeString(ptr.typ))
}
}
} else {
printf(r.qpos, "this %s cannot contain any dynamic types.", r.typ)
}
} else {
// Show labels for other expressions.
if ptr := r.ptrs[0]; len(ptr.labels) > 0 {
printf(r.qpos, "this %s may point to these objects:",
r.qpos.TypeString(r.typ))
printLabels(printf, ptr.labels, "\t")
} else {
printf(r.qpos, "this %s may not point to anything.",
r.qpos.TypeString(r.typ))
}
}
}
func (r *pointstoResult) toSerial(res *serial.Result, fset *token.FileSet) {
var pts []serial.PointsTo
for _, ptr := range r.ptrs {
var namePos string
if nt, ok := deref(ptr.typ).(*types.Named); ok {
namePos = fset.Position(nt.Obj().Pos()).String()
}
var labels []serial.PointsToLabel
for _, l := range ptr.labels {
labels = append(labels, serial.PointsToLabel{
Pos: fset.Position(l.Pos()).String(),
Desc: l.String(),
})
}
pts = append(pts, serial.PointsTo{
Type: r.qpos.TypeString(ptr.typ),
NamePos: namePos,
Labels: labels,
})
}
res.PointsTo = pts
}
type byTypeString []pointerResult
func (a byTypeString) Len() int { return len(a) }
func (a byTypeString) Less(i, j int) bool { return a[i].typ.String() < a[j].typ.String() }
func (a byTypeString) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type byPosAndString []*pointer.Label
func (a byPosAndString) Len() int { return len(a) }
func (a byPosAndString) Less(i, j int) bool {
cmp := a[i].Pos() - a[j].Pos()
return cmp < 0 || (cmp == 0 && a[i].String() < a[j].String())
}
func (a byPosAndString) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func printLabels(printf printfFunc, labels []*pointer.Label, prefix string) {
// TODO(adonovan): due to context-sensitivity, many of these
// labels may differ only by context, which isn't apparent.
for _, label := range labels {
printf(label, "%s%s", prefix, label)
}
}

149
oracle/pos.go Normal file
View File

@ -0,0 +1,149 @@
package oracle
// This file defines utilities for working with file positions.
import (
"fmt"
"go/parser"
"go/token"
"os"
"path/filepath"
"strconv"
"strings"
"code.google.com/p/go.tools/astutil"
)
// parseOctothorpDecimal returns the numeric value if s matches "#%d",
// otherwise -1.
func parseOctothorpDecimal(s string) int {
if s != "" && s[0] == '#' {
if s, err := strconv.ParseInt(s[1:], 10, 32); err == nil {
return int(s)
}
}
return -1
}
// parsePosFlag parses a string of the form "file:pos" or
// file:start,end" where pos, start, end match #%d and represent byte
// offsets, and returns its components.
//
// (Numbers without a '#' prefix are reserved for future use,
// e.g. to indicate line/column positions.)
//
func parsePosFlag(posFlag string) (filename string, startOffset, endOffset int, err error) {
if posFlag == "" {
err = fmt.Errorf("no source position specified (-pos flag)")
return
}
colon := strings.LastIndex(posFlag, ":")
if colon < 0 {
err = fmt.Errorf("invalid source position -pos=%q", posFlag)
return
}
filename, offset := posFlag[:colon], posFlag[colon+1:]
startOffset = -1
endOffset = -1
if hyphen := strings.Index(offset, ","); hyphen < 0 {
// e.g. "foo.go:#123"
startOffset = parseOctothorpDecimal(offset)
endOffset = startOffset
} else {
// e.g. "foo.go:#123,#456"
startOffset = parseOctothorpDecimal(offset[:hyphen])
endOffset = parseOctothorpDecimal(offset[hyphen+1:])
}
if startOffset < 0 || endOffset < 0 {
err = fmt.Errorf("invalid -pos offset %q", offset)
return
}
return
}
// findQueryPos searches fset for filename and translates the
// specified file-relative byte offsets into token.Pos form. It
// returns an error if the file was not found or the offsets were out
// of bounds.
//
func findQueryPos(fset *token.FileSet, filename string, startOffset, endOffset int) (start, end token.Pos, err error) {
var file *token.File
fset.Iterate(func(f *token.File) bool {
if sameFile(filename, f.Name()) {
// (f.Name() is absolute)
file = f
return false // done
}
return true // continue
})
if file == nil {
err = fmt.Errorf("couldn't find file containing position")
return
}
// Range check [start..end], inclusive of both end-points.
if 0 <= startOffset && startOffset <= file.Size() {
start = file.Pos(int(startOffset))
} else {
err = fmt.Errorf("start position is beyond end of file")
return
}
if 0 <= endOffset && endOffset <= file.Size() {
end = file.Pos(int(endOffset))
} else {
err = fmt.Errorf("end position is beyond end of file")
return
}
return
}
// sameFile returns true if x and y have the same basename and denote
// the same file.
//
func sameFile(x, y string) bool {
if filepath.Base(x) == filepath.Base(y) { // (optimisation)
if xi, err := os.Stat(x); err == nil {
if yi, err := os.Stat(y); err == nil {
return os.SameFile(xi, yi)
}
}
}
return false
}
// fastQueryPos parses the -pos flag and returns a QueryPos.
// It parses only a single file, and does not run the type checker.
//
// Caveat: the token.{FileSet,Pos} info it contains is not comparable
// with that from the oracle's FileSet! (We don't accept oracle.fset
// as a parameter because we don't want the same filename to appear
// multiple times in one FileSet.)
//
func fastQueryPos(posFlag string) (*QueryPos, error) {
filename, startOffset, endOffset, err := parsePosFlag(posFlag)
if err != nil {
return nil, err
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, 0)
if err != nil {
return nil, err
}
start, end, err := findQueryPos(fset, filename, startOffset, endOffset)
if err != nil {
return nil, err
}
path, exact := astutil.PathEnclosingInterval(f, start, end)
if path == nil {
return nil, fmt.Errorf("no syntax here")
}
return &QueryPos{fset, start, end, path, exact, nil}, nil
}

View File

@ -30,21 +30,21 @@ func referrers(o *Oracle, qpos *QueryPos) (queryResult, error) {
}
// Iterate over all go/types' resolver facts for the entire program.
var refs []token.Pos
var refs []*ast.Ident
for _, info := range o.typeInfo {
for id2, obj2 := range info.Objects {
if sameObj(obj, obj2) {
if id2.NamePos == obj.Pos() {
continue // skip defining ident
}
refs = append(refs, id2.NamePos)
refs = append(refs, id2)
}
}
}
sort.Sort(byPos(refs))
sort.Sort(byNamePos(refs))
return &referrersResult{
query: id.NamePos,
query: id,
obj: obj,
refs: refs,
}, nil
@ -65,14 +65,22 @@ func sameObj(x, y types.Object) bool {
return false
}
// -------- utils --------
type byNamePos []*ast.Ident
func (p byNamePos) Len() int { return len(p) }
func (p byNamePos) Less(i, j int) bool { return p[i].NamePos < p[j].NamePos }
func (p byNamePos) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
type referrersResult struct {
query token.Pos // identifer of query
query *ast.Ident // identifer of query
obj types.Object // object it denotes
refs []token.Pos // set of all other references to it
refs []*ast.Ident // set of all other references to it
}
func (r *referrersResult) display(printf printfFunc) {
if r.query != r.obj.Pos() {
if r.query.Pos() != r.obj.Pos() {
printf(r.query, "reference to %s", r.obj.Name())
}
// TODO(adonovan): pretty-print object using same logic as
@ -85,16 +93,18 @@ func (r *referrersResult) display(printf printfFunc) {
}
}
// TODO(adonovan): encode extent, not just Pos info, in Serial form.
func (r *referrersResult) toSerial(res *serial.Result, fset *token.FileSet) {
referrers := &serial.Referrers{
Pos: fset.Position(r.query).String(),
Pos: fset.Position(r.query.Pos()).String(),
Desc: r.obj.String(),
}
if pos := r.obj.Pos(); pos != token.NoPos { // Package objects have no Pos()
referrers.ObjPos = fset.Position(pos).String()
}
for _, ref := range r.refs {
referrers.Refs = append(referrers.Refs, fset.Position(ref).String())
referrers.Refs = append(referrers.Refs, fset.Position(ref.NamePos).String())
}
res.Referrers = referrers
}

View File

@ -32,6 +32,12 @@ type Referrers struct {
Refs []string `json:"refs,omitempty"` // locations of all references
}
// A Definition is the result of a 'definition' query.
type Definition struct {
ObjPos string `json:"objpos,omitempty"` // location of the definition
Desc string `json:"desc"` // description of the denoted object
}
type CalleesItem struct {
Name string `json:"name"` // full name of called function
Pos string `json:"pos"` // location of called function
@ -104,7 +110,25 @@ type Implements struct {
CPos string `json:"cpos"` // location of its definition
}
// A DescribePTALabel describes a pointer analysis label.
// A SyntaxNode is one element of a stack of enclosing syntax nodes in
// a "what" query.
type SyntaxNode struct {
Description string `json:"desc"` // description of syntax tree
Start int `json:"start"` // start offset (0-based)
End int `json:"end"` // end offset
}
// A What is the result of the "what" query, which quickly identifies
// the selection, parsing only a single file. It is intended for use
// in low-latency GUIs.
type What struct {
Enclosing []SyntaxNode `json:"enclosing"` // enclosing nodes of syntax tree
Modes []string `json:"modes"` // query modes enabled for this selection.
SrcDir string `json:"srcdir,omitempty"` // $GOROOT src directory containing queried package
ImportPath string `json:"importpath,omitempty"` // import path of queried package
}
// A PointsToLabel describes a pointer analysis label.
//
// A "label" is an object that may be pointed to by a pointer, map,
// channel, 'func', slice or interface. Labels include:
@ -116,35 +140,33 @@ type Implements struct {
// - channels, maps and arrays created by make()
// - and their subelements, e.g. "alloc.y[*].z"
//
type DescribePTALabel struct {
type PointsToLabel struct {
Pos string `json:"pos"` // location of syntax that allocated the object
Desc string `json:"desc"` // description of the label
}
// A DescribePointer describes a single pointer: its type and the
// set of "labels" it points to.
// A PointsTo is one element of the result of a 'pointsto' query on an
// expression. It describes a single pointer: its type and the set of
// "labels" it points to.
//
type DescribePointer struct {
Type string `json:"type"` // (concrete) type of the pointer
NamePos string `json:"namepos,omitempty"` // location of type defn, if Named
Labels []DescribePTALabel `json:"labels,omitempty"` // pointed-to objects
}
// A DescribeValue is the additional result of a 'describe' query
// if the selection indicates a value or expression.
//
// If the described value is an interface, it will have one PTS entry
// If the pointer is of interface type, it will have one PTS entry
// describing each concrete type that it may contain. For each
// concrete type that is a pointer, the PTS entry describes the labels
// it may point to. The same is true for reflect.Values, except the
// dynamic types needn't be concrete.
//
type PointsTo struct {
Type string `json:"type"` // (concrete) type of the pointer
NamePos string `json:"namepos,omitempty"` // location of type defn, if Named
Labels []PointsToLabel `json:"labels,omitempty"` // pointed-to objects
}
// A DescribeValue is the additional result of a 'describe' query
// if the selection indicates a value or expression.
type DescribeValue struct {
Type string `json:"type"` // type of the expression
Value string `json:"value,omitempty"` // value of the expression, if constant
ObjPos string `json:"objpos,omitempty"` // location of the definition, if an Ident
PTAErr string `json:"ptaerr,omitempty"` // reason pointer analysis wasn't attempted
PTS []*DescribePointer `json:"pts,omitempty"` // points-to set; an interface may have many
Type string `json:"type"` // type of the expression
Value string `json:"value,omitempty"` // value of the expression, if constant
ObjPos string `json:"objpos,omitempty"` // location of the definition, if an Ident
}
type DescribeMethod struct {
@ -200,8 +222,6 @@ type PTAWarning struct {
// A Result is the common result of any oracle query.
// It contains a query-specific result element.
//
// TODO(adonovan): perhaps include other info such as: analysis scope,
// raw query position, stack of ast nodes, query package, etc.
type Result struct {
Mode string `json:"mode"` // mode of the query
@ -211,11 +231,14 @@ type Result struct {
Callers []Caller `json:"callers,omitempty"`
Callgraph []CallGraph `json:"callgraph,omitempty"`
Callstack *CallStack `json:"callstack,omitempty"`
Definition *Definition `json:"definition,omitempty"`
Describe *Describe `json:"describe,omitempty"`
Freevars []*FreeVar `json:"freevars,omitempty"`
Implements []*Implements `json:"implements,omitempty"`
Peers *Peers `json:"peers,omitempty"`
PointsTo []PointsTo `json:"pointsto,omitempty"`
Referrers *Referrers `json:"referrers,omitempty"`
What *What `json:"what,omitempty"`
Warnings []PTAWarning `json:"warnings,omitempty"` // warnings from pointer analysis
}

View File

@ -4,12 +4,12 @@ package main
// See go.tools/oracle/oracle_test.go for explanation.
// See calls.golden for expected query results.
func A(x *int) { // @describe describe-A-x "x"
func A(x *int) { // @pointsto pointsto-A-x "x"
// @callers callers-A "^"
// @callstack callstack-A "^"
}
func B(x *int) { // @describe describe-B-x "x"
func B(x *int) { // @pointsto pointsto-B-x "x"
// @callers callers-B "^"
}
@ -28,7 +28,7 @@ func store(ptr **int, value *int) {
func call(f func() *int) {
// Result points to anon function.
f() // @describe describe-result-f "f"
f() // @pointsto pointsto-result-f "f"
// Target of call is anon function.
f() // @callees callees-main.call-f "f"
@ -42,10 +42,10 @@ func main() {
apply(B, &b)
var c, d int
var pc, pd *int // @describe describe-pc "pc"
var pc, pd *int // @pointsto pointsto-pc "pc"
store(&pc, &c)
store(&pd, &d)
_ = pd // @describe describe-pd "pd"
_ = pd // @pointsto pointsto-pd "pd"
call(func() *int {
// We are called twice from main.call
@ -71,12 +71,12 @@ func main() {
var dynamic = func() {}
// Within dead code, dynamic calls have no callees.
func deadcode() {
main() // @callees callees-err-deadcode2 "main"
// @callers callers-err-deadcode "^"
// @callstack callstack-err-deadcode "^"
// Within dead code, dynamic calls have no callees.
dynamic() // @callees callees-err-deadcode3 "dynamic"
}

View File

@ -1,6 +1,5 @@
-------- @describe describe-A-x --------
definition of var x *int
value may point to these labels:
-------- @pointsto pointsto-A-x --------
this *int may point to these objects:
a
b
@ -10,9 +9,8 @@ main.A
dynamic function call from main.apply
static function call from main.main
-------- @describe describe-B-x --------
definition of var x *int
value may point to these labels:
-------- @pointsto pointsto-B-x --------
this *int may point to these objects:
a
b
@ -35,10 +33,8 @@ main.store is called from these 2 sites:
static function call from main.main
static function call from main.main
-------- @describe describe-result-f --------
reference to var f func() *int
defined here
value may point to these labels:
-------- @pointsto pointsto-result-f --------
this func() *int may point to these objects:
func@50.7
-------- @callees callees-main.call-f --------
@ -54,15 +50,12 @@ main.call is called from these 2 sites:
this static function call dispatches to:
main.apply
-------- @describe describe-pc --------
definition of var pc *int
value may point to these labels:
-------- @pointsto pointsto-pc --------
this *int may point to these objects:
c
-------- @describe describe-pd --------
reference to var pd *int
defined here
value may point to these labels:
-------- @pointsto pointsto-pd --------
this *int may point to these objects:
d
-------- @callees callees-err-no-call --------

View File

@ -79,18 +79,7 @@
"detail": "value",
"value": {
"type": "*int",
"objpos": "testdata/src/main/describe-json.go:11:2",
"pts": [
{
"type": "*int",
"labels": [
{
"pos": "testdata/src/main/describe-json.go:10:6",
"desc": "s.x[*]"
}
]
}
]
"objpos": "testdata/src/main/describe-json.go:11:2"
}
}
}-------- @describe desc-val-i --------
@ -102,23 +91,7 @@
"detail": "value",
"value": {
"type": "I",
"objpos": "testdata/src/main/describe-json.go:14:6",
"pts": [
{
"type": "*D",
"namepos": "testdata/src/main/describe-json.go:28:6",
"labels": [
{
"pos": "testdata/src/main/describe-json.go:16:10",
"desc": "new"
}
]
},
{
"type": "C",
"namepos": "testdata/src/main/describe-json.go:27:6"
}
]
"objpos": "testdata/src/main/describe-json.go:14:6"
}
}
}-------- @describe desc-stmt --------

View File

@ -69,13 +69,11 @@ func main() { // @describe func-def-main "main"
go main() // @describe go-stmt "go"
panic(3) // @describe builtin-ref-panic "panic"
}
func deadcode() {
var a int // @describe var-decl-stmt "var a int"
// Pointer analysis can't run on dead code.
var b = &a // @describe b "b"
_ = b
var a2 int // @describe var-decl-stmt "var a2 int"
_ = a2
var _ int // @describe var-decl-stmt2 "var _ int"
var _ int // @describe var-def-blank "_"
}
type I interface { // @describe def-iface-I "I"

View File

@ -1,18 +1,17 @@
-------- @describe pkgdecl --------
definition of package "describe"
type C int
method (*describe.C) f()
type D struct{}
method (describe.D) f()
type I interface{f()}
method (describe.I) f()
const c untyped integer = 0
type cake float64
func deadcode func()
var global *string
func main func()
const pi untyped float = 3141/1000
const pie cake = 1768225803696341/562949953421312
type C int
method (*C) f()
type D struct{}
method (D) f()
type I interface{f()}
method (I) f()
const c untyped integer = 0
type cake float64
var global *string
func main func()
const pi untyped float = 3141/1000
const pie cake = 1768225803696341/562949953421312
-------- @describe type-ref-builtin --------
reference to built-in type float64
@ -76,58 +75,37 @@ defined here
-------- @describe ref-anon --------
reference to var anon func()
defined here
value may point to these labels:
func@31.10
-------- @describe ref-global --------
reference to var global *string
defined here
value may point to these labels:
new
-------- @describe var-def-x-1 --------
definition of var x *int
value may point to these labels:
a
-------- @describe var-ref-x-1 --------
reference to var x *int
defined here
value may point to these labels:
a
-------- @describe var-def-x-2 --------
reference to var x *int
defined here
value may point to these labels:
b
-------- @describe var-ref-x-2 --------
reference to var x *int
defined here
value may point to these labels:
b
-------- @describe var-ref-i-C --------
reference to var i I
defined here
this I may contain these dynamic types:
*C, may point to:
new
-------- @describe var-ref-i-D --------
reference to var i I
defined here
this I may contain these dynamic types:
D
-------- @describe var-ref-i --------
reference to var i I
defined here
this I may contain these dynamic types:
*C, may point to:
new
D
-------- @describe const-local-pi --------
definition of const localpi untyped float
@ -160,14 +138,10 @@ index expression of type (*int, bool)
-------- @describe mapval --------
reference to var mapval *int
defined here
value may point to these labels:
a
-------- @describe m --------
reference to var m map[string]*int
defined here
value may point to these labels:
makemap
-------- @describe defer-stmt --------
defer statement
@ -179,11 +153,13 @@ go statement
function call (or conversion) of type ()
-------- @describe var-decl-stmt --------
definition of var a int
definition of var a2 int
-------- @describe b --------
definition of var b *int
no points-to information: PTA did not encounter this expression (dead code?)
-------- @describe var-decl-stmt2 --------
definition of var _ int
-------- @describe var-def-blank --------
definition of var _ int
-------- @describe def-iface-I --------
definition of type I (size 16, align 8)

View File

@ -1,6 +1,6 @@
-------- @freevars fv1 --------
Free identifiers:
type C main.C
type C
const exp int
var x int

View File

@ -20,7 +20,7 @@ func main() {
var t lib.Type // @describe ref-type "Type"
p := t.Method(&a) // @describe ref-method "Method"
print(*p + 1) // @describe p "p "
print(*p + 1) // @pointsto p "p "
var _ lib.Type // @describe ref-pkg "lib"
}

View File

@ -3,7 +3,7 @@ import of package "lib"
const Const untyped integer = 3
func Func func()
type Type int
method (lib.Type) Method(x *int) *int
method (Type) Method(x *int) *int
var Var int
-------- @describe ref-const --------
@ -28,10 +28,8 @@ Method set:
reference to method func (lib.Type).Method(x *int) *int
defined here
-------- @describe p --------
reference to var p *int
defined here
value may point to these labels:
-------- @pointsto p --------
this *int may point to these objects:
imports.a
-------- @describe ref-pkg --------
@ -39,6 +37,6 @@ reference to package "lib"
const Const untyped integer = 3
func Func func()
type Type int
method (lib.Type) Method(x *int) *int
method (Type) Method(x *int) *int
var Var int

View File

@ -20,15 +20,15 @@ func main() {
b := 3
chB <- &b
<-chA // @describe describe-chA "chA"
<-chA2 // @describe describe-chA2 "chA2"
<-chB // @describe describe-chB "chB"
<-chA // @pointsto pointsto-chA "chA"
<-chA2 // @pointsto pointsto-chA2 "chA2"
<-chB // @pointsto pointsto-chB "chB"
select {
case rA := <-chA: // @peers peer-recv-chA "<-"
_ = rA // @describe describe-rA "rA"
_ = rA // @pointsto pointsto-rA "rA"
case rB := <-chB: // @peers peer-recv-chB "<-"
_ = rB // @describe describe-rB "rB"
_ = rB // @pointsto pointsto-rB "rB"
case <-chA: // @peers peer-recv-chA' "<-"

View File

@ -1,20 +1,14 @@
-------- @describe describe-chA --------
reference to var chA chan *int
defined here
value may point to these labels:
-------- @pointsto pointsto-chA --------
this chan *int may point to these objects:
makechan
makechan
-------- @describe describe-chA2 --------
reference to var chA2 chan *int
defined here
value may point to these labels:
-------- @pointsto pointsto-chA2 --------
this chan *int may point to these objects:
makechan
-------- @describe describe-chB --------
reference to var chB chan *int
defined here
value may point to these labels:
-------- @pointsto pointsto-chB --------
this chan *int may point to these objects:
makechan
-------- @peers peer-recv-chA --------
@ -29,10 +23,8 @@ This channel of type chan *int may be:
received from, here
received from, here
-------- @describe describe-rA --------
reference to var rA *int
defined here
value may point to these labels:
-------- @pointsto pointsto-rA --------
this *int may point to these objects:
peers.a2
a1
@ -43,10 +35,8 @@ This channel of type chan *int may be:
received from, here
received from, here
-------- @describe describe-rB --------
reference to var rB *int
defined here
value may point to these labels:
-------- @pointsto pointsto-rB --------
this *int may point to these objects:
b
-------- @peers peer-recv-chA' --------

View File

@ -0,0 +1,27 @@
package pointsto
// Tests of 'pointsto' queries, -format=json.
// See go.tools/oracle/oracle_test.go for explanation.
// See pointsto-json.golden for expected query results.
func main() { //
var s struct{ x [3]int }
p := &s.x[0] // @pointsto val-p "p"
_ = p
var i I = C(0)
if i == nil {
i = new(D)
}
print(i) // @pointsto val-i "\\bi\\b"
}
type I interface {
f()
}
type C int
type D struct{}
func (c C) f() {}
func (d *D) f() {}

View File

@ -0,0 +1,34 @@
-------- @pointsto val-p --------
{
"mode": "pointsto",
"pointsto": [
{
"type": "*int",
"labels": [
{
"pos": "testdata/src/main/pointsto-json.go:8:6",
"desc": "s.x[*]"
}
]
}
]
}-------- @pointsto val-i --------
{
"mode": "pointsto",
"pointsto": [
{
"type": "*D",
"namepos": "testdata/src/main/pointsto-json.go:24:6",
"labels": [
{
"pos": "testdata/src/main/pointsto-json.go:14:10",
"desc": "new"
}
]
},
{
"type": "C",
"namepos": "testdata/src/main/pointsto-json.go:23:6"
}
]
}

68
oracle/testdata/src/main/pointsto.go vendored Normal file
View File

@ -0,0 +1,68 @@
package pointsto
// Tests of 'pointsto' query.
// See go.tools/oracle/oracle_test.go for explanation.
// See pointsto.golden for expected query results.
const pi = 3.141 // @pointsto const "pi"
var global = new(string) // NB: ssa.Global is indirect, i.e. **string
func main() {
livecode()
// func objects
_ = main // @pointsto func-ref-main "main"
_ = (*C).f // @pointsto func-ref-*C.f "..C..f"
_ = D.f // @pointsto func-ref-D.f "D.f"
_ = I.f // @pointsto func-ref-I.f "I.f"
var d D
var i I
_ = d.f // @pointsto func-ref-d.f "d.f"
_ = i.f // @pointsto func-ref-i.f "i.f"
// var objects
anon := func() {
_ = d.f // @pointsto ref-lexical-d.f "d.f"
}
_ = anon // @pointsto ref-anon "anon"
_ = global // @pointsto ref-global "global"
// SSA affords some local flow sensitivity.
var a, b int
var x = &a // @pointsto var-def-x-1 "x"
_ = x // @pointsto var-ref-x-1 "x"
x = &b // @pointsto var-def-x-2 "x"
_ = x // @pointsto var-ref-x-2 "x"
i = new(C) // @pointsto var-ref-i-C "i"
if i != nil {
i = D{} // @pointsto var-ref-i-D "i"
}
print(i) // @pointsto var-ref-i "\\bi\\b"
m := map[string]*int{"a": &a}
mapval, _ := m["a"] // @pointsto map-lookup,ok "m..a.."
_ = mapval // @pointsto mapval "mapval"
_ = m // @pointsto m "m"
panic(3) // @pointsto builtin-panic "panic"
}
func livecode() {} // @pointsto func-live "livecode"
func deadcode() { // @pointsto func-dead "deadcode"
// Pointer analysis can't run on dead code.
var b = new(int) // @pointsto b "b"
_ = b
}
type I interface {
f()
}
type C int
type D struct{}
func (c *C) f() {}
func (d D) f() {}

View File

@ -0,0 +1,93 @@
-------- @pointsto const --------
Error: pointer analysis wants an expression of reference type; got untyped float
-------- @pointsto func-ref-main --------
this func() may point to these objects:
pointsto.main
-------- @pointsto func-ref-*C.f --------
this func() may point to these objects:
(*pointsto.C).f
-------- @pointsto func-ref-D.f --------
this func() may point to these objects:
(pointsto.D).f
-------- @pointsto func-ref-I.f --------
this func() may point to these objects:
(pointsto.I).f
-------- @pointsto func-ref-d.f --------
this func() may point to these objects:
(pointsto.D).f
-------- @pointsto func-ref-i.f --------
this func() may point to these objects:
(pointsto.I).f
-------- @pointsto ref-lexical-d.f --------
this func() may point to these objects:
(pointsto.D).f
-------- @pointsto ref-anon --------
this func() may point to these objects:
func@25.10
-------- @pointsto ref-global --------
this *string may point to these objects:
new
-------- @pointsto var-def-x-1 --------
this *int may point to these objects:
a
-------- @pointsto var-ref-x-1 --------
this *int may point to these objects:
a
-------- @pointsto var-def-x-2 --------
this *int may point to these objects:
b
-------- @pointsto var-ref-x-2 --------
this *int may point to these objects:
b
-------- @pointsto var-ref-i-C --------
this I may contain these dynamic types:
*C, may point to:
new
-------- @pointsto var-ref-i-D --------
this I may contain these dynamic types:
D
-------- @pointsto var-ref-i --------
this I may contain these dynamic types:
*C, may point to:
new
D
-------- @pointsto map-lookup,ok --------
Error: pointer analysis wants an expression of reference type; got (*int, bool)
-------- @pointsto mapval --------
this *int may point to these objects:
a
-------- @pointsto m --------
this map[string]*int may point to these objects:
makemap
-------- @pointsto builtin-panic --------
Error: pointer analysis wants an expression of reference type; got ()
-------- @pointsto func-live --------
Error: pointer analysis did not find expression (dead code?)
-------- @pointsto func-dead --------
Error: pointer analysis did not find expression (dead code?)
-------- @pointsto b --------
Error: pointer analysis did not find expression (dead code?)

View File

@ -1,7 +1,7 @@
package reflection
// This is a test of 'describe', but we split it into a separate file
// so that describe.go doesn't have to import "reflect" each time.
// This is a test of 'pointsto', but we split it into a separate file
// so that pointsto.go doesn't have to import "reflect" each time.
import "reflect"
@ -20,11 +20,11 @@ func main() {
mrv = reflect.ValueOf(&a)
}
_ = mrv // @describe mrv "mrv"
p1 := mrv.Interface() // @describe p1 "p1"
p2 := mrv.MapKeys() // @describe p2 "p2"
p3 := p2[0] // @describe p3 "p3"
p4 := reflect.TypeOf(p1) // @describe p4 "p4"
_ = mrv // @pointsto mrv "mrv"
p1 := mrv.Interface() // @pointsto p1 "p1"
p2 := mrv.MapKeys() // @pointsto p2 "p2"
p3 := p2[0] // @pointsto p3 "p3"
p4 := reflect.TypeOf(p1) // @pointsto p4 "p4"
_, _, _, _ = p1, p2, p3, p4
}

View File

@ -1,6 +1,4 @@
-------- @describe mrv --------
reference to var mrv reflect.Value
defined here
-------- @pointsto mrv --------
this reflect.Value may contain these dynamic types:
*bool, may point to:
reflection.b
@ -9,8 +7,7 @@ this reflect.Value may contain these dynamic types:
map[*int]*bool, may point to:
makemap
-------- @describe p1 --------
definition of var p1 interface{}
-------- @pointsto p1 --------
this interface{} may contain these dynamic types:
*bool, may point to:
reflection.b
@ -19,19 +16,16 @@ this interface{} may contain these dynamic types:
map[*int]*bool, may point to:
makemap
-------- @describe p2 --------
definition of var p2 []reflect.Value
value may point to these labels:
-------- @pointsto p2 --------
this []reflect.Value may point to these objects:
<alloc in (reflect.Value).MapKeys>
-------- @describe p3 --------
definition of var p3 reflect.Value
-------- @pointsto p3 --------
this reflect.Value may contain these dynamic types:
*int, may point to:
reflection.a
-------- @describe p4 --------
definition of var p4 reflect.Type
-------- @pointsto p4 --------
this reflect.Type may contain these dynamic types:
*reflect.rtype, may point to:
*bool

9
oracle/testdata/src/main/what-json.go vendored Normal file
View File

@ -0,0 +1,9 @@
package what
// Tests of 'what' queries, -format=json.
// See go.tools/oracle/oracle_test.go for explanation.
// See what-json.golden for expected query results.
func main() {
f() // @what call "f"
}

View File

@ -0,0 +1,52 @@
-------- @what call --------
{
"mode": "what",
"what": {
"enclosing": [
{
"desc": "identifier",
"start": 179,
"end": 180
},
{
"desc": "function call (or conversion)",
"start": 179,
"end": 182
},
{
"desc": "expression statement",
"start": 179,
"end": 182
},
{
"desc": "block",
"start": 176,
"end": 202
},
{
"desc": "function declaration",
"start": 164,
"end": 202
},
{
"desc": "source file",
"start": 0,
"end": 202
}
],
"modes": [
"callees",
"callers",
"callgraph",
"callstack",
"definition",
"describe",
"freevars",
"implements",
"pointsto",
"referrers"
],
"srcdir": "testdata/src",
"importpath": "main"
}
}

11
oracle/testdata/src/main/what.go vendored Normal file
View File

@ -0,0 +1,11 @@
package what // @what pkgdecl "what"
// Tests of 'what' queries.
// See go.tools/oracle/oracle_test.go for explanation.
// See what.golden for expected query results.
func main() {
f() // @what call "f"
var ch chan int // @what var "var"
<-ch // @what recv "ch"
}

39
oracle/testdata/src/main/what.golden vendored Normal file
View File

@ -0,0 +1,39 @@
-------- @what pkgdecl --------
identifier
source file
modes: [callgraph definition describe freevars implements pointsto referrers]
srcdir: testdata/src
import path: main
-------- @what call --------
identifier
function call (or conversion)
expression statement
block
function declaration
source file
modes: [callees callers callgraph callstack definition describe freevars implements pointsto referrers]
srcdir: testdata/src
import path: main
-------- @what var --------
variable declaration
variable declaration statement
block
function declaration
source file
modes: [callers callgraph callstack describe freevars implements pointsto]
srcdir: testdata/src
import path: main
-------- @what recv --------
identifier
unary <- operation
expression statement
block
function declaration
source file
modes: [callers callgraph callstack definition describe freevars implements peers pointsto referrers]
srcdir: testdata/src
import path: main

197
oracle/what.go Normal file
View File

@ -0,0 +1,197 @@
// Copyright 2013 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
import (
"fmt"
"go/ast"
"go/build"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
"code.google.com/p/go.tools/astutil"
"code.google.com/p/go.tools/oracle/serial"
)
// what reports all the information about the query selection that can be
// obtained from parsing only its containing source file.
// It is intended to be a very low-latency query callable from GUI
// tools, e.g. to populate a menu of options of slower queries about
// the selected location.
//
func what(posFlag string, buildContext *build.Context) (*Result, error) {
qpos, err := fastQueryPos(posFlag)
if err != nil {
return nil, err
}
// (ignore errors)
srcdir, importPath, _ := guessImportPath(qpos.fset.File(qpos.start).Name(), buildContext)
// Determine which query modes are applicable to the selection.
enable := map[string]bool{
"callgraph": true, // whole program; always enabled
"implements": true, // whole package; always enabled
"freevars": qpos.end > qpos.start, // nonempty selection?
"describe": true, // any syntax; always enabled
}
for _, n := range qpos.path {
switch n := n.(type) {
case *ast.Ident:
enable["definition"] = true
enable["referrers"] = true
case *ast.CallExpr:
enable["callees"] = true
case *ast.FuncDecl:
enable["callers"] = true
enable["callstack"] = true
case *ast.SendStmt:
enable["peers"] = true
case *ast.UnaryExpr:
if n.Op == token.ARROW {
enable["peers"] = true
}
}
// For pointsto, we approximate findInterestingNode.
if _, ok := enable["pointsto"]; !ok {
switch n.(type) {
case ast.Stmt,
*ast.ArrayType,
*ast.StructType,
*ast.FuncType,
*ast.InterfaceType,
*ast.MapType,
*ast.ChanType:
enable["pointsto"] = false // not an expr
case ast.Expr, ast.Decl, *ast.ValueSpec:
enable["pointsto"] = true // an expr, maybe
default:
// Comment, Field, KeyValueExpr, etc: ascend.
}
}
}
// If we don't have an exact selection, disable modes that need one.
if !qpos.exact {
for _, minfo := range modes {
if minfo.needs&needExactPos != 0 {
enable[minfo.name] = false
}
}
}
var modes []string
for mode := range enable {
modes = append(modes, mode)
}
sort.Strings(modes)
return &Result{
mode: "what",
fset: qpos.fset,
q: &whatResult{
path: qpos.path,
srcdir: srcdir,
importPath: importPath,
modes: modes,
},
}, nil
}
// guessImportPath finds the package containing filename, and returns
// its source directory (an element of $GOPATH) and its import path
// relative to it.
//
// TODO(adonovan): what about _test.go files that are not part of the
// package?
//
func guessImportPath(filename string, buildContext *build.Context) (srcdir, importPath string, err error) {
absFile, err := filepath.Abs(filename)
if err != nil {
err = fmt.Errorf("can't form absolute path of %s", filename)
return
}
absFileDir := segments(filepath.Dir(absFile))
// Find the innermost directory in $GOPATH that encloses filename.
minD := 1024
for _, gopathDir := range buildContext.SrcDirs() {
absDir, err := filepath.Abs(gopathDir)
if err != nil {
continue // e.g. non-existent dir on $GOPATH
}
d := prefixLen(segments(absDir), absFileDir)
// If there are multiple matches,
// prefer the innermost enclosing directory
// (smallest d).
if d >= 0 && d < minD {
minD = d
srcdir = gopathDir
importPath = strings.Join(absFileDir[len(absFileDir)-minD:], string(os.PathSeparator))
}
}
if srcdir == "" {
err = fmt.Errorf("can't find package for file %s", filename)
}
return
}
func segments(path string) []string {
return strings.Split(path, string(os.PathSeparator))
}
// prefixLen returns the length of the remainder of y if x is a prefix
// of y, a negative number otherwise.
func prefixLen(x, y []string) int {
d := len(y) - len(x)
if d >= 0 {
for i := range x {
if y[i] != x[i] {
return -1 // not a prefix
}
}
}
return d
}
type whatResult struct {
path []ast.Node
modes []string
srcdir string
importPath string
}
func (r *whatResult) display(printf printfFunc) {
for _, n := range r.path {
printf(n, "%s", astutil.NodeDescription(n))
}
printf(nil, "modes: %s", r.modes)
printf(nil, "srcdir: %s", r.srcdir)
printf(nil, "import path: %s", r.importPath)
}
func (r *whatResult) toSerial(res *serial.Result, fset *token.FileSet) {
var enclosing []serial.SyntaxNode
for _, n := range r.path {
enclosing = append(enclosing, serial.SyntaxNode{
Description: astutil.NodeDescription(n),
Start: fset.Position(n.Pos()).Offset,
End: fset.Position(n.End()).Offset,
})
}
res.What = &serial.What{
Modes: r.modes,
SrcDir: r.srcdir,
ImportPath: r.importPath,
Enclosing: enclosing,
}
}

View File

@ -101,6 +101,8 @@ func (l Label) Pos() token.Pos {
// append.y[*].z (array allocated by append)
// makeslice.y[*].z (array allocated via make)
//
// TODO(adonovan): expose func LabelString(*types.Package, Label).
//
func (l Label) String() string {
var s string
switch v := l.obj.data.(type) {