1
0
mirror of https://github.com/golang/go synced 2024-11-18 17:54:57 -07:00

go.tools/oracle: optionally restrict 'callgraph' query to a single package.

If a -pos argument is specified, a 'callgraph' query reports only the
functions within the query package.  This produces a far more manageable
amount of information, and because we don't need to package-qualify the
names, the result is easier to read.

Added tests:
- callgraph query with/without -pos
  (The test driver was extended to allow "nopos" queries.)
- callers and callees queries don't return wrappers

Also, in go/callgraph:
- (*Node).String, (*Edge).String
- (*Graph).DeleteSyntheticNodes eliminates synthetic wrapper functions,
  preserving topology.  Used in all four oracle "call*" queries.
- (*Graph).DeleteNode

LGTM=crawshaw
R=crawshaw
CC=golang-codereviews
https://golang.org/cl/66240044
This commit is contained in:
Alan Donovan 2014-02-21 10:46:02 -05:00
parent 49eaa56ed1
commit 99b2441d95
14 changed files with 274 additions and 96 deletions

View File

@ -39,7 +39,11 @@ package callgraph
// More generally, we could eliminate "uninteresting" nodes such as
// nodes from packages we don't care about.
import "code.google.com/p/go.tools/go/ssa"
import (
"fmt"
"code.google.com/p/go.tools/go/ssa"
)
// A Graph represents a call graph.
//
@ -77,6 +81,10 @@ type Node struct {
Out []*Edge // unordered set of outgoing call edges (n.Out[*].Caller == n)
}
func (n *Node) String() string {
return fmt.Sprintf("n%d:%s", n.ID, n.Func)
}
// A Edge represents an edge in the call graph.
//
// Site is nil for edges originating in synthetic or intrinsic
@ -87,6 +95,10 @@ type Edge struct {
Callee *Node
}
func (e Edge) String() string {
return fmt.Sprintf("%s --> %s", e.Caller, e.Callee)
}
// AddEdge adds the edge (caller, site, callee) to the call graph.
func AddEdge(caller *Node, site ssa.CallInstruction, callee *Node) {
e := &Edge{caller, site, callee}

View File

@ -4,6 +4,8 @@
package callgraph
import "code.google.com/p/go.tools/go/ssa"
// This file provides various utilities over call graphs, such as
// visitation and path search.
@ -75,3 +77,80 @@ func PathSearch(start *Node, isEnd func(*Node) bool) []*Edge {
}
return search(start)
}
// DeleteSyntheticNodes removes from call graph g all nodes for
// synthetic functions (except g.Root and package initializers),
// preserving the topology.
func (g *Graph) DeleteSyntheticNodes() {
for fn, cgn := range g.Nodes {
if cgn == g.Root || fn.Synthetic == "" || isInit(cgn.Func) {
continue // keep
}
for _, eIn := range cgn.In {
for _, eOut := range cgn.Out {
AddEdge(eIn.Caller, eIn.Site, eOut.Callee)
}
}
g.DeleteNode(cgn)
}
}
func isInit(fn *ssa.Function) bool {
return fn.Pkg != nil && fn.Pkg.Func("init") == fn
}
// DeleteNode removes node n and its edges from the graph g.
// (NB: not efficient for batch deletion.)
func (g *Graph) DeleteNode(n *Node) {
n.deleteIns()
n.deleteOuts()
delete(g.Nodes, n.Func)
}
// deleteIns deletes all incoming edges to n.
func (n *Node) deleteIns() {
for _, e := range n.In {
removeOutEdge(e)
}
n.In = nil
}
// deleteOuts deletes all outgoing edges from n.
func (n *Node) deleteOuts() {
for _, e := range n.Out {
removeInEdge(e)
}
n.Out = nil
}
// removeOutEdge removes edge.Caller's outgoing edge 'edge'.
func removeOutEdge(edge *Edge) {
caller := edge.Caller
n := len(caller.Out)
for i, e := range caller.Out {
if e == edge {
// Replace it with the final element and shrink the slice.
caller.Out[i] = caller.Out[n-1]
caller.Out[n-1] = nil // aid GC
caller.Out = caller.Out[:n-1]
return
}
}
panic("edge not found: " + edge.String())
}
// removeInEdge removes edge.Callee's incoming edge 'edge'.
func removeInEdge(edge *Edge) {
caller := edge.Callee
n := len(caller.In)
for i, e := range caller.In {
if e == edge {
// Replace it with the final element and shrink the slice.
caller.In[i] = caller.In[n-1]
caller.In[n-1] = nil // aid GC
caller.In = caller.In[:n-1]
return
}
}
panic("edge not found: " + edge.String())
}

View File

@ -17,9 +17,6 @@ import (
// 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, qpos *QueryPos) (queryResult, error) {
pkg := o.prog.Package(qpos.info.Pkg)
if pkg == nil {
@ -104,6 +101,7 @@ func findCallees(o *Oracle, site ssa.CallInstruction) ([]*ssa.Function, error) {
// Dynamic call: use pointer analysis.
o.ptaConfig.BuildCallGraph = true
cg := ptrAnalysis(o).CallGraph
cg.DeleteSyntheticNodes()
// Find all call edges from the site.
n := cg.Nodes[site.Parent()]

View File

@ -16,8 +16,6 @@ import (
// 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, qpos *QueryPos) (queryResult, error) {
pkg := o.prog.Package(qpos.info.Pkg)
if pkg == nil {
@ -38,6 +36,7 @@ func callers(o *Oracle, qpos *QueryPos) (queryResult, error) {
// call found to originate from target.
o.ptaConfig.BuildCallGraph = true
cg := ptrAnalysis(o).CallGraph
cg.DeleteSyntheticNodes()
edges := cg.CreateNode(target).In
// TODO(adonovan): sort + dedup calls to ensure test determinism.

View File

@ -5,46 +5,96 @@
package oracle
import (
"fmt"
"go/token"
"sort"
"code.google.com/p/go.tools/go/callgraph"
"code.google.com/p/go.tools/go/ssa"
"code.google.com/p/go.tools/go/types"
"code.google.com/p/go.tools/oracle/serial"
)
// doCallgraph displays the entire callgraph of the current program.
//
// TODO(adonovan): add options for restricting the display to a region
// of interest: function, package, subgraph, dirtree, goroutine, etc.
//
// TODO(adonovan): add an option to partition edges by call site.
//
// TODO(adonovan): elide nodes for synthetic functions?
//
func doCallgraph(o *Oracle, _ *QueryPos) (queryResult, error) {
// doCallgraph displays the entire callgraph of the current program,
// or if a query -pos was provided, the query package.
func doCallgraph(o *Oracle, qpos *QueryPos) (queryResult, error) {
buildSSA(o)
// Run the pointer analysis and build the complete callgraph.
// Run the pointer analysis and build the callgraph.
o.ptaConfig.BuildCallGraph = true
ptares := ptrAnalysis(o)
cg := ptrAnalysis(o).CallGraph
cg.DeleteSyntheticNodes()
return &callgraphResult{
callgraph: ptares.CallGraph,
}, nil
var qpkg *types.Package
var roots []*callgraph.Node
if qpos == nil {
// No -pos provided: show complete callgraph.
roots = append(roots, cg.Root)
} else {
// A query -pos was provided: restrict result to
// functions belonging to the query package.
qpkg = qpos.info.Pkg
isQueryPkg := func(fn *ssa.Function) bool {
return fn.Pkg != nil && fn.Pkg.Object == qpkg
}
// First compute the nodes to keep and remove.
var nodes, remove []*callgraph.Node
for fn, cgn := range cg.Nodes {
if isQueryPkg(fn) {
nodes = append(nodes, cgn)
} else {
remove = append(remove, cgn)
}
}
// Compact the Node.ID sequence of the remaining
// nodes, preserving the original order.
sort.Sort(nodesByID(nodes))
for i, cgn := range nodes {
cgn.ID = i
}
// Compute the set of roots:
// in-package nodes with out-of-package callers.
// For determinism, roots are ordered by original Node.ID.
for _, cgn := range nodes {
for _, e := range cgn.In {
if !isQueryPkg(e.Caller.Func) {
roots = append(roots, cgn)
break
}
}
}
// Finally, discard all out-of-package nodes.
for _, cgn := range remove {
cg.DeleteNode(cgn)
}
}
return &callgraphResult{qpkg, cg.Nodes, roots}, nil
}
type callgraphResult struct {
callgraph *callgraph.Graph
qpkg *types.Package
nodes map[*ssa.Function]*callgraph.Node
roots []*callgraph.Node
}
func (r *callgraphResult) display(printf printfFunc) {
descr := "the entire program"
if r.qpkg != nil {
descr = fmt.Sprintf("package %s", r.qpkg.Path())
}
printf(nil, `
Below is a call graph of the entire program.
Below is a call graph of %s.
The numbered nodes form a spanning tree.
Non-numbered nodes indicate back- or cross-edges to the node whose
number follows in parentheses.
`)
`, descr)
printed := make(map[*callgraph.Node]int)
var print func(caller *callgraph.Node, indent int)
@ -61,17 +111,25 @@ Non-numbered nodes indicate back- or cross-edges to the node whose
}
sort.Sort(funcs)
printf(caller.Func, "%d\t%*s%s", num, 4*indent, "", caller.Func)
printf(caller.Func, "%d\t%*s%s", num, 4*indent, "", caller.Func.RelString(r.qpkg))
for _, callee := range funcs {
print(r.callgraph.Nodes[callee], indent+1)
print(r.nodes[callee], indent+1)
}
} else {
printf(caller, "\t%*s%s (%d)", 4*indent, "", caller.Func, num)
printf(caller.Func, "\t%*s%s (%d)", 4*indent, "", caller.Func.RelString(r.qpkg), num)
}
}
print(r.callgraph.Root, 0)
for _, root := range r.roots {
print(root, 0)
}
}
type nodesByID []*callgraph.Node
func (s nodesByID) Len() int { return len(s) }
func (s nodesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s nodesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }
type funcsByName []*ssa.Function
func (s funcsByName) Len() int { return len(s) }
@ -79,8 +137,8 @@ func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s funcsByName) Less(i, j int) bool { return s[i].String() < s[j].String() }
func (r *callgraphResult) toSerial(res *serial.Result, fset *token.FileSet) {
cg := make([]serial.CallGraph, len(r.callgraph.Nodes))
for _, n := range r.callgraph.Nodes {
cg := make([]serial.CallGraph, len(r.nodes))
for _, n := range r.nodes {
j := &cg[n.ID]
fn := n.Func
j.Name = fn.String()

View File

@ -43,6 +43,7 @@ func callstack(o *Oracle, qpos *QueryPos) (queryResult, error) {
// Run the pointer analysis and build the complete call graph.
o.ptaConfig.BuildCallGraph = true
cg := ptrAnalysis(o).CallGraph
cg.DeleteSyntheticNodes()
// Search for an arbitrary path from a root to the target function.
isEnd := func(n *callgraph.Node) bool { return n.Func == target }

View File

@ -257,12 +257,9 @@ func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *buil
return nil, err
}
var qpos *QueryPos
if minfo.needs&(needPos|needExactPos) != 0 {
qpos, err = ParseQueryPos(iprog, pos, minfo.needs&needExactPos != 0)
if err != nil {
return nil, err
}
qpos, err := ParseQueryPos(iprog, pos, minfo.needs&needExactPos != 0)
if err != nil && minfo.needs&(needPos|needExactPos) != 0 {
return nil, err
}
// SSA is built and we have the QueryPos.

View File

@ -51,11 +51,11 @@ import (
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
id string // unique id
verb string // query mode, e.g. "callees"
posn token.Position // position of of query
filename string
queryPos string // value of -pos flag
}
func parseRegexp(text string) (*regexp.Regexp, error) {
@ -108,36 +108,40 @@ func parseQueries(t *testing.T, filename string) []*query {
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],
posn: posn,
}
if match[3] != `"nopos"` {
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.queryPos = fmt.Sprintf("%s:#%d,#%d",
filename, linestart+loc[0], linestart+loc[1])
}
queries = append(queries, q)
queriesById[id] = q
}
@ -168,7 +172,7 @@ func doQuery(out io.Writer, q *query, useJson bool) {
buildContext.GOPATH = "testdata"
res, err := oracle.Query([]string{q.filename},
q.verb,
fmt.Sprintf("%s:#%d,#%d", q.filename, q.start, q.end),
q.queryPos,
nil, // ptalog,
&buildContext,
true) // reflection

View File

@ -2,29 +2,21 @@
{
"mode": "callgraph",
"callgraph": [
{
"name": "\u003croot\u003e",
"pos": "-",
"children": [
1,
5
]
},
{
"name": "main.main",
"pos": "testdata/src/main/callgraph-json.go:24:6",
"children": [
1,
2,
3,
4
3
]
},
{
"name": "main.call",
"pos": "testdata/src/main/callgraph-json.go:12:6",
"children": [
6,
7
5,
6
]
},
{
@ -35,7 +27,7 @@
"name": "main.call2",
"pos": "testdata/src/main/callgraph-json.go:19:6",
"children": [
8
7
]
},
{

View File

@ -4,6 +4,8 @@ package main
// See go.tools/oracle/oracle_test.go for explanation.
// See callgraph.golden for expected query results.
import "lib"
func A() {}
func B() {}
@ -45,10 +47,14 @@ func main() {
f()
}
i.f()
lib.Func()
}
func deadcode() {
main()
}
// @callgraph callgraph "^"
// @callgraph callgraph-main "^"
// @callgraph callgraph-complete "nopos"

View File

@ -1,4 +1,20 @@
-------- @callgraph callgraph --------
-------- @callgraph callgraph-main --------
Below is a call graph of package main.
The numbered nodes form a spanning tree.
Non-numbered nodes indicate back- or cross-edges to the node whose
number follows in parentheses.
0 init
1 main
2 call
3 A
4 B
5 call2
6 func@33.8
7 nop
-------- @callgraph callgraph-complete --------
Below is a call graph of the entire program.
The numbered nodes form a spanning tree.
@ -7,11 +23,13 @@ Non-numbered nodes indicate back- or cross-edges to the node whose
0 <root>
1 main.init
2 main.main
3 main.call
4 main.A
5 main.B
6 main.call2
7 func@31.8
8 main.nop
2 lib.init
3 main.main
4 lib.Func
5 main.call
6 main.A
7 main.B
8 main.call2
9 func@33.8
10 main.nop

View File

@ -1,13 +1,11 @@
-------- @callgraph callgraph --------
Below is a call graph of the entire program.
Below is a call graph of package main.
The numbered nodes form a spanning tree.
Non-numbered nodes indicate back- or cross-edges to the node whose
number follows in parentheses.
0 <root>
1 main.init
2 reflect.init
3 main.main
4 main.f
0 init
1 main
2 f

View File

@ -67,6 +67,15 @@ func main() {
f()
}
i.f() // @callees callees-err-nil-interface "i.f"
i = new(myint)
i.f() // @callees callees-not-a-wrapper "f"
}
type myint int
func (myint) f() {
// @callers callers-not-a-wrapper "^"
}
var dynamic = func() {}
@ -88,5 +97,4 @@ var global = 123 // @callers callers-global "global"
// are in turn called by it.
func init() {
// @callstack callstack-init "^"
}

View File

@ -79,6 +79,14 @@ dynamic function call on nil value
-------- @callees callees-err-nil-interface --------
dynamic method call on nil value
-------- @callees callees-not-a-wrapper --------
this dynamic method call dispatches to:
(main.myint).f
-------- @callers callers-not-a-wrapper --------
(main.myint).f is called from these 1 sites:
dynamic method call from main.main
-------- @callees callees-err-deadcode2 --------
this static function call dispatches to:
main.main