go.tools/oracle: an oracle that answers questions about Go source code.

+ Tests.
+ Emacs integration.
+ Emacs integration test.
+ very rudimentary Vim integration.  Needs some love from a Vim user.

TODO (in follow-ups):
- More tests would be good.
  We'll need to make the output order deterministic in more places.
- Documentation.

R=gri, crawshaw, dominik.honnef
CC=golang-dev
https://golang.org/cl/9502043
This commit is contained in:
Alan Donovan 2013-08-27 17:58:26 -04:00
parent be2647ec01
commit e08d89f3ed
28 changed files with 3125 additions and 1 deletions

50
cmd/oracle/emacs-test.bash Executable file
View File

@ -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"

104
cmd/oracle/main.go Normal file
View File

@ -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 [<flag> ...] [<file.go> ...] [<arg> ...]
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)
}
}

180
cmd/oracle/oracle.el Normal file
View File

@ -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 "<f4>") #'go-oracle-describe)))
(provide 'go-oracle)

62
cmd/oracle/oracle.vim Normal file
View File

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

97
oracle/callees.go Normal file
View File

@ -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())
}
}
}

73
oracle/callers.go Normal file
View File

@ -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())
}
}
}
}

63
oracle/callgraph.go Normal file
View File

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

86
oracle/callstack.go Normal file
View File

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

715
oracle/describe.go Normal file
View File

@ -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()
}

143
oracle/freevars.go Normal file
View File

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

84
oracle/implements.go Normal file
View File

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

381
oracle/oracle.go Normal file
View File

@ -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()
}

218
oracle/oracle_test.go Normal file
View File

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

147
oracle/peers.go Normal file
View File

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

14
oracle/testdata/src/lib/lib.go vendored Normal file
View File

@ -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

86
oracle/testdata/src/main/calls.go vendored Normal file
View File

@ -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 "^"
}

106
oracle/testdata/src/main/calls.golden vendored Normal file
View File

@ -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

75
oracle/testdata/src/main/describe.go vendored Normal file
View File

@ -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() {}

165
oracle/testdata/src/main/describe.golden vendored Normal file
View File

@ -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()

37
oracle/testdata/src/main/freevars.go vendored Normal file
View File

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

View File

@ -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

29
oracle/testdata/src/main/implements.go vendored Normal file
View File

@ -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 }

View File

@ -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

26
oracle/testdata/src/main/imports.go vendored Normal file
View File

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

46
oracle/testdata/src/main/imports.golden vendored Normal file
View File

@ -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

40
oracle/testdata/src/main/peers.go vendored Normal file
View File

@ -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 {
}
}

73
oracle/testdata/src/main/peers.golden vendored Normal file
View File

@ -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

View File

@ -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
}