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"
|
"text/template"
|
||||||
|
|
||||||
"golang.org/x/tools/go/callgraph"
|
"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/rta"
|
||||||
|
"golang.org/x/tools/go/callgraph/static"
|
||||||
"golang.org/x/tools/go/loader"
|
"golang.org/x/tools/go/loader"
|
||||||
"golang.org/x/tools/go/pointer"
|
"golang.org/x/tools/go/pointer"
|
||||||
"golang.org/x/tools/go/ssa"
|
"golang.org/x/tools/go/ssa"
|
||||||
|
|
@ -51,21 +53,30 @@ const Usage = `callgraph: display the the call graph of a Go program.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
callgraph [-algo=rta|pta] [-test] [-format=...] <args>...
|
callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] <args>...
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
|
||||||
-algo Specifies the call-graph construction algorithm. One of:
|
-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)
|
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.
|
-test Include the package's tests in the analysis.
|
||||||
|
|
||||||
-format Specifies the format in which each call graph edge is displayed.
|
-format Specifies the format in which each call graph edge is displayed.
|
||||||
One of:
|
One of:
|
||||||
"digraph": output suitable for input to
|
|
||||||
|
digraph output suitable for input to
|
||||||
golang.org/x/tools/cmd/digraph.
|
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.
|
All other values are interpreted using text/template syntax.
|
||||||
The default value is:
|
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 := ssa.Create(iprog, 0)
|
||||||
prog.BuildAll()
|
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 ------------------------------------------
|
// -- call graph construction ------------------------------------------
|
||||||
|
|
||||||
var cg *callgraph.Graph
|
var cg *callgraph.Graph
|
||||||
|
|
||||||
switch algo {
|
switch algo {
|
||||||
|
case "static":
|
||||||
|
cg = static.CallGraph(prog)
|
||||||
|
|
||||||
|
case "cha":
|
||||||
|
cg = cha.CallGraph(prog)
|
||||||
|
|
||||||
case "pta":
|
case "pta":
|
||||||
|
main, err := mainPackage(prog, tests)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
config := &pointer.Config{
|
config := &pointer.Config{
|
||||||
Mains: []*ssa.Package{main},
|
Mains: []*ssa.Package{main},
|
||||||
BuildCallGraph: true,
|
BuildCallGraph: true,
|
||||||
|
|
@ -217,6 +206,10 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
|
||||||
cg = ptares.CallGraph
|
cg = ptares.CallGraph
|
||||||
|
|
||||||
case "rta":
|
case "rta":
|
||||||
|
main, err := mainPackage(prog, tests)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
roots := []*ssa.Function{
|
roots := []*ssa.Function{
|
||||||
main.Func("init"),
|
main.Func("init"),
|
||||||
main.Func("main"),
|
main.Func("main"),
|
||||||
|
|
@ -279,6 +272,37 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
|
||||||
return nil
|
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 {
|
type Edge struct {
|
||||||
Caller *ssa.Function
|
Caller *ssa.Function
|
||||||
Callee *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