diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go index 0c381b91..7f77dd13 100644 --- a/cmd/callgraph/main.go +++ b/cmd/callgraph/main.go @@ -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=...] ... + callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] ... 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 diff --git a/go/callgraph/cha/cha.go b/go/callgraph/cha/cha.go new file mode 100644 index 00000000..2fbc72e0 --- /dev/null +++ b/go/callgraph/cha/cha.go @@ -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 +} diff --git a/go/callgraph/cha/cha_test.go b/go/callgraph/cha/cha_test.go new file mode 100644 index 00000000..3648f4dc --- /dev/null +++ b/go/callgraph/cha/cha_test.go @@ -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()) +} diff --git a/go/callgraph/cha/testdata/func.go b/go/callgraph/cha/testdata/func.go new file mode 100644 index 00000000..ad483f10 --- /dev/null +++ b/go/callgraph/cha/testdata/func.go @@ -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 diff --git a/go/callgraph/cha/testdata/iface.go b/go/callgraph/cha/testdata/iface.go new file mode 100644 index 00000000..1622ec15 --- /dev/null +++ b/go/callgraph/cha/testdata/iface.go @@ -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 diff --git a/go/callgraph/cha/testdata/recv.go b/go/callgraph/cha/testdata/recv.go new file mode 100644 index 00000000..5ba48e93 --- /dev/null +++ b/go/callgraph/cha/testdata/recv.go @@ -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 diff --git a/go/callgraph/static/static.go b/go/callgraph/static/static.go new file mode 100644 index 00000000..f787fffa --- /dev/null +++ b/go/callgraph/static/static.go @@ -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 +} diff --git a/go/callgraph/static/static_test.go b/go/callgraph/static/static_test.go new file mode 100644 index 00000000..5a74ca1a --- /dev/null +++ b/go/callgraph/static/static_test.go @@ -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) + } +}