// 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 contains the implementation of the oracle tool whose // command-line is provided by code.google.com/p/go.tools/cmd/oracle. // // http://golang.org/s/oracle-design // http://golang.org/s/oracle-user-manual // 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). import ( "bytes" "fmt" "go/ast" "go/build" "go/printer" "go/token" "io" "os" "path/filepath" "strconv" "strings" "time" "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" ) // 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 } // 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. 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) ) type modeInfo struct { name string needs int impl func(*Oracle, *QueryPos) (queryResult, error) } var modes = []*modeInfo{ {"callees", needPTA | needExactPos, callees}, {"callers", needPTA | needPos, callers}, {"callgraph", needPTA, callgraph}, {"callstack", needPTA | needPos, callstack}, {"describe", needPTA | needSSADebug | needExactPos, describe}, {"freevars", needPos, freevars}, {"implements", needPos, implements}, {"peers", needPTA | needSSADebug | needPos, peers}, {"referrers", needAllTypeInfo | needPos, referrers}, } func findMode(mode string) *modeInfo { for _, m := range modes { if m.name == mode { return m } } return nil } type printfFunc func(pos interface{}, format string, args ...interface{}) // queryResult is the interface of each query-specific result type. type queryResult interface { toSerial(res *serial.Result, fset *token.FileSet) display(printf printfFunc) } // A QueryPos represents the position provided as input to a query: // a textual extent in the program's source code, the AST node it // corresponds to, and the package to which it belongs. // Instances are created by ParseQueryPos. // type QueryPos struct { 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 } // TypeString prints type T relative to the query position. func (qpos *QueryPos) TypeString(T types.Type) string { return types.TypeString(qpos.info.Pkg, T) } // ObjectString prints object obj relative to the query position. func (qpos *QueryPos) ObjectString(obj types.Object) string { return types.ObjectString(qpos.info.Pkg, obj) } // 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{}) q queryResult // the query-specific result mode string // query mode warnings []pointer.Warning // pointer analysis warnings } // Serial returns an instance of serial.Result, which implements the // {xml,json}.Marshaler interfaces so that query results can be // serialized as JSON or XML. // func (res *Result) Serial() *serial.Result { resj := &serial.Result{Mode: res.mode} res.q.toSerial(resj, res.fset) for _, w := range res.warnings { resj.Warnings = append(resj.Warnings, serial.PTAWarning{ Pos: res.fset.Position(w.Pos).String(), Message: w.Message, }) } return resj } // Query runs a single oracle query. // // args specify the main package in importer.LoadInitialPackages syntax. // mode is the query mode ("callers", etc). // ptalog is the (optional) pointer-analysis log file. // buildContext is the go/build configuration for locating packages. // reflection determines whether to model reflection soundly (currently slow). // // Clients that intend to perform multiple queries against the same // analysis scope should use this pattern instead: // // imp := importer.New(&importer.Config{Build: buildContext}) // o, err := oracle.New(imp, args, nil) // if err != nil { ... } // for ... { // qpos, err := oracle.ParseQueryPos(imp, pos, needExact) // if err != nil { ... } // // res, err := o.Query(mode, qpos) // if err != nil { ... } // // // use res // } // // TODO(adonovan): the ideal 'needsExact' parameter for ParseQueryPos // 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) { 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) 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 } } // SSA is built and we have the QueryPos. // Release the other ASTs and type info to the GC. imp = nil return o.query(minfo, qpos) } // 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. // It must not yet have loaded any packages. // // args specify the main package in importer.LoadInitialPackages syntax. // // ptalog is the (optional) pointer-analysis log file. // reflection determines whether to model reflection soundly (currently slow). // func New(imp *importer.Importer, args []string, ptalog io.Writer, reflection bool) (*Oracle, error) { return newOracle(imp, args, ptalog, needAll, reflection) } 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 // 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 } 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 { m := make(map[*types.Package]*importer.PackageInfo) for _, p := range imp.AllPackages() { m[p.Pkg] = p } o.typeInfo = m } // Create SSA package for the initial packages and their dependencies. if needs&needSSA != 0 { start = time.Now() // Create SSA packages. if err := o.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 for _, info := range initialPkgInfos { initialPkg := o.prog.Package(info.Pkg) // Add package to the pointer analysis scope. if initialPkg.Func("main") != nil { o.config.Mains = append(o.config.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 o.config.Mains == nil { return nil, fmt.Errorf("analysis scope has no main and no tests") } if needs&needSSADebug != 0 { for _, pkg := range o.prog.AllPackages() { pkg.SetDebugMode(true) } } o.timers["SSA-create"] = time.Since(start) } return o, nil } // Query runs the query of the specified mode and selection. func (o *Oracle) Query(mode string, qpos *QueryPos) (*Result, error) { minfo := findMode(mode) if minfo == nil { return nil, fmt.Errorf("invalid mode type: %q", mode) } return o.query(minfo, qpos) } func (o *Oracle) query(minfo *modeInfo, qpos *QueryPos) (*Result, error) { res := &Result{ mode: minfo.name, fset: o.prog.Fset, fprintf: o.fprintf, // captures o.prog, o.{start,end}Pos for later printing } var err error res.q, err = minfo.impl(o, qpos) if err != nil { return nil, err } return res, nil } // ParseQueryPos parses the source query position pos. // If needExact, it must identify a single AST subtree. // func ParseQueryPos(imp *importer.Importer, pos string, needExact bool) (*QueryPos, error) { start, end, err := parseQueryPos(imp.Fset, pos) if err != nil { return nil, err } info, path, exact := imp.PathEnclosingInterval(start, end) if path == nil { return nil, fmt.Errorf("no syntax here") } if needExact && !exact { return nil, fmt.Errorf("ambiguous selection within %s", importer.NodeDescription(path[0])) } return &QueryPos{start, end, info, path}, 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...) } res.q.display(printf) // Print warnings after the main output. if res.warnings != nil { fmt.Fprintln(out, "\nPointer analysis warnings:") for _, w := range res.warnings { printf(w.Pos, "warning: "+w.Message) } } } // ---------- Utilities ---------- // buildSSA constructs the SSA representation of Go-source function bodies. // 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 } // unparen returns e with any enclosing parentheses stripped. func unparen(e ast.Expr) ast.Expr { for { p, ok := e.(*ast.ParenExpr) if !ok { break } e = p.X } return e } // deref returns a pointer's element type; otherwise it returns typ. func deref(typ types.Type) types.Type { if p, ok := typ.Underlying().(*types.Pointer); ok { return p.Elem() } return typ } // fprintf prints to w a message of the form "location: message\n" // where location is derived from pos. // // pos must be one of: // - a token.Pos, denoting a position // - an ast.Node, denoting an interval // - anything with a Pos() method: // ssa.Member, ssa.Value, ssa.Instruction, types.Object, pointer.Label, etc. // - a QueryPos, denoting the extent of the user's query. // - nil, meaning no position at all. // // The output format is is compatible with the 'gnu' // compilation-error-regexp in Emacs' compilation mode. // TODO(adonovan): support other editors. // func (o *Oracle) fprintf(w io.Writer, pos interface{}, format string, args ...interface{}) { var start, end token.Pos switch pos := pos.(type) { case ast.Node: start = pos.Pos() end = pos.End() case token.Pos: start = pos end = start case interface { Pos() token.Pos }: start = pos.Pos() end = start case *QueryPos: start = pos.start end = pos.end case nil: // no-op default: panic(fmt.Sprintf("invalid pos: %T", pos)) } if sp := o.prog.Fset.Position(start); start == end { // (prints "-: " for token.NoPos) fmt.Fprintf(w, "%s: ", sp) } else { ep := o.prog.Fset.Position(end) // The -1 below is a concession to Emacs's broken use of // inclusive (not half-open) intervals. // Other editors may not want it. // TODO(adonovan): add an -editor=vim|emacs|acme|auto // flag; auto uses EMACS=t / VIM=... / etc env vars. fmt.Fprintf(w, "%s:%d.%d-%d.%d: ", sp.Filename, sp.Line, sp.Column, ep.Line, ep.Column-1) } fmt.Fprintf(w, format, args...) io.WriteString(w, "\n") } // 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() }