diff --git a/astutil/enclosing.go b/astutil/enclosing.go index f5b17876..d24ab606 100644 --- a/astutil/enclosing.go +++ b/astutil/enclosing.go @@ -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: diff --git a/cmd/oracle/main.go b/cmd/oracle/main.go index ead98353..cd4bf155 100644 --- a/cmd/oracle/main.go +++ b/cmd/oracle/main.go @@ -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) } diff --git a/cmd/oracle/oracle.el b/cmd/oracle/oracle.el index 03b267be..dacf9da1 100644 --- a/cmd/oracle/oracle.el +++ b/cmd/oracle/oracle.el @@ -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." diff --git a/importer/importer.go b/importer/importer.go index 38fb0c30..daf94bac 100644 --- a/importer/importer.go +++ b/importer/importer.go @@ -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 } diff --git a/importer/pkginfo.go b/importer/pkginfo.go index b0d099f3..7cdfec1c 100644 --- a/importer/pkginfo.go +++ b/importer/pkginfo.go @@ -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 +} diff --git a/oracle/TODO b/oracle/TODO new file mode 100644 index 00000000..0951de1e --- /dev/null +++ b/oracle/TODO @@ -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. diff --git a/oracle/callees.go b/oracle/callees.go index a80772d0..d3507e83 100644 --- a/oracle/callees.go +++ b/oracle/callees.go @@ -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. diff --git a/oracle/callers.go b/oracle/callers.go index e773a2d6..0236fab0 100644 --- a/oracle/callers.go +++ b/oracle/callers.go @@ -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 { diff --git a/oracle/callgraph.go b/oracle/callgraph.go index 745a5cf5..53e559b8 100644 --- a/oracle/callgraph.go +++ b/oracle/callgraph.go @@ -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{ diff --git a/oracle/callstack.go b/oracle/callstack.go index 57adfff2..38dcdaf9 100644 --- a/oracle/callstack.go +++ b/oracle/callstack.go @@ -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. diff --git a/oracle/definition.go b/oracle/definition.go new file mode 100644 index 00000000..16ba5eb3 --- /dev/null +++ b/oracle/definition.go @@ -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 +} diff --git a/oracle/describe.go b/oracle/describe.go index 1c11f283..11d4fe7a 100644 --- a/oracle/describe.go +++ b/oracle/describe.go @@ -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 { diff --git a/oracle/freevars.go b/oracle/freevars.go index 0241bc25..a142d15a 100644 --- a/oracle/freevars.go +++ b/oracle/freevars.go @@ -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() +} diff --git a/oracle/implements.go b/oracle/implements.go index 76b2e1fd..ec5924f4 100644 --- a/oracle/implements.go +++ b/oracle/implements.go @@ -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 { diff --git a/oracle/oracle.go b/oracle/oracle.go index 459435d1..4d4887fd 100644 --- a/oracle/oracle.go +++ b/oracle/oracle.go @@ -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() -} diff --git a/oracle/oracle_test.go b/oracle/oracle_test.go index f78a5d08..03c3ea2a 100644 --- a/oracle/oracle_test.go +++ b/oracle/oracle_test.go @@ -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) diff --git a/oracle/peers.go b/oracle/peers.go index bfd2e11d..13a91d3d 100644 --- a/oracle/peers.go +++ b/oracle/peers.go @@ -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++ } diff --git a/oracle/pointsto.go b/oracle/pointsto.go new file mode 100644 index 00000000..9ebaf5f5 --- /dev/null +++ b/oracle/pointsto.go @@ -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) + } +} diff --git a/oracle/pos.go b/oracle/pos.go new file mode 100644 index 00000000..4ae30a6e --- /dev/null +++ b/oracle/pos.go @@ -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 +} diff --git a/oracle/referrers.go b/oracle/referrers.go index 0388930d..24b5414a 100644 --- a/oracle/referrers.go +++ b/oracle/referrers.go @@ -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 } diff --git a/oracle/serial/serial.go b/oracle/serial/serial.go index ac1ee6d6..afb95265 100644 --- a/oracle/serial/serial.go +++ b/oracle/serial/serial.go @@ -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 } diff --git a/oracle/testdata/src/main/calls.go b/oracle/testdata/src/main/calls.go index bd117d74..1135f7e8 100644 --- a/oracle/testdata/src/main/calls.go +++ b/oracle/testdata/src/main/calls.go @@ -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" } diff --git a/oracle/testdata/src/main/calls.golden b/oracle/testdata/src/main/calls.golden index 364da94b..db3d730e 100644 --- a/oracle/testdata/src/main/calls.golden +++ b/oracle/testdata/src/main/calls.golden @@ -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 -------- diff --git a/oracle/testdata/src/main/describe-json.golden b/oracle/testdata/src/main/describe-json.golden index db917ee9..987b0c32 100644 --- a/oracle/testdata/src/main/describe-json.golden +++ b/oracle/testdata/src/main/describe-json.golden @@ -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 -------- diff --git a/oracle/testdata/src/main/describe.go b/oracle/testdata/src/main/describe.go index 20a04b16..69e0a754 100644 --- a/oracle/testdata/src/main/describe.go +++ b/oracle/testdata/src/main/describe.go @@ -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" diff --git a/oracle/testdata/src/main/describe.golden b/oracle/testdata/src/main/describe.golden index b5861691..810a0bd6 100644 --- a/oracle/testdata/src/main/describe.golden +++ b/oracle/testdata/src/main/describe.golden @@ -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) diff --git a/oracle/testdata/src/main/freevars.golden b/oracle/testdata/src/main/freevars.golden index a4c2c77c..fd65e999 100644 --- a/oracle/testdata/src/main/freevars.golden +++ b/oracle/testdata/src/main/freevars.golden @@ -1,6 +1,6 @@ -------- @freevars fv1 -------- Free identifiers: -type C main.C +type C const exp int var x int diff --git a/oracle/testdata/src/main/imports.go b/oracle/testdata/src/main/imports.go index 17c5e507..111c86ef 100644 --- a/oracle/testdata/src/main/imports.go +++ b/oracle/testdata/src/main/imports.go @@ -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" } diff --git a/oracle/testdata/src/main/imports.golden b/oracle/testdata/src/main/imports.golden index b1173961..0a66c80e 100644 --- a/oracle/testdata/src/main/imports.golden +++ b/oracle/testdata/src/main/imports.golden @@ -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 diff --git a/oracle/testdata/src/main/peers.go b/oracle/testdata/src/main/peers.go index 0fa1e4cb..65ec9076 100644 --- a/oracle/testdata/src/main/peers.go +++ b/oracle/testdata/src/main/peers.go @@ -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' "<-" diff --git a/oracle/testdata/src/main/peers.golden b/oracle/testdata/src/main/peers.golden index 3b5a3980..e6b8a65e 100644 --- a/oracle/testdata/src/main/peers.golden +++ b/oracle/testdata/src/main/peers.golden @@ -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' -------- diff --git a/oracle/testdata/src/main/pointsto-json.go b/oracle/testdata/src/main/pointsto-json.go new file mode 100644 index 00000000..79d7d3dc --- /dev/null +++ b/oracle/testdata/src/main/pointsto-json.go @@ -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() {} diff --git a/oracle/testdata/src/main/pointsto-json.golden b/oracle/testdata/src/main/pointsto-json.golden new file mode 100644 index 00000000..b3f85116 --- /dev/null +++ b/oracle/testdata/src/main/pointsto-json.golden @@ -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" + } + ] +} \ No newline at end of file diff --git a/oracle/testdata/src/main/pointsto.go b/oracle/testdata/src/main/pointsto.go new file mode 100644 index 00000000..796ec942 --- /dev/null +++ b/oracle/testdata/src/main/pointsto.go @@ -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() {} diff --git a/oracle/testdata/src/main/pointsto.golden b/oracle/testdata/src/main/pointsto.golden new file mode 100644 index 00000000..1f8035e2 --- /dev/null +++ b/oracle/testdata/src/main/pointsto.golden @@ -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?) diff --git a/oracle/testdata/src/main/reflection.go b/oracle/testdata/src/main/reflection.go index fc11c2f2..b10df0b2 100644 --- a/oracle/testdata/src/main/reflection.go +++ b/oracle/testdata/src/main/reflection.go @@ -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 } diff --git a/oracle/testdata/src/main/reflection.golden b/oracle/testdata/src/main/reflection.golden index 86dd7a68..4782132b 100644 --- a/oracle/testdata/src/main/reflection.golden +++ b/oracle/testdata/src/main/reflection.golden @@ -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: --------- @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 diff --git a/oracle/testdata/src/main/what-json.go b/oracle/testdata/src/main/what-json.go new file mode 100644 index 00000000..d07a6c90 --- /dev/null +++ b/oracle/testdata/src/main/what-json.go @@ -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" +} diff --git a/oracle/testdata/src/main/what-json.golden b/oracle/testdata/src/main/what-json.golden new file mode 100644 index 00000000..13860dde --- /dev/null +++ b/oracle/testdata/src/main/what-json.golden @@ -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" + } +} \ No newline at end of file diff --git a/oracle/testdata/src/main/what.go b/oracle/testdata/src/main/what.go new file mode 100644 index 00000000..041e9215 --- /dev/null +++ b/oracle/testdata/src/main/what.go @@ -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" +} diff --git a/oracle/testdata/src/main/what.golden b/oracle/testdata/src/main/what.golden new file mode 100644 index 00000000..437fdc82 --- /dev/null +++ b/oracle/testdata/src/main/what.golden @@ -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 + diff --git a/oracle/what.go b/oracle/what.go new file mode 100644 index 00000000..5ead2792 --- /dev/null +++ b/oracle/what.go @@ -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, + } +} diff --git a/pointer/labels.go b/pointer/labels.go index 9c6ce114..da3e2350 100644 --- a/pointer/labels.go +++ b/pointer/labels.go @@ -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) {