623 lines
18 KiB
Go
623 lines
18 KiB
Go
|
|
// Copyright 2023 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 main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
_ "embed"
|
||
|
|
"encoding/json"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"go/ast"
|
||
|
|
"go/token"
|
||
|
|
"go/types"
|
||
|
|
"io"
|
||
|
|
"log"
|
||
|
|
"maps"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"regexp"
|
||
|
|
"runtime"
|
||
|
|
"runtime/pprof"
|
||
|
|
"slices"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"text/template"
|
||
|
|
|
||
|
|
"golang.org/x/telemetry"
|
||
|
|
"golang.org/x/tools/go/callgraph"
|
||
|
|
"golang.org/x/tools/go/callgraph/rta"
|
||
|
|
"golang.org/x/tools/go/packages"
|
||
|
|
"golang.org/x/tools/go/ssa"
|
||
|
|
"golang.org/x/tools/go/ssa/ssautil"
|
||
|
|
"golang.org/x/tools/internal/typesinternal"
|
||
|
|
)
|
||
|
|
|
||
|
|
//go:embed doc.go
|
||
|
|
var doc string
|
||
|
|
|
||
|
|
// flags
|
||
|
|
var (
|
||
|
|
testFlag = flag.Bool("test", false, "include implicit test packages and executables")
|
||
|
|
tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)")
|
||
|
|
|
||
|
|
filterFlag = flag.String("filter", "<module>", "report only packages matching this regular expression (default: module of first package)")
|
||
|
|
generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files")
|
||
|
|
whyLiveFlag = flag.String("whylive", "", "show a path from main to the named function")
|
||
|
|
formatFlag = flag.String("f", "", "format output records using template")
|
||
|
|
jsonFlag = flag.Bool("json", false, "output JSON records")
|
||
|
|
cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file")
|
||
|
|
memProfile = flag.String("memprofile", "", "write memory profile to this file")
|
||
|
|
)
|
||
|
|
|
||
|
|
func usage() {
|
||
|
|
// Extract the content of the /* ... */ comment in doc.go.
|
||
|
|
_, after, _ := strings.Cut(doc, "/*\n")
|
||
|
|
doc, _, _ := strings.Cut(after, "*/")
|
||
|
|
io.WriteString(flag.CommandLine.Output(), doc+`
|
||
|
|
Flags:
|
||
|
|
|
||
|
|
`)
|
||
|
|
flag.PrintDefaults()
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
telemetry.Start(telemetry.Config{ReportCrashes: true})
|
||
|
|
|
||
|
|
log.SetPrefix("deadcode: ")
|
||
|
|
log.SetFlags(0) // no time prefix
|
||
|
|
|
||
|
|
flag.Usage = usage
|
||
|
|
flag.Parse()
|
||
|
|
if len(flag.Args()) == 0 {
|
||
|
|
usage()
|
||
|
|
os.Exit(2)
|
||
|
|
}
|
||
|
|
|
||
|
|
if *cpuProfile != "" {
|
||
|
|
f, err := os.Create(*cpuProfile)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := pprof.StartCPUProfile(f); err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
// NB: profile won't be written in case of error.
|
||
|
|
defer pprof.StopCPUProfile()
|
||
|
|
}
|
||
|
|
|
||
|
|
if *memProfile != "" {
|
||
|
|
f, err := os.Create(*memProfile)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
// NB: profile won't be written in case of error.
|
||
|
|
defer func() {
|
||
|
|
runtime.GC() // get up-to-date statistics
|
||
|
|
if err := pprof.WriteHeapProfile(f); err != nil {
|
||
|
|
log.Fatalf("Writing memory profile: %v", err)
|
||
|
|
}
|
||
|
|
f.Close()
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reject bad output options early.
|
||
|
|
if *formatFlag != "" {
|
||
|
|
if *jsonFlag {
|
||
|
|
log.Fatalf("you cannot specify both -f=template and -json")
|
||
|
|
}
|
||
|
|
if _, err := template.New("deadcode").Parse(*formatFlag); err != nil {
|
||
|
|
log.Fatalf("invalid -f: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load, parse, and type-check the complete program(s).
|
||
|
|
cfg := &packages.Config{
|
||
|
|
BuildFlags: []string{"-tags=" + *tagsFlag},
|
||
|
|
Mode: packages.LoadAllSyntax | packages.NeedModule,
|
||
|
|
Tests: *testFlag,
|
||
|
|
}
|
||
|
|
initial, err := packages.Load(cfg, flag.Args()...)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("Load: %v", err)
|
||
|
|
}
|
||
|
|
if len(initial) == 0 {
|
||
|
|
log.Fatalf("no packages")
|
||
|
|
}
|
||
|
|
if packages.PrintErrors(initial) > 0 {
|
||
|
|
log.Fatalf("packages contain errors")
|
||
|
|
}
|
||
|
|
|
||
|
|
// If -filter is unset, use first module (if available).
|
||
|
|
if *filterFlag == "<module>" {
|
||
|
|
seen := make(map[string]bool)
|
||
|
|
var patterns []string
|
||
|
|
for _, pkg := range initial {
|
||
|
|
if pkg.Module != nil && pkg.Module.Path != "" && !seen[pkg.Module.Path] {
|
||
|
|
seen[pkg.Module.Path] = true
|
||
|
|
patterns = append(patterns, regexp.QuoteMeta(pkg.Module.Path))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if patterns != nil {
|
||
|
|
*filterFlag = "^(" + strings.Join(patterns, "|") + ")\\b"
|
||
|
|
} else {
|
||
|
|
*filterFlag = "" // match any
|
||
|
|
}
|
||
|
|
}
|
||
|
|
filter, err := regexp.Compile(*filterFlag)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("-filter: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create SSA-form program representation
|
||
|
|
// and find main packages.
|
||
|
|
prog, pkgs := ssautil.AllPackages(initial, ssa.InstantiateGenerics)
|
||
|
|
prog.Build()
|
||
|
|
|
||
|
|
mains := ssautil.MainPackages(pkgs)
|
||
|
|
if len(mains) == 0 {
|
||
|
|
log.Fatalf("no main packages")
|
||
|
|
}
|
||
|
|
var roots []*ssa.Function
|
||
|
|
for _, main := range mains {
|
||
|
|
roots = append(roots, main.Func("init"), main.Func("main"))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Gather all source-level functions,
|
||
|
|
// as the user interface is expressed in terms of them.
|
||
|
|
//
|
||
|
|
// We ignore synthetic wrappers, and nested functions. Literal
|
||
|
|
// functions passed as arguments to other functions are of
|
||
|
|
// course address-taken and there exists a dynamic call of
|
||
|
|
// that signature, so when they are unreachable, it is
|
||
|
|
// invariably because the parent is unreachable.
|
||
|
|
var (
|
||
|
|
sourceFuncs []*ssa.Function
|
||
|
|
generated = make(map[string]bool)
|
||
|
|
interfaceTypes = make(map[*types.Package][]*types.Interface)
|
||
|
|
)
|
||
|
|
packages.Visit(initial, nil, func(p *packages.Package) {
|
||
|
|
// Collect interfaces by package for marker method identification.
|
||
|
|
var interfaces []*types.Interface
|
||
|
|
scope := p.Types.Scope()
|
||
|
|
for _, name := range scope.Names() {
|
||
|
|
if typeName, ok := scope.Lookup(name).(*types.TypeName); ok &&
|
||
|
|
types.IsInterface(typeName.Type()) {
|
||
|
|
interfaces = append(interfaces, typeName.Type().Underlying().(*types.Interface))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
interfaceTypes[p.Types] = interfaces
|
||
|
|
|
||
|
|
for _, file := range p.Syntax {
|
||
|
|
for _, decl := range file.Decls {
|
||
|
|
if decl, ok := decl.(*ast.FuncDecl); ok {
|
||
|
|
obj := p.TypesInfo.Defs[decl.Name].(*types.Func)
|
||
|
|
fn := prog.FuncValue(obj)
|
||
|
|
sourceFuncs = append(sourceFuncs, fn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ast.IsGenerated(file) {
|
||
|
|
generated[p.Fset.File(file.Pos()).Name()] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Compute the reachabilty from main.
|
||
|
|
// (Build a call graph only for -whylive.)
|
||
|
|
res := rta.Analyze(roots, *whyLiveFlag != "")
|
||
|
|
|
||
|
|
// Subtle: the -test flag causes us to analyze test variants
|
||
|
|
// such as "package p as compiled for p.test" or even "for q.test".
|
||
|
|
// This leads to multiple distinct ssa.Function instances that
|
||
|
|
// represent the same source declaration, and it is essentially
|
||
|
|
// impossible to discover this from the SSA representation
|
||
|
|
// (since it has lost the connection to go/packages.Package.ID).
|
||
|
|
//
|
||
|
|
// So, we de-duplicate such variants by position:
|
||
|
|
// if any one of them is live, we consider all of them live.
|
||
|
|
// (We use Position not Pos to avoid assuming that files common
|
||
|
|
// to packages "p" and "p [p.test]" were parsed only once.)
|
||
|
|
reachablePosn := make(map[token.Position]bool)
|
||
|
|
for fn := range res.Reachable {
|
||
|
|
if fn.Pos().IsValid() || fn.Name() == "init" {
|
||
|
|
reachablePosn[prog.Fset.Position(fn.Pos())] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The -whylive=fn flag causes deadcode to explain why a function
|
||
|
|
// is not dead, by showing a path to it from some root.
|
||
|
|
if *whyLiveFlag != "" {
|
||
|
|
targets := make(map[*ssa.Function]bool)
|
||
|
|
for _, fn := range sourceFuncs {
|
||
|
|
if prettyName(fn, true) == *whyLiveFlag {
|
||
|
|
targets[fn] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(targets) == 0 {
|
||
|
|
// Function is not part of the program.
|
||
|
|
//
|
||
|
|
// TODO(adonovan): improve the UX here in case
|
||
|
|
// of spelling or syntax mistakes. Some ideas:
|
||
|
|
// - a cmd/callgraph command to enumerate
|
||
|
|
// available functions.
|
||
|
|
// - a deadcode -live flag to compute the complement.
|
||
|
|
// - a syntax hint: example.com/pkg.Func or (example.com/pkg.Type).Method
|
||
|
|
// - report the element of AllFunctions with the smallest
|
||
|
|
// Levenshtein distance from *whyLiveFlag.
|
||
|
|
// - permit -whylive=regexp. But beware of spurious
|
||
|
|
// matches (e.g. fmt.Print matches fmt.Println)
|
||
|
|
// and the annoyance of having to quote parens (*T).f.
|
||
|
|
log.Fatalf("function %q not found in program", *whyLiveFlag)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Opt: remove the unreachable ones.
|
||
|
|
for fn := range targets {
|
||
|
|
if !reachablePosn[prog.Fset.Position(fn.Pos())] {
|
||
|
|
delete(targets, fn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(targets) == 0 {
|
||
|
|
log.Fatalf("function %s is dead code", *whyLiveFlag)
|
||
|
|
}
|
||
|
|
|
||
|
|
res.CallGraph.DeleteSyntheticNodes() // inline synthetic wrappers (except inits)
|
||
|
|
root, path := pathSearch(roots, res, targets)
|
||
|
|
if root == nil {
|
||
|
|
// RTA doesn't add callgraph edges for reflective calls.
|
||
|
|
log.Fatalf("%s is reachable only through reflection", *whyLiveFlag)
|
||
|
|
}
|
||
|
|
if len(path) == 0 {
|
||
|
|
// No edges => one of the targets is a root.
|
||
|
|
// Rather than (confusingly) print nothing, make this an error.
|
||
|
|
log.Fatalf("%s is a root", root.Func)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build a list of jsonEdge records
|
||
|
|
// to print as -json or -f=template.
|
||
|
|
var edges []any
|
||
|
|
for _, edge := range path {
|
||
|
|
edges = append(edges, jsonEdge{
|
||
|
|
Initial: cond(len(edges) == 0, prettyName(edge.Caller.Func, true), ""),
|
||
|
|
Kind: cond(isStaticCall(edge), "static", "dynamic"),
|
||
|
|
Position: toJSONPosition(prog.Fset.Position(edge.Pos())),
|
||
|
|
Callee: prettyName(edge.Callee.Func, true),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
format := `{{if .Initial}}{{printf "%19s%s\n" "" .Initial}}{{end}}{{printf "%8s@L%.4d --> %s" .Kind .Position.Line .Callee}}`
|
||
|
|
if *formatFlag != "" {
|
||
|
|
format = *formatFlag
|
||
|
|
}
|
||
|
|
printObjects(format, edges)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Group unreachable functions by package path.
|
||
|
|
byPkgPath := make(map[string]map[*ssa.Function]bool)
|
||
|
|
for _, fn := range sourceFuncs {
|
||
|
|
posn := prog.Fset.Position(fn.Pos())
|
||
|
|
|
||
|
|
if !reachablePosn[posn] {
|
||
|
|
reachablePosn[posn] = true // suppress dups with same pos
|
||
|
|
|
||
|
|
pkgpath := fn.Pkg.Pkg.Path()
|
||
|
|
m, ok := byPkgPath[pkgpath]
|
||
|
|
if !ok {
|
||
|
|
m = make(map[*ssa.Function]bool)
|
||
|
|
byPkgPath[pkgpath] = m
|
||
|
|
}
|
||
|
|
m[fn] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build array of jsonPackage objects.
|
||
|
|
var packages []any
|
||
|
|
for _, pkgpath := range slices.Sorted(maps.Keys(byPkgPath)) {
|
||
|
|
if !filter.MatchString(pkgpath) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
m := byPkgPath[pkgpath]
|
||
|
|
|
||
|
|
// Print functions that appear within the same file in
|
||
|
|
// declaration order. This tends to keep related
|
||
|
|
// methods such as (T).Marshal and (*T).Unmarshal
|
||
|
|
// together better than sorting.
|
||
|
|
fns := slices.Collect(maps.Keys(m))
|
||
|
|
sort.Slice(fns, func(i, j int) bool {
|
||
|
|
xposn := prog.Fset.Position(fns[i].Pos())
|
||
|
|
yposn := prog.Fset.Position(fns[j].Pos())
|
||
|
|
if xposn.Filename != yposn.Filename {
|
||
|
|
return xposn.Filename < yposn.Filename
|
||
|
|
}
|
||
|
|
return xposn.Line < yposn.Line
|
||
|
|
})
|
||
|
|
|
||
|
|
var functions []jsonFunction
|
||
|
|
for _, fn := range fns {
|
||
|
|
posn := prog.Fset.Position(fn.Pos())
|
||
|
|
|
||
|
|
// Without -generated, skip functions declared in
|
||
|
|
// generated Go files.
|
||
|
|
// (Functions called by them may still be reported.)
|
||
|
|
gen := generated[posn.Filename]
|
||
|
|
if gen && !*generatedFlag {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Marker methods should not be reported
|
||
|
|
marker := isMarkerMethod(fn, interfaceTypes[fn.Pkg.Pkg])
|
||
|
|
if marker {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
functions = append(functions, jsonFunction{
|
||
|
|
Name: prettyName(fn, false),
|
||
|
|
Position: toJSONPosition(posn),
|
||
|
|
Generated: gen,
|
||
|
|
Marker: marker,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
if len(functions) > 0 {
|
||
|
|
packages = append(packages, jsonPackage{
|
||
|
|
Name: fns[0].Pkg.Pkg.Name(),
|
||
|
|
Path: pkgpath,
|
||
|
|
Funcs: functions,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default line-oriented format: "a/b/c.go:1:2: unreachable func: T.f"
|
||
|
|
format := `{{range .Funcs}}{{printf "%s: unreachable func: %s\n" .Position .Name}}{{end}}`
|
||
|
|
if *formatFlag != "" {
|
||
|
|
format = *formatFlag
|
||
|
|
}
|
||
|
|
printObjects(format, packages)
|
||
|
|
}
|
||
|
|
|
||
|
|
// prettyName is a fork of Function.String designed to reduce
|
||
|
|
// go/ssa's fussy punctuation symbols, e.g. "(*pkg.T).F" -> "pkg.T.F".
|
||
|
|
//
|
||
|
|
// It only works for functions that remain after
|
||
|
|
// callgraph.Graph.DeleteSyntheticNodes: source-level named functions
|
||
|
|
// and methods, their anonymous functions, and synthetic package
|
||
|
|
// initializers.
|
||
|
|
func prettyName(fn *ssa.Function, qualified bool) string {
|
||
|
|
var buf strings.Builder
|
||
|
|
|
||
|
|
// optional package qualifier
|
||
|
|
if qualified && fn.Pkg != nil {
|
||
|
|
fmt.Fprintf(&buf, "%s.", fn.Pkg.Pkg.Path())
|
||
|
|
}
|
||
|
|
|
||
|
|
var format func(*ssa.Function)
|
||
|
|
format = func(fn *ssa.Function) {
|
||
|
|
// anonymous?
|
||
|
|
if fn.Parent() != nil {
|
||
|
|
format(fn.Parent())
|
||
|
|
i := slices.Index(fn.Parent().AnonFuncs, fn)
|
||
|
|
fmt.Fprintf(&buf, "$%d", i+1)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// method receiver?
|
||
|
|
if recv := fn.Signature.Recv(); recv != nil {
|
||
|
|
_, named := typesinternal.ReceiverNamed(recv)
|
||
|
|
buf.WriteString(named.Obj().Name())
|
||
|
|
buf.WriteByte('.')
|
||
|
|
}
|
||
|
|
|
||
|
|
// function/method name
|
||
|
|
buf.WriteString(fn.Name())
|
||
|
|
}
|
||
|
|
format(fn)
|
||
|
|
|
||
|
|
return buf.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
// printObjects formats an array of objects, either as JSON or using a
|
||
|
|
// template, following the manner of 'go list (-json|-f=template)'.
|
||
|
|
func printObjects(format string, objects []any) {
|
||
|
|
if *jsonFlag {
|
||
|
|
out, err := json.MarshalIndent(objects, "", "\t")
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("internal error: %v", err)
|
||
|
|
}
|
||
|
|
os.Stdout.Write(out)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// -f=template. Parse can't fail: we checked it earlier.
|
||
|
|
tmpl := template.Must(template.New("deadcode").Parse(format))
|
||
|
|
for _, object := range objects {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
if err := tmpl.Execute(&buf, object); err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' {
|
||
|
|
buf.WriteByte('\n')
|
||
|
|
}
|
||
|
|
os.Stdout.Write(buf.Bytes())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// pathSearch returns the shortest path from one of the roots to one
|
||
|
|
// of the targets (along with the root itself), or zero if no path was found.
|
||
|
|
func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) {
|
||
|
|
// Search breadth-first (for shortest path) from the root.
|
||
|
|
//
|
||
|
|
// We don't use the virtual CallGraph.Root node as we wish to
|
||
|
|
// choose the order in which we search entrypoints:
|
||
|
|
// non-test packages before test packages,
|
||
|
|
// main functions before init functions.
|
||
|
|
|
||
|
|
// Sort roots into preferred order.
|
||
|
|
importsTesting := func(fn *ssa.Function) bool {
|
||
|
|
isTesting := func(p *types.Package) bool { return p.Path() == "testing" }
|
||
|
|
return slices.ContainsFunc(fn.Pkg.Pkg.Imports(), isTesting)
|
||
|
|
}
|
||
|
|
sort.Slice(roots, func(i, j int) bool {
|
||
|
|
x, y := roots[i], roots[j]
|
||
|
|
xtest := importsTesting(x)
|
||
|
|
ytest := importsTesting(y)
|
||
|
|
if xtest != ytest {
|
||
|
|
return !xtest // non-tests before tests
|
||
|
|
}
|
||
|
|
xinit := x.Name() == "init"
|
||
|
|
yinit := y.Name() == "init"
|
||
|
|
if xinit != yinit {
|
||
|
|
return !xinit // mains before inits
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
})
|
||
|
|
|
||
|
|
search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) {
|
||
|
|
// seen maps each encountered node to its predecessor on the
|
||
|
|
// path to a root node, or to nil for root itself.
|
||
|
|
seen := make(map[*callgraph.Node]*callgraph.Edge)
|
||
|
|
bfs := func(root *callgraph.Node) []*callgraph.Edge {
|
||
|
|
queue := []*callgraph.Node{root}
|
||
|
|
seen[root] = nil
|
||
|
|
for len(queue) > 0 {
|
||
|
|
node := queue[0]
|
||
|
|
queue = queue[1:]
|
||
|
|
|
||
|
|
// found a path?
|
||
|
|
if targets[node.Func] {
|
||
|
|
path := []*callgraph.Edge{} // non-nil in case len(path)=0
|
||
|
|
for {
|
||
|
|
edge := seen[node]
|
||
|
|
if edge == nil {
|
||
|
|
slices.Reverse(path)
|
||
|
|
return path
|
||
|
|
}
|
||
|
|
path = append(path, edge)
|
||
|
|
node = edge.Caller
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, edge := range node.Out {
|
||
|
|
if allowDynamic || isStaticCall(edge) {
|
||
|
|
if _, ok := seen[edge.Callee]; !ok {
|
||
|
|
seen[edge.Callee] = edge
|
||
|
|
queue = append(queue, edge.Callee)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
for _, rootFn := range roots {
|
||
|
|
root := res.CallGraph.Nodes[rootFn]
|
||
|
|
if root == nil {
|
||
|
|
// Missing call graph node for root.
|
||
|
|
// TODO(adonovan): seems like a bug in rta.
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if path := bfs(root); path != nil {
|
||
|
|
return root, path
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, allowDynamic := range []bool{false, true} {
|
||
|
|
if root, path := search(allowDynamic); path != nil {
|
||
|
|
return root, path
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- utilities --
|
||
|
|
|
||
|
|
func isStaticCall(edge *callgraph.Edge) bool {
|
||
|
|
return edge.Site != nil && edge.Site.Common().StaticCallee() != nil
|
||
|
|
}
|
||
|
|
|
||
|
|
var cwd, _ = os.Getwd()
|
||
|
|
|
||
|
|
func toJSONPosition(posn token.Position) jsonPosition {
|
||
|
|
// Use cwd-relative filename if possible.
|
||
|
|
filename := posn.Filename
|
||
|
|
if rel, err := filepath.Rel(cwd, filename); err == nil && !strings.HasPrefix(rel, "..") {
|
||
|
|
filename = rel
|
||
|
|
}
|
||
|
|
|
||
|
|
return jsonPosition{filename, posn.Line, posn.Column}
|
||
|
|
}
|
||
|
|
|
||
|
|
func cond[T any](cond bool, t, f T) T {
|
||
|
|
if cond {
|
||
|
|
return t
|
||
|
|
} else {
|
||
|
|
return f
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// isMarkerMethod reports whether fn is a marker method:
|
||
|
|
// an unexported, empty-bodied method with no parameters or results
|
||
|
|
// that implements some named interface type in the same package.
|
||
|
|
func isMarkerMethod(fn *ssa.Function, interfaceTypes []*types.Interface) bool {
|
||
|
|
// Is it an unexported method of no params/results?
|
||
|
|
if !(fn.Signature.Recv() != nil &&
|
||
|
|
!ast.IsExported(fn.Name()) &&
|
||
|
|
fn.Signature.Params() == nil &&
|
||
|
|
fn.Signature.Results() == nil) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Does the method have an empty body?
|
||
|
|
body := fn.Syntax().(*ast.FuncDecl).Body
|
||
|
|
if body == nil || len(body.List) > 0 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Does it implement some named interface type in this package?
|
||
|
|
return slices.ContainsFunc(interfaceTypes, func(iface *types.Interface) bool {
|
||
|
|
return types.Implements(fn.Signature.Recv().Type(), iface)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// -- output protocol (for JSON or text/template) --
|
||
|
|
|
||
|
|
// Keep in sync with doc comment!
|
||
|
|
|
||
|
|
type jsonFunction struct {
|
||
|
|
Name string // name (sans package qualifier)
|
||
|
|
Position jsonPosition // file/line/column of declaration
|
||
|
|
Generated bool // function is declared in a generated .go file
|
||
|
|
Marker bool // function is a marker interface method
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f jsonFunction) String() string { return f.Name }
|
||
|
|
|
||
|
|
type jsonPackage struct {
|
||
|
|
Name string // declared name
|
||
|
|
Path string // full import path
|
||
|
|
Funcs []jsonFunction // non-empty list of package's dead functions
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p jsonPackage) String() string { return p.Path }
|
||
|
|
|
||
|
|
// The Initial and Callee names are package-qualified.
|
||
|
|
type jsonEdge struct {
|
||
|
|
Initial string `json:",omitempty"` // initial entrypoint (main or init); first edge only
|
||
|
|
Kind string // = static | dynamic
|
||
|
|
Position jsonPosition
|
||
|
|
Callee string
|
||
|
|
}
|
||
|
|
|
||
|
|
type jsonPosition struct {
|
||
|
|
File string
|
||
|
|
Line, Col int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p jsonPosition) String() string {
|
||
|
|
return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col)
|
||
|
|
}
|