diff --git a/cmd/oracle/main.go b/cmd/oracle/main.go index 77af07e0..1717a235 100644 --- a/cmd/oracle/main.go +++ b/cmd/oracle/main.go @@ -16,6 +16,8 @@ package main import ( "bufio" + "bytes" + "encoding/json" "flag" "fmt" "io" @@ -37,6 +39,8 @@ var modeFlag = flag.String("mode", "", var ptalogFlag = flag.String("ptalog", "", "Location of the points-to analysis log file, or empty to disable logging.") +var formatFlag = flag.String("format", "plain", "Output format: 'plain' or 'json'.") + const usage = `Go source code oracle. Usage: oracle [ ...] [ ...] [ ...] Use -help flag to display options. @@ -63,6 +67,16 @@ func init() { } runtime.GOMAXPROCS(n) } + + // For now, 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. + // TODO(adonovan): make the importer do this. + if os.Getenv("CGO_ENABLED") != "0" { + fmt.Fprint(os.Stderr, "Warning: CGO_ENABLED=0 not specified; "+ + "analysis of cgo code may be less precise.\n") + } } func main() { @@ -99,8 +113,35 @@ func main() { defer pprof.StopCPUProfile() } - if err := oracle.Main(args, *modeFlag, *posFlag, ptalog, os.Stdout, nil); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) + // -format flag + if *formatFlag != "json" && *formatFlag != "plain" { + fmt.Fprintf(os.Stderr, "illegal -format value: %q", *formatFlag) os.Exit(1) } + + // Ask the oracle. + res, err := oracle.Query(args, *modeFlag, *posFlag, ptalog, nil) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Print the result. + switch *formatFlag { + case "json": + b, err := json.Marshal(res) + if err != nil { + fmt.Fprintf(os.Stderr, "JSON error: %s\n", err.Error()) + os.Exit(1) + } + var buf bytes.Buffer + if err := json.Indent(&buf, b, "", "\t"); err != nil { + fmt.Fprintf(os.Stderr, "json.Indent failed: %s", err) + os.Exit(1) + } + os.Stdout.Write(buf.Bytes()) + + case "plain": + res.WriteTo(os.Stdout) + } } diff --git a/oracle/callees.go b/oracle/callees.go index ad714ee9..1528cb81 100644 --- a/oracle/callees.go +++ b/oracle/callees.go @@ -6,9 +6,13 @@ package oracle import ( "go/ast" + "go/token" + "sort" "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/pointer" + "code.google.com/p/go.tools/ssa" ) // Callees reports the possible callees of the function call site @@ -51,51 +55,78 @@ func callees(o *oracle) (queryResult, error) { // The presence of a key indicates this call site is // interesting even if the value is nil. querySites := make(map[pointer.CallSite][]pointer.CallGraphNode) + var arbitrarySite pointer.CallSite 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] + arbitrarySite = site } } - o.config.Call = func(site pointer.CallSite, caller, callee pointer.CallGraphNode) { + o.config.Call = func(site pointer.CallSite, callee pointer.CallGraphNode) { if targets, ok := querySites[site]; ok { querySites[site] = append(targets, callee) } } ptrAnalysis(o) + if arbitrarySite == nil { + return nil, o.errorf(call.Lparen, "this call site is unreachable in this analysis") + } + + // Compute union of callees across all contexts. + funcsMap := make(map[*ssa.Function]bool) + for _, callees := range querySites { + for _, callee := range callees { + funcsMap[callee.Func()] = true + } + } + funcs := make([]*ssa.Function, 0, len(funcsMap)) + for f := range funcsMap { + funcs = append(funcs, f) + } + sort.Sort(byFuncPos(funcs)) + return &calleesResult{ - call: call, - querySites: querySites, + site: arbitrarySite, + funcs: funcs, }, nil } type calleesResult struct { - call *ast.CallExpr - querySites map[pointer.CallSite][]pointer.CallGraphNode + site pointer.CallSite + funcs []*ssa.Function } -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()) +func (r *calleesResult) display(printf printfFunc) { + if len(r.funcs) == 0 { + // dynamic call on a provably nil func/interface + printf(r.site, "%s on nil value", r.site.Description()) + } else { + printf(r.site, "this %s dispatches to:", r.site.Description()) + for _, callee := range r.funcs { + printf(callee, "\t%s", callee) } } } + +func (r *calleesResult) toJSON(res *json.Result, fset *token.FileSet) { + j := &json.Callees{ + Pos: r.site.Caller().Func().Prog.Fset.Position(r.site.Pos()).String(), + Desc: r.site.Description(), + } + for _, callee := range r.funcs { + j.Callees = append(j.Callees, &json.CalleesItem{ + Name: callee.String(), + Pos: callee.Prog.Fset.Position(callee.Pos()).String(), + }) + } + res.Callees = j +} + +type byFuncPos []*ssa.Function + +func (a byFuncPos) Len() int { return len(a) } +func (a byFuncPos) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() } +func (a byFuncPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/oracle/callers.go b/oracle/callers.go index b9315d6a..bfe14145 100644 --- a/oracle/callers.go +++ b/oracle/callers.go @@ -5,6 +5,9 @@ package oracle import ( + "go/token" + + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/pointer" "code.google.com/p/go.tools/ssa" ) @@ -32,12 +35,14 @@ func callers(o *oracle) (queryResult, error) { // 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) { + var calls []pointer.CallSite + o.config.Call = func(site pointer.CallSite, callee pointer.CallGraphNode) { if callee.Func() == target { - calls = append(calls, callersCall{site, caller}) + calls = append(calls, site) } } + // TODO(adonovan): sort calls, to ensure test determinism. + root := ptrAnalysis(o) return &callersResult{ @@ -50,28 +55,36 @@ func callers(o *oracle) (queryResult, error) { type callersResult struct { target *ssa.Function root pointer.CallGraphNode - calls []callersCall + calls []pointer.CallSite } -type callersCall struct { - site pointer.CallSite - caller pointer.CallGraphNode -} - -func (r *callersResult) display(o *oracle) { +func (r *callersResult) display(printf printfFunc) { if r.calls == nil { - o.printf(r.target, "%s is not reachable in this program.", r.target) + 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") + printf(r.target, "%s is called from these %d sites:", r.target, len(r.calls)) + for _, site := range r.calls { + if site.Caller() == r.root { + printf(r.target, "the root of the call graph") } else { - o.printf(call.site, "\t%s from %s", - call.site.Description(), call.caller.Func()) + printf(site, "\t%s from %s", site.Description(), site.Caller().Func()) } } } - +} + +func (r *callersResult) toJSON(res *json.Result, fset *token.FileSet) { + var callers []json.Caller + for _, site := range r.calls { + var c json.Caller + c.Caller = site.Caller().Func().String() + if site.Caller() == r.root { + c.Desc = "synthetic call" + } else { + c.Pos = site.Caller().Func().Prog.Fset.Position(site.Pos()).String() + c.Desc = site.Description() + } + callers = append(callers, c) + } + res.Callers = callers } diff --git a/oracle/callgraph.go b/oracle/callgraph.go index 0dae7dcf..43f49c8e 100644 --- a/oracle/callgraph.go +++ b/oracle/callgraph.go @@ -5,8 +5,10 @@ package oracle import ( + "go/token" "strings" + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/pointer" ) @@ -16,7 +18,12 @@ import ( // context sensitivity. // // TODO(adonovan): add options for restricting the display to a region -// of interest: function, package, subgraph, dirtree, etc. +// of interest: function, package, subgraph, dirtree, goroutine, etc. +// +// TODO(adonovan): add an option to project away context sensitivity. +// The callgraph API should provide this feature. +// +// TODO(adonovan): elide nodes for synthetic functions? // func callgraph(o *oracle) (queryResult, error) { buildSSA(o) @@ -26,34 +33,35 @@ func callgraph(o *oracle) (queryResult, error) { o.config.Call = callgraph.AddEdge root := ptrAnalysis(o) + // Assign (preorder) numbers to all the callgraph nodes. + // TODO(adonovan): the callgraph API should do this for us. + numbering := make(map[pointer.CallGraphNode]int) + var number func(cgn pointer.CallGraphNode) + number = func(cgn pointer.CallGraphNode) { + if _, ok := numbering[cgn]; !ok { + numbering[cgn] = len(numbering) + for callee := range callgraph[cgn] { + number(callee) + } + } + } + number(root) + return &callgraphResult{ root: root, callgraph: callgraph, + numbering: numbering, }, nil } type callgraphResult struct { root pointer.CallGraphNode callgraph pointer.CallGraph - - numbering map[pointer.CallGraphNode]int // used by display + numbering map[pointer.CallGraphNode]int } -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, ` +func (r *callgraphResult) display(printf printfFunc) { + 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 @@ -62,6 +70,33 @@ 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) + seen := make(map[pointer.CallGraphNode]bool) + var print func(cgn pointer.CallGraphNode, indent int) + print = func(cgn pointer.CallGraphNode, indent int) { + n := r.numbering[cgn] + if !seen[cgn] { + seen[cgn] = true + printf(cgn.Func(), "%d\t%s%s", n, strings.Repeat(" ", indent), cgn.Func()) + for callee := range r.callgraph[cgn] { + print(callee, indent+1) + } + } else { + printf(cgn.Func(), "\t%s%s (%d)", strings.Repeat(" ", indent), cgn.Func(), n) + } + } + print(r.root, 0) +} + +func (r *callgraphResult) toJSON(res *json.Result, fset *token.FileSet) { + cg := make([]json.CallGraph, len(r.numbering)) + for n, i := range r.numbering { + j := &cg[i] + fn := n.Func() + j.Name = fn.String() + j.Pos = fn.Prog.Fset.Position(fn.Pos()).String() + for callee := range r.callgraph[n] { + j.Children = append(j.Children, r.numbering[callee]) + } + } + res.Callgraph = cg } diff --git a/oracle/callstack.go b/oracle/callstack.go index c873a751..53c27b60 100644 --- a/oracle/callstack.go +++ b/oracle/callstack.go @@ -5,6 +5,9 @@ package oracle import ( + "go/token" + + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/pointer" "code.google.com/p/go.tools/ssa" ) @@ -42,49 +45,69 @@ func callstack(o *oracle) (queryResult, error) { o.config.Call = callgraph.AddEdge root := ptrAnalysis(o) + seen := make(map[pointer.CallGraphNode]bool) + var callstack []pointer.CallSite + + // Use depth-first search to find an arbitrary path from a + // root to the target function. + var search func(cgn pointer.CallGraphNode) bool + search = func(cgn pointer.CallGraphNode) bool { + if !seen[cgn] { + seen[cgn] = true + if cgn.Func() == target { + return true + } + for callee, site := range callgraph[cgn] { + if search(callee) { + callstack = append(callstack, site) + return true + } + } + } + return false + } + + for toplevel := range callgraph[root] { + if search(toplevel) { + break + } + } + return &callstackResult{ target: target, - root: root, - callgraph: callgraph, + callstack: callstack, }, nil } type callstackResult struct { target *ssa.Function - root pointer.CallGraphNode - callgraph pointer.CallGraph - - seen map[pointer.CallGraphNode]bool // used by display + callstack []pointer.CallSite } -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 - } +func (r *callstackResult) display(printf printfFunc) { + if r.callstack != nil { + printf(false, "Found a call path from root to %s", r.target) + printf(r.target, "%s", r.target) + for _, site := range r.callstack { + printf(site, "%s from %s", site.Description(), site.Caller().Func()) } + } else { + printf(r.target, "%s is unreachable in this analysis scope", r.target) } - 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 - } +func (r *callstackResult) toJSON(res *json.Result, fset *token.FileSet) { + var callers []json.Caller + for _, site := range r.callstack { + callers = append(callers, json.Caller{ + Pos: site.Caller().Func().Prog.Fset.Position(site.Pos()).String(), + Caller: site.Caller().Func().String(), + Desc: site.Description(), + }) + } + res.Callstack = &json.CallStack{ + Pos: r.target.Prog.Fset.Position(r.target.Pos()).String(), + Target: r.target.String(), + Callers: callers, } - o.printf(r.target, "%s is unreachable in this analysis scope", r.target) } diff --git a/oracle/describe.go b/oracle/describe.go index 6dd77875..ac36c120 100644 --- a/oracle/describe.go +++ b/oracle/describe.go @@ -9,18 +9,19 @@ import ( "fmt" "go/ast" "go/token" + "os" "sort" "strconv" "strings" + "code.google.com/p/go.tools/go/exact" "code.google.com/p/go.tools/go/types" "code.google.com/p/go.tools/importer" + "code.google.com/p/go.tools/oracle/json" "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 @@ -29,9 +30,11 @@ import ( // - its points-to set (for a pointer-like expression) // - its concrete types (for an interface expression) and their points-to sets. // +// All printed sets are sorted to ensure determinism. +// func describe(o *oracle) (queryResult, error) { if false { // debugging - o.printf(o.queryPath[0], "you selected: %s %s", + o.fprintf(os.Stderr, o.queryPath[0], "you selected: %s %s", importer.NodeDescription(o.queryPath[0]), pathToString2(o.queryPath)) } @@ -61,9 +64,16 @@ type describeUnknownResult struct { node ast.Node } -func (r *describeUnknownResult) display(o *oracle) { +func (r *describeUnknownResult) display(printf printfFunc) { // Nothing much to say about misc syntax. - o.printf(r.node, "%s", importer.NodeDescription(r.node)) + printf(r.node, "%s", importer.NodeDescription(r.node)) +} + +func (r *describeUnknownResult) toJSON(res *json.Result, fset *token.FileSet) { + res.Describe = &json.Describe{ + Desc: importer.NodeDescription(r.node), + Pos: fset.Position(r.node.Pos()).String(), + } } type action int @@ -99,7 +109,8 @@ func findInterestingNode(pkginfo *importer.PackageInfo, path []ast.Node) ([]ast. // 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. + // describes the (anonymous) struct type and concludes "no methods". + // We should ascend to the enclosing type decl, if any. for len(path) > 0 { switch n := path[0].(type) { @@ -344,6 +355,15 @@ func describeValue(o *oracle, path []ast.Node) (*describeValueResult, error) { // From this point on, we cannot fail with an error. // Failure to run the pointer analysis will be reported later. + // + // Our disposition to pointer analysis may be one of the following: + // - ok: ssa.Value was const or func. + // - error: no ssa.Value for expr (e.g. trivially dead code) + // - ok: ssa.Value is non-pointerlike + // - error: no Pointer for ssa.Value (e.g. analytically unreachable) + // - ok: Pointer has empty points-to set + // - ok: Pointer has non-empty points-to set + // ptaErr is non-nil only in the "error:" cases. var value ssa.Value var ptaErr error @@ -367,113 +387,188 @@ func describeValue(o *oracle, path []ast.Node) (*describeValueResult, error) { } // Run pointer analysis of the selected SSA value. - var ptrs []pointer.Pointer + var ptrs []pointerResult if value != nil { buildSSA(o) o.config.QueryValues = map[ssa.Value][]pointer.Pointer{value: nil} ptrAnalysis(o) - ptrs = o.config.QueryValues[value] + + // Combine the PT sets from all contexts. + pointers := o.config.QueryValues[value] + if pointers == nil { + ptaErr = fmt.Errorf("PTA did not encounter this expression (dead code?)") + } + pts := pointer.PointsToCombined(pointers) + + if _, ok := value.Type().Underlying().(*types.Interface); ok { + // Show concrete types for interface expression. + if concs := pts.ConcreteTypes(); concs.Len() > 0 { + concs.Iterate(func(conc types.Type, pta interface{}) { + combined := pointer.PointsToCombined(pta.([]pointer.Pointer)) + labels := combined.Labels() + sort.Sort(byPosAndString(labels)) // to ensure determinism + ptrs = append(ptrs, pointerResult{conc, labels}) + }) + } + } else { + // Show labels for other expressions. + labels := pts.Labels() + sort.Sort(byPosAndString(labels)) // to ensure determinism + ptrs = append(ptrs, pointerResult{value.Type(), labels}) + } } + sort.Sort(byTypeString(ptrs)) // to ensure determinism + + typ := o.queryPkgInfo.TypeOf(expr) + constVal := o.queryPkgInfo.ValueOf(expr) return &describeValueResult{ - expr: expr, - obj: obj, - value: value, - ptaErr: ptaErr, - ptrs: ptrs, + expr: expr, + typ: typ, + constVal: constVal, + obj: obj, + 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 +type pointerResult struct { + typ types.Type // type of the pointer (always concrete) + labels []*pointer.Label } -func (r *describeValueResult) display(o *oracle) { +type describeValueResult struct { + expr ast.Expr // query node + typ types.Type // type of expression + constVal exact.Value // value of expression, if constant + obj types.Object // var/func/const object, if expr was Ident + ptaErr error // reason why pointer analysis couldn't be run, or failed + ptrs []pointerResult // pointer info (typ is concrete => len==1) +} + +func (r *describeValueResult) display(printf printfFunc) { suffix := "" - if val := o.queryPkgInfo.ValueOf(r.expr); val != nil { - suffix = fmt.Sprintf(" of constant value %s", val) + if r.constVal != nil { + suffix = fmt.Sprintf(" of constant value %s", r.constVal) } // 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) + printf(r.expr, "definition of %s%s", r.obj, suffix) } else { // referring ident - o.printf(r.expr, "reference to %s%s", r.obj, suffix) + printf(r.expr, "reference to %s%s", r.obj, suffix) if def := r.obj.Pos(); def != token.NoPos { - o.printf(def, "defined here") + printf(def, "defined here") } } } else { desc := importer.NodeDescription(r.expr) if suffix != "" { // constant expression - o.printf(r.expr, "%s%s", desc, suffix) + 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)) + printf(r.expr, "%s of type %s", desc, r.typ) } } - if r.value == nil { - // pointer analysis was not run - if r.ptaErr != nil { - o.printf(r.expr, "no pointer analysis: %s", r.ptaErr) - } + // pointer analysis could not be run + if r.ptaErr != nil { + printf(r.expr, "no points-to information: %s", r.ptaErr) return } if r.ptrs == nil { - o.printf(r.expr, "pointer analysis did not analyze this expression (dead code?)") - return + return // PTA was not invoked (not an error) } // 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 { + if _, ok := r.typ.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{}) { + if len(r.ptrs) > 0 { + printf(false, "interface may contain these concrete types:") + for _, ptr := range r.ptrs { var obj types.Object - if nt, ok := deref(conc).(*types.Named); ok { + if nt, ok := deref(ptr.typ).(*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") + if len(ptr.labels) > 0 { + printf(obj, "\t%s, may point to:", ptr.typ) + printLabels(printf, ptr.labels, "\t\t") } else { - o.printf(obj, "\t%s", conc) + printf(obj, "\t%s", ptr.typ) } - }) + } } else { - o.printf(o, "interface cannot contain any concrete values.") + printf(false, "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") + if ptr := r.ptrs[0]; len(ptr.labels) > 0 { + printf(false, "value may point to these labels:") + printLabels(printf, ptr.labels, "\t") } else { - o.printf(o, "value cannot point to anything.") + printf(false, "value cannot point to anything.") } } } +func (r *describeValueResult) toJSON(res *json.Result, fset *token.FileSet) { + var value, objpos, ptaerr string + if r.constVal != nil { + value = r.constVal.String() + } + if r.obj != nil { + objpos = fset.Position(r.obj.Pos()).String() + } + if r.ptaErr != nil { + ptaerr = r.ptaErr.Error() + } + + var pts []*json.DescribePointer + for _, ptr := range r.ptrs { + var namePos string + if nt, ok := deref(ptr.typ).(*types.Named); ok { + namePos = fset.Position(nt.Obj().Pos()).String() + } + var labels []json.DescribePTALabel + for _, l := range ptr.labels { + labels = append(labels, json.DescribePTALabel{ + Pos: fset.Position(l.Pos()).String(), + Desc: l.String(), + }) + } + pts = append(pts, &json.DescribePointer{ + Type: ptr.typ.String(), + NamePos: namePos, + Labels: labels, + }) + } + + res.Describe = &json.Describe{ + Desc: importer.NodeDescription(r.expr), + Pos: fset.Position(r.expr.Pos()).String(), + Detail: "value", + Value: &json.DescribeValue{ + Type: r.typ.String(), + Value: value, + ObjPos: objpos, + PTAErr: ptaerr, + PTS: pts, + }, + } +} + +type byTypeString []pointerResult + +func (a byTypeString) Len() int { return len(a) } +func (a byTypeString) Less(i, j int) bool { return a[i].typ.String() < a[j].typ.String() } +func (a byTypeString) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + type byPosAndString []*pointer.Label func (a byPosAndString) Len() int { return len(a) } @@ -483,13 +578,11 @@ func (a byPosAndString) Less(i, j int) bool { } 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)) +func printLabels(printf printfFunc, labels []*pointer.Label, prefix string) { // TODO(adonovan): due to context-sensitivity, many of these // labels may differ only by context, which isn't apparent. for _, label := range labels { - o.printf(label, "%s%s", prefix, label) + printf(label, "%s%s", prefix, label) } } @@ -523,40 +616,62 @@ func describeType(o *oracle, path []ast.Node) (*describeTypeResult, error) { return nil, o.errorf(n, "unexpected AST for type: %T", n) } - return &describeTypeResult{path[0], description, t}, nil + return &describeTypeResult{ + node: path[0], + description: description, + typ: t, + methods: accessibleMethods(t, o.queryPkgInfo.Pkg), + }, nil } type describeTypeResult struct { node ast.Node description string typ types.Type + methods []*types.Selection } -func (r *describeTypeResult) display(o *oracle) { - o.printf(r.node, "%s", r.description) +func (r *describeTypeResult) display(printf printfFunc) { + 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()) + 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) + if len(r.methods) > 0 { + printf(r.node, "Method set:") + for _, meth := range r.methods { + printf(meth.Obj(), "\t%s", meth) } } else { - o.printf(r.node, "No methods.") + printf(r.node, "No methods.") } } } +func (r *describeTypeResult) toJSON(res *json.Result, fset *token.FileSet) { + var namePos, nameDef string + if nt, ok := r.typ.(*types.Named); ok { + namePos = fset.Position(nt.Obj().Pos()).String() + nameDef = nt.Underlying().String() + } + res.Describe = &json.Describe{ + Desc: r.description, + Pos: fset.Position(r.node.Pos()).String(), + Detail: "type", + Type: &json.DescribeType{ + Type: r.typ.String(), + NamePos: namePos, + NameDef: nameDef, + Methods: methodsToJSON(r.methods, fset), + }, + } +} + // ---- PACKAGE ------------------------------------------------------------ func describePackage(o *oracle, path []ast.Node) (*describePackageResult, error) { @@ -589,58 +704,70 @@ func describePackage(o *oracle, path []ast.Node) (*describePackageResult, error) 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 members []*describeMember + // NB: package "unsafe" has no object. + if pkg := o.prog.PackagesByPath[importPath]; pkg != nil { + // Compute set of exported package members in lexicographic order. var names []string - maxname := 0 - for name := range p.Members { - if samePkg || ast.IsExported(name) { - if l := len(name); l > maxname { - maxname = l - } + for name := range pkg.Members { + if pkg.Object == o.queryPkgInfo.Pkg || ast.IsExported(name) { names = append(names, name) } } - sort.Strings(names) - // Print the members. + // Enumerate the package members. for _, name := range names { - mem := p.Members[name] - o.printf(mem, "%s", formatMember(mem, maxname)) - // Print method set. + mem := pkg.Members[name] + var methods []*types.Selection 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) - } - } + methods = accessibleMethods(mem.Type(), o.queryPkgInfo.Pkg) } + members = append(members, &describeMember{ + mem, + methods, + }) + } + } + + return &describePackageResult{o.prog.Fset, path[0], description, importPath, members}, nil +} + +type describePackageResult struct { + fset *token.FileSet + node ast.Node + description string + path string + members []*describeMember // in lexicographic name order +} + +type describeMember struct { + mem ssa.Member + methods []*types.Selection // in types.MethodSet order +} + +func (r *describePackageResult) display(printf printfFunc) { + printf(r.node, "%s", r.description) + + // Compute max width of name "column". + maxname := 0 + for _, mem := range r.members { + if l := len(mem.mem.Name()); l > maxname { + maxname = l + } + } + + for _, mem := range r.members { + printf(mem.mem, "\t%s", formatMember(mem.mem, maxname)) + for _, meth := range mem.methods { + 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()) + fmt.Fprintf(&buf, "%-5s %-*s", mem.Token(), maxname, mem.Name()) switch mem := mem.(type) { case *ssa.NamedConst: fmt.Fprintf(&buf, " %s = %s", mem.Type(), mem.Value.Name()) @@ -673,6 +800,39 @@ func formatMember(mem ssa.Member, maxname int) string { return buf.String() } +func (r *describePackageResult) toJSON(res *json.Result, fset *token.FileSet) { + var members []*json.DescribeMember + for _, mem := range r.members { + typ := mem.mem.Type() + var val string + switch mem := mem.mem.(type) { + case *ssa.NamedConst: + val = mem.Value.Value.String() + case *ssa.Type: + typ = typ.Underlying() + case *ssa.Global: + typ = deref(typ) + } + members = append(members, &json.DescribeMember{ + Name: mem.mem.Name(), + Type: typ.String(), + Value: val, + Pos: fset.Position(mem.mem.Pos()).String(), + Kind: mem.mem.Token().String(), + Methods: methodsToJSON(mem.methods, fset), + }) + } + res.Describe = &json.Describe{ + Desc: r.description, + Pos: fset.Position(r.node.Pos()).String(), + Detail: "package", + Package: &json.DescribePackage{ + Path: r.path, + Members: members, + }, + } +} + // ---- STATEMENT ------------------------------------------------------------ func describeStmt(o *oracle, path []ast.Node) (*describeStmtResult, error) { @@ -689,16 +849,25 @@ func describeStmt(o *oracle, path []ast.Node) (*describeStmtResult, error) { // Nothing much to say about statements. description = importer.NodeDescription(n) } - return &describeStmtResult{path[0], description}, nil + return &describeStmtResult{o.prog.Fset, path[0], description}, nil } type describeStmtResult struct { + fset *token.FileSet node ast.Node description string } -func (r *describeStmtResult) display(o *oracle) { - o.printf(r.node, "%s", r.description) +func (r *describeStmtResult) display(printf printfFunc) { + printf(r.node, "%s", r.description) +} + +func (r *describeStmtResult) toJSON(res *json.Result, fset *token.FileSet) { + res.Describe = &json.Describe{ + Desc: r.description, + Pos: fset.Position(r.node.Pos()).String(), + Detail: "unknown", + } } // ------------------- Utilities ------------------- @@ -717,3 +886,28 @@ func pathToString2(path []ast.Node) string { fmt.Fprint(&buf, "]") return buf.String() } + +func accessibleMethods(t types.Type, from *types.Package) []*types.Selection { + var methods []*types.Selection + for _, meth := range ssa.IntuitiveMethodSet(t) { + if isAccessibleFrom(meth.Obj(), from) { + methods = append(methods, meth) + } + } + return methods +} + +func isAccessibleFrom(obj types.Object, pkg *types.Package) bool { + return ast.IsExported(obj.Name()) || obj.Pkg() == pkg +} + +func methodsToJSON(methods []*types.Selection, fset *token.FileSet) []json.DescribeMethod { + var jmethods []json.DescribeMethod + for _, meth := range methods { + jmethods = append(jmethods, json.DescribeMethod{ + Name: meth.String(), + Pos: fset.Position(meth.Obj().Pos()).String(), + }) + } + return jmethods +} diff --git a/oracle/freevars.go b/oracle/freevars.go index b50fba59..1540f0bd 100644 --- a/oracle/freevars.go +++ b/oracle/freevars.go @@ -6,8 +6,11 @@ package oracle import ( "go/ast" + "go/token" + "sort" "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/oracle/json" ) // freevars displays the lexical (not package-level) free variables of @@ -72,7 +75,8 @@ func freevars(o *oracle) (queryResult, error) { // Maps each reference that is free in the selection // to the object it refers to. - freeRefs := make(map[string]freevarsRef) + // The map de-duplicates repeated references. + refsMap := make(map[string]freevarsRef) // Visit all the identifiers in the selected ASTs. ast.Inspect(o.queryPath[0], func(n ast.Node) bool { @@ -84,15 +88,39 @@ func freevars(o *oracle) (queryResult, error) { // (freevars permits inexact selections, // like two stmts in a block.) if o.startPos <= n.Pos() && n.End() <= o.endPos { + var obj types.Object + var prune bool switch n := n.(type) { case *ast.Ident: - if obj := id(n); obj != nil { - freeRefs[o.printNode(n)] = freevarsRef{n, obj} - } + obj = id(n) case *ast.SelectorExpr: - if obj := sel(n); obj != nil { - freeRefs[o.printNode(n)] = freevarsRef{n, obj} + obj = sel(n) + prune = true + } + + if obj != nil { + var kind string + switch 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(obj) + } + + typ := o.queryPkgInfo.TypeOf(n.(ast.Expr)) + ref := freevarsRef{kind, o.printNode(n), typ, obj} + refsMap[ref.ref] = ref + + if prune { return false // don't descend } } @@ -101,47 +129,59 @@ func freevars(o *oracle) (queryResult, error) { return true // descend }) + refs := make([]freevarsRef, 0, len(refsMap)) + for _, ref := range refsMap { + refs = append(refs, ref) + } + sort.Sort(byRef(refs)) + return &freevarsResult{ - refs: freeRefs, + fset: o.prog.Fset, + refs: refs, }, nil } type freevarsResult struct { - refs map[string]freevarsRef + fset *token.FileSet + refs []freevarsRef } type freevarsRef struct { - expr ast.Expr + kind string + ref string + typ types.Type obj types.Object } -func (r *freevarsResult) display(o *oracle) { +func (r *freevarsResult) display(printf printfFunc) { 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) + printf(false, "No free identifers.") + } else { + printf(false, "Free identifers:") + for _, ref := range r.refs { + printf(ref.obj, "%s %s %s", ref.kind, ref.ref, ref.typ) } - 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) } } + +func (r *freevarsResult) toJSON(res *json.Result, fset *token.FileSet) { + var refs []*json.FreeVar + for _, ref := range r.refs { + refs = append(refs, + &json.FreeVar{ + Pos: fset.Position(ref.obj.Pos()).String(), + Kind: ref.kind, + Ref: ref.ref, + Type: ref.typ.String(), + }) + } + res.Freevars = refs +} + +// -------- utils -------- + +type byRef []freevarsRef + +func (p byRef) Len() int { return len(p) } +func (p byRef) Less(i, j int) bool { return p[i].ref < p[j].ref } +func (p byRef) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/oracle/implements.go b/oracle/implements.go index 75f6d31e..dc3a356c 100644 --- a/oracle/implements.go +++ b/oracle/implements.go @@ -5,7 +5,10 @@ package oracle import ( + "go/token" + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/ssa" ) @@ -15,7 +18,7 @@ import ( // // TODO(adonovan): more features: // - should we include pairs of types belonging to -// different packages in the 'implements' relation?// +// 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? @@ -62,8 +65,9 @@ func implements(o *oracle) (queryResult, error) { facts = append(facts, fact) } } + // TODO(adonovan): sort facts to ensure test nondeterminism. - return &implementsResult{facts}, nil + return &implementsResult{o.prog.Fset, facts}, nil } type implementsFact struct { @@ -72,17 +76,30 @@ type implementsFact struct { } type implementsResult struct { + fset *token.FileSet facts []implementsFact // facts are grouped by interface } -func (r *implementsResult) display(o *oracle) { - // TODO(adonovan): sort to ensure test nondeterminism. +func (r *implementsResult) display(printf printfFunc) { var prevIface *types.Named for _, fact := range r.facts { if fact.iface != prevIface { - o.printf(fact.iface.Obj(), "\tInterface %s:", fact.iface) + printf(fact.iface.Obj(), "\tInterface %s:", fact.iface) prevIface = fact.iface } - o.printf(deref(fact.conc).(*types.Named).Obj(), "\t\t%s", fact.conc) + printf(deref(fact.conc).(*types.Named).Obj(), "\t\t%s", fact.conc) } } + +func (r *implementsResult) toJSON(res *json.Result, fset *token.FileSet) { + var facts []*json.Implements + for _, fact := range r.facts { + facts = append(facts, &json.Implements{ + I: fact.iface.String(), + IPos: fset.Position(fact.iface.Obj().Pos()).String(), + C: fact.conc.String(), + CPos: fset.Position(deref(fact.conc).(*types.Named).Obj().Pos()).String(), + }) + } + res.Implements = facts +} diff --git a/oracle/json/json.go b/oracle/json/json.go new file mode 100644 index 00000000..96135ce5 --- /dev/null +++ b/oracle/json/json.go @@ -0,0 +1,210 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json defines the oracle's JSON schema. +package json + +// All 'pos' strings are of the form "file:line:col". +// TODO(adonovan): improve performance by sharing filename strings. +// TODO(adonovan): improve precision by providing the start/end +// interval when available. +// +// TODO(adonovan): consider richer encodings of types, functions, +// methods, etc. + +// A Peers is the result of a 'peers' query. +// If Allocs is empty, the selected channel can't point to anything. +type Peers struct { + Pos string `json:"pos"` // location of the selected channel op (<-) + Type string `json:"type"` // type of the selected channel + Allocs []string `json:"allocs,omitempty"` // locations of aliased make(chan) ops + Sends []string `json:"sends,omitempty"` // locations of aliased ch<-x ops + Receives []string `json:"receives,omitempty"` // locations of aliased <-ch ops +} + +type CalleesItem struct { + Name string `json:"name"` // full name of called function + Pos string `json:"pos"` // location of called function +} + +// A Callees is the result of a 'callees' query. +// +// Callees is nonempty unless the call was a dynamic call on a +// provably nil func or interface value. +type Callees struct { + Pos string `json:"pos"` // location of selected call site + Desc string `json:"desc"` // description of call site + Callees []*CalleesItem `json:"callees,omitempty"` // set of possible call targets +} + +// A Caller is one element of the slice returned by a 'callers' query. +// (Callstack also contains a similar slice.) +// +// The root of the callgraph has an unspecified "Caller" string. +type Caller struct { + Pos string `json:"pos,omitempty"` // location of the calling function + Desc string `json:"desc"` // description of call site + Caller string `json:"caller"` // full name of calling function +} + +// A CallGraph is one element of the slice returned by a 'callgraph' query. +// The index of each item in the slice is used to identify it in the +// Callers adjacency list. +// +// Multiple nodes may have the same Name due to context-sensitive +// treatment of some functions. +// +// TODO(adonovan): perhaps include edge labels (i.e. callsites). +type CallGraph struct { + Name string `json:"name"` // full name of function + Pos string `json:"pos"` // location of function + Children []int `json:"children,omitempty"` // indices of child nodes in callgraph list +} + +// A CallStack is the result of a 'callstack' query. +// It indicates an arbitrary path from the root of the callgraph to +// the query function. +// +// If the Callers slice is empty, the function was unreachable in this +// analysis scope. +type CallStack struct { + Pos string `json:"pos"` // location of the selected function + Target string `json:"target"` // the selected function + Callers []Caller `json:"callers"` // enclosing calls, innermost first. +} + +// A FreeVar is one element of the slice returned by a 'freevars' +// query. Each one identifies an expression referencing a local +// identifier defined outside the selected region. +type FreeVar struct { + Pos string `json:"pos"` // location of the identifier's definition + Kind string `json:"kind"` // one of {var,func,type,const,label} + Ref string `json:"ref"` // referring expression (e.g. "x" or "x.y.z") + Type string `json:"type"` // type of the expression +} + +// An Implements is one element of the result of an 'implements' query. +// Each one indicates a row in the "implements" relation over +// package-level named types defined by the package containing the +// selection. +type Implements struct { + I string `json:"i"` // full name of the interface type + IPos string `json:"ipos"` // location of its definition + C string `json:"c"` // full name of the concrete type + CPos string `json:"cpos"` // location of its definition +} + +// A DescribePTALabel describes a pointer analysis label. +// +// A "label" is an object that may be pointed to by a pointer, map, +// channel, 'func', slice or interface. Labels include: +// - functions +// - globals +// - arrays created by literals (e.g. []byte("foo")) and conversions ([]byte(s)) +// - stack- and heap-allocated variables (including composite literals) +// - arrays allocated by append() +// - channels, maps and arrays created by make() +// - and their subelements, e.g. "alloc.y[*].z" +// +type DescribePTALabel struct { + Pos string `json:"pos"` // location of syntax that allocated the object + Desc string `json:"desc"` // description of the label +} + +// A DescribePointer describes a single pointer: its type and the +// set of "labels" it points to. +// +type DescribePointer struct { + Type string `json:"type"` // (concrete) type of the pointer + NamePos string `json:"namepos"` // location of type defn, if Named + Labels []DescribePTALabel `json:"labels,omitempty"` // pointed-to objects +} + +// A DescribeValue is the additional result of a 'describe' query +// if the selection indicates a value or expression. +// +// If the described value is an interface, it will have one PTS entry +// describing each concrete type that it may contain. For each +// concrete type that is a pointer, the PTS entry describes the labels +// it may point to. +// +type DescribeValue struct { + Type string `json:"type"` // type of the expression + Value string `json:"value,omitempty"` // value of the expression, if constant + ObjPos string `json:"objpos,omitempty"` // location of the definition, if an Ident + PTAErr string `json:"ptaerr,omitempty"` // reason pointer analysis wasn't attempted + PTS []*DescribePointer `json:"pts,omitempty"` // points-to set; an interface may have many +} + +type DescribeMethod struct { + Name string `json:"name"` // method name, as defined by types.Selection.String() + Pos string `json:"pos"` // location of the method's definition +} + +// A DescribeType is the additional result of a 'describe' query +// if the selection indicates a type. +type DescribeType struct { + Type string `json:"type"` // the string form of the type + NamePos string `json:"namepos,omitempty"` // location of definition of type, if named + NameDef string `json:"namedef,omitempty"` // underlying definition of type, if named + Methods []DescribeMethod `json:"methods,omitempty"` // methods of the type +} + +type DescribeMember struct { + Name string `json:"name"` // name of member + Type string `json:"type,omitempty"` // type of member (underlying, if 'type') + Value string `json:"value,omitempty"` // value of member (if 'const') + Pos string `json:"pos"` // location of definition of member + Kind string `json:"kind"` // one of {var,const,func,type} + Methods []DescribeMethod `json:"methods,omitempty"` // methods (if member is a type) +} + +// A DescribePackage is the additional result of a 'describe' if +// the selection indicates a package. +type DescribePackage struct { + Path string `json:"path"` // import path of the package + Members []*DescribeMember `json:"members,omitempty"` // accessible members of the package +} + +// A Describe is the result of a 'describe' query. +// It may contain an element describing the selected semantic entity +// in detail. +type Describe struct { + Desc string `json:"desc"` // description of the selected syntax node + Pos string `json:"pos"` // location of the selected syntax node + Detail string `json:"detail,omitempty"` // one of {package, type, value}, or "". + + // At most one of the following fields is populated: + // the one specified by 'detail'. + Package *DescribePackage `json:"package,omitempty"` + Type *DescribeType `json:"type,omitempty"` + Value *DescribeValue `json:"value,omitempty"` +} + +type PTAWarning struct { + Pos string `json:"pos"` // location associated with warning + Message string `json:"message"` // warning message +} + +// A Result is the common result of any oracle query. +// It contains a query-specific result element. +// +// TODO(adonovan): perhaps include other info such as: analysis scope, +// raw query position, stack of ast nodes, query package, etc. +type Result struct { + Mode string `json:"mode"` // mode of the query + + // Exactly one of the following fields is populated: + // the one specified by 'mode'. + Callees *Callees `json:"callees,omitempty"` + Callers []Caller `json:"callers,omitempty"` + Callgraph []CallGraph `json:"callgraph,omitempty"` + Callstack *CallStack `json:"callstack,omitempty"` + Describe *Describe `json:"describe,omitempty"` + Freevars []*FreeVar `json:"freevars,omitempty"` + Implements []*Implements `json:"implements,omitempty"` + Peers *Peers `json:"peers,omitempty"` + + Warnings []PTAWarning `json:"warnings,omitempty"` // warnings from pointer analysis +} diff --git a/oracle/oracle.go b/oracle/oracle.go index c366e601..2a5599d4 100644 --- a/oracle/oracle.go +++ b/oracle/oracle.go @@ -4,7 +4,7 @@ package oracle -// This file defines oracle.Main, the entry point for the oracle tool. +// This file defines oracle.Query, the entry point for the oracle tool. // The actual executable is defined in cmd/oracle. // TODO(adonovan): new query: show all statements that may update the @@ -12,6 +12,7 @@ package oracle import ( "bytes" + encjson "encoding/json" "errors" "fmt" "go/ast" @@ -27,6 +28,7 @@ import ( "code.google.com/p/go.tools/go/types" "code.google.com/p/go.tools/importer" + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/pointer" "code.google.com/p/go.tools/ssa" ) @@ -71,25 +73,59 @@ var modes = map[string]modeInfo{ "peers": modeInfo{WholeSource | SSA | Pos, peers}, } +type printfFunc func(pos interface{}, format string, args ...interface{}) + +// queryResult is the interface of each query-specific result type. type queryResult interface { - display(o *oracle) + toJSON(res *json.Result, fset *token.FileSet) + display(printf printfFunc) } -// Main runs the oracle. +type warning struct { + pos token.Pos + format string + args []interface{} +} + +// A Result encapsulates the result of an oracle.Query. +// +// Result instances implement the json.Marshaler interface, i.e. they +// can be JSON-serialized. +type Result struct { + fset *token.FileSet + // fprintf is a closure over the oracle's fileset and start/end position. + fprintf func(w io.Writer, pos interface{}, format string, args ...interface{}) + q queryResult // the query-specific result + mode string // query mode + warnings []warning // pointer analysis warnings +} + +func (res *Result) MarshalJSON() ([]byte, error) { + resj := &json.Result{Mode: res.mode} + res.q.toJSON(resj, res.fset) + for _, w := range res.warnings { + resj.Warnings = append(resj.Warnings, json.PTAWarning{ + Pos: res.fset.Position(w.pos).String(), + Message: fmt.Sprintf(w.format, w.args...), + }) + } + return encjson.Marshal(resj) +} + +// Query 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 { +func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *build.Context) (*Result, error) { minfo, ok := modes[mode] if !ok { if mode == "" { - return errors.New("You must specify a -mode to perform.") + return nil, errors.New("you must specify a -mode to perform") } - return fmt.Errorf("Invalid mode type: %q.", mode) + return nil, fmt.Errorf("invalid mode type: %q", mode) } var loader importer.SourceLoader @@ -98,20 +134,14 @@ func Main(args []string, mode, pos string, ptalog, out io.Writer, 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 + var res Result o.config.Warn = func(pos token.Pos, format string, args ...interface{}) { - warnings = append(warnings, warning{pos, format, args}) + res.warnings = append(res.warnings, warning{pos, format, args}) } // Phase timing diagnostics. @@ -128,7 +158,7 @@ func Main(args []string, mode, pos string, ptalog, out io.Writer, buildContext * start := time.Now() initialPkgInfo, _, err := importer.CreatePackageFromArgs(imp, args) if err != nil { - return err // I/O, parser or type error + return nil, err // I/O, parser or type error } o.timers["load/parse/type"] = time.Since(start) @@ -137,16 +167,16 @@ func Main(args []string, mode, pos string, ptalog, out io.Writer, buildContext * var err error o.startPos, o.endPos, err = parseQueryPos(o.prog.Fset, pos) if err != nil { - return err + return nil, 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") + return nil, o.errorf(false, "no syntax here") } if minfo.needs&ExactPos != 0 && !exact { - return o.errorf(o.queryPath[0], "ambiguous selection within %s", + return nil, o.errorf(o.queryPath[0], "ambiguous selection within %s", importer.NodeDescription(o.queryPath[0])) } } @@ -166,7 +196,7 @@ func Main(args []string, mode, pos string, ptalog, out io.Writer, buildContext * // 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") + return nil, o.errorf(false, "analysis scope has no main() entry points") } } o.config.Mains = append(o.config.Mains, initialPkg) @@ -185,22 +215,30 @@ func Main(args []string, mode, pos string, ptalog, out io.Writer, buildContext * // Release the other ASTs and type info to the GC. imp = nil - result, err := minfo.impl(o) + res.q, err = minfo.impl(o) if err != nil { - return err + return nil, err } - // TODO(adonovan): use this as a seam for testing. - result.display(o) + res.mode = mode + res.fset = o.prog.Fset + res.fprintf = o.fprintf // captures o.prog, o.{start,end}Pos for later printing + return &res, nil +} + +// WriteTo writes the oracle query result res to out in a compiler diagnostic format. +func (res *Result) WriteTo(out io.Writer) { + printf := func(pos interface{}, format string, args ...interface{}) { + res.fprintf(out, pos, format, args...) + } + res.q.display(printf) // Print warnings after the main output. - if warnings != nil { - fmt.Fprintln(o.out, "\nPointer analysis warnings:") - for _, w := range warnings { - o.fprintf(o.out, w.pos, "warning: "+w.format, w.args...) + if res.warnings != nil { + fmt.Fprintln(out, "\nPointer analysis warnings:") + for _, w := range res.warnings { + printf(w.pos, "warning: "+w.format, w.args...) } } - - return nil } // ---------- Utilities ---------- @@ -338,7 +376,8 @@ func deref(typ types.Type) types.Type { // - 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. +// - a bool, meaning the extent [o.startPos, o.endPos) of the user's query. +// (the value is ignored) // - nil, meaning no position at all. // // The output format is is compatible with the 'gnu' @@ -359,7 +398,7 @@ func (o *oracle) fprintf(w io.Writer, pos interface{}, format string, args ...in }: start = pos.Pos() end = start - case *oracle: + case bool: start = o.startPos end = o.endPos case nil: @@ -385,11 +424,6 @@ func (o *oracle) fprintf(w io.Writer, pos interface{}, format string, args ...in 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 diff --git a/oracle/oracle_test.go b/oracle/oracle_test.go index 17bb670d..9dba5b46 100644 --- a/oracle/oracle_test.go +++ b/oracle/oracle_test.go @@ -26,12 +26,9 @@ package oracle_test // % 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" + "encoding/json" "flag" "fmt" "go/build" @@ -159,23 +156,40 @@ func stripLocation(line string) string { // doQuery poses query q to the oracle and writes its response and // error (if any) to out. -func doQuery(out io.Writer, q *query) { +func doQuery(out io.Writer, q *query, useJson bool) { 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}, + res, err := oracle.Query([]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)) + /*PTA-log=*/ nil, &buildContext) + if err != nil { + fmt.Fprintf(out, "\nError: %s\n", stripLocation(err.Error())) + return } - if err != nil { - fmt.Fprintf(out, "Error: %s\n", stripLocation(err.Error())) + if useJson { + // JSON output + b, err := json.Marshal(res) + if err != nil { + fmt.Fprintf(out, "JSON error: %s\n", err.Error()) + return + } + var buf bytes.Buffer + if err := json.Indent(&buf, b, "", "\t"); err != nil { + fmt.Fprintf(out, "json.Indent failed: %s", err) + return + } + out.Write(buf.Bytes()) + } else { + // "plain" (compiler diagnostic format) output + capture := new(bytes.Buffer) // capture standard output + res.WriteTo(capture) + for _, line := range strings.Split(capture.String(), "\n") { + fmt.Fprintf(out, "%s\n", stripLocation(line)) + } } } @@ -187,12 +201,19 @@ func TestOracle(t *testing.T) { for _, filename := range []string{ "testdata/src/main/calls.go", + "testdata/src/main/callgraph.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", + // JSON: + "testdata/src/main/callgraph-json.go", + "testdata/src/main/calls-json.go", + "testdata/src/main/peers-json.go", + "testdata/src/main/describe-json.go", } { + useJson := strings.HasSuffix(filename, "-json.go") queries := parseQueries(t, filename) golden := filename + "lden" got := filename + "t" @@ -206,7 +227,7 @@ func TestOracle(t *testing.T) { // 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) + doQuery(gotfh, q, useJson) } // Compare foo.got with foo.golden. diff --git a/oracle/peers.go b/oracle/peers.go index cae7e461..07e807e3 100644 --- a/oracle/peers.go +++ b/oracle/peers.go @@ -7,8 +7,10 @@ package oracle import ( "go/ast" "go/token" + "sort" "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/oracle/json" "code.google.com/p/go.tools/pointer" "code.google.com/p/go.tools/ssa" ) @@ -52,7 +54,10 @@ func peers(o *oracle) (queryResult, error) { // 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() + // We compare channels by element types, not channel types, to + // ignore both directionality and type names. + queryType := queryOp.ch.Type() + queryElemType := queryType.Underlying().(*types.Chan).Elem() channels := map[ssa.Value][]pointer.Pointer{queryOp.ch: nil} i := 0 for _, op := range ops { @@ -71,10 +76,35 @@ func peers(o *oracle) (queryResult, error) { // Combine the PT sets from all contexts. queryChanPts := pointer.PointsToCombined(channels[queryOp.ch]) + // Ascertain which make(chan) labels the query's channel can alias. + var makes []token.Pos + for _, label := range queryChanPts.Labels() { + makes = append(makes, label.Pos()) + } + sort.Sort(byPos(makes)) + + // Ascertain which send/receive operations can alias the same make(chan) labels. + var sends, receives []token.Pos + for _, op := range ops { + for _, ptr := range o.config.QueryValues[op.ch] { + if ptr != nil && ptr.PointsTo().Intersects(queryChanPts) { + if op.dir == ast.SEND { + sends = append(sends, op.pos) + } else { + receives = append(receives, op.pos) + } + } + } + } + sort.Sort(byPos(sends)) + sort.Sort(byPos(receives)) + return &peersResult{ - queryOp: queryOp, - ops: ops, - queryChanPts: queryChanPts, + queryPos: arrowPos, + queryType: queryType, + makes: makes, + sends: sends, + receives: receives, }, nil } @@ -122,35 +152,49 @@ func chanOps(instr ssa.Instruction) []chanOp { } type peersResult struct { - queryOp chanOp - ops []chanOp - queryChanPts pointer.PointsToSet + queryPos token.Pos // of queried '<-' token + queryType types.Type // type of queried channel + makes, sends, receives []token.Pos // positions of alisaed makechan/send/receive instrs } -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.") +func (r *peersResult) display(printf printfFunc) { + if len(r.makes) == 0 { + printf(r.queryPos, "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") + printf(r.queryPos, "This channel of type %s may be:", r.queryType) + for _, alloc := range r.makes { + printf(alloc, "\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) - } - } + for _, send := range r.sends { + printf(send, "\tsent to, here") + } + for _, receive := range r.receives { + printf(receive, "\treceived from, here") } } + +func (r *peersResult) toJSON(res *json.Result, fset *token.FileSet) { + peers := &json.Peers{ + Pos: fset.Position(r.queryPos).String(), + Type: r.queryType.String(), + } + for _, alloc := range r.makes { + peers.Allocs = append(peers.Allocs, fset.Position(alloc).String()) + } + for _, send := range r.sends { + peers.Sends = append(peers.Sends, fset.Position(send).String()) + } + for _, receive := range r.receives { + peers.Receives = append(peers.Receives, fset.Position(receive).String()) + } + res.Peers = peers +} + +// -------- utils -------- + +type byPos []token.Pos + +func (p byPos) Len() int { return len(p) } +func (p byPos) Less(i, j int) bool { return p[i] < p[j] } +func (p byPos) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/oracle/testdata/src/main/callgraph-json.go b/oracle/testdata/src/main/callgraph-json.go new file mode 100644 index 00000000..33708fd0 --- /dev/null +++ b/oracle/testdata/src/main/callgraph-json.go @@ -0,0 +1,54 @@ +package main + +// Tests of call-graph queries, -format=json. +// See go.tools/oracle/oracle_test.go for explanation. +// See callgraph-json.golden for expected query results. + +func A() {} + +func B() {} + +// call is not (yet) treated context-sensitively. +func call(f func()) { + f() +} + +// nop *is* treated context-sensitively. +func nop() {} + +func call2(f func()) { + f() + f() +} + +func main() { + call(A) + call(B) + + nop() + nop() + + call2(func() { + // called twice from main.call2, + // but call2 is not context sensitive (yet). + }) + + print("builtin") + _ = string("type conversion") + call(nil) + if false { + main() + } + var nilFunc func() + nilFunc() + var i interface { + f() + } + i.f() +} + +func deadcode() { + main() +} + +// @callgraph callgraph "^" diff --git a/oracle/testdata/src/main/callgraph-json.golden b/oracle/testdata/src/main/callgraph-json.golden new file mode 100644 index 00000000..e2aaaf5a --- /dev/null +++ b/oracle/testdata/src/main/callgraph-json.golden @@ -0,0 +1,63 @@ +-------- @callgraph callgraph -------- +{ + "mode": "callgraph", + "callgraph": [ + { + "name": "\u003croot\u003e", + "pos": "-", + "children": [ + 1, + 2 + ] + }, + { + "name": "main.init", + "pos": "-" + }, + { + "name": "main.main", + "pos": "testdata/src/main/callgraph-json.go:24:6", + "children": [ + 3, + 6, + 7, + 8 + ] + }, + { + "name": "main.call", + "pos": "testdata/src/main/callgraph-json.go:12:6", + "children": [ + 4, + 5 + ] + }, + { + "name": "main.A", + "pos": "testdata/src/main/callgraph-json.go:7:6" + }, + { + "name": "main.B", + "pos": "testdata/src/main/callgraph-json.go:9:6" + }, + { + "name": "main.nop", + "pos": "testdata/src/main/callgraph-json.go:17:6" + }, + { + "name": "main.nop", + "pos": "testdata/src/main/callgraph-json.go:17:6" + }, + { + "name": "main.call2", + "pos": "testdata/src/main/callgraph-json.go:19:6", + "children": [ + 9 + ] + }, + { + "name": "func@31.8", + "pos": "testdata/src/main/callgraph-json.go:31:8" + } + ] +} \ No newline at end of file diff --git a/oracle/testdata/src/main/callgraph.go b/oracle/testdata/src/main/callgraph.go new file mode 100644 index 00000000..c29d206d --- /dev/null +++ b/oracle/testdata/src/main/callgraph.go @@ -0,0 +1,54 @@ +package main + +// Tests of call-graph queries. +// See go.tools/oracle/oracle_test.go for explanation. +// See callgraph.golden for expected query results. + +func A() {} + +func B() {} + +// call is not (yet) treated context-sensitively. +func call(f func()) { + f() +} + +// nop *is* treated context-sensitively. +func nop() {} + +func call2(f func()) { + f() + f() +} + +func main() { + call(A) + call(B) + + nop() + nop() + + call2(func() { + // called twice from main.call2, + // but call2 is not context sensitive (yet). + }) + + print("builtin") + _ = string("type conversion") + call(nil) + if false { + main() + } + var nilFunc func() + nilFunc() + var i interface { + f() + } + i.f() +} + +func deadcode() { + main() +} + +// @callgraph callgraph "^" diff --git a/oracle/testdata/src/main/callgraph.golden b/oracle/testdata/src/main/callgraph.golden new file mode 100644 index 00000000..50f4b32f --- /dev/null +++ b/oracle/testdata/src/main/callgraph.golden @@ -0,0 +1,20 @@ +-------- @callgraph callgraph -------- + +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. + +0 +1 main.init +2 main.main +3 main.call +4 main.A +5 main.B +6 main.nop +7 main.nop +8 main.call2 +9 func@31.8 + diff --git a/oracle/testdata/src/main/calls-json.go b/oracle/testdata/src/main/calls-json.go new file mode 100644 index 00000000..1c7a6c99 --- /dev/null +++ b/oracle/testdata/src/main/calls-json.go @@ -0,0 +1,16 @@ +package main + +// Tests of call-graph queries, -format=json. +// See go.tools/oracle/oracle_test.go for explanation. +// See calls-json.golden for expected query results. + +func call(f func()) { + f() // @callees @callees-f "f" +} + +func main() { + call(func() { + // @callers callers-main.anon "^" + // @callstack callstack-main.anon "^" + }) +} diff --git a/oracle/testdata/src/main/calls-json.golden b/oracle/testdata/src/main/calls-json.golden new file mode 100644 index 00000000..574578af --- /dev/null +++ b/oracle/testdata/src/main/calls-json.golden @@ -0,0 +1,33 @@ +-------- @callees @callees-f -------- +{ + "mode": "callees", + "callees": { + "pos": "testdata/src/main/calls-json.go:8:3", + "desc": "dynamic function call", + "callees": [ + { + "name": "func@12.7", + "pos": "testdata/src/main/calls-json.go:12:7" + } + ] + } +}-------- @callstack callstack-main.anon -------- +{ + "mode": "callstack", + "callstack": { + "pos": "testdata/src/main/calls-json.go:12:7", + "target": "func@12.7", + "callers": [ + { + "pos": "testdata/src/main/calls-json.go:8:3", + "desc": "dynamic function call", + "caller": "main.call" + }, + { + "pos": "testdata/src/main/calls-json.go:12:6", + "desc": "static function call", + "caller": "main.main" + } + ] + } +} \ No newline at end of file diff --git a/oracle/testdata/src/main/calls.golden b/oracle/testdata/src/main/calls.golden index 4cac0a53..e0312884 100644 --- a/oracle/testdata/src/main/calls.golden +++ b/oracle/testdata/src/main/calls.golden @@ -82,7 +82,8 @@ Error: this is a type conversion, not a function call Error: ambiguous selection within function call (or conversion) -------- @callees callees-err-deadcode1 -------- -this call site is unreachable in this analysis + +Error: this call site is unreachable in this analysis -------- @callees callees-err-nil-func -------- dynamic function call on nil value @@ -91,7 +92,8 @@ dynamic function call on nil value dynamic method call on nil value -------- @callees callees-err-deadcode2 -------- -this call site is unreachable in this analysis + +Error: this call site is unreachable in this analysis -------- @callstack callstack-err-deadcode -------- main.deadcode is unreachable in this analysis scope diff --git a/oracle/testdata/src/main/describe-json.go b/oracle/testdata/src/main/describe-json.go new file mode 100644 index 00000000..7e4220e1 --- /dev/null +++ b/oracle/testdata/src/main/describe-json.go @@ -0,0 +1,30 @@ +package describe // @describe pkgdecl "describe" + +// @implements implements "^" + +// Tests of 'describe' and 'implements' queries, -format=json. +// See go.tools/oracle/oracle_test.go for explanation. +// See describe-json.golden for expected query results. + +func main() { // + var s struct{ x [3]int } + p := &s.x[0] // @describe desc-val-p "p" + + var i I = C(0) + if i == nil { + i = new(D) + } + _ = i // @describe desc-val-i "i" + + go main() // @describe desc-stmt "go" +} + +type I interface { + f() +} + +type C int // @describe desc-type-C "C" +type D struct{} + +func (c C) f() {} +func (d *D) f() {} diff --git a/oracle/testdata/src/main/describe-json.golden b/oracle/testdata/src/main/describe-json.golden new file mode 100644 index 00000000..f623f6e7 --- /dev/null +++ b/oracle/testdata/src/main/describe-json.golden @@ -0,0 +1,164 @@ +-------- @describe pkgdecl -------- +{ + "mode": "describe", + "describe": { + "desc": "definition of package \"main\"", + "pos": "testdata/src/main/describe-json.go:1:9", + "detail": "package", + "package": { + "path": "main", + "members": [ + { + "name": "C", + "type": "int", + "pos": "testdata/src/main/describe-json.go:26:6", + "kind": "type", + "methods": [ + { + "name": "method (describe.C) f()", + "pos": "testdata/src/main/describe-json.go:29:12" + } + ] + }, + { + "name": "D", + "type": "struct{}", + "pos": "testdata/src/main/describe-json.go:27:6", + "kind": "type", + "methods": [ + { + "name": "method (*describe.D) f()", + "pos": "testdata/src/main/describe-json.go:30:13" + } + ] + }, + { + "name": "I", + "type": "interface{f()}", + "pos": "testdata/src/main/describe-json.go:22:6", + "kind": "type", + "methods": [ + { + "name": "method (describe.I) f()", + "pos": "testdata/src/main/describe-json.go:23:2" + } + ] + }, + { + "name": "init", + "type": "func()", + "pos": "-", + "kind": "func" + }, + { + "name": "init$guard", + "type": "bool", + "pos": "-", + "kind": "var" + }, + { + "name": "main", + "type": "func()", + "pos": "testdata/src/main/describe-json.go:9:6", + "kind": "func" + } + ] + } + } +}-------- @implements implements -------- +{ + "mode": "implements", + "implements": [ + { + "i": "describe.I", + "ipos": "testdata/src/main/describe-json.go:22:6", + "c": "describe.C", + "cpos": "testdata/src/main/describe-json.go:26:6" + }, + { + "i": "describe.I", + "ipos": "testdata/src/main/describe-json.go:22:6", + "c": "*describe.D", + "cpos": "testdata/src/main/describe-json.go:27:6" + } + ] +}-------- @describe desc-val-p -------- +{ + "mode": "describe", + "describe": { + "desc": "identifier", + "pos": "testdata/src/main/describe-json.go:11:2", + "detail": "value", + "value": { + "type": "*int", + "objpos": "testdata/src/main/describe-json.go:11:2", + "pts": [ + { + "type": "*int", + "namepos": "", + "labels": [ + { + "pos": "testdata/src/main/describe-json.go:10:6", + "desc": "s.x[*]" + } + ] + } + ] + } + } +}-------- @describe desc-val-i -------- +{ + "mode": "describe", + "describe": { + "desc": "identifier", + "pos": "testdata/src/main/describe-json.go:17:6", + "detail": "value", + "value": { + "type": "describe.I", + "objpos": "testdata/src/main/describe-json.go:13:6", + "pts": [ + { + "type": "*describe.D", + "namepos": "testdata/src/main/describe-json.go:27:6", + "labels": [ + { + "pos": "testdata/src/main/describe-json.go:15:10", + "desc": "new" + } + ] + }, + { + "type": "describe.C", + "namepos": "testdata/src/main/describe-json.go:26:6" + } + ] + } + } +}-------- @describe desc-stmt -------- +{ + "mode": "describe", + "describe": { + "desc": "go statement", + "pos": "testdata/src/main/describe-json.go:19:2", + "detail": "unknown" + } +}-------- @describe desc-type-C -------- +{ + "mode": "describe", + "describe": { + "desc": "definition of type describe.C", + "pos": "testdata/src/main/describe-json.go:26:6", + "detail": "type", + "type": { + "type": "describe.C", + "namepos": "testdata/src/main/describe-json.go:26:6", + "namedef": "int", + "methods": [ + { + "name": "method (describe.C) f()", + "pos": "testdata/src/main/describe-json.go:29:12" + } + ] + } + } +} \ No newline at end of file diff --git a/oracle/testdata/src/main/describe.golden b/oracle/testdata/src/main/describe.golden index e16c2bd3..a1e86571 100644 --- a/oracle/testdata/src/main/describe.golden +++ b/oracle/testdata/src/main/describe.golden @@ -126,7 +126,7 @@ 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 +no points-to information: can't locate SSA Value for expression in main.main -------- @describe mapval -------- reference to var mapval *int @@ -151,7 +151,7 @@ definition of var a int -------- @describe b -------- definition of var b *int -pointer analysis did not analyze this expression (dead code?) +no points-to information: PTA did not encounter this expression (dead code?) -------- @describe def-iface-I -------- definition of type describe.I diff --git a/oracle/testdata/src/main/freevars.golden b/oracle/testdata/src/main/freevars.golden index 678653db..030d3a6b 100644 --- a/oracle/testdata/src/main/freevars.golden +++ b/oracle/testdata/src/main/freevars.golden @@ -1,14 +1,14 @@ -------- @freevars fv1 -------- Free identifers: -var x int type C main.C -const exp untyped integer +const exp int +var x int -------- @freevars fv2 -------- Free identifers: -var s.x int var s.t.a int var s.t.b int +var s.x int var x int var y int32 diff --git a/oracle/testdata/src/main/peers-json.go b/oracle/testdata/src/main/peers-json.go new file mode 100644 index 00000000..1f5beb20 --- /dev/null +++ b/oracle/testdata/src/main/peers-json.go @@ -0,0 +1,13 @@ +package peers + +// Tests of channel 'peers' query, -format=json. +// See go.tools/oracle/oracle_test.go for explanation. +// See peers-json.golden for expected query results. + +func main() { + chA := make(chan *int) + <-chA + select { + case <-chA: // @peers peer-recv-chA "<-" + } +} diff --git a/oracle/testdata/src/main/peers-json.golden b/oracle/testdata/src/main/peers-json.golden new file mode 100644 index 00000000..80eb3c43 --- /dev/null +++ b/oracle/testdata/src/main/peers-json.golden @@ -0,0 +1,15 @@ +-------- @peers peer-recv-chA -------- +{ + "mode": "peers", + "peers": { + "pos": "testdata/src/main/peers-json.go:11:7", + "type": "chan *int", + "allocs": [ + "testdata/src/main/peers-json.go:8:13" + ], + "receives": [ + "testdata/src/main/peers-json.go:9:2", + "testdata/src/main/peers-json.go:11:7" + ] + } +} \ No newline at end of file diff --git a/oracle/testdata/src/main/peers.golden b/oracle/testdata/src/main/peers.golden index b3ec7dd7..81133848 100644 --- a/oracle/testdata/src/main/peers.golden +++ b/oracle/testdata/src/main/peers.golden @@ -22,12 +22,12 @@ 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 + received from, here + received from, here + received from, here + received from, here -------- @describe describe-rA -------- reference to var rA *int @@ -54,20 +54,20 @@ 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 + received from, here + received from, here + received from, 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 + received from, here + received from, here + received from, here + received from, here diff --git a/pointer/analysis.go b/pointer/analysis.go index c842443c..dee75a82 100644 --- a/pointer/analysis.go +++ b/pointer/analysis.go @@ -243,7 +243,7 @@ func Analyze(config *Config) CallGraphNode { // Notify the client of the call graph, if // they're interested. if Call != nil { - Call(site, site.caller, cgn) + Call(site, cgn) } // Warn about calls to non-intrinsic external functions. diff --git a/pointer/api.go b/pointer/api.go index 0587fe0c..37c9e9e5 100644 --- a/pointer/api.go +++ b/pointer/api.go @@ -25,6 +25,8 @@ type Config struct { // Call is invoked for each discovered call-graph edge. The // call-graph is a multigraph over CallGraphNodes with edges // labelled by the CallSite that gives rise to the edge. + // (The caller node is available as site.Caller()) + // // Clients that wish to construct a call graph may provide // CallGraph.AddEdge here. // @@ -32,7 +34,7 @@ type Config struct { // distinguish separate calls to the same function depending // on the context. // - Call func(site CallSite, caller, callee CallGraphNode) + Call func(site CallSite, callee CallGraphNode) // CallSite is invoked for each call-site encountered in the // program. diff --git a/pointer/callgraph.go b/pointer/callgraph.go index ef26a367..af328a9e 100644 --- a/pointer/callgraph.go +++ b/pointer/callgraph.go @@ -108,7 +108,8 @@ func (c *callsite) String() string { // type CallGraph map[CallGraphNode]map[CallGraphNode]CallSite -func (cg CallGraph) AddEdge(site CallSite, caller, callee CallGraphNode) { +func (cg CallGraph) AddEdge(site CallSite, callee CallGraphNode) { + caller := site.Caller() callees := cg[caller] if callees == nil { callees = make(map[CallGraphNode]CallSite)