cmd/callgraph: add -algo=static and -algo=cha options.
"static" ignores dynamic calls altogether. "cha" uses Class Hierarchy Analysis, which assumes that a dynamic call may dispatch to any func or method that satisfies the type. Both these algorithms can work on partial programs, e.g. libraries without a main function or tests. (This feature was requested after my talk last night.) + Tests. LGTM=sameer R=sameer, minux CC=golang-codereviews, gri https://golang.org/cl/176780043
This commit is contained in:
parent
ce7df396da
commit
a9651d6ad7
|
|
@ -31,7 +31,9 @@ import (
|
|||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/callgraph/cha"
|
||||
"golang.org/x/tools/go/callgraph/rta"
|
||||
"golang.org/x/tools/go/callgraph/static"
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/pointer"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
|
|
@ -51,21 +53,30 @@ const Usage = `callgraph: display the the call graph of a Go program.
|
|||
|
||||
Usage:
|
||||
|
||||
callgraph [-algo=rta|pta] [-test] [-format=...] <args>...
|
||||
callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] <args>...
|
||||
|
||||
Flags:
|
||||
|
||||
-algo Specifies the call-graph construction algorithm. One of:
|
||||
"rta": Rapid Type Analysis (simple and fast)
|
||||
"pta": inclusion-based Points-To Analysis (slower but more precise)
|
||||
-algo Specifies the call-graph construction algorithm, one of:
|
||||
|
||||
static static calls only (unsound)
|
||||
cha Class Hierarchy Analysis
|
||||
rta Rapid Type Analysis
|
||||
pta inclusion-based Points-To Analysis
|
||||
|
||||
The algorithms are ordered by increasing precision in their
|
||||
treatment of dynamic calls (and thus also computational cost).
|
||||
RTA and PTA require a whole program (main or test), and
|
||||
include only functions reachable from main.
|
||||
|
||||
-test Include the package's tests in the analysis.
|
||||
|
||||
-format Specifies the format in which each call graph edge is displayed.
|
||||
One of:
|
||||
"digraph": output suitable for input to
|
||||
|
||||
digraph output suitable for input to
|
||||
golang.org/x/tools/cmd/digraph.
|
||||
"graphviz": output in AT&T GraphViz (.dot) format.
|
||||
graphviz output in AT&T GraphViz (.dot) format.
|
||||
|
||||
All other values are interpreted using text/template syntax.
|
||||
The default value is:
|
||||
|
|
@ -168,44 +179,22 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
|
|||
prog := ssa.Create(iprog, 0)
|
||||
prog.BuildAll()
|
||||
|
||||
// Determine the main package.
|
||||
// TODO(adonovan): allow independent control over tests, mains
|
||||
// and libraries.
|
||||
// TODO(adonovan): put this logic in a library; we keep reinventing it.
|
||||
var main *ssa.Package
|
||||
pkgs := prog.AllPackages()
|
||||
if tests {
|
||||
// If -test, use all packages' tests.
|
||||
if len(pkgs) > 0 {
|
||||
main = prog.CreateTestMainPackage(pkgs...)
|
||||
}
|
||||
if main == nil {
|
||||
return fmt.Errorf("no tests")
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use main.main.
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Object.Name() == "main" {
|
||||
main = pkg
|
||||
if main.Func("main") == nil {
|
||||
return fmt.Errorf("no func main() in main package")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if main == nil {
|
||||
return fmt.Errorf("no main package")
|
||||
}
|
||||
}
|
||||
|
||||
// Invariant: main package has a main() function.
|
||||
|
||||
// -- call graph construction ------------------------------------------
|
||||
|
||||
var cg *callgraph.Graph
|
||||
|
||||
switch algo {
|
||||
case "static":
|
||||
cg = static.CallGraph(prog)
|
||||
|
||||
case "cha":
|
||||
cg = cha.CallGraph(prog)
|
||||
|
||||
case "pta":
|
||||
main, err := mainPackage(prog, tests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &pointer.Config{
|
||||
Mains: []*ssa.Package{main},
|
||||
BuildCallGraph: true,
|
||||
|
|
@ -217,6 +206,10 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
|
|||
cg = ptares.CallGraph
|
||||
|
||||
case "rta":
|
||||
main, err := mainPackage(prog, tests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roots := []*ssa.Function{
|
||||
main.Func("init"),
|
||||
main.Func("main"),
|
||||
|
|
@ -279,6 +272,37 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
|
|||
return nil
|
||||
}
|
||||
|
||||
// mainPackage returns the main package to analyze.
|
||||
// The resulting package has a main() function.
|
||||
func mainPackage(prog *ssa.Program, tests bool) (*ssa.Package, error) {
|
||||
pkgs := prog.AllPackages()
|
||||
|
||||
// TODO(adonovan): allow independent control over tests, mains and libraries.
|
||||
// TODO(adonovan): put this logic in a library; we keep reinventing it.
|
||||
|
||||
if tests {
|
||||
// If -test, use all packages' tests.
|
||||
if len(pkgs) > 0 {
|
||||
if main := prog.CreateTestMainPackage(pkgs...); main != nil {
|
||||
return main, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no tests")
|
||||
}
|
||||
|
||||
// Otherwise, use the first package named main.
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Object.Name() == "main" {
|
||||
if pkg.Func("main") == nil {
|
||||
return nil, fmt.Errorf("no func main() in main package")
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no main package")
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Caller *ssa.Function
|
||||
Callee *ssa.Function
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
// Package cha computes the call graph of a Go program using the Class
|
||||
// Hierarchy Analysis (CHA) algorithm.
|
||||
//
|
||||
// CHA was first described in "Optimization of Object-Oriented Programs
|
||||
// Using Static Class Hierarchy Analysis", Jeffrey Dean, David Grove,
|
||||
// and Craig Chambers, ECOOP'95.
|
||||
//
|
||||
// CHA is related to RTA (see go/callgraph/rta); the difference is that
|
||||
// CHA conservatively computes the entire "implements" relation between
|
||||
// interfaces and concrete types ahead of time, whereas RTA uses dynamic
|
||||
// programming to construct it on the fly as it encounters new functions
|
||||
// reachable from main. CHA may thus include spurious call edges for
|
||||
// types that haven't been instantiated yet, or types that are never
|
||||
// instantiated.
|
||||
//
|
||||
// Since CHA conservatively assumes that all functions are address-taken
|
||||
// and all concrete types are put into interfaces, it is sound to run on
|
||||
// partial programs, such as libraries without a main or test function.
|
||||
//
|
||||
package cha
|
||||
|
||||
import (
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
"golang.org/x/tools/go/types"
|
||||
"golang.org/x/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// CallGraph computes the call graph of the specified program using the
|
||||
// Class Hierarchy Analysis algorithm.
|
||||
//
|
||||
func CallGraph(prog *ssa.Program) *callgraph.Graph {
|
||||
cg := callgraph.New(nil) // TODO(adonovan) eliminate concept of rooted callgraph
|
||||
|
||||
allFuncs := ssautil.AllFunctions(prog)
|
||||
|
||||
// funcsBySig contains all functions, keyed by signature. It is
|
||||
// the effective set of address-taken functions used to resolve
|
||||
// a dynamic call of a particular signature.
|
||||
var funcsBySig typeutil.Map // value is []*ssa.Function
|
||||
|
||||
// methodsByName contains all methods,
|
||||
// grouped by name for efficient lookup.
|
||||
methodsByName := make(map[string][]*ssa.Function)
|
||||
|
||||
// methodsMemo records, for every abstract method call call I.f on
|
||||
// interface type I, the set of concrete methods C.f of all
|
||||
// types C that satisfy interface I.
|
||||
methodsMemo := make(map[*types.Func][]*ssa.Function)
|
||||
lookupMethods := func(m *types.Func) []*ssa.Function {
|
||||
methods, ok := methodsMemo[m]
|
||||
if !ok {
|
||||
I := m.Type().(*types.Signature).Recv().Type().Underlying().(*types.Interface)
|
||||
for _, f := range methodsByName[m.Name()] {
|
||||
C := f.Signature.Recv().Type() // named or *named
|
||||
if types.Implements(C, I) {
|
||||
methods = append(methods, f)
|
||||
}
|
||||
}
|
||||
methodsMemo[m] = methods
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
for f := range allFuncs {
|
||||
if f.Signature.Recv() == nil {
|
||||
// Package initializers can never be address-taken.
|
||||
if f.Name() == "init" && f.Synthetic == "package initializer" {
|
||||
continue
|
||||
}
|
||||
funcs, _ := funcsBySig.At(f.Signature).([]*ssa.Function)
|
||||
funcs = append(funcs, f)
|
||||
funcsBySig.Set(f.Signature, funcs)
|
||||
} else {
|
||||
methodsByName[f.Name()] = append(methodsByName[f.Name()], f)
|
||||
}
|
||||
}
|
||||
|
||||
addEdge := func(fnode *callgraph.Node, site ssa.CallInstruction, g *ssa.Function) {
|
||||
gnode := cg.CreateNode(g)
|
||||
callgraph.AddEdge(fnode, site, gnode)
|
||||
}
|
||||
|
||||
addEdges := func(fnode *callgraph.Node, site ssa.CallInstruction, callees []*ssa.Function) {
|
||||
// Because every call to a highly polymorphic and
|
||||
// frequently used abstract method such as
|
||||
// (io.Writer).Write is assumed to call every concrete
|
||||
// Write method in the program, the call graph can
|
||||
// contain a lot of duplication.
|
||||
//
|
||||
// TODO(adonovan): opt: consider factoring the callgraph
|
||||
// API so that the Callers component of each edge is a
|
||||
// slice of nodes, not a singleton.
|
||||
for _, g := range callees {
|
||||
addEdge(fnode, site, g)
|
||||
}
|
||||
}
|
||||
|
||||
for f := range allFuncs {
|
||||
fnode := cg.CreateNode(f)
|
||||
for _, b := range f.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
if site, ok := instr.(ssa.CallInstruction); ok {
|
||||
call := site.Common()
|
||||
if call.IsInvoke() {
|
||||
addEdges(fnode, site, lookupMethods(call.Method))
|
||||
} else if g := call.StaticCallee(); g != nil {
|
||||
addEdge(fnode, site, g)
|
||||
} else if _, ok := call.Value.(*ssa.Builtin); !ok {
|
||||
callees, _ := funcsBySig.At(call.Signature()).([]*ssa.Function)
|
||||
addEdges(fnode, site, callees)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cg
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2014 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 cha_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/callgraph/cha"
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
var inputs = []string{
|
||||
"testdata/func.go",
|
||||
"testdata/iface.go",
|
||||
"testdata/recv.go",
|
||||
}
|
||||
|
||||
func expectation(f *ast.File) (string, token.Pos) {
|
||||
for _, c := range f.Comments {
|
||||
text := strings.TrimSpace(c.Text())
|
||||
if t := strings.TrimPrefix(text, "WANT:\n"); t != text {
|
||||
return t, c.Pos()
|
||||
}
|
||||
}
|
||||
return "", token.NoPos
|
||||
}
|
||||
|
||||
// TestCHA runs CHA on each file in inputs, prints the dynamic edges of
|
||||
// the call graph, and compares it with the golden results embedded in
|
||||
// the WANT comment at the end of the file.
|
||||
//
|
||||
func TestCHA(t *testing.T) {
|
||||
for _, filename := range inputs {
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Errorf("couldn't read file '%s': %s", filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
conf := loader.Config{
|
||||
SourceImports: true,
|
||||
ParserMode: parser.ParseComments,
|
||||
}
|
||||
f, err := conf.ParseFile(filename, content)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
want, pos := expectation(f)
|
||||
if pos == token.NoPos {
|
||||
t.Errorf("No WANT: comment in %s", filename)
|
||||
continue
|
||||
}
|
||||
|
||||
conf.CreateFromFiles("main", f)
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
prog := ssa.Create(iprog, 0)
|
||||
mainPkg := prog.Package(iprog.Created[0].Pkg)
|
||||
prog.BuildAll()
|
||||
|
||||
cg := cha.CallGraph(prog)
|
||||
|
||||
if got := printGraph(cg, mainPkg.Object); got != want {
|
||||
t.Errorf("%s: got:\n%s\nwant:\n%s",
|
||||
prog.Fset.Position(pos), got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printGraph(cg *callgraph.Graph, from *types.Package) string {
|
||||
var edges []string
|
||||
callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error {
|
||||
if strings.Contains(e.Description(), "dynamic") {
|
||||
edges = append(edges, fmt.Sprintf("%s --> %s",
|
||||
e.Caller.Func.RelString(from),
|
||||
e.Callee.Func.RelString(from)))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(edges)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("Dynamic calls\n")
|
||||
for _, edge := range edges {
|
||||
fmt.Fprintf(&buf, " %s\n", edge)
|
||||
}
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//+build ignore
|
||||
|
||||
package main
|
||||
|
||||
// Test of dynamic function calls; no interfaces.
|
||||
|
||||
func A(int) {}
|
||||
|
||||
var (
|
||||
B = func(int) {}
|
||||
C = func(int) {}
|
||||
)
|
||||
|
||||
func f() {
|
||||
pfn := B
|
||||
pfn(0) // calls A, B, C, even though A is not even address-taken
|
||||
}
|
||||
|
||||
// WANT:
|
||||
// Dynamic calls
|
||||
// f --> A
|
||||
// f --> init$1
|
||||
// f --> init$2
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
//+build ignore
|
||||
|
||||
package main
|
||||
|
||||
// Test of interface calls. None of the concrete types are ever
|
||||
// instantiated or converted to interfaces.
|
||||
|
||||
type I interface {
|
||||
f()
|
||||
}
|
||||
|
||||
type J interface {
|
||||
f()
|
||||
g()
|
||||
}
|
||||
|
||||
type C int // implements I
|
||||
|
||||
func (*C) f()
|
||||
|
||||
type D int // implements I and J
|
||||
|
||||
func (*D) f()
|
||||
func (*D) g()
|
||||
|
||||
func one(i I, j J) {
|
||||
i.f() // calls *C and *D
|
||||
}
|
||||
|
||||
func two(i I, j J) {
|
||||
j.f() // calls *D (but not *C, even though it defines method f)
|
||||
}
|
||||
|
||||
func three(i I, j J) {
|
||||
j.g() // calls *D
|
||||
}
|
||||
|
||||
func four(i I, j J) {
|
||||
Jf := J.f
|
||||
if unknown {
|
||||
Jf = nil // suppress SSA constant propagation
|
||||
}
|
||||
Jf(nil) // calls *D
|
||||
}
|
||||
|
||||
func five(i I, j J) {
|
||||
jf := j.f
|
||||
if unknown {
|
||||
jf = nil // suppress SSA constant propagation
|
||||
}
|
||||
jf() // calls *D
|
||||
}
|
||||
|
||||
var unknown bool
|
||||
|
||||
// WANT:
|
||||
// Dynamic calls
|
||||
// (J).f$bound --> (*D).f
|
||||
// (J).f$thunk --> (*D).f
|
||||
// five --> (J).f$bound
|
||||
// four --> (J).f$thunk
|
||||
// one --> (*C).f
|
||||
// one --> (*D).f
|
||||
// three --> (*D).g
|
||||
// two --> (*D).f
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//+build ignore
|
||||
|
||||
package main
|
||||
|
||||
type I interface {
|
||||
f()
|
||||
}
|
||||
|
||||
type J interface {
|
||||
g()
|
||||
}
|
||||
|
||||
type C int // C and *C implement I; *C implements J
|
||||
|
||||
func (C) f()
|
||||
func (*C) g()
|
||||
|
||||
type D int // *D implements I and J
|
||||
|
||||
func (*D) f()
|
||||
func (*D) g()
|
||||
|
||||
func f(i I) {
|
||||
i.f() // calls C, *C, *D
|
||||
}
|
||||
|
||||
func g(j J) {
|
||||
j.g() // calls *C, *D
|
||||
}
|
||||
|
||||
// WANT:
|
||||
// Dynamic calls
|
||||
// f --> (*C).f
|
||||
// f --> (*D).f
|
||||
// f --> (C).f
|
||||
// g --> (*C).g
|
||||
// g --> (*D).g
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Package static computes the call graph of a Go program containing
|
||||
// only static call edges.
|
||||
package static
|
||||
|
||||
import (
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
)
|
||||
|
||||
// CallGraph computes the call graph of the specified program
|
||||
// considering only static calls.
|
||||
//
|
||||
func CallGraph(prog *ssa.Program) *callgraph.Graph {
|
||||
cg := callgraph.New(nil) // TODO(adonovan) eliminate concept of rooted callgraph
|
||||
|
||||
// TODO(adonovan): opt: use only a single pass over the ssa.Program.
|
||||
for f := range ssautil.AllFunctions(prog) {
|
||||
fnode := cg.CreateNode(f)
|
||||
for _, b := range f.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
if site, ok := instr.(ssa.CallInstruction); ok {
|
||||
if g := site.Common().StaticCallee(); g != nil {
|
||||
gnode := cg.CreateNode(g)
|
||||
callgraph.AddEdge(fnode, site, gnode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cg
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2014 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 static_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/callgraph/static"
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
)
|
||||
|
||||
const input = `package P
|
||||
|
||||
type C int
|
||||
func (C) f()
|
||||
|
||||
type I interface{f()}
|
||||
|
||||
func f() {
|
||||
p := func() {}
|
||||
g()
|
||||
p() // SSA constant propagation => static
|
||||
|
||||
if unknown {
|
||||
p = h
|
||||
}
|
||||
p() // dynamic
|
||||
|
||||
C(0).f()
|
||||
}
|
||||
|
||||
func g() {
|
||||
var i I = C(0)
|
||||
i.f()
|
||||
}
|
||||
|
||||
func h()
|
||||
|
||||
var unknown bool
|
||||
`
|
||||
|
||||
func TestStatic(t *testing.T) {
|
||||
conf := loader.Config{ParserMode: parser.ParseComments}
|
||||
f, err := conf.ParseFile("P.go", input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conf.CreateFromFiles("P", f)
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
P := iprog.Created[0].Pkg
|
||||
|
||||
prog := ssa.Create(iprog, 0)
|
||||
prog.BuildAll()
|
||||
|
||||
cg := static.CallGraph(prog)
|
||||
|
||||
var edges []string
|
||||
callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error {
|
||||
edges = append(edges, fmt.Sprintf("%s -> %s",
|
||||
e.Caller.Func.RelString(P),
|
||||
e.Callee.Func.RelString(P)))
|
||||
return nil
|
||||
})
|
||||
sort.Strings(edges)
|
||||
|
||||
want := []string{
|
||||
"(*C).f -> (C).f",
|
||||
"f -> (C).f",
|
||||
"f -> f$1",
|
||||
"f -> g",
|
||||
}
|
||||
if !reflect.DeepEqual(edges, want) {
|
||||
t.Errorf("Got edges %v, want %v", edges, want)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue