diff --git a/cmd/oracle/emacs-test.bash b/cmd/oracle/emacs-test.bash new file mode 100755 index 00000000..b5258979 --- /dev/null +++ b/cmd/oracle/emacs-test.bash @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Simple test of Go oracle/Emacs integration. +# Requires that GOROOT and GOPATH are set. +# Side effect: builds and installs oracle in $GOROOT. + +set -eu + +[ -z "$GOROOT" ] && { echo "Error: GOROOT is unset." >&2; exit 1; } +[ -z "$GOPATH" ] && { echo "Error: GOPATH is unset." >&2; exit 1; } + +log=/tmp/$(basename $0)-$$.log +thisdir=$(dirname $0) + +function die() { + echo "Error: $@." + cat $log + exit 1 +} >&2 + +trap "rm -f $log" EXIT + +# Build and install oracle. +go get code.google.com/p/go.tools/cmd/oracle || die "'go get' failed" +mv -f $GOPATH/bin/oracle $GOROOT/bin/ +$GOROOT/bin/oracle >$log 2>&1 || true # (prints usage and exits 1) +grep -q "Usage: oracle" $log || die "$GOROOT/bin/oracle not installed" + + +# Run Emacs, set the scope to the oracle tool itself, +# load ./main.go, and describe the "fmt" import. +emacs --batch --no-splash --no-window-system --no-init \ + --load $GOROOT/misc/emacs/go-mode.el \ + --load $thisdir/oracle.el \ + --eval ' +(progn + (setq go-oracle-scope "code.google.com/p/go.tools/cmd/oracle") + (find-file "'$thisdir'/main.go") + (search-forward "\"fmt\"") + (backward-char) + (go-oracle-describe) + (princ (with-current-buffer "*go-oracle*" + (buffer-substring-no-properties (point-min) (point-max)))) + (kill-emacs 0)) +' main.go >$log 2>&1 || die "emacs command failed" + +# Check that Println is mentioned. +grep -q "fmt/print.go.*func Println" $log || die "didn't find expected lines in log; got:" + +echo "PASS" diff --git a/cmd/oracle/main.go b/cmd/oracle/main.go new file mode 100644 index 00000000..5b516a2c --- /dev/null +++ b/cmd/oracle/main.go @@ -0,0 +1,104 @@ +// oracle: a tool for answering questions about Go source code. +// +// Each query prints its results to the standard output in an +// editor-friendly format. Currently this is just text in a generic +// compiler diagnostic format, but in future we could provide +// sexpr/json/python formats for the raw data so that editors can +// provide more sophisticated UIs. +// +// Every line of output is of the form "pos: text", where pos = "-" if unknown. +// +package main + +import ( + "bufio" + "flag" + "fmt" + "io" + "log" + "os" + "runtime" + "runtime/pprof" + + "code.google.com/p/go.tools/oracle" +) + +// TODO(adonovan): use a format that permits spaces in filenames, and +// doesn't require shell quoting. +var posFlag = flag.String("pos", "", + "Filename and offset or extent of a syntax element about which to query, "+ + "e.g. 'foo.go 123-456', 'bar.go 123'.") + +var modeFlag = flag.String("mode", "", + "Mode of query to perform: callers, callees, callstack, callgraph, describe.") + +var ptalogFlag = flag.String("ptalog", "pta.log", + "Location of the points-to analysis log file, or empty to disable logging.") + +const usage = `Go source code oracle. +Usage: oracle [ ...] [ ...] [ ...] +Use -help flag to display options. + +Examples: +% oracle -pos 'hello.go 123' hello.go +% oracle -pos 'hello.go 123 456' hello.go +` + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") + +// TODO(adonovan): the caller must---before go/build.init +// runs---specify CGO_ENABLED=0, which entails the "!cgo" go/build +// tag, preferring (dummy) Go to native C implementations of +// cgoLookupHost et al. + +func init() { + // If $GOMAXPROCS isn't set, use the full capacity of the machine. + // For small machines, use at least 4 threads. + if os.Getenv("GOMAXPROCS") == "" { + n := runtime.NumCPU() + if n < 4 { + n = 4 + } + runtime.GOMAXPROCS(n) + } +} + +func main() { + flag.Parse() + args := flag.Args() + + if len(args) == 0 { + fmt.Fprint(os.Stderr, usage) + os.Exit(1) + } + + // Set up points-to analysis log file. + var ptalog io.Writer + if *ptalogFlag != "" { + if f, err := os.Create(*ptalogFlag); err != nil { + log.Fatalf(err.Error()) + } else { + buf := bufio.NewWriter(f) + ptalog = buf + defer func() { + buf.Flush() + f.Close() + }() + } + } + + // Profiling support. + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + if err := oracle.Main(args, *modeFlag, *posFlag, ptalog, os.Stdout, nil); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/cmd/oracle/oracle.el b/cmd/oracle/oracle.el new file mode 100644 index 00000000..4cae84f2 --- /dev/null +++ b/cmd/oracle/oracle.el @@ -0,0 +1,180 @@ +;;; +;;; Integration of the Go 'oracle' analysis tool into Emacs. +;;; +;;; To install the Go oracle, run: +;;; % export GOROOT=... GOPATH=... +;;; % go get code.google.com/p/go.tools/cmd/oracle +;;; % mv $GOPATH/bin/oracle $GOROOT/bin/ +;;; +;;; Load this file into Emacs and set go-oracle-scope to your +;;; configuration. Then, find a file of Go source code, select an +;;; expression of interest, and press F4 (for "describe") or run one +;;; of the other go-oracle-xxx commands. +;;; +;;; TODO(adonovan): simplify installation and configuration by making +;;; oracle a subcommand of 'go tool'. + +(require 'compile) +(require 'go-mode) +(require 'cl) + +(defgroup go-oracle nil + "Options specific to the Go oracle." + :group 'go) + +(defcustom go-oracle-command (concat (car (go-root-and-paths)) "/bin/oracle") + "The Go oracle command; the default is $GOROOT/bin/oracle." + :type 'string + :group 'go-oracle) + +(defcustom go-oracle-scope "" + "The scope of the analysis. See `go-oracle-set-scope'." + :type 'string + :group 'go-oracle) + +(defvar go-oracle--scope-history + nil + "History of values supplied to `go-oracle-set-scope'.") + +(defun go-oracle-set-scope () + "Sets the scope for the Go oracle, prompting the user to edit the +previous scope. + +The scope specifies a set of arguments, separated by spaces. +It may be: +1) a set of packages whose main() functions will be analyzed. +2) a list of *.go filenames; they will treated like as a single + package (see #3). +3) a single package whose main() function and/or Test* functions + will be analyzed. + +In the common case, this is similar to the argument(s) you would +specify to 'go build'." + (interactive) + (let ((scope (read-from-minibuffer "Go oracle scope: " + go-oracle-scope + nil + nil + 'go-oracle--scope-history))) + (if (string-equal "" scope) + (error "You must specify a non-empty scope for the Go oracle")) + (setq go-oracle-scope scope))) + +(defun go-oracle--run (mode) + "Run the Go oracle in the specified MODE, passing it the +selected region of the current buffer. Process the output to +replace each file name with a small hyperlink. Display the +result." + (if (not buffer-file-name) + (error "Cannot use oracle on a buffer without a file name")) + ;; It's not sufficient to save a modified buffer since if + ;; gofmt-before-save is on the before-save-hook, saving will + ;; disturb the selected region. + (if (buffer-modified-p) + (error "Please save the buffer before invoking go-oracle")) + (if (string-equal "" go-oracle-scope) + (go-oracle-set-scope)) + (let* ((filename (file-truename buffer-file-name)) + (pos (if (use-region-p) + (format "%s-%s" + (1- (go--position-bytes (region-beginning))) + (1- (go--position-bytes (region-end)))) + (format "%s" (1- (position-bytes (point)))))) + ;; This would be simpler if we could just run 'go tool oracle'. + (env-vars (go-root-and-paths)) + (goroot-env (concat "GOROOT=" (car env-vars))) + (gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":")))) + (with-current-buffer (get-buffer-create "*go-oracle*") + (setq buffer-read-only nil) + (erase-buffer) + (insert "Go Oracle\n") + (let ((args (append (list go-oracle-command nil t nil + "-ptalog=" ; avoid writing the huge log + (format "-pos=%s %s" filename pos) + (format "-mode=%s" mode)) + (split-string go-oracle-scope " " t)))) + ;; Log the command to *Messages*, for debugging. + (message "Command: %s:" args) + (message nil) ; clears/shrinks minibuffer + + (message "Running oracle...") + ;; Use dynamic binding to modify/restore the environment + (let ((process-environment (list* goroot-env gopath-env "CGO_ENABLED=0" process-environment))) + (apply #'call-process args))) + (insert "\n") + (compilation-mode) + (setq compilation-error-screen-columns nil) + (let ((w (display-buffer (current-buffer)))) + (balance-windows) + (shrink-window-if-larger-than-buffer w) + (set-window-point w (point-min))) + + ;; 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 + ;; and we'll do the markup directly. + (let ((buffer-read-only nil) + (p 1)) + (while (not (null p)) + (let ((np (compilation-next-single-property-change p 'compilation-message))) + (message "Post-processing link (%d%%)" (/ (* p 100) (point-max))) + (if np + (when (equal (line-number-at-pos p) (line-number-at-pos np)) + ;; np is (typically) the space following ":"; consume it too. + (put-text-property p np 'display "▶") + (goto-char np) + (insert " "))) + (setq p np))) + (message nil))))) + +(defun go-oracle-callees () + "Show possible callees of the function call at the current point." + (interactive) + (go-oracle--run "callees")) + +(defun go-oracle-callers () + "Show the set of callers of the function containing the current point." + (interactive) + (go-oracle--run "callers")) + +(defun go-oracle-callgraph () + "Show the callgraph of the current program." + (interactive) + (go-oracle--run "callgraph")) + +(defun go-oracle-callstack () + "Show an arbitrary path from a root of the call graph to the +function containing the current point." + (interactive) + (go-oracle--run "callstack")) + +(defun go-oracle-describe () + "Describe the expression at the current point." + (interactive) + (go-oracle--run "describe")) + +(defun go-oracle-implements () + "Describe the 'implements' relation for types in the package +containing the current point." + (interactive) + (go-oracle--run "implements")) + +(defun go-oracle-freevars () + "Enumerate the free variables of the current selection." + (interactive) + (go-oracle--run "freevars")) + +(defun go-oracle-channel-peers () + "Enumerate the set of possible corresponding sends/receives for +this channel receive/send operation." + (interactive) + (go-oracle--run "peers")) + +;; TODO(adonovan): don't mutate the keymap; just document how users +;; can do this themselves. But that means freezing the API, so don't +;; do that yet; wait till v1.0. +(add-hook 'go-mode-hook + #'(lambda () (local-set-key (kbd "") #'go-oracle-describe))) + +(provide 'go-oracle) diff --git a/cmd/oracle/oracle.vim b/cmd/oracle/oracle.vim new file mode 100644 index 00000000..ac84826d --- /dev/null +++ b/cmd/oracle/oracle.vim @@ -0,0 +1,62 @@ +" -*- text -*- +" oracle.vim -- Vim integration for the Go oracle. +" +" Load with (e.g.) :source oracle.vim +" Call with (e.g.) :GoOracleDescribe +" while cursor or selection is over syntax of interest. +" Run :copen to show the quick-fix file. +" +" This is an absolutely rudimentary integration of the Go Oracle into +" Vim's quickfix mechanism and it needs a number of usability +" improvements before it can be practically useful to Vim users. +" Voluntary contributions welcomed! +" +" TODO(adonovan): +" - prompt/save the buffer if modified. +" - reject buffers with no filename. +" - hide all filenames in quickfix buffer. + + +" Users should customize this to their analysis scope, e.g. main package(s). +let s:scope = "/home/adonovan/go3/got/d.go" + +" The path to the Go oracle executable. +let s:go_oracle = "$GOROOT/bin/oracle" + +" Enable Vim to recognize GNU-style 'file:line.col-line.col: message' format. +set errorformat+=%f:%l.%c-%*[0-9].%*[0-9]:\ %m + +func! s:RunOracle(mode) abort + let s:pos = line2byte(line("."))+col(".") + let s:errfile = tempname() + let s:cmd = printf("!%s -ptalog= -mode=%s '-pos=%s %d' %s >%s", + \ s:go_oracle, a:mode, bufname(""), s:pos, s:scope, s:errfile) + execute s:cmd + execute "cfile " . s:errfile +endfun + +" Describe the expression at the current point. +command! GoOracleDescribe + \ call s:RunOracle("describe") + +" Show possible callees of the function call at the current point. +command! GoOracleCallees + \ call s:RunOracle("callees") + +" Show the set of callers of the function containing the current point. +command! GoOracleCallers + \ call s:RunOracle("callers") + +" Show the callgraph of the current program. +command! GoOracleCallgraph + \ call s:RunOracle("callgraph") + +" Describe the 'implements' relation for types in the +" package containing the current point. +command! GoOracleImplements + \ call s:RunOracle("implements") + +" Enumerate the set of possible corresponding sends/receives for +" this channel receive/send operation. +command! GoOracleChannelPeers + \ call s:RunOracle("peers") diff --git a/oracle/callees.go b/oracle/callees.go new file mode 100644 index 00000000..b2122a27 --- /dev/null +++ b/oracle/callees.go @@ -0,0 +1,97 @@ +package oracle + +import ( + "go/ast" + + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/pointer" +) + +// Callees reports the possible callees of the function call site +// identified by the specified source location. +// +// TODO(adonovan): if a callee is a wrapper, show the callee's callee. +// +func callees(o *oracle) (queryResult, error) { + // Determine the enclosing call for the specified position. + var call *ast.CallExpr + for _, n := range o.queryPath { + if call, _ = n.(*ast.CallExpr); call != nil { + break + } + } + if call == nil { + return nil, o.errorf(o.queryPath[0], "there is no function call here") + } + // TODO(adonovan): issue an error if the call is "too far + // away" from the current selection, as this most likely is + // not what the user intended. + + // Reject type conversions. + if o.queryPkgInfo.IsType(call.Fun) { + return nil, o.errorf(call, "this is a type conversion, not a function call") + } + + // Reject calls to built-ins. + if b, ok := o.queryPkgInfo.TypeOf(call.Fun).(*types.Builtin); ok { + return nil, o.errorf(call, "this is a call to the built-in '%s' operator", b.Name()) + } + + buildSSA(o) + + // Compute the subgraph of the callgraph for callsite(s) + // arising from 'call'. There may be more than one if its + // enclosing function was treated context-sensitively. + // (Or zero if it was in dead code.) + // + // The presence of a key indicates this call site is + // interesting even if the value is nil. + querySites := make(map[pointer.CallSite][]pointer.CallGraphNode) + o.config.CallSite = func(site pointer.CallSite) { + if site.Pos() == call.Lparen { + // Not a no-op! Ensures key is + // present even if value is nil: + querySites[site] = querySites[site] + } + } + o.config.Call = func(site pointer.CallSite, caller, callee pointer.CallGraphNode) { + if targets, ok := querySites[site]; ok { + querySites[site] = append(targets, callee) + } + } + ptrAnalysis(o) + + return &calleesResult{ + call: call, + querySites: querySites, + }, nil +} + +type calleesResult struct { + call *ast.CallExpr + querySites map[pointer.CallSite][]pointer.CallGraphNode +} + +func (r *calleesResult) display(o *oracle) { + // Print the set of discovered call edges. + if len(r.querySites) == 0 { + // e.g. it appears within "if false {...}" or within a dead function. + o.printf(r.call.Lparen, "this call site is unreachable in this analysis") + } + + // TODO(adonovan): sort, to ensure test determinism. + // TODO(adonovan): compute union of callees across all contexts. + for site, callees := range r.querySites { + if callees == nil { + // dynamic call on a provably nil func/interface + o.printf(site, "%s on nil value", site.Description()) + continue + } + + // TODO(adonovan): sort, to ensure test determinism. + o.printf(site, "this %s dispatches to:", site.Description()) + for _, callee := range callees { + o.printf(callee.Func(), "\t%s", callee.Func()) + } + } +} diff --git a/oracle/callers.go b/oracle/callers.go new file mode 100644 index 00000000..c6a36c93 --- /dev/null +++ b/oracle/callers.go @@ -0,0 +1,73 @@ +package oracle + +import ( + "code.google.com/p/go.tools/pointer" + "code.google.com/p/go.tools/ssa" +) + +// Callers reports the possible callers of the function +// immediately enclosing the specified source location. +// +// TODO(adonovan): if a caller is a wrapper, show the caller's caller. +// +func callers(o *oracle) (queryResult, error) { + pkg := o.prog.Package(o.queryPkgInfo.Pkg) + if pkg == nil { + return nil, o.errorf(o.queryPath[0], "no SSA package") + } + if !ssa.HasEnclosingFunction(pkg, o.queryPath) { + return nil, o.errorf(o.queryPath[0], "this position is not inside a function") + } + + buildSSA(o) + + target := ssa.EnclosingFunction(pkg, o.queryPath) + if target == nil { + return nil, o.errorf(o.queryPath[0], "no SSA function built for this location (dead code?)") + } + + // Run the pointer analysis, recording each + // call found to originate from target. + var calls []callersCall + o.config.Call = func(site pointer.CallSite, caller, callee pointer.CallGraphNode) { + if callee.Func() == target { + calls = append(calls, callersCall{site, caller}) + } + } + root := ptrAnalysis(o) + + return &callersResult{ + target: target, + root: root, + calls: calls, + }, nil +} + +type callersResult struct { + target *ssa.Function + root pointer.CallGraphNode + calls []callersCall +} + +type callersCall struct { + site pointer.CallSite + caller pointer.CallGraphNode +} + +func (r *callersResult) display(o *oracle) { + if r.calls == nil { + o.printf(r.target, "%s is not reachable in this program.", r.target) + } else { + o.printf(r.target, "%s is called from these %d sites:", r.target, len(r.calls)) + // TODO(adonovan): sort, to ensure test determinism. + for _, call := range r.calls { + if call.caller == r.root { + o.printf(r.target, "the root of the call graph") + } else { + o.printf(call.site, "\t%s from %s", + call.site.Description(), call.caller.Func()) + } + } + } + +} diff --git a/oracle/callgraph.go b/oracle/callgraph.go new file mode 100644 index 00000000..d944a42b --- /dev/null +++ b/oracle/callgraph.go @@ -0,0 +1,63 @@ +package oracle + +import ( + "strings" + + "code.google.com/p/go.tools/pointer" +) + +// callgraph displays the entire callgraph of the current program. +// +// Nodes may be seem to appear multiple times due to (limited) +// context sensitivity. +// +// TODO(adonovan): add options for restricting the display to a region +// of interest: function, package, subgraph, dirtree, etc. +// +func callgraph(o *oracle) (queryResult, error) { + buildSSA(o) + + // Run the pointer analysis and build the complete callgraph. + callgraph := make(pointer.CallGraph) + o.config.Call = callgraph.AddEdge + root := ptrAnalysis(o) + + return &callgraphResult{ + root: root, + callgraph: callgraph, + }, nil +} + +type callgraphResult struct { + root pointer.CallGraphNode + callgraph pointer.CallGraph + + numbering map[pointer.CallGraphNode]int // used by display +} + +func (r *callgraphResult) print(o *oracle, cgn pointer.CallGraphNode, indent int) { + if n := r.numbering[cgn]; n == 0 { + n = 1 + len(r.numbering) + r.numbering[cgn] = n + o.printf(cgn.Func(), "%d\t%s%s", n, strings.Repeat(" ", indent), cgn.Func()) + for callee := range r.callgraph[cgn] { + r.print(o, callee, indent+1) + } + } else { + o.printf(cgn.Func(), "\t%s%s (%d)", strings.Repeat(" ", indent), cgn.Func(), n) + } +} + +func (r *callgraphResult) display(o *oracle) { + o.printf(nil, ` +Below is a call graph of the entire program. +The numbered nodes form a spanning tree. +Non-numbered nodes indicate back- or cross-edges to the node whose + number follows in parentheses. +Some nodes may appear multiple times due to context-sensitive + treatment of some calls. +`) + + r.numbering = make(map[pointer.CallGraphNode]int) + r.print(o, r.root, 0) +} diff --git a/oracle/callstack.go b/oracle/callstack.go new file mode 100644 index 00000000..6dc3cf10 --- /dev/null +++ b/oracle/callstack.go @@ -0,0 +1,86 @@ +package oracle + +import ( + "code.google.com/p/go.tools/pointer" + "code.google.com/p/go.tools/ssa" +) + +// Callstack displays an arbitrary path from a root of the callgraph +// to the function at the current position. +// +// The information may be misleading in a context-insensitive +// analysis. e.g. the call path X->Y->Z might be infeasible if Y never +// calls Z when it is called from X. TODO(adonovan): think about UI. +// +// TODO(adonovan): permit user to specify a starting point other than +// the analysis root. +// +func callstack(o *oracle) (queryResult, error) { + pkg := o.prog.Package(o.queryPkgInfo.Pkg) + if pkg == nil { + return nil, o.errorf(o.queryPath[0], "no SSA package") + } + + if !ssa.HasEnclosingFunction(pkg, o.queryPath) { + return nil, o.errorf(o.queryPath[0], "this position is not inside a function") + } + + buildSSA(o) + + target := ssa.EnclosingFunction(pkg, o.queryPath) + if target == nil { + return nil, o.errorf(o.queryPath[0], + "no SSA function built for this location (dead code?)") + } + + // Run the pointer analysis and build the complete call graph. + callgraph := make(pointer.CallGraph) + o.config.Call = callgraph.AddEdge + root := ptrAnalysis(o) + + return &callstackResult{ + target: target, + root: root, + callgraph: callgraph, + }, nil +} + +type callstackResult struct { + target *ssa.Function + root pointer.CallGraphNode + callgraph pointer.CallGraph + + seen map[pointer.CallGraphNode]bool // used by display +} + +func (r *callstackResult) search(o *oracle, cgn pointer.CallGraphNode) bool { + if !r.seen[cgn] { + r.seen[cgn] = true + if cgn.Func() == r.target { + o.printf(o, "Found a call path from root to %s", r.target) + o.printf(r.target, "%s", r.target) + return true + } + for callee, site := range r.callgraph[cgn] { + if r.search(o, callee) { + o.printf(site, "%s from %s", site.Description(), cgn.Func()) + return true + } + } + } + return false +} + +func (r *callstackResult) display(o *oracle) { + // Show only an arbitrary path from a root to the current function. + // We use depth-first search. + + r.seen = make(map[pointer.CallGraphNode]bool) + + for toplevel := range r.callgraph[r.root] { + if r.search(o, toplevel) { + return + } + } + o.printf(r.target, "%s is unreachable in this analysis scope", r.target) +} diff --git a/oracle/describe.go b/oracle/describe.go new file mode 100644 index 00000000..3698918e --- /dev/null +++ b/oracle/describe.go @@ -0,0 +1,715 @@ +package oracle + +import ( + "bytes" + "fmt" + "go/ast" + "go/token" + "sort" + "strconv" + "strings" + + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/importer" + "code.google.com/p/go.tools/pointer" + "code.google.com/p/go.tools/ssa" +) + +// TODO(adonovan): all printed sets must be sorted to ensure test determinism. + +// describe describes the syntax node denoted by the query position, +// including: +// - its syntactic category +// - the location of the definition of its referent (for identifiers) +// - its type and method set (for an expression or type expression) +// - its points-to set (for a pointer-like expression) +// - its concrete types (for an interface expression) and their points-to sets. +// +func describe(o *oracle) (queryResult, error) { + if false { // debugging + o.printf(o.queryPath[0], "you selected: %s %s", + importer.NodeDescription(o.queryPath[0]), pathToString2(o.queryPath)) + } + + path, action := findInterestingNode(o.queryPkgInfo, o.queryPath) + switch action { + case actionExpr: + return describeValue(o, path) + + case actionType: + return describeType(o, path) + + case actionPackage: + return describePackage(o, path) + + case actionStmt: + return describeStmt(o, path) + + case actionUnknown: + return &describeUnknownResult{path[0]}, nil + + default: + panic(action) // unreachable + } +} + +type describeUnknownResult struct { + node ast.Node +} + +func (r *describeUnknownResult) display(o *oracle) { + // Nothing much to say about misc syntax. + o.printf(r.node, "%s", importer.NodeDescription(r.node)) +} + +type action int + +const ( + actionUnknown action = iota // None of the below + actionExpr // FuncDecl, true Expr or Ident(types.{Const,Var}) + actionType // type Expr or Ident(types.TypeName). + actionStmt // Stmt or Ident(types.Label) + actionPackage // Ident(types.Package) or ImportSpec +) + +// findInterestingNode classifies the syntax node denoted by path as one of: +// - an expression, part of an expression or a reference to a constant +// or variable; +// - a type, part of a type, or a reference to a named type; +// - a statement, part of a statement, or a label referring to a statement; +// - part of a package declaration or import spec. +// - none of the above. +// and returns the most "interesting" associated node, which may be +// the same node, an ancestor or a descendent. +// +func findInterestingNode(pkginfo *importer.PackageInfo, path []ast.Node) ([]ast.Node, action) { + // TODO(adonovan): integrate with go/types/stdlib_test.go and + // apply this to every AST node we can find to make sure it + // doesn't crash. + + // TODO(adonovan): audit for ParenExpr safety, esp. since we + // traverse up and down. + + // TODO(adonovan): if the users selects the "." in + // "fmt.Fprintf()", they'll get an ambiguous selection error; + // we won't even reach here. Can we do better? + + // TODO(adonovan): describing a field within 'type T struct {...}' + // describes the (anonymous) struct type and concludes "no methods". Fix. + + for len(path) > 0 { + switch n := path[0].(type) { + case *ast.GenDecl: + if len(n.Specs) == 1 { + // Descend to sole {Import,Type,Value}Spec child. + path = append([]ast.Node{n.Specs[0]}, path...) + continue + } + return path, actionUnknown // uninteresting + + case *ast.FuncDecl: + // Descend to function name. + path = append([]ast.Node{n.Name}, path...) + continue + + case *ast.ImportSpec: + return path, actionPackage + + case *ast.ValueSpec: + if len(n.Names) == 1 { + // Descend to sole Ident child. + path = append([]ast.Node{n.Names[0]}, path...) + continue + } + return path, actionUnknown // uninteresting + + case *ast.TypeSpec: + // Descend to type name. + path = append([]ast.Node{n.Name}, path...) + continue + + case ast.Stmt: + return path, actionStmt + + case *ast.ArrayType, + *ast.StructType, + *ast.FuncType, + *ast.InterfaceType, + *ast.MapType, + *ast.ChanType: + return path, actionType + + case *ast.Comment, *ast.CommentGroup, *ast.File, *ast.KeyValueExpr, *ast.CommClause: + return path, actionUnknown // uninteresting + + case *ast.Ellipsis: + // Continue to enclosing node. + // e.g. [...]T in ArrayType + // f(x...) in CallExpr + // f(x...T) in FuncType + + case *ast.Field: + // TODO(adonovan): this needs more thought, + // since fields can be so many things. + if len(n.Names) == 1 { + // Descend to sole Ident child. + path = append([]ast.Node{n.Names[0]}, path...) + continue + } + // Zero names (e.g. anon field in struct) + // or multiple field or param names: + // continue to enclosing field list. + + case *ast.FieldList: + // Continue to enclosing node: + // {Struct,Func,Interface}Type or FuncDecl. + + case *ast.BasicLit: + if _, ok := path[1].(*ast.ImportSpec); ok { + return path[1:], actionPackage + } + return path, actionExpr + + case *ast.SelectorExpr: + if pkginfo.ObjectOf(n.Sel) == nil { + // Is this reachable? + return path, actionUnknown + } + // Descend to .Sel child. + path = append([]ast.Node{n.Sel}, path...) + continue + + case *ast.Ident: + switch obj := pkginfo.ObjectOf(n).(type) { + case *types.Package: + return path, actionPackage + + case *types.Const: + return path, actionExpr + + case *types.Label: + return path, actionStmt + + case *types.TypeName: + return path, actionType + + case *types.Var: + // For x in 'struct {x T}', return struct type, for now. + if _, ok := path[1].(*ast.Field); ok { + _ = path[2].(*ast.FieldList) // assertion + if _, ok := path[3].(*ast.StructType); ok { + return path[3:], actionType + } + } + return path, actionExpr + + case *types.Func: + // For f in 'interface {f()}', return the interface type, for now. + if _, ok := path[1].(*ast.Field); ok { + _ = path[2].(*ast.FieldList) // assertion + if _, ok := path[3].(*ast.InterfaceType); ok { + return path[3:], actionType + } + } + + // For reference to built-in function, return enclosing call. + if _, ok := obj.Type().(*types.Builtin); ok { + // Ascend to enclosing function call. + path = path[1:] + continue + } + + return path, actionExpr + } + + // No object. + switch path[1].(type) { + case *ast.SelectorExpr: + // Return enclosing selector expression. + return path[1:], actionExpr + + case *ast.Field: + // TODO(adonovan): test this. + // e.g. all f in: + // struct { f, g int } + // interface { f() } + // func (f T) method(f, g int) (f, g bool) + // + // switch path[3].(type) { + // case *ast.FuncDecl: + // case *ast.StructType: + // case *ast.InterfaceType: + // } + // + // return path[1:], actionExpr + // + // Unclear what to do with these. + // Struct.Fields -- field + // Interface.Methods -- field + // FuncType.{Params.Results} -- actionExpr + // FuncDecl.Recv -- actionExpr + + case *ast.ImportSpec: + // TODO(adonovan): fix: why no package object? go/types bug? + return path[1:], actionPackage + + default: + // e.g. blank identifier (go/types bug?) + // or y in "switch y := x.(type)" (go/types bug?) + fmt.Printf("unknown reference %s in %T\n", n, path[1]) + return path, actionUnknown + } + + case *ast.StarExpr: + if pkginfo.IsType(n) { + return path, actionType + } + return path, actionExpr + + case ast.Expr: + // All Expr but {BasicLit,Ident,StarExpr} are + // "true" expressions that evaluate to a value. + return path, actionExpr + } + + // Ascend to parent. + path = path[1:] + } + + return nil, actionUnknown // unreachable +} + +// ---- VALUE ------------------------------------------------------------ + +// ssaValueForIdent returns the ssa.Value for the ast.Ident 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 ssaValueForIdent(o *oracle, obj types.Object, path []ast.Node) (ssa.Value, error) { + if obj, ok := obj.(*types.Var); ok { + pkg := o.prog.Package(o.queryPkgInfo.Pkg) + pkg.Build() + if v := o.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 { + v = nil + } + return v, nil + } + return nil, fmt.Errorf("can't locate SSA Value for var %s", obj.Name()) + } + + // Don't run pointer analysis on const/func objects. + return nil, nil +} + +// 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(o *oracle, path []ast.Node) (ssa.Value, error) { + pkg := o.prog.Package(o.queryPkgInfo.Pkg) + pkg.SetDebugMode(true) + pkg.Build() + + fn := ssa.EnclosingFunction(pkg, path) + if fn == nil { + return nil, fmt.Errorf("no SSA function built for this location (dead code?)") + } + + if v := fn.ValueForExpr(path[0].(ast.Expr)); v != nil { + return v, nil + } + + return nil, fmt.Errorf("can't locate SSA Value for expression in %s", fn) +} + +func describeValue(o *oracle, path []ast.Node) (*describeValueResult, error) { + var expr ast.Expr + switch n := path[0].(type) { + case *ast.ValueSpec: + // ambiguous ValueSpec containing multiple names + return nil, o.errorf(n, "multiple value specification") + case ast.Expr: + expr = n + default: + // Is this reachable? + return nil, o.errorf(n, "unexpected AST for expr: %T", n) + } + + // From this point on, we cannot fail with an error. + // Failure to run the pointer analysis will be reported later. + + var value ssa.Value + var ptaErr error + var obj types.Object + + // Determine the ssa.Value for the expression. + if id, ok := expr.(*ast.Ident); ok { + // def/ref of func/var/const object + obj = o.queryPkgInfo.ObjectOf(id) + value, ptaErr = ssaValueForIdent(o, obj, path) + } else { + // any other expression + if o.queryPkgInfo.ValueOf(expr) == nil { // non-constant? + value, ptaErr = ssaValueForExpr(o, path) + } + } + + // Don't run pointer analysis on non-pointerlike types. + if value != nil && !pointer.CanPoint(value.Type()) { + value = nil + } + + // Run pointer analysis of the selected SSA value. + var ptrs []pointer.Pointer + if value != nil { + buildSSA(o) + + o.config.QueryValues = map[ssa.Value][]pointer.Pointer{value: nil} + ptrAnalysis(o) + ptrs = o.config.QueryValues[value] + } + + return &describeValueResult{ + expr: expr, + obj: obj, + value: value, + ptaErr: ptaErr, + ptrs: ptrs, + }, nil +} + +type describeValueResult struct { + expr ast.Expr // query node + obj types.Object // var/func/const object, if expr was Ident + value ssa.Value // ssa.Value for pointer analysis query + ptaErr error // explanation of why we couldn't run pointer analysis + ptrs []pointer.Pointer // result of pointer analysis query +} + +func (r *describeValueResult) display(o *oracle) { + suffix := "" + if val := o.queryPkgInfo.ValueOf(r.expr); val != nil { + suffix = fmt.Sprintf(" of constant value %s", val) + } + + // Describe the expression. + if r.obj != nil { + if r.obj.Pos() == r.expr.Pos() { + // defining ident + o.printf(r.expr, "definition of %s%s", r.obj, suffix) + } else { + // referring ident + o.printf(r.expr, "reference to %s%s", r.obj, suffix) + if def := r.obj.Pos(); def != token.NoPos { + o.printf(def, "defined here") + } + } + } else { + desc := importer.NodeDescription(r.expr) + if suffix != "" { + // constant expression + o.printf(r.expr, "%s%s", desc, suffix) + } else { + // non-constant expression + o.printf(r.expr, "%s of type %s", desc, o.queryPkgInfo.TypeOf(r.expr)) + } + } + + if r.value == nil { + // pointer analysis was not run + if r.ptaErr != nil { + o.printf(r.expr, "no pointer analysis: %s", r.ptaErr) + } + return + } + + if r.ptrs == nil { + o.printf(r.expr, "pointer analysis did not analyze this expression (dead code?)") + return + } + + // Display the results of pointer analysis. + + // Combine the PT sets from all contexts. + pts := pointer.PointsToCombined(r.ptrs) + + // Report which make(chan) labels the query's channel can alias. + if _, ok := r.value.Type().Underlying().(*types.Interface); ok { + // Show concrete types for interface expression. + if concs := pts.ConcreteTypes(); concs.Len() > 0 { + o.printf(o, "interface may contain these concrete types:") + // TODO(adonovan): must sort to ensure deterministic test behaviour. + concs.Iterate(func(conc types.Type, ptrs interface{}) { + var obj types.Object + if nt, ok := deref(conc).(*types.Named); ok { + obj = nt.Obj() + } + + pts := pointer.PointsToCombined(ptrs.([]pointer.Pointer)) + if labels := pts.Labels(); len(labels) > 0 { + o.printf(obj, "\t%s, may point to:", conc) + printLabels(o, labels, "\t\t") + } else { + o.printf(obj, "\t%s", conc) + } + }) + } else { + o.printf(o, "interface cannot contain any concrete values.") + } + } else { + // Show labels for other expressions. + if labels := pts.Labels(); len(labels) > 0 { + o.printf(o, "value may point to these labels:") + printLabels(o, labels, "\t") + } else { + o.printf(o, "value cannot point to anything.") + } + } +} + +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(o *oracle, labels []*pointer.Label, prefix string) { + // Sort, to ensure deterministic test behaviour. + sort.Sort(byPosAndString(labels)) + // TODO(adonovan): due to context-sensitivity, many of these + // labels may differ only by context, which isn't apparent. + for _, label := range labels { + o.printf(label, "%s%s", prefix, label) + } +} + +// ---- TYPE ------------------------------------------------------------ + +func describeType(o *oracle, path []ast.Node) (*describeTypeResult, error) { + var description string + var t types.Type + switch n := path[0].(type) { + case *ast.Ident: + t = o.queryPkgInfo.TypeOf(n) + switch t := t.(type) { + case *types.Basic: + description = "reference to built-in type " + t.String() + + case *types.Named: + isDef := t.Obj().Pos() == n.Pos() // see caveats at isDef above + if isDef { + description = "definition of type " + t.String() + } else { + description = "reference to type " + t.String() + } + } + + case ast.Expr: + t = o.queryPkgInfo.TypeOf(n) + description = "type " + t.String() + + default: + // Unreachable? + return nil, o.errorf(n, "unexpected AST for type: %T", n) + } + + return &describeTypeResult{path[0], description, t}, nil +} + +type describeTypeResult struct { + node ast.Node + description string + typ types.Type +} + +func (r *describeTypeResult) display(o *oracle) { + o.printf(r.node, "%s", r.description) + + // Show the underlying type for a reference to a named type. + if nt, ok := r.typ.(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { + o.printf(nt.Obj(), "defined as %s", nt.Underlying()) + } + + // Print the method set, if the type kind is capable of bearing methods. + switch r.typ.(type) { + case *types.Interface, *types.Struct, *types.Named: + // TODO(adonovan): don't show unexported methods if + // r.typ belongs to a package other than the query + // package. + if m := ssa.IntuitiveMethodSet(r.typ); m != nil { + o.printf(r.node, "Method set:") + for _, meth := range m { + o.printf(meth.Obj(), "\t%s", meth) + } + } else { + o.printf(r.node, "No methods.") + } + } +} + +// ---- PACKAGE ------------------------------------------------------------ + +func describePackage(o *oracle, path []ast.Node) (*describePackageResult, error) { + var description string + var importPath string + switch n := path[0].(type) { + case *ast.ImportSpec: + // importPath = o.queryPkgInfo.ObjectOf(n.Name).(*types.Package).Path() + // description = "import of package " + importPath + // TODO(gri): o.queryPkgInfo.ObjectOf(n.Name) may be nil. + // e.g. "fmt" import in cmd/oracle/main.go. Why? + // Workaround: + description = "import of package " + n.Path.Value + importPath, _ = strconv.Unquote(n.Path.Value) + + case *ast.Ident: + importPath = o.queryPkgInfo.ObjectOf(n).(*types.Package).Path() + if _, isDef := path[1].(*ast.File); isDef { + description = fmt.Sprintf("definition of package %q", importPath) + } else { + description = fmt.Sprintf("reference to package %q", importPath) + } + if importPath == "" { + // TODO(gri): fix. + return nil, o.errorf(n, "types.Package.Path() returned \"\"\n") + } + + default: + // Unreachable? + return nil, o.errorf(n, "unexpected AST for package: %T", n) + } + + pkg := o.prog.PackagesByPath[importPath] + + return &describePackageResult{path[0], description, pkg}, nil +} + +type describePackageResult struct { + node ast.Node + description string + pkg *ssa.Package +} + +func (r *describePackageResult) display(o *oracle) { + o.printf(r.node, "%s", r.description) + // TODO(adonovan): factor this into a testable utility function. + if p := r.pkg; p != nil { + samePkg := p.Object == o.queryPkgInfo.Pkg + + // Describe exported package members, in lexicographic order. + + // Compute max width of name "column". + var names []string + maxname := 0 + for name := range p.Members { + if samePkg || ast.IsExported(name) { + if l := len(name); l > maxname { + maxname = l + } + names = append(names, name) + } + } + + sort.Strings(names) + + // Print the members. + for _, name := range names { + mem := p.Members[name] + o.printf(mem, "%s", formatMember(mem, maxname)) + // Print method set. + if mem, ok := mem.(*ssa.Type); ok { + for _, meth := range ssa.IntuitiveMethodSet(mem.Type()) { + if samePkg || ast.IsExported(meth.Obj().Name()) { + o.printf(meth.Obj(), "\t\t%s", meth) + } + } + } + } + } +} + +func formatMember(mem ssa.Member, maxname int) string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "\t%-5s %-*s", mem.Token(), maxname, mem.Name()) + switch mem := mem.(type) { + case *ssa.NamedConst: + fmt.Fprintf(&buf, " %s = %s", mem.Type(), mem.Value.Name()) + + case *ssa.Function: + fmt.Fprintf(&buf, " %s", mem.Type()) + + case *ssa.Type: + // Abbreviate long aggregate type names. + var abbrev string + switch t := mem.Type().Underlying().(type) { + case *types.Interface: + if t.NumMethods() > 1 { + abbrev = "interface{...}" + } + case *types.Struct: + if t.NumFields() > 1 { + abbrev = "struct{...}" + } + } + if abbrev == "" { + fmt.Fprintf(&buf, " %s", mem.Type().Underlying()) + } else { + fmt.Fprintf(&buf, " %s", abbrev) + } + + case *ssa.Global: + fmt.Fprintf(&buf, " %s", deref(mem.Type())) + } + return buf.String() +} + +// ---- STATEMENT ------------------------------------------------------------ + +func describeStmt(o *oracle, path []ast.Node) (*describeStmtResult, error) { + var description string + switch n := path[0].(type) { + case *ast.Ident: + if o.queryPkgInfo.ObjectOf(n).Pos() == n.Pos() { + description = "labelled statement" + } else { + description = "reference to labelled statement" + } + + default: + // Nothing much to say about statements. + description = importer.NodeDescription(n) + } + return &describeStmtResult{path[0], description}, nil +} + +type describeStmtResult struct { + node ast.Node + description string +} + +func (r *describeStmtResult) display(o *oracle) { + o.printf(r.node, "%s", r.description) +} + +// ------------------- Utilities ------------------- + +// pathToString returns a string containing the concrete types of the +// nodes in path. +func pathToString2(path []ast.Node) string { + var buf bytes.Buffer + fmt.Fprint(&buf, "[") + for i, n := range path { + if i > 0 { + fmt.Fprint(&buf, " ") + } + fmt.Fprint(&buf, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) + } + fmt.Fprint(&buf, "]") + return buf.String() +} diff --git a/oracle/freevars.go b/oracle/freevars.go new file mode 100644 index 00000000..e4d7c4ef --- /dev/null +++ b/oracle/freevars.go @@ -0,0 +1,143 @@ +package oracle + +import ( + "go/ast" + + "code.google.com/p/go.tools/go/types" +) + +// freevars displays the lexical (not package-level) free variables of +// the selection. +// +// It treats A.B.C as a separate variable from A to reveal the parts +// of an aggregate type that are actually needed. +// This aids refactoring. +// +// TODO(adonovan): optionally display the free references to +// file/package scope objects, and to objects from other packages. +// Depending on where the resulting function abstraction will go, +// these might be interesting. Perhaps group the results into three +// bands. +// +func freevars(o *oracle) (queryResult, error) { + file := o.queryPath[len(o.queryPath)-1] // the enclosing file + + // The id and sel functions return non-nil if they denote an + // object o or selection o.x.y that is referenced by the + // selection but defined neither within the selection nor at + // file scope, i.e. it is in the lexical environment. + var id func(n *ast.Ident) types.Object + var sel func(n *ast.SelectorExpr) types.Object + + sel = func(n *ast.SelectorExpr) types.Object { + switch x := unparen(n.X).(type) { + case *ast.SelectorExpr: + return sel(x) + case *ast.Ident: + return id(x) + } + return nil + } + + id = func(n *ast.Ident) types.Object { + obj := o.queryPkgInfo.ObjectOf(n) + if obj == nil { + return nil // TODO(adonovan): fix: this fails for *types.Label. + panic(o.errorf(n, "no types.Object for ast.Ident")) + } + if _, ok := obj.(*types.Package); ok { + return nil // imported package + } + if n.Pos() == obj.Pos() { + return nil // this ident is the definition, not a reference + } + if !(file.Pos() <= obj.Pos() && obj.Pos() <= file.End()) { + return nil // not defined in this file + } + if obj.Parent() == nil { + return nil // e.g. interface method TODO(adonovan): what else? + } + if obj.Parent() == o.queryPkgInfo.Scopes[file] { + return nil // defined at file scope + } + if o.startPos <= obj.Pos() && obj.Pos() <= o.endPos { + return nil // defined within selection => not free + } + return obj + } + + // Maps each reference that is free in the selection + // to the object it refers to. + freeRefs := make(map[string]freevarsRef) + + // Visit all the identifiers in the selected ASTs. + ast.Inspect(o.queryPath[0], func(n ast.Node) bool { + if n == nil { + return true // popping DFS stack + } + + // Is this node contained within the selection? + // (freevars permits inexact selections, + // like two stmts in a block.) + if o.startPos <= n.Pos() && n.End() <= o.endPos { + switch n := n.(type) { + case *ast.Ident: + if obj := id(n); obj != nil { + freeRefs[o.printNode(n)] = freevarsRef{n, obj} + } + + case *ast.SelectorExpr: + if obj := sel(n); obj != nil { + freeRefs[o.printNode(n)] = freevarsRef{n, obj} + return false // don't descend + } + } + } + + return true // descend + }) + + return &freevarsResult{ + refs: freeRefs, + }, nil +} + +type freevarsResult struct { + refs map[string]freevarsRef +} + +type freevarsRef struct { + expr ast.Expr + obj types.Object +} + +func (r *freevarsResult) display(o *oracle) { + if len(r.refs) == 0 { + o.printf(o, "No free identifers.") + return + } + + o.printf(o, "Free identifers:") + for s, ref := range r.refs { + typ := ref.obj.Type() + if _, ok := ref.expr.(*ast.SelectorExpr); ok { + typ = o.queryPkgInfo.TypeOf(ref.expr) + } + var kind string + switch ref.obj.(type) { + case *types.Var: + kind = "var" + case *types.Func: + kind = "func" + case *types.TypeName: + kind = "type" + case *types.Const: + kind = "const" + case *types.Label: + kind = "label" + default: + panic(ref.obj) + } + o.printf(ref.obj, "%s %s %s", kind, s, typ) + } +} diff --git a/oracle/implements.go b/oracle/implements.go new file mode 100644 index 00000000..6746e8b5 --- /dev/null +++ b/oracle/implements.go @@ -0,0 +1,84 @@ +package oracle + +import ( + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/ssa" +) + +// Implements displays the 'implements" relation among all +// package-level named types in the package containing the query +// position. +// +// TODO(adonovan): more features: +// - should we include pairs of types belonging to +// different packages in the 'implements' relation?// +// - should we restrict the query to the type declaration identified +// by the query position, if any, and use all types in the package +// otherwise? +// - should we show types that are local to functions? +// They can only have methods via promotion. +// - abbreviate the set of concrete types implementing the empty +// interface. +// - should we scan the instruction stream for MakeInterface +// instructions and report which concrete->interface conversions +// actually occur, with examples? (NB: this is not a conservative +// answer due to ChangeInterface, i.e. subtyping among interfaces.) +// +func implements(o *oracle) (queryResult, error) { + pkg := o.prog.Package(o.queryPkgInfo.Pkg) + if pkg == nil { + return nil, o.errorf(o.queryPath[0], "no SSA package") + } + + // Compute set of named interface/concrete types at package level. + var interfaces, concretes []*types.Named + for _, mem := range pkg.Members { + if t, ok := mem.(*ssa.Type); ok { + nt := t.Type().(*types.Named) + if _, ok := nt.Underlying().(*types.Interface); ok { + interfaces = append(interfaces, nt) + } else { + concretes = append(concretes, nt) + } + } + } + + // For each interface, show the concrete types that implement it. + var facts []implementsFact + for _, iface := range interfaces { + fact := implementsFact{iface: iface} + for _, conc := range concretes { + if types.IsAssignableTo(conc, iface) { + fact.conc = conc + } else if ptr := types.NewPointer(conc); types.IsAssignableTo(ptr, iface) { + fact.conc = ptr + } else { + continue + } + facts = append(facts, fact) + } + } + + return &implementsResult{facts}, nil +} + +type implementsFact struct { + iface *types.Named + conc types.Type // Named or Pointer(Named) +} + +type implementsResult struct { + facts []implementsFact // facts are grouped by interface +} + +func (r *implementsResult) display(o *oracle) { + // TODO(adonovan): sort to ensure test nondeterminism. + var prevIface *types.Named + for _, fact := range r.facts { + if fact.iface != prevIface { + o.printf(fact.iface.Obj(), "\tInterface %s:", fact.iface) + prevIface = fact.iface + } + o.printf(deref(fact.conc).(*types.Named).Obj(), "\t\t%s", fact.conc) + } +} diff --git a/oracle/oracle.go b/oracle/oracle.go new file mode 100644 index 00000000..47aa914f --- /dev/null +++ b/oracle/oracle.go @@ -0,0 +1,381 @@ +package oracle + +// This file defines oracle.Main, 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" + "errors" + "fmt" + "go/ast" + "go/build" + "go/printer" + "go/token" + "io" + "os" + "path/filepath" + "time" + + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/importer" + "code.google.com/p/go.tools/pointer" + "code.google.com/p/go.tools/ssa" +) + +type oracle struct { + out io.Writer // standard output + prog *ssa.Program // the SSA program [need&SSA] + config pointer.Config // pointer analysis configuration + + // need&(Pos|ExactPos): + startPos, endPos token.Pos // source extent of query + queryPkgInfo *importer.PackageInfo // type info for the queried package + queryPath []ast.Node // AST path from query node to root of ast.File + + timers map[string]time.Duration // phase timing information +} + +// A set of bits indicating the analytical requirements of each mode. +const ( + Pos = 1 << iota // needs a position + ExactPos // needs an exact AST selection; implies Pos + SSA // needs SSA intermediate form + WholeSource // needs ASTs/SSA (not just types) for whole program + + // TODO(adonovan): implement more efficiently than WholeSource|SSA. + TypedAST = WholeSource | SSA // needs typed AST for the queried package; implies Pos +) + +type modeInfo struct { + needs int + impl func(*oracle) (queryResult, error) +} + +var modes = map[string]modeInfo{ + "callees": modeInfo{WholeSource | SSA | ExactPos, callees}, + "callers": modeInfo{WholeSource | SSA | Pos, callers}, + "callgraph": modeInfo{WholeSource | SSA, callgraph}, + "callstack": modeInfo{WholeSource | SSA | Pos, callstack}, + "describe": modeInfo{WholeSource | SSA | ExactPos, describe}, + "freevars": modeInfo{TypedAST | Pos, freevars}, + "implements": modeInfo{TypedAST | Pos, implements}, + "peers": modeInfo{WholeSource | SSA | Pos, peers}, +} + +type queryResult interface { + display(o *oracle) +} + +// Main runs the oracle. +// args specify the main package in importer.CreatePackageFromArgs syntax. +// mode is the query mode ("callers", etc). +// pos is the selection in parseQueryPos() syntax. +// ptalog is the (optional) pointer-analysis log file. +// out is the standard output stream. +// buildContext is the optional configuration for locating packages. +// +func Main(args []string, mode, pos string, ptalog, out io.Writer, buildContext *build.Context) error { + minfo, ok := modes[mode] + if !ok { + if mode == "" { + return errors.New("You must specify a -mode to perform.") + } + return fmt.Errorf("Invalid mode type '%s'.", mode) + } + + var loader importer.SourceLoader + if minfo.needs&WholeSource != 0 { + loader = importer.MakeGoBuildLoader(buildContext) + } + imp := importer.New(&importer.Config{Loader: loader}) + o := &oracle{ + out: out, + prog: ssa.NewProgram(imp.Fset, 0), + timers: make(map[string]time.Duration), + } + o.config.Log = ptalog + + type warning struct { + pos token.Pos + format string + args []interface{} + } + var warnings []warning + o.config.Warn = func(pos token.Pos, format string, args ...interface{}) { + warnings = append(warnings, warning{pos, format, args}) + } + + // Phase timing diagnostics. + if false { + defer func() { + fmt.Println() + for name, duration := range o.timers { + fmt.Printf("# %-30s %s\n", name, duration) + } + }() + } + + // Load/parse/type-check program from args. + start := time.Now() + initialPkgInfo, _, err := importer.CreatePackageFromArgs(imp, args) + if err != nil { + return err // I/O, parser or type error + } + o.timers["load/parse/type"] = time.Since(start) + + // Parse the source query position. + if minfo.needs&(Pos|ExactPos) != 0 { + var err error + o.startPos, o.endPos, err = parseQueryPos(o.prog.Fset, pos) + if err != nil { + return err + } + + var exact bool + o.queryPkgInfo, o.queryPath, exact = imp.PathEnclosingInterval(o.startPos, o.endPos) + if o.queryPath == nil { + return o.errorf(o, "no syntax here") + } + if minfo.needs&ExactPos != 0 && !exact { + return o.errorf(o.queryPath[0], "ambiguous selection within %s", + importer.NodeDescription(o.queryPath[0])) + } + } + + // Create SSA package for the initial package and its dependencies. + if minfo.needs&SSA != 0 { + start = time.Now() + + // All packages. + for _, info := range imp.Packages { + o.prog.CreatePackage(info) // create ssa.Package + } + + // Initial package (specified on command line) + initialPkg := o.prog.Package(initialPkgInfo.Pkg) + + // Add package to the pointer analysis scope. + if initialPkg.Func("main") == nil { + if initialPkg.CreateTestMainFunction() == nil { + return o.errorf(o, "analysis scope has no main() entry points") + } + } + o.config.Mains = append(o.config.Mains, initialPkg) + + // Query package. + if o.queryPkgInfo != nil { + pkg := o.prog.Package(o.queryPkgInfo.Pkg) + pkg.SetDebugMode(true) + pkg.Build() + } + + o.timers["SSA-create"] = time.Since(start) + } + + // SSA is built and we have query{Path,PkgInfo}. + // Release the other ASTs and type info to the GC. + imp = nil + + result, err := minfo.impl(o) + if err != nil { + return err + } + // TODO(adonovan): use this as a seam for testing. + result.display(o) + + // Print warnings after the main output. + if warnings != nil { + fmt.Fprintln(o.out, "\nPointer analysis warnings:") + for _, w := range warnings { + o.fprintf(o.out, w.pos, "warning: "+w.format, w.args...) + } + } + + return nil +} + +// ---------- 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 the synthetic +// root of the callgraph. +// +func ptrAnalysis(o *oracle) pointer.CallGraphNode { + start := time.Now() + root := pointer.Analyze(&o.config) + o.timers["pointer analysis"] = time.Since(start) + return root +} + +// parseQueryPos parses a string of the form "file pos" or file +// start-end" where pos, start, end are decimal integers, and returns +// the extent to which it refers. +// +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 + } + var filename string + var startOffset, endOffset int + n, err := fmt.Sscanf(queryPos, "%s %d-%d", &filename, &startOffset, &endOffset) + if n != 3 { + n, err = fmt.Sscanf(queryPos, "%s %d", &filename, &startOffset) + if n != 2 { + err = fmt.Errorf("invalid source position -pos=%q", queryPos) + return + } + endOffset = startOffset + } + + 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. +// - o *oracle, meaning the extent [o.startPos, o.endPos) 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 *oracle: + start = o.startPos + end = o.endPos + 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") +} + +// printf is like fprintf, but writes to to o.out. +func (o *oracle) printf(pos interface{}, format string, args ...interface{}) { + o.fprintf(o.out, pos, format, args...) +} + +// errorf is like fprintf, but returns a formatted error string. +func (o *oracle) errorf(pos interface{}, format string, args ...interface{}) error { + var buf bytes.Buffer + o.fprintf(&buf, pos, format, args...) + return errors.New(buf.String()) +} + +// 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 new file mode 100644 index 00000000..736c4a1a --- /dev/null +++ b/oracle/oracle_test.go @@ -0,0 +1,218 @@ +package oracle_test + +// This file defines a test framework for oracle queries. +// +// The files beneath testdata/src/main contain Go programs containing +// query annotations of the form: +// +// @verb id "select" +// +// where verb is the query mode (e.g. "callers"), id is a unique name +// for this query, and "select" is a regular expression matching the +// substring of the current line that is the query's input selection. +// +// The expected output for each query is provided in the accompanying +// .golden file. +// +// (Location information is not included because it's too fragile to +// display as text. TODO(adonovan): think about how we can test its +// correctness, since it is critical information.) +// +// Run this test with: +// % go test code.google.com/p/go.tools/oracle -update +// to update the golden files. + +// TODO(adonovan): improve coverage: +// - output of @callgraph is nondeterministic. +// - as are lists of labels. + +import ( + "bytes" + "flag" + "fmt" + "go/build" + "go/parser" + "go/token" + "io" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + "testing" + + "code.google.com/p/go.tools/oracle" +) + +var updateFlag = flag.Bool("update", false, "Update the golden files.") + +type query struct { + id string // unique id + verb string // query mode, e.g. "callees" + posn token.Position // position of of query + filename string + start, end int // selection of file to pass to oracle +} + +func parseRegexp(text string) (*regexp.Regexp, error) { + pattern, err := strconv.Unquote(text) + if err != nil { + return nil, fmt.Errorf("can't unquote %s", text) + } + return regexp.Compile(pattern) +} + +// parseQueries parses and returns the queries in the named file. +func parseQueries(t *testing.T, filename string) []*query { + filedata, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + + // Parse the file once to discover the test queries. + var fset token.FileSet + f, err := parser.ParseFile(&fset, filename, filedata, + parser.DeclarationErrors|parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + lines := bytes.Split(filedata, []byte("\n")) + + var queries []*query + queriesById := make(map[string]*query) + + // Find all annotations of these forms: + expectRe := regexp.MustCompile(`@([a-z]+)\s+(\S+)\s+(\".*)$`) // @verb id "regexp" + for _, c := range f.Comments { + text := strings.TrimSpace(c.Text()) + if text == "" || text[0] != '@' { + continue + } + posn := fset.Position(c.Pos()) + + // @verb id "regexp" + match := expectRe.FindStringSubmatch(text) + if match == nil { + t.Errorf("%s: ill-formed query: %s", posn, text) + continue + } + + id := match[2] + if prev, ok := queriesById[id]; ok { + t.Errorf("%s: duplicate id %s", posn, id) + t.Errorf("%s: previously used here", prev.posn) + continue + } + + selectRe, err := parseRegexp(match[3]) + if err != nil { + t.Errorf("%s: %s", posn, err) + continue + } + + // Find text of the current line, sans query. + // (Queries must be // not /**/ comments.) + line := lines[posn.Line-1][:posn.Column-1] + + // Apply regexp to current line to find input selection. + loc := selectRe.FindIndex(line) + if loc == nil { + t.Errorf("%s: selection pattern %s doesn't match line %q", + posn, match[3], string(line)) + continue + } + + // Assumes ASCII. TODO(adonovan): test on UTF-8. + linestart := posn.Offset - (posn.Column - 1) + + // Compute the file offsets + q := &query{ + id: id, + verb: match[1], + posn: posn, + filename: filename, + start: linestart + loc[0], + end: linestart + loc[1], + } + queries = append(queries, q) + queriesById[id] = q + } + + // Return the slice, not map, for deterministic iteration. + return queries +} + +// stripLocation removes a "file:line: " prefix. +func stripLocation(line string) string { + if i := strings.Index(line, ": "); i >= 0 { + line = line[i+2:] + } + return line +} + +// doQuery poses query q to the oracle and writes its response and +// error (if any) to out. +func doQuery(out io.Writer, q *query) { + fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id) + + capture := new(bytes.Buffer) // capture standard output + var buildContext = build.Default + buildContext.GOPATH = "testdata" + err := oracle.Main([]string{q.filename}, + q.verb, + fmt.Sprintf("%s %d-%d", q.filename, q.start, q.end), + /*PTA-log=*/ nil, capture, &buildContext) + + for _, line := range strings.Split(capture.String(), "\n") { + fmt.Fprintf(out, "%s\n", stripLocation(line)) + } + + if err != nil { + fmt.Fprintf(out, "Error: %s\n", stripLocation(err.Error())) + } +} + +func TestOracle(t *testing.T) { + for _, filename := range []string{ + "testdata/src/main/calls.go", + "testdata/src/main/describe.go", + "testdata/src/main/freevars.go", + "testdata/src/main/implements.go", + "testdata/src/main/imports.go", + "testdata/src/main/peers.go", + } { + queries := parseQueries(t, filename) + golden := filename + "lden" + got := filename + "t" + gotfh, err := os.Create(got) + if err != nil { + t.Errorf("Create(%s) failed: %s", got, err) + continue + } + defer gotfh.Close() + + // Run the oracle on each query, redirecting its output + // and error (if any) to the foo.got file. + for _, q := range queries { + doQuery(gotfh, q) + } + + // Compare foo.got with foo.golden. + cmd := exec.Command("/usr/bin/diff", "-u3", golden, got) // assumes POSIX + buf := new(bytes.Buffer) + cmd.Stdout = buf + if err := cmd.Run(); err != nil { + t.Errorf("Oracle tests for %s failed: %s.\n%s\n", + filename, err, buf) + + if *updateFlag { + t.Logf("Updating %s...", golden) + if err := exec.Command("/bin/cp", got, golden).Run(); err != nil { + t.Errorf("Update failed: %s", err) + } + } + } + } +} diff --git a/oracle/peers.go b/oracle/peers.go new file mode 100644 index 00000000..644dfccb --- /dev/null +++ b/oracle/peers.go @@ -0,0 +1,147 @@ +package oracle + +import ( + "go/ast" + "go/token" + + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/pointer" + "code.google.com/p/go.tools/ssa" +) + +// peers enumerates, for a given channel send (or receive) operation, +// the set of possible receives (or sends) that correspond to it. +// +// TODO(adonovan): support reflect.{Select,Recv,Send}. +// TODO(adonovan): permit the user to query based on a MakeChan (not send/recv), +// or the implicit receive in "for v := range ch". +// +func peers(o *oracle) (queryResult, error) { + // Determine the enclosing send/receive op for the specified position. + var arrowPos token.Pos + for _, n := range o.queryPath { + switch n := n.(type) { + case *ast.UnaryExpr: + if n.Op == token.ARROW { + arrowPos = n.OpPos + goto found + } + case *ast.SendStmt: + arrowPos = n.Arrow + goto found + } + } + return nil, o.errorf(o.queryPath[0], "there is no send/receive here") +found: + + buildSSA(o) + + var queryOp chanOp // the originating send or receive operation + var ops []chanOp // all sends/receives of opposite direction + + // Look at all send/receive instructions in the whole ssa.Program. + // Build a list of those of same type to query. + allFuncs := ssa.AllFunctions(o.prog) + for fn := range allFuncs { + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + for _, op := range chanOps(instr) { + ops = append(ops, op) + if op.pos == arrowPos { + queryOp = op // we found the query op + } + } + } + } + } + if queryOp.ch == nil { + return nil, o.errorf(arrowPos, "ssa.Instruction for send/receive not found") + } + + // Discard operations of wrong channel element type. + // Build set of channel ssa.Values as query to pointer analysis. + queryElemType := queryOp.ch.Type().Underlying().(*types.Chan).Elem() + channels := map[ssa.Value][]pointer.Pointer{queryOp.ch: nil} + i := 0 + for _, op := range ops { + if types.IsIdentical(op.ch.Type().Underlying().(*types.Chan).Elem(), queryElemType) { + channels[op.ch] = nil + ops[i] = op + i++ + } + } + ops = ops[:i] + + // Run the pointer analysis. + o.config.QueryValues = channels + ptrAnalysis(o) + + // Combine the PT sets from all contexts. + queryChanPts := pointer.PointsToCombined(channels[queryOp.ch]) + + return &peersResult{ + queryOp: queryOp, + ops: ops, + queryChanPts: queryChanPts, + }, nil +} + +// chanOp abstracts an ssa.Send, ssa.Unop(ARROW), or a SelectState. +type chanOp struct { + ch ssa.Value + dir ast.ChanDir + pos token.Pos +} + +// chanOps returns a slice of all the channel operations in the instruction. +func chanOps(instr ssa.Instruction) []chanOp { + // TODO(adonovan): handle calls to reflect.{Select,Recv,Send} too. + var ops []chanOp + switch instr := instr.(type) { + case *ssa.UnOp: + if instr.Op == token.ARROW { + ops = append(ops, chanOp{instr.X, ast.RECV, instr.Pos()}) + } + case *ssa.Send: + ops = append(ops, chanOp{instr.Chan, ast.SEND, instr.Pos()}) + case *ssa.Select: + for _, st := range instr.States { + ops = append(ops, chanOp{st.Chan, st.Dir, st.Pos}) + } + } + return ops +} + +type peersResult struct { + queryOp chanOp + ops []chanOp + queryChanPts pointer.PointsToSet +} + +func (r *peersResult) display(o *oracle) { + // Report which make(chan) labels the query's channel can alias. + labels := r.queryChanPts.Labels() + if len(labels) == 0 { + o.printf(r.queryOp.pos, "This channel can't point to anything.") + return + } + o.printf(r.queryOp.pos, "This channel of type %s may be:", r.queryOp.ch.Type()) + // TODO(adonovan): sort, to ensure test determinism. + for _, label := range labels { + o.printf(label, "\tallocated here") + } + + // Report which send/receive operations can alias the same make(chan) labels. + for _, op := range r.ops { + // TODO(adonovan): sort, to ensure test determinism. + for _, ptr := range o.config.QueryValues[op.ch] { + if ptr != nil && ptr.PointsTo().Intersects(r.queryChanPts) { + verb := "received from" + if op.dir == ast.SEND { + verb = "sent to" + } + o.printf(op.pos, "\t%s, here", verb) + } + } + } +} diff --git a/oracle/testdata/src/lib/lib.go b/oracle/testdata/src/lib/lib.go new file mode 100644 index 00000000..0603d4b4 --- /dev/null +++ b/oracle/testdata/src/lib/lib.go @@ -0,0 +1,14 @@ +package lib + +type Type int + +func (Type) Method(x *int) *int { + return x +} + +func Func() { +} + +const Const = 3 + +var Var = 0 diff --git a/oracle/testdata/src/main/calls.go b/oracle/testdata/src/main/calls.go new file mode 100644 index 00000000..aa7e3ef9 --- /dev/null +++ b/oracle/testdata/src/main/calls.go @@ -0,0 +1,86 @@ +package main + +// Tests of call-graph queries. +// 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" + // @callers callers-A "^" + // @callstack callstack-A "^" +} + +func B(x *int) { // @describe describe-B-x "x" + // @callers callers-B "^" +} + +// apply is not (yet) treated context-sensitively. +func apply(f func(x *int), x *int) { + f(x) // @callees callees-apply "f" + // @callers callers-apply "^" +} + +// store *is* treated context-sensitively, +// so the points-to sets for pc, pd are precise. +func store(ptr **int, value *int) { + *ptr = value + // @callers callers-store "^" +} + +func call(f func() *int) { + // Result points to anon function. + f() // @describe describe-result-f "f" + + // Target of call is anon function. + f() // @callees callees-main.call-f "f" + + // @callers callers-main.call "^" +} + +func main() { + var a, b int + apply(A, &a) // @callees callees-main-apply1 "app" + apply(B, &b) + + var c, d int + var pc, pd *int // @describe describe-pc "pc" + store(&pc, &c) + store(&pd, &d) + _ = pd // @describe describe-pd "pd" + + call(func() *int { + // We are called twice from main.call + // @callers callers-main.anon "^" + return &a + }) + + // Errors + _ = "no function call here" // @callees callees-err-no-call "no" + print("builtin") // @callees callees-err-builtin "builtin" + _ = string("type conversion") // @callees callees-err-conversion "str" + call(nil) // @callees callees-err-bad-selection "call\\(nil" + if false { + main() // @callees callees-err-deadcode1 "main" + } + var nilFunc func() + nilFunc() // @callees callees-err-nil-func "nilFunc" + var i interface { + f() + } + i.f() // @callees callees-err-nil-interface "i.f" +} + +func deadcode() { + main() // @callees callees-err-deadcode2 "main" + // @callers callers-err-deadcode "^" + // @callstack callstack-err-deadcode "^" +} + +// This code belongs to init. +var global = 123 // @callers callers-global "global" + +// init may be called by other packages' inits, or in this case, the +// root of the callgraph. +func init() { + // @callers callers-init "^" + +} diff --git a/oracle/testdata/src/main/calls.golden b/oracle/testdata/src/main/calls.golden new file mode 100644 index 00000000..4cac0a53 --- /dev/null +++ b/oracle/testdata/src/main/calls.golden @@ -0,0 +1,106 @@ +-------- @describe describe-A-x -------- +definition of var x *int +value may point to these labels: + a + b + +-------- @callstack callstack-A -------- +Found a call path from root to main.A +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: + a + b + +-------- @callers callers-B -------- +main.B is called from these 1 sites: + dynamic function call from main.apply + +-------- @callees callees-apply -------- +this dynamic function call dispatches to: + main.A + main.B + +-------- @callers callers-apply -------- +main.apply is called from these 2 sites: + static function call from main.main + static function call from main.main + +-------- @callers callers-store -------- +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: + func@50.7 + +-------- @callees callees-main.call-f -------- +this dynamic function call dispatches to: + func@50.7 + +-------- @callers callers-main.call -------- +main.call is called from these 2 sites: + static function call from main.main + static function call from main.main + +-------- @callees callees-main-apply1 -------- +this static function call dispatches to: + main.apply + +-------- @describe describe-pc -------- +definition of var pc *int +value may point to these labels: + c + +-------- @describe describe-pd -------- +reference to var pd *int +defined here +value may point to these labels: + d + +-------- @callees callees-err-no-call -------- + +Error: there is no function call here + +-------- @callees callees-err-builtin -------- + +Error: this is a call to the built-in 'print' operator + +-------- @callees callees-err-conversion -------- + +Error: this is a type conversion, not a function call + +-------- @callees callees-err-bad-selection -------- + +Error: ambiguous selection within function call (or conversion) + +-------- @callees callees-err-deadcode1 -------- +this call site is unreachable in this analysis + +-------- @callees callees-err-nil-func -------- +dynamic function call on nil value + +-------- @callees callees-err-nil-interface -------- +dynamic method call on nil value + +-------- @callees callees-err-deadcode2 -------- +this call site is unreachable in this analysis + +-------- @callstack callstack-err-deadcode -------- +main.deadcode is unreachable in this analysis scope + +-------- @callers callers-global -------- +main.init is called from these 1 sites: +the root of the call graph + +-------- @callers callers-init -------- +main.init is called from these 1 sites: +the root of the call graph + diff --git a/oracle/testdata/src/main/describe.go b/oracle/testdata/src/main/describe.go new file mode 100644 index 00000000..52629170 --- /dev/null +++ b/oracle/testdata/src/main/describe.go @@ -0,0 +1,75 @@ +package describe // @describe pkgdecl "describe" + +// Tests of 'describe' query. +// See go.tools/oracle/oracle_test.go for explanation. +// See describe.golden for expected query results. + +// TODO(adonovan): more coverage of the (extensive) logic. + +type cake float64 // @describe type-ref-builtin "float64" + +const pi = 3.141 // @describe const-def-pi "pi" +const pie = cake(pi) // @describe const-def-pie "pie" +const _ = pi // @describe const-ref-pi "pi" + +func main() { // @describe func-def-main "main" + // func objects + _ = main // @describe func-ref-main "main" + _ = (*C).f // @describe func-ref-*C.f "..C..f" + _ = D.f // @describe func-ref-D.f "D.f" + _ = I.f // @describe func-ref-I.f "I.f" + var d D // @describe type-D "D" + var i I // @describe type-I "I" + _ = d.f // @describe func-ref-d.f "d.f" + _ = i.f // @describe func-ref-i.f "i.f" + + // var objects + anon := func() { + _ = d // @describe ref-lexical-d "d" + } + _ = anon // @describe ref-anon "anon" + + // SSA affords some local flow sensitivity. + var a, b int + var x = &a // @describe var-def-x-1 "x" + _ = x // @describe var-ref-x-1 "x" + x = &b // @describe var-def-x-2 "x" + _ = x // @describe var-ref-x-2 "x" + + // const objects + const localpi = 3.141 // @describe const-local-pi "localpi" + const localpie = cake(pi) // @describe const-local-pie "localpie" + const _ = localpi // @describe const-ref-localpi "localpi" + + // type objects + type T int // @describe type-def-T "T" + var three T = 3 // @describe type-ref-T "T" + + print(1 + 2*3) // @describe const-expr " 2.3" + print(real(1+2i) - 3) // @describe const-expr2 "real.*3" + + m := map[string]*int{"a": &a} + // TODO(adonovan): fix spurious error in map-lookup,ok result. + mapval, _ := m["a"] // @describe map-lookup,ok "m..a.." + _ = mapval // @describe mapval "mapval" + _ = m // @describe m "m" + + defer main() // @describe defer-stmt "defer" + go main() // @describe go-stmt "go" +} + +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" +} + +type I interface { // @describe def-iface-I "I" + f() // @describe def-imethod-I.f "f" +} + +type C int +type D struct{} + +func (c *C) f() {} +func (d D) f() {} diff --git a/oracle/testdata/src/main/describe.golden b/oracle/testdata/src/main/describe.golden new file mode 100644 index 00000000..e16c2bd3 --- /dev/null +++ b/oracle/testdata/src/main/describe.golden @@ -0,0 +1,165 @@ +-------- @describe pkgdecl -------- +definition of package "main" + type C int + method (*describe.C) f() + type D struct{} + method (describe.D) f() + type I interface{f()} + method (describe.I) f() + type cake float64 + func deadcode func() + func init func() + var init$guard bool + func main func() + const pi untyped float = 3141/1000:untyped float + const pie describe.cake = 1768225803696341/562949953421312:describe.cake + +-------- @describe type-ref-builtin -------- +reference to built-in type float64 + +-------- @describe const-def-pi -------- +definition of const pi untyped float + +-------- @describe const-def-pie -------- +definition of const pie describe.cake + +-------- @describe const-ref-pi -------- +reference to const pi untyped float of constant value 3141/1000 +defined here + +-------- @describe func-def-main -------- +definition of func describe.main() + +-------- @describe func-ref-main -------- +reference to func describe.main() +defined here + +-------- @describe func-ref-*C.f -------- +reference to func (*describe.C).f() +defined here + +-------- @describe func-ref-D.f -------- +reference to func (describe.D).f() +defined here + +-------- @describe func-ref-I.f -------- +reference to func (describe.I).f() +defined here + +-------- @describe type-D -------- +reference to type describe.D +defined as struct{} +Method set: + method (describe.D) f() + +-------- @describe type-I -------- +reference to type describe.I +defined as interface{f()} +Method set: + method (describe.I) f() + +-------- @describe func-ref-d.f -------- +reference to func (describe.D).f() +defined here + +-------- @describe func-ref-i.f -------- +reference to func (describe.I).f() +defined here + +-------- @describe ref-lexical-d -------- +reference to var d describe.D +defined here + +-------- @describe ref-anon -------- +reference to var anon func() +defined here +value may point to these labels: + func@27.10 + +-------- @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 const-local-pi -------- +definition of const localpi untyped float + +-------- @describe const-local-pie -------- +definition of const localpie describe.cake + +-------- @describe const-ref-localpi -------- +reference to const localpi untyped float of constant value 3141/1000 +defined here + +-------- @describe type-def-T -------- +definition of type describe.T +No methods. + +-------- @describe type-ref-T -------- +reference to type describe.T +defined as int +No methods. + +-------- @describe const-expr -------- +binary * operation of constant value 6 + +-------- @describe const-expr2 -------- +binary - operation of constant value -2 + +-------- @describe map-lookup,ok -------- +index expression of type (*int, bool) +no pointer analysis: can't locate SSA Value for expression in main.main + +-------- @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 + +-------- @describe go-stmt -------- +go statement + +-------- @describe var-decl-stmt -------- +definition of var a int + +-------- @describe b -------- +definition of var b *int +pointer analysis did not analyze this expression (dead code?) + +-------- @describe def-iface-I -------- +definition of type describe.I +Method set: + method (describe.I) f() + +-------- @describe def-imethod-I.f -------- +type interface{f()} +Method set: + method (interface{f()}) f() + diff --git a/oracle/testdata/src/main/freevars.go b/oracle/testdata/src/main/freevars.go new file mode 100644 index 00000000..82633d5a --- /dev/null +++ b/oracle/testdata/src/main/freevars.go @@ -0,0 +1,37 @@ +package main + +// Tests of 'freevars' query. +// See go.tools/oracle/oracle_test.go for explanation. +// See freevars.golden for expected query results. + +// TODO(adonovan): it's hard to test this query in a single line of gofmt'd code. + +type T struct { + a, b int +} + +type S struct { + x int + t T +} + +func main() { + type C int + x := 1 + const exp = 6 + if y := 2; x+y+int(C(3)) != exp { // @freevars fv1 "if.*{" + panic("expected 6") + } + + var s S + + for x, y := range "foo" { + println(s.x + s.t.a + s.t.b + x + int(y)) // @freevars fv2 "print.*y." + } + + // TODO(adonovan): enable when go/types supports labels. +loop: // #@freevars fv-def-label "loop:" + for { + break loop // #@freevars fv-ref-label "break loop" + } +} diff --git a/oracle/testdata/src/main/freevars.golden b/oracle/testdata/src/main/freevars.golden new file mode 100644 index 00000000..678653db --- /dev/null +++ b/oracle/testdata/src/main/freevars.golden @@ -0,0 +1,14 @@ +-------- @freevars fv1 -------- +Free identifers: +var x int +type C main.C +const exp untyped integer + +-------- @freevars fv2 -------- +Free identifers: +var s.x int +var s.t.a int +var s.t.b int +var x int +var y int32 + diff --git a/oracle/testdata/src/main/implements.go b/oracle/testdata/src/main/implements.go new file mode 100644 index 00000000..81154265 --- /dev/null +++ b/oracle/testdata/src/main/implements.go @@ -0,0 +1,29 @@ +package main + +// Tests of 'implements' query. +// See go.tools/oracle/oracle_test.go for explanation. +// See implements.golden for expected query results. + +// @implements impl "" + +func main() { +} + +type E interface{} + +type F interface { + f() +} + +type FG interface { + f() + g() int +} + +type C int +type D struct{} + +func (c *C) f() {} +func (d D) f() {} + +func (d *D) g() int { return 0 } diff --git a/oracle/testdata/src/main/implements.golden b/oracle/testdata/src/main/implements.golden new file mode 100644 index 00000000..b4252145 --- /dev/null +++ b/oracle/testdata/src/main/implements.golden @@ -0,0 +1,10 @@ +-------- @implements impl -------- + Interface main.E: + main.C + main.D + Interface main.F: + *main.C + main.D + Interface main.FG: + *main.D + diff --git a/oracle/testdata/src/main/imports.go b/oracle/testdata/src/main/imports.go new file mode 100644 index 00000000..17c5e507 --- /dev/null +++ b/oracle/testdata/src/main/imports.go @@ -0,0 +1,26 @@ +package imports + +import ( + "lib" // @describe ref-pkg-import "lib" +) + +// Tests that import another package. (To make the tests run quickly, +// we avoid using imports in all the other tests. Remember, each +// query causes parsing and typechecking of the whole program.) +// +// See go.tools/oracle/oracle_test.go for explanation. +// See imports.golden for expected query results. + +var a int + +func main() { + const c = lib.Const // @describe ref-const "Const" + lib.Func() // @describe ref-func "Func" + lib.Var++ // @describe ref-var "Var" + var t lib.Type // @describe ref-type "Type" + p := t.Method(&a) // @describe ref-method "Method" + + print(*p + 1) // @describe 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 new file mode 100644 index 00000000..39c13e2e --- /dev/null +++ b/oracle/testdata/src/main/imports.golden @@ -0,0 +1,46 @@ +-------- @describe ref-pkg-import -------- +import of package "lib" + const Const untyped integer = 3:untyped integer + func Func func() + type Type int + method (lib.Type) Method(x *int) *int + var Var int + +-------- @describe ref-const -------- +reference to const Const untyped integer +defined here + +-------- @describe ref-func -------- +reference to func lib.Func() +defined here + +-------- @describe ref-var -------- +reference to var Var int +defined here +value may point to these labels: + lib.Var + +-------- @describe ref-type -------- +reference to type lib.Type +defined as int +Method set: + method (lib.Type) Method(x *int) *int + +-------- @describe ref-method -------- +reference to func (lib.Type).Method(x *int) *int +defined here + +-------- @describe p -------- +reference to var p *int +defined here +value may point to these labels: + main.a + +-------- @describe ref-pkg -------- +reference to package "lib" + const Const untyped integer = 3:untyped integer + func Func func() + type Type int + method (lib.Type) Method(x *int) *int + var Var int + diff --git a/oracle/testdata/src/main/peers.go b/oracle/testdata/src/main/peers.go new file mode 100644 index 00000000..0fa1e4cb --- /dev/null +++ b/oracle/testdata/src/main/peers.go @@ -0,0 +1,40 @@ +package peers + +// Tests of channel 'peers' query. +// See go.tools/oracle/oracle_test.go for explanation. +// See peers.golden for expected query results. + +var a2 int + +func main() { + chA := make(chan *int) + a1 := 1 + chA <- &a1 + + chA2 := make(chan *int, 2) + if a2 == 0 { + chA = chA2 + } + + chB := make(chan *int) + b := 3 + chB <- &b + + <-chA // @describe describe-chA "chA" + <-chA2 // @describe describe-chA2 "chA2" + <-chB // @describe describe-chB "chB" + + select { + case rA := <-chA: // @peers peer-recv-chA "<-" + _ = rA // @describe describe-rA "rA" + case rB := <-chB: // @peers peer-recv-chB "<-" + _ = rB // @describe describe-rB "rB" + + case <-chA: // @peers peer-recv-chA' "<-" + + case chA2 <- &a2: // @peers peer-send-chA' "<-" + } + + for _ = range chA { + } +} diff --git a/oracle/testdata/src/main/peers.golden b/oracle/testdata/src/main/peers.golden new file mode 100644 index 00000000..b3ec7dd7 --- /dev/null +++ b/oracle/testdata/src/main/peers.golden @@ -0,0 +1,73 @@ +-------- @describe describe-chA -------- +reference to var chA chan *int +defined here +value may point to these labels: + makechan + makechan + +-------- @describe describe-chA2 -------- +reference to var chA2 chan *int +defined here +value may point to these labels: + makechan + +-------- @describe describe-chB -------- +reference to var chB chan *int +defined here +value may point to these labels: + makechan + +-------- @peers peer-recv-chA -------- +This channel of type chan *int may be: + allocated here + allocated here + sent to, here + received from, here + received from, here + received from, here + received from, here + sent to, here + received from, here + +-------- @describe describe-rA -------- +reference to var rA *int +defined here +value may point to these labels: + main.a2 + a1 + +-------- @peers peer-recv-chB -------- +This channel of type chan *int may be: + allocated here + sent to, here + received from, here + received from, here + +-------- @describe describe-rB -------- +reference to var rB *int +defined here +value may point to these labels: + b + +-------- @peers peer-recv-chA' -------- +This channel of type chan *int may be: + allocated here + allocated here + sent to, here + received from, here + received from, here + received from, here + received from, here + sent to, here + received from, here + +-------- @peers peer-send-chA' -------- +This channel of type chan *int may be: + allocated here + received from, here + received from, here + received from, here + received from, here + sent to, here + received from, here + diff --git a/pointer/util.go b/pointer/util.go index 41bcc24c..e722eef5 100644 --- a/pointer/util.go +++ b/pointer/util.go @@ -14,7 +14,7 @@ func CanPoint(T types.Type) bool { case *types.Named: return CanPoint(T.Underlying()) - case *types.Pointer, *types.Interface, *types.Map, *types.Chan, *types.Signature: + case *types.Pointer, *types.Interface, *types.Map, *types.Chan, *types.Signature, *types.Slice: return true }