mirror of
https://github.com/golang/go
synced 2024-11-18 16:14:46 -07:00
aa46a01996
In go1.10, go/packages falls back to loading all packages from source but not typechecking function bodies for imports. The ssautil.Packages function would nonetheless provide the partially-typed ASTs to the SSA builder, which would crash. Now Packages only passes syntax trees to the SSA builder for the initial packages, which are the only ones guaranteed to be fully typed. It is impossible to discern whether the caller of Packages intends to build SSA code for dependencies, as in some clients such as cmd/callgraph, so we add a new function, AllPackages, that expresses this intent. Fixes golang/go#28106 Change-Id: I6a88b7c7545e9de90b61f5bee0e6de3d2e21b548 Reviewed-on: https://go-review.googlesource.com/c/141686 Reviewed-by: Michael Matloob <matloob@golang.org> Run-TryBot: Michael Matloob <matloob@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
350 lines
9.4 KiB
Go
350 lines
9.4 KiB
Go
// 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.
|
|
|
|
// callgraph: a tool for reporting the call graph of a Go program.
|
|
// See Usage for details, or run with -help.
|
|
package main // import "golang.org/x/tools/cmd/callgraph"
|
|
|
|
// TODO(adonovan):
|
|
//
|
|
// Features:
|
|
// - restrict graph to a single package
|
|
// - output
|
|
// - functions reachable from root (use digraph tool?)
|
|
// - unreachable functions (use digraph tool?)
|
|
// - dynamic (runtime) types
|
|
// - indexed output (numbered nodes)
|
|
// - JSON output
|
|
// - additional template fields:
|
|
// callee file/line/col
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/build"
|
|
"go/token"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"runtime"
|
|
"text/template"
|
|
|
|
"golang.org/x/tools/go/buildutil"
|
|
"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/packages"
|
|
"golang.org/x/tools/go/pointer"
|
|
"golang.org/x/tools/go/ssa"
|
|
"golang.org/x/tools/go/ssa/ssautil"
|
|
)
|
|
|
|
// flags
|
|
var (
|
|
algoFlag = flag.String("algo", "rta",
|
|
`Call graph construction algorithm (static, cha, rta, pta)`)
|
|
|
|
testFlag = flag.Bool("test", false,
|
|
"Loads test code (*_test.go) for imported packages")
|
|
|
|
formatFlag = flag.String("format",
|
|
"{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}",
|
|
"A template expression specifying how to format an edge")
|
|
|
|
ptalogFlag = flag.String("ptalog", "",
|
|
"Location of the points-to analysis log file, or empty to disable logging.")
|
|
)
|
|
|
|
func init() {
|
|
flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
|
}
|
|
|
|
const Usage = `callgraph: display the the call graph of a Go program.
|
|
|
|
Usage:
|
|
|
|
callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] package...
|
|
|
|
Flags:
|
|
|
|
-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
|
|
golang.org/x/tools/cmd/digraph.
|
|
graphviz output in AT&T GraphViz (.dot) format.
|
|
|
|
All other values are interpreted using text/template syntax.
|
|
The default value is:
|
|
|
|
{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}
|
|
|
|
The structure passed to the template is (effectively):
|
|
|
|
type Edge struct {
|
|
Caller *ssa.Function // calling function
|
|
Callee *ssa.Function // called function
|
|
|
|
// Call site:
|
|
Filename string // containing file
|
|
Offset int // offset within file of '('
|
|
Line int // line number
|
|
Column int // column number of call
|
|
Dynamic string // "static" or "dynamic"
|
|
Description string // e.g. "static method call"
|
|
}
|
|
|
|
Caller and Callee are *ssa.Function values, which print as
|
|
"(*sync/atomic.Mutex).Lock", but other attributes may be
|
|
derived from them, e.g. Caller.Pkg.Pkg.Path yields the
|
|
import path of the enclosing package. Consult the go/ssa
|
|
API documentation for details.
|
|
|
|
Examples:
|
|
|
|
Show the call graph of the trivial web server application:
|
|
|
|
callgraph -format digraph $GOROOT/src/net/http/triv.go
|
|
|
|
Same, but show only the packages of each function:
|
|
|
|
callgraph -format '{{.Caller.Pkg.Pkg.Path}} -> {{.Callee.Pkg.Pkg.Path}}' \
|
|
$GOROOT/src/net/http/triv.go | sort | uniq
|
|
|
|
Show functions that make dynamic calls into the 'fmt' test package,
|
|
using the pointer analysis algorithm:
|
|
|
|
callgraph -format='{{.Caller}} -{{.Dynamic}}-> {{.Callee}}' -test -algo=pta fmt |
|
|
sed -ne 's/-dynamic-/--/p' |
|
|
sed -ne 's/-->.*fmt_test.*$//p' | sort | uniq
|
|
|
|
Show all functions directly called by the callgraph tool's main function:
|
|
|
|
callgraph -format=digraph golang.org/x/tools/cmd/callgraph |
|
|
digraph succs golang.org/x/tools/cmd/callgraph.main
|
|
`
|
|
|
|
func init() {
|
|
// If $GOMAXPROCS isn't set, use the full capacity of the machine.
|
|
// For small machines, use at least 4 threads.
|
|
if os.Getenv("GOMAXPROCS") == "" {
|
|
n := runtime.NumCPU()
|
|
if n < 4 {
|
|
n = 4
|
|
}
|
|
runtime.GOMAXPROCS(n)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
if err := doCallgraph("", "", *algoFlag, *formatFlag, *testFlag, flag.Args()); err != nil {
|
|
fmt.Fprintf(os.Stderr, "callgraph: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
var stdout io.Writer = os.Stdout
|
|
|
|
func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) error {
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(os.Stderr, Usage)
|
|
return nil
|
|
}
|
|
|
|
cfg := &packages.Config{
|
|
Mode: packages.LoadAllSyntax,
|
|
Tests: tests,
|
|
Dir: dir,
|
|
}
|
|
if gopath != "" {
|
|
cfg.Env = append(os.Environ(), "GOPATH="+gopath) // to enable testing
|
|
}
|
|
initial, err := packages.Load(cfg, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if packages.PrintErrors(initial) > 0 {
|
|
return fmt.Errorf("packages contain errors")
|
|
}
|
|
|
|
// Create and build SSA-form program representation.
|
|
prog, pkgs := ssautil.AllPackages(initial, 0)
|
|
prog.Build()
|
|
|
|
// -- call graph construction ------------------------------------------
|
|
|
|
var cg *callgraph.Graph
|
|
|
|
switch algo {
|
|
case "static":
|
|
cg = static.CallGraph(prog)
|
|
|
|
case "cha":
|
|
cg = cha.CallGraph(prog)
|
|
|
|
case "pta":
|
|
// Set up points-to analysis log file.
|
|
var ptalog io.Writer
|
|
if *ptalogFlag != "" {
|
|
if f, err := os.Create(*ptalogFlag); err != nil {
|
|
log.Fatalf("Failed to create PTA log file: %s", err)
|
|
} else {
|
|
buf := bufio.NewWriter(f)
|
|
ptalog = buf
|
|
defer func() {
|
|
if err := buf.Flush(); err != nil {
|
|
log.Printf("flush: %s", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
log.Printf("close: %s", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
mains, err := mainPackages(pkgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config := &pointer.Config{
|
|
Mains: mains,
|
|
BuildCallGraph: true,
|
|
Log: ptalog,
|
|
}
|
|
ptares, err := pointer.Analyze(config)
|
|
if err != nil {
|
|
return err // internal error in pointer analysis
|
|
}
|
|
cg = ptares.CallGraph
|
|
|
|
case "rta":
|
|
mains, err := mainPackages(pkgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var roots []*ssa.Function
|
|
for _, main := range mains {
|
|
roots = append(roots, main.Func("init"), main.Func("main"))
|
|
}
|
|
rtares := rta.Analyze(roots, true)
|
|
cg = rtares.CallGraph
|
|
|
|
// NB: RTA gives us Reachable and RuntimeTypes too.
|
|
|
|
default:
|
|
return fmt.Errorf("unknown algorithm: %s", algo)
|
|
}
|
|
|
|
cg.DeleteSyntheticNodes()
|
|
|
|
// -- output------------------------------------------------------------
|
|
|
|
var before, after string
|
|
|
|
// Pre-canned formats.
|
|
switch format {
|
|
case "digraph":
|
|
format = `{{printf "%q %q" .Caller .Callee}}`
|
|
|
|
case "graphviz":
|
|
before = "digraph callgraph {\n"
|
|
after = "}\n"
|
|
format = ` {{printf "%q" .Caller}} -> {{printf "%q" .Callee}}`
|
|
}
|
|
|
|
tmpl, err := template.New("-format").Parse(format)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid -format template: %v", err)
|
|
}
|
|
|
|
// Allocate these once, outside the traversal.
|
|
var buf bytes.Buffer
|
|
data := Edge{fset: prog.Fset}
|
|
|
|
fmt.Fprint(stdout, before)
|
|
if err := callgraph.GraphVisitEdges(cg, func(edge *callgraph.Edge) error {
|
|
data.position.Offset = -1
|
|
data.edge = edge
|
|
data.Caller = edge.Caller.Func
|
|
data.Callee = edge.Callee.Func
|
|
|
|
buf.Reset()
|
|
if err := tmpl.Execute(&buf, &data); err != nil {
|
|
return err
|
|
}
|
|
stdout.Write(buf.Bytes())
|
|
if len := buf.Len(); len == 0 || buf.Bytes()[len-1] != '\n' {
|
|
fmt.Fprintln(stdout)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprint(stdout, after)
|
|
return nil
|
|
}
|
|
|
|
// mainPackages returns the main packages to analyze.
|
|
// Each resulting package is named "main" and has a main function.
|
|
func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) {
|
|
var mains []*ssa.Package
|
|
for _, p := range pkgs {
|
|
if p != nil && p.Pkg.Name() == "main" && p.Func("main") != nil {
|
|
mains = append(mains, p)
|
|
}
|
|
}
|
|
if len(mains) == 0 {
|
|
return nil, fmt.Errorf("no main packages")
|
|
}
|
|
return mains, nil
|
|
}
|
|
|
|
type Edge struct {
|
|
Caller *ssa.Function
|
|
Callee *ssa.Function
|
|
|
|
edge *callgraph.Edge
|
|
fset *token.FileSet
|
|
position token.Position // initialized lazily
|
|
}
|
|
|
|
func (e *Edge) pos() *token.Position {
|
|
if e.position.Offset == -1 {
|
|
e.position = e.fset.Position(e.edge.Pos()) // called lazily
|
|
}
|
|
return &e.position
|
|
}
|
|
|
|
func (e *Edge) Filename() string { return e.pos().Filename }
|
|
func (e *Edge) Column() int { return e.pos().Column }
|
|
func (e *Edge) Line() int { return e.pos().Line }
|
|
func (e *Edge) Offset() int { return e.pos().Offset }
|
|
|
|
func (e *Edge) Dynamic() string {
|
|
if e.edge.Site != nil && e.edge.Site.Common().StaticCallee() == nil {
|
|
return "dynamic"
|
|
}
|
|
return "static"
|
|
}
|
|
|
|
func (e *Edge) Description() string { return e.edge.Description() }
|