352 lines
9.4 KiB
Go
352 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.
|
||
|
|
||
|
package analysis
|
||
|
|
||
|
// This file computes the CALLERS and CALLEES relations from the call
|
||
|
// graph. CALLERS/CALLEES information is displayed in the lower pane
|
||
|
// when a "func" token or ast.CallExpr.Lparen is clicked, respectively.
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"go/ast"
|
||
|
"go/token"
|
||
|
"go/types"
|
||
|
"log"
|
||
|
"math/big"
|
||
|
"sort"
|
||
|
|
||
|
"golang.org/x/tools/go/callgraph"
|
||
|
"golang.org/x/tools/go/ssa"
|
||
|
)
|
||
|
|
||
|
// doCallgraph computes the CALLEES and CALLERS relations.
|
||
|
func (a *analysis) doCallgraph(cg *callgraph.Graph) {
|
||
|
log.Print("Deleting synthetic nodes...")
|
||
|
// TODO(adonovan): opt: DeleteSyntheticNodes is asymptotically
|
||
|
// inefficient and can be (unpredictably) slow.
|
||
|
cg.DeleteSyntheticNodes()
|
||
|
log.Print("Synthetic nodes deleted")
|
||
|
|
||
|
// Populate nodes of package call graphs (PCGs).
|
||
|
for _, n := range cg.Nodes {
|
||
|
a.pcgAddNode(n.Func)
|
||
|
}
|
||
|
// Within each PCG, sort funcs by name.
|
||
|
for _, pcg := range a.pcgs {
|
||
|
pcg.sortNodes()
|
||
|
}
|
||
|
|
||
|
calledFuncs := make(map[ssa.CallInstruction]map[*ssa.Function]bool)
|
||
|
callingSites := make(map[*ssa.Function]map[ssa.CallInstruction]bool)
|
||
|
for _, n := range cg.Nodes {
|
||
|
for _, e := range n.Out {
|
||
|
if e.Site == nil {
|
||
|
continue // a call from a synthetic node such as <root>
|
||
|
}
|
||
|
|
||
|
// Add (site pos, callee) to calledFuncs.
|
||
|
// (Dynamic calls only.)
|
||
|
callee := e.Callee.Func
|
||
|
|
||
|
a.pcgAddEdge(n.Func, callee)
|
||
|
|
||
|
if callee.Synthetic != "" {
|
||
|
continue // call of a package initializer
|
||
|
}
|
||
|
|
||
|
if e.Site.Common().StaticCallee() == nil {
|
||
|
// dynamic call
|
||
|
// (CALLEES information for static calls
|
||
|
// is computed using SSA information.)
|
||
|
lparen := e.Site.Common().Pos()
|
||
|
if lparen != token.NoPos {
|
||
|
fns := calledFuncs[e.Site]
|
||
|
if fns == nil {
|
||
|
fns = make(map[*ssa.Function]bool)
|
||
|
calledFuncs[e.Site] = fns
|
||
|
}
|
||
|
fns[callee] = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add (callee, site) to callingSites.
|
||
|
fns := callingSites[callee]
|
||
|
if fns == nil {
|
||
|
fns = make(map[ssa.CallInstruction]bool)
|
||
|
callingSites[callee] = fns
|
||
|
}
|
||
|
fns[e.Site] = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// CALLEES.
|
||
|
log.Print("Callees...")
|
||
|
for site, fns := range calledFuncs {
|
||
|
var funcs funcsByPos
|
||
|
for fn := range fns {
|
||
|
funcs = append(funcs, fn)
|
||
|
}
|
||
|
sort.Sort(funcs)
|
||
|
|
||
|
a.addCallees(site, funcs)
|
||
|
}
|
||
|
|
||
|
// CALLERS
|
||
|
log.Print("Callers...")
|
||
|
for callee, sites := range callingSites {
|
||
|
pos := funcToken(callee)
|
||
|
if pos == token.NoPos {
|
||
|
log.Printf("CALLERS: skipping %s: no pos", callee)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
var this *types.Package // for relativizing names
|
||
|
if callee.Pkg != nil {
|
||
|
this = callee.Pkg.Pkg
|
||
|
}
|
||
|
|
||
|
// Compute sites grouped by parent, with text and URLs.
|
||
|
sitesByParent := make(map[*ssa.Function]sitesByPos)
|
||
|
for site := range sites {
|
||
|
fn := site.Parent()
|
||
|
sitesByParent[fn] = append(sitesByParent[fn], site)
|
||
|
}
|
||
|
var funcs funcsByPos
|
||
|
for fn := range sitesByParent {
|
||
|
funcs = append(funcs, fn)
|
||
|
}
|
||
|
sort.Sort(funcs)
|
||
|
|
||
|
v := callersJSON{
|
||
|
Callee: callee.String(),
|
||
|
Callers: []callerJSON{}, // (JS wants non-nil)
|
||
|
}
|
||
|
for _, fn := range funcs {
|
||
|
caller := callerJSON{
|
||
|
Func: prettyFunc(this, fn),
|
||
|
Sites: []anchorJSON{}, // (JS wants non-nil)
|
||
|
}
|
||
|
sites := sitesByParent[fn]
|
||
|
sort.Sort(sites)
|
||
|
for _, site := range sites {
|
||
|
pos := site.Common().Pos()
|
||
|
if pos != token.NoPos {
|
||
|
caller.Sites = append(caller.Sites, anchorJSON{
|
||
|
Text: fmt.Sprintf("%d", a.prog.Fset.Position(pos).Line),
|
||
|
Href: a.posURL(pos, len("(")),
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
v.Callers = append(v.Callers, caller)
|
||
|
}
|
||
|
|
||
|
fi, offset := a.fileAndOffset(pos)
|
||
|
fi.addLink(aLink{
|
||
|
start: offset,
|
||
|
end: offset + len("func"),
|
||
|
title: fmt.Sprintf("%d callers", len(sites)),
|
||
|
onclick: fmt.Sprintf("onClickCallers(%d)", fi.addData(v)),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// PACKAGE CALLGRAPH
|
||
|
log.Print("Package call graph...")
|
||
|
for pkg, pcg := range a.pcgs {
|
||
|
// Maps (*ssa.Function).RelString() to index in JSON CALLGRAPH array.
|
||
|
index := make(map[string]int)
|
||
|
|
||
|
// Treat exported functions (and exported methods of
|
||
|
// exported named types) as roots even if they aren't
|
||
|
// actually called from outside the package.
|
||
|
for i, n := range pcg.nodes {
|
||
|
if i == 0 || n.fn.Object() == nil || !n.fn.Object().Exported() {
|
||
|
continue
|
||
|
}
|
||
|
recv := n.fn.Signature.Recv()
|
||
|
if recv == nil || deref(recv.Type()).(*types.Named).Obj().Exported() {
|
||
|
roots := &pcg.nodes[0].edges
|
||
|
roots.SetBit(roots, i, 1)
|
||
|
}
|
||
|
index[n.fn.RelString(pkg.Pkg)] = i
|
||
|
}
|
||
|
|
||
|
json := a.pcgJSON(pcg)
|
||
|
|
||
|
// TODO(adonovan): pkg.Path() is not unique!
|
||
|
// It is possible to declare a non-test package called x_test.
|
||
|
a.result.pkgInfo(pkg.Pkg.Path()).setCallGraph(json, index)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// addCallees adds client data and links for the facts that site calls fns.
|
||
|
func (a *analysis) addCallees(site ssa.CallInstruction, fns []*ssa.Function) {
|
||
|
v := calleesJSON{
|
||
|
Descr: site.Common().Description(),
|
||
|
Callees: []anchorJSON{}, // (JS wants non-nil)
|
||
|
}
|
||
|
var this *types.Package // for relativizing names
|
||
|
if p := site.Parent().Package(); p != nil {
|
||
|
this = p.Pkg
|
||
|
}
|
||
|
|
||
|
for _, fn := range fns {
|
||
|
v.Callees = append(v.Callees, anchorJSON{
|
||
|
Text: prettyFunc(this, fn),
|
||
|
Href: a.posURL(funcToken(fn), len("func")),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
fi, offset := a.fileAndOffset(site.Common().Pos())
|
||
|
fi.addLink(aLink{
|
||
|
start: offset,
|
||
|
end: offset + len("("),
|
||
|
title: fmt.Sprintf("%d callees", len(v.Callees)),
|
||
|
onclick: fmt.Sprintf("onClickCallees(%d)", fi.addData(v)),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// -- utilities --------------------------------------------------------
|
||
|
|
||
|
// stable order within packages but undefined across packages.
|
||
|
type funcsByPos []*ssa.Function
|
||
|
|
||
|
func (a funcsByPos) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() }
|
||
|
func (a funcsByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||
|
func (a funcsByPos) Len() int { return len(a) }
|
||
|
|
||
|
type sitesByPos []ssa.CallInstruction
|
||
|
|
||
|
func (a sitesByPos) Less(i, j int) bool { return a[i].Common().Pos() < a[j].Common().Pos() }
|
||
|
func (a sitesByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||
|
func (a sitesByPos) Len() int { return len(a) }
|
||
|
|
||
|
func funcToken(fn *ssa.Function) token.Pos {
|
||
|
switch syntax := fn.Syntax().(type) {
|
||
|
case *ast.FuncLit:
|
||
|
return syntax.Type.Func
|
||
|
case *ast.FuncDecl:
|
||
|
return syntax.Type.Func
|
||
|
}
|
||
|
return token.NoPos
|
||
|
}
|
||
|
|
||
|
// prettyFunc pretty-prints fn for the user interface.
|
||
|
// TODO(adonovan): return HTML so we have more markup freedom.
|
||
|
func prettyFunc(this *types.Package, fn *ssa.Function) string {
|
||
|
if fn.Parent() != nil {
|
||
|
return fmt.Sprintf("%s in %s",
|
||
|
types.TypeString(fn.Signature, types.RelativeTo(this)),
|
||
|
prettyFunc(this, fn.Parent()))
|
||
|
}
|
||
|
if fn.Synthetic != "" && fn.Name() == "init" {
|
||
|
// (This is the actual initializer, not a declared 'func init').
|
||
|
if fn.Pkg.Pkg == this {
|
||
|
return "package initializer"
|
||
|
}
|
||
|
return fmt.Sprintf("%q package initializer", fn.Pkg.Pkg.Path())
|
||
|
}
|
||
|
return fn.RelString(this)
|
||
|
}
|
||
|
|
||
|
// -- intra-package callgraph ------------------------------------------
|
||
|
|
||
|
// pcgNode represents a node in the package call graph (PCG).
|
||
|
type pcgNode struct {
|
||
|
fn *ssa.Function
|
||
|
pretty string // cache of prettyFunc(fn)
|
||
|
edges big.Int // set of callee func indices
|
||
|
}
|
||
|
|
||
|
// A packageCallGraph represents the intra-package edges of the global call graph.
|
||
|
// The zeroth node indicates "all external functions".
|
||
|
type packageCallGraph struct {
|
||
|
nodeIndex map[*ssa.Function]int // maps func to node index (a small int)
|
||
|
nodes []*pcgNode // maps node index to node
|
||
|
}
|
||
|
|
||
|
// sortNodes populates pcg.nodes in name order and updates the nodeIndex.
|
||
|
func (pcg *packageCallGraph) sortNodes() {
|
||
|
nodes := make([]*pcgNode, 0, len(pcg.nodeIndex))
|
||
|
nodes = append(nodes, &pcgNode{fn: nil, pretty: "<external>"})
|
||
|
for fn := range pcg.nodeIndex {
|
||
|
nodes = append(nodes, &pcgNode{
|
||
|
fn: fn,
|
||
|
pretty: prettyFunc(fn.Pkg.Pkg, fn),
|
||
|
})
|
||
|
}
|
||
|
sort.Sort(pcgNodesByPretty(nodes[1:]))
|
||
|
for i, n := range nodes {
|
||
|
pcg.nodeIndex[n.fn] = i
|
||
|
}
|
||
|
pcg.nodes = nodes
|
||
|
}
|
||
|
|
||
|
func (pcg *packageCallGraph) addEdge(caller, callee *ssa.Function) {
|
||
|
var callerIndex int
|
||
|
if caller.Pkg == callee.Pkg {
|
||
|
// intra-package edge
|
||
|
callerIndex = pcg.nodeIndex[caller]
|
||
|
if callerIndex < 1 {
|
||
|
panic(caller)
|
||
|
}
|
||
|
}
|
||
|
edges := &pcg.nodes[callerIndex].edges
|
||
|
edges.SetBit(edges, pcg.nodeIndex[callee], 1)
|
||
|
}
|
||
|
|
||
|
func (a *analysis) pcgAddNode(fn *ssa.Function) {
|
||
|
if fn.Pkg == nil {
|
||
|
return
|
||
|
}
|
||
|
pcg, ok := a.pcgs[fn.Pkg]
|
||
|
if !ok {
|
||
|
pcg = &packageCallGraph{nodeIndex: make(map[*ssa.Function]int)}
|
||
|
a.pcgs[fn.Pkg] = pcg
|
||
|
}
|
||
|
pcg.nodeIndex[fn] = -1
|
||
|
}
|
||
|
|
||
|
func (a *analysis) pcgAddEdge(caller, callee *ssa.Function) {
|
||
|
if callee.Pkg != nil {
|
||
|
a.pcgs[callee.Pkg].addEdge(caller, callee)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// pcgJSON returns a new slice of callgraph JSON values.
|
||
|
func (a *analysis) pcgJSON(pcg *packageCallGraph) []*PCGNodeJSON {
|
||
|
var nodes []*PCGNodeJSON
|
||
|
for _, n := range pcg.nodes {
|
||
|
|
||
|
// TODO(adonovan): why is there no good way to iterate
|
||
|
// over the set bits of a big.Int?
|
||
|
var callees []int
|
||
|
nbits := n.edges.BitLen()
|
||
|
for j := 0; j < nbits; j++ {
|
||
|
if n.edges.Bit(j) == 1 {
|
||
|
callees = append(callees, j)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var pos token.Pos
|
||
|
if n.fn != nil {
|
||
|
pos = funcToken(n.fn)
|
||
|
}
|
||
|
nodes = append(nodes, &PCGNodeJSON{
|
||
|
Func: anchorJSON{
|
||
|
Text: n.pretty,
|
||
|
Href: a.posURL(pos, len("func")),
|
||
|
},
|
||
|
Callees: callees,
|
||
|
})
|
||
|
}
|
||
|
return nodes
|
||
|
}
|
||
|
|
||
|
type pcgNodesByPretty []*pcgNode
|
||
|
|
||
|
func (a pcgNodesByPretty) Less(i, j int) bool { return a[i].pretty < a[j].pretty }
|
||
|
func (a pcgNodesByPretty) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||
|
func (a pcgNodesByPretty) Len() int { return len(a) }
|