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:
parent
be2647ec01
commit
e08d89f3ed
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "^"
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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() {}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue