whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,648 @@
|
||||
// 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.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"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>" {
|
||||
if mod := initial[0].Module; mod != nil && mod.Path != "" {
|
||||
*filterFlag = "^" + regexp.QuoteMeta(mod.Path) + "\\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)
|
||||
packages.Visit(initial, nil, func(p *packages.Package) {
|
||||
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 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
|
||||
pkgpaths := keys(byPkgPath)
|
||||
sort.Strings(pkgpaths)
|
||||
for _, pkgpath := range pkgpaths {
|
||||
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 := 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
|
||||
}
|
||||
|
||||
functions = append(functions, jsonFunction{
|
||||
Name: prettyName(fn, false),
|
||||
Position: toJSONPosition(posn),
|
||||
Generated: gen,
|
||||
})
|
||||
}
|
||||
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 := 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())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adonovan): use go1.21's ast.IsGenerated.
|
||||
|
||||
// isGenerated reports whether the file was generated by a program,
|
||||
// not handwritten, by detecting the special comment described
|
||||
// at https://go.dev/s/generatedcode.
|
||||
//
|
||||
// The syntax tree must have been parsed with the ParseComments flag.
|
||||
// Example:
|
||||
//
|
||||
// f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly)
|
||||
// if err != nil { ... }
|
||||
// gen := ast.IsGenerated(f)
|
||||
func isGenerated(file *ast.File) bool {
|
||||
_, ok := generator(file)
|
||||
return ok
|
||||
}
|
||||
|
||||
func generator(file *ast.File) (string, bool) {
|
||||
for _, group := range file.Comments {
|
||||
for _, comment := range group.List {
|
||||
if comment.Pos() > file.Package {
|
||||
break // after package declaration
|
||||
}
|
||||
// opt: check Contains first to avoid unnecessary array allocation in Split.
|
||||
const prefix = "// Code generated "
|
||||
if strings.Contains(comment.Text, prefix) {
|
||||
for _, line := range strings.Split(comment.Text, "\n") {
|
||||
if rest, ok := strings.CutPrefix(line, prefix); ok {
|
||||
if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok {
|
||||
return gen, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 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 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// -- 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// -- from the future --
|
||||
|
||||
// TODO(adonovan): use go1.22's slices and maps packages.
|
||||
|
||||
func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
|
||||
return indexFunc(s, f) >= 0
|
||||
}
|
||||
|
||||
func indexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||
for i := range s {
|
||||
if f(s[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func index[S ~[]E, E comparable](s S, v E) int {
|
||||
for i := range s {
|
||||
if v == s[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func reverse[S ~[]E, E any](s S) {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
}
|
||||
|
||||
func keys[M ~map[K]V, K comparable, V any](m M) []K {
|
||||
r := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
r = append(r, k)
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
// Test runs the deadcode command on each scenario
|
||||
// described by a testdata/*.txtar file.
|
||||
func Test(t *testing.T) {
|
||||
testenv.NeedsTool(t, "go")
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skipf("the dependencies are not available on android")
|
||||
}
|
||||
|
||||
exe := buildDeadcode(t)
|
||||
|
||||
matches, err := filepath.Glob("testdata/*.txtar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, filename := range matches {
|
||||
filename := filename
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ar, err := txtar.ParseFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the archive files to the temp directory.
|
||||
tmpdir := t.TempDir()
|
||||
for _, f := range ar.Files {
|
||||
filename := filepath.Join(tmpdir, f.Name)
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filename, f.Data, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse archive comment as directives of these forms:
|
||||
//
|
||||
// [!]deadcode args... command-line arguments
|
||||
// [!]want arg expected/unwanted string in output (or stderr)
|
||||
//
|
||||
// Args may be Go-quoted strings.
|
||||
type testcase struct {
|
||||
linenum int
|
||||
args []string
|
||||
wantErr bool
|
||||
want map[string]bool // string -> sense
|
||||
}
|
||||
var cases []*testcase
|
||||
var current *testcase
|
||||
for i, line := range strings.Split(string(ar.Comment), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue // skip blanks and comments
|
||||
}
|
||||
|
||||
words, err := words(line)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot break line into words: %v (%s)", err, line)
|
||||
}
|
||||
switch kind := words[0]; kind {
|
||||
case "deadcode", "!deadcode":
|
||||
current = &testcase{
|
||||
linenum: i + 1,
|
||||
want: make(map[string]bool),
|
||||
args: words[1:],
|
||||
wantErr: kind[0] == '!',
|
||||
}
|
||||
cases = append(cases, current)
|
||||
case "want", "!want":
|
||||
if current == nil {
|
||||
t.Fatalf("'want' directive must be after 'deadcode'")
|
||||
}
|
||||
if len(words) != 2 {
|
||||
t.Fatalf("'want' directive needs argument <<%s>>", line)
|
||||
}
|
||||
current.want[words[1]] = kind[0] != '!'
|
||||
default:
|
||||
t.Fatalf("%s: invalid directive %q", filename, kind)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("L%d", tc.linenum), func(t *testing.T) {
|
||||
// Run the command.
|
||||
cmd := exec.Command(exe, tc.args...)
|
||||
cmd.Stdout = new(bytes.Buffer)
|
||||
cmd.Stderr = new(bytes.Buffer)
|
||||
cmd.Dir = tmpdir
|
||||
cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on")
|
||||
var got string
|
||||
if err := cmd.Run(); err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr)
|
||||
}
|
||||
got = fmt.Sprint(cmd.Stderr)
|
||||
} else {
|
||||
if tc.wantErr {
|
||||
t.Fatalf("deadcode succeeded unexpectedly (stdout=%s)", cmd.Stdout)
|
||||
}
|
||||
got = fmt.Sprint(cmd.Stdout)
|
||||
}
|
||||
|
||||
// Check each want directive.
|
||||
for str, sense := range tc.want {
|
||||
ok := true
|
||||
if strings.Contains(got, str) != sense {
|
||||
if sense {
|
||||
t.Errorf("missing %q", str)
|
||||
} else {
|
||||
t.Errorf("unwanted %q", str)
|
||||
}
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("got: <<%s>>", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// buildDeadcode builds the deadcode executable.
|
||||
// It returns its path, and a cleanup function.
|
||||
func buildDeadcode(t *testing.T) string {
|
||||
bin := filepath.Join(t.TempDir(), "deadcode")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", bin)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("Building deadcode: %v\n%s", err, out)
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
// words breaks a string into words, respecting
|
||||
// Go string quotations around words with spaces.
|
||||
func words(s string) ([]string, error) {
|
||||
var words []string
|
||||
for s != "" {
|
||||
s = strings.TrimSpace(s)
|
||||
var word string
|
||||
if s[0] == '"' || s[0] == '`' {
|
||||
prefix, err := strconv.QuotedPrefix(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = s[len(prefix):]
|
||||
word, _ = strconv.Unquote(prefix)
|
||||
} else {
|
||||
prefix, rest, _ := strings.Cut(s, " ")
|
||||
s = rest
|
||||
word = prefix
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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.
|
||||
|
||||
/*
|
||||
The deadcode command reports unreachable functions in Go programs.
|
||||
|
||||
Usage: deadcode [flags] package...
|
||||
|
||||
The deadcode command loads a Go program from source then uses Rapid
|
||||
Type Analysis (RTA) to build a call graph of all the functions
|
||||
reachable from the program's main function. Any functions that are not
|
||||
reachable are reported as dead code, grouped by package.
|
||||
|
||||
Packages are expressed in the notation of 'go list' (or other
|
||||
underlying build system if you are using an alternative
|
||||
golang.org/x/go/packages driver). Only executable (main) packages are
|
||||
considered starting points for the analysis.
|
||||
|
||||
The -test flag causes it to analyze test executables too. Tests
|
||||
sometimes make use of functions that would otherwise appear to be dead
|
||||
code, and public API functions reported as dead with -test indicate
|
||||
possible gaps in your test coverage. Bear in mind that an Example test
|
||||
function without an "Output:" comment is merely documentation:
|
||||
it is dead code, and does not contribute coverage.
|
||||
|
||||
The -filter flag restricts results to packages that match the provided
|
||||
regular expression; its default value is the module name of the first
|
||||
package. Use -filter= to display all results.
|
||||
|
||||
Example: show all dead code within the gopls module:
|
||||
|
||||
$ deadcode -test golang.org/x/tools/gopls/...
|
||||
|
||||
The analysis can soundly analyze dynamic calls though func values,
|
||||
interface methods, and reflection. However, it does not currently
|
||||
understand the aliasing created by //go:linkname directives, so it
|
||||
will fail to recognize that calls to a linkname-annotated function
|
||||
with no body in fact dispatch to the function named in the annotation.
|
||||
This may result in the latter function being spuriously reported as dead.
|
||||
|
||||
By default, the tool does not report dead functions in generated files,
|
||||
as determined by the special comment described in
|
||||
https://go.dev/s/generatedcode. Use the -generated flag to include them.
|
||||
|
||||
In any case, just because a function is reported as dead does not mean
|
||||
it is unconditionally safe to delete it. For example, a dead function
|
||||
may be referenced by another dead function, and a dead method may be
|
||||
required to satisfy an interface that is never called.
|
||||
Some judgement is required.
|
||||
|
||||
The analysis is valid only for a single GOOS/GOARCH/-tags configuration,
|
||||
so a function reported as dead may be live in a different configuration.
|
||||
Consider running the tool once for each configuration of interest.
|
||||
Consider using a line-oriented output format (see below) to make it
|
||||
easier to compute the intersection of results across all runs.
|
||||
|
||||
# Output
|
||||
|
||||
The command supports three output formats.
|
||||
|
||||
With no flags, the command prints the name and location of each dead
|
||||
function in the form of a typical compiler diagnostic, for example:
|
||||
|
||||
$ deadcode -f='{{range .Funcs}}{{println .Position}}{{end}}' -test ./gopls/...
|
||||
gopls/internal/protocol/command.go:1206:6: unreachable func: openClientEditor
|
||||
gopls/internal/template/parse.go:414:18: unreachable func: Parsed.WriteNode
|
||||
gopls/internal/template/parse.go:419:18: unreachable func: wrNode.writeNode
|
||||
|
||||
With the -json flag, the command prints an array of Package
|
||||
objects, as defined by the JSON schema (see below).
|
||||
|
||||
With the -f=template flag, the command executes the specified template
|
||||
on each Package record. So, this template shows dead functions grouped
|
||||
by package:
|
||||
|
||||
$ deadcode -f='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .Name}}{{end}}{{println}}' -test ./gopls/...
|
||||
golang.org/x/tools/gopls/internal/lsp
|
||||
openClientEditor
|
||||
|
||||
golang.org/x/tools/gopls/internal/template
|
||||
Parsed.WriteNode
|
||||
wrNode.writeNode
|
||||
|
||||
# Why is a function not dead?
|
||||
|
||||
The -whylive=function flag explain why the named function is not dead
|
||||
by showing an arbitrary shortest path to it from one of the main functions.
|
||||
(To enumerate the functions in a program, or for more sophisticated
|
||||
call graph queries, use golang.org/x/tools/cmd/callgraph.)
|
||||
|
||||
Fully static call paths are preferred over paths involving dynamic
|
||||
edges, even if longer. Paths starting from a non-test package are
|
||||
preferred over those from tests. Paths from main functions are
|
||||
preferred over paths from init functions.
|
||||
|
||||
The result is a list of Edge objects (see JSON schema below).
|
||||
Again, the -json and -f=template flags may be used to control
|
||||
the formatting of the list of Edge objects.
|
||||
The default format shows, for each edge in the path, whether the call
|
||||
is static or dynamic, and its source line number. For example:
|
||||
|
||||
$ deadcode -whylive=bytes.Buffer.String -test ./cmd/deadcode/...
|
||||
golang.org/x/tools/cmd/deadcode.main
|
||||
static@L0117 --> golang.org/x/tools/go/packages.Load
|
||||
static@L0262 --> golang.org/x/tools/go/packages.defaultDriver
|
||||
static@L0305 --> golang.org/x/tools/go/packages.goListDriver
|
||||
static@L0153 --> golang.org/x/tools/go/packages.goListDriver$1
|
||||
static@L0154 --> golang.org/x/tools/go/internal/packagesdriver.GetSizesForArgsGolist
|
||||
static@L0044 --> bytes.Buffer.String
|
||||
|
||||
# JSON schema
|
||||
|
||||
type Package struct {
|
||||
Name string // declared name
|
||||
Path string // full import path
|
||||
Funcs []Function // list of dead functions within it
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Name string // name (sans package qualifier)
|
||||
Position Position // file/line/column of function declaration
|
||||
Generated bool // function is declared in a generated .go file
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Initial string // initial entrypoint (main or init); first edge only
|
||||
Kind string // = static | dynamic
|
||||
Position Position // file/line/column of call site
|
||||
Callee string // target of the call
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
File string // name of file
|
||||
Line, Col int // line and byte index, both 1-based
|
||||
}
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,38 @@
|
||||
# Test of basic functionality.
|
||||
|
||||
deadcode -filter= example.com
|
||||
|
||||
want "T.Goodbye"
|
||||
want "T.Goodbye2"
|
||||
want "T.Goodbye3"
|
||||
!want "T.Hello"
|
||||
want "unreferenced"
|
||||
|
||||
want "Scanf"
|
||||
want "Printf"
|
||||
!want "Println"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type T int
|
||||
|
||||
func main() {
|
||||
var x T
|
||||
x.Hello()
|
||||
}
|
||||
|
||||
func (T) Hello() { fmt.Println("hello") }
|
||||
func (T) Goodbye() { fmt.Println("goodbye") }
|
||||
func (*T) Goodbye2() { fmt.Println("goodbye2") }
|
||||
func (*A) Goodbye3() { fmt.Println("goodbye3") }
|
||||
|
||||
type A = T
|
||||
|
||||
func unreferenced() {}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Test of -filter flag.
|
||||
|
||||
deadcode -filter=other.net example.com
|
||||
|
||||
want `other.net`
|
||||
want `Dead`
|
||||
!want `Live`
|
||||
|
||||
!want `example.com`
|
||||
!want `unreferenced`
|
||||
|
||||
-- go.work --
|
||||
use example.com
|
||||
use other.net
|
||||
|
||||
-- example.com/go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- example.com/main.go --
|
||||
package main
|
||||
|
||||
import "other.net"
|
||||
|
||||
func main() {
|
||||
other.Live()
|
||||
}
|
||||
|
||||
func unreferenced() {}
|
||||
|
||||
-- other.net/go.mod --
|
||||
module other.net
|
||||
go 1.18
|
||||
|
||||
-- other.net/other.go --
|
||||
package other
|
||||
|
||||
func Live() {}
|
||||
func Dead() {}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Test of -generated flag output.
|
||||
|
||||
deadcode "-f={{range .Funcs}}{{$.Name}}.{{.Name}}{{end}}" example.com
|
||||
!want "main.main"
|
||||
want "main.Dead1"
|
||||
!want "main.Dead2"
|
||||
|
||||
deadcode "-f={{range .Funcs}}{{$.Name}}.{{.Name}}{{end}}" -generated example.com
|
||||
!want "main.main"
|
||||
want "main.Dead1"
|
||||
want "main.Dead2"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
func Dead1() {}
|
||||
|
||||
-- gen.go --
|
||||
// Code generated by hand. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
func Dead2() {}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Regression test for issue 65915: the enumeration of source-level
|
||||
# functions used the flawed ssautil.AllFunctions, causing it to
|
||||
# miss some unexported ones.
|
||||
|
||||
deadcode -filter= example.com
|
||||
|
||||
want "unreachable func: example.UnUsed"
|
||||
want "unreachable func: example.unUsed"
|
||||
want "unreachable func: PublicExample.UnUsed"
|
||||
want "unreachable func: PublicExample.unUsed"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
type example struct{}
|
||||
|
||||
func (e example) UnUsed() {}
|
||||
|
||||
func (e example) Used() {}
|
||||
|
||||
func (e example) unUsed() {}
|
||||
|
||||
func (e example) used() {}
|
||||
|
||||
type PublicExample struct{}
|
||||
|
||||
func (p PublicExample) UnUsed() {}
|
||||
|
||||
func (p PublicExample) Used() {}
|
||||
|
||||
func (p PublicExample) unUsed() {}
|
||||
|
||||
func (p PublicExample) used() {}
|
||||
|
||||
func main() {
|
||||
example{}.Used()
|
||||
example{}.used()
|
||||
PublicExample{}.Used()
|
||||
PublicExample{}.used()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Test of -whylive with reflective call
|
||||
# (regression test for golang/go#67915).
|
||||
|
||||
# The live function is reached via reflection:
|
||||
|
||||
deadcode example.com
|
||||
want "unreachable func: dead"
|
||||
!want "unreachable func: live"
|
||||
|
||||
# Reflective calls have Edge.Site=nil, which formerly led to a crash
|
||||
# when -whylive would compute its position. Now it has NoPos.
|
||||
|
||||
deadcode -whylive=example.com.live example.com
|
||||
want " example.com.main"
|
||||
want " static@L0006 --> reflect.Value.Call"
|
||||
want "dynamic@L0000 --> example.com.live"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "reflect"
|
||||
|
||||
func main() {
|
||||
reflect.ValueOf(live).Call(nil)
|
||||
}
|
||||
|
||||
func live() {
|
||||
println("hello")
|
||||
}
|
||||
|
||||
func dead() {
|
||||
println("goodbye")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Very minimal test of -json flag.
|
||||
|
||||
deadcode -json example.com/p
|
||||
|
||||
want `"Path": "example.com/p",`
|
||||
want `"Name": "DeadFunc",`
|
||||
want `"Generated": false`
|
||||
want `"Line": 5,`
|
||||
want `"Col": 6`
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- p/p.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
|
||||
func DeadFunc() {}
|
||||
|
||||
type T int
|
||||
func (*T) DeadMethod() {}
|
||||
@@ -0,0 +1,32 @@
|
||||
# Test of line-oriented output.
|
||||
|
||||
deadcode `-f={{range .Funcs}}{{printf "%s: %s.%s\n" .Position $.Path .Name}}{{end}}` -filter= example.com
|
||||
|
||||
want "main.go:13:10: example.com.T.Goodbye"
|
||||
!want "example.com.T.Hello"
|
||||
want "main.go:15:6: example.com.unreferenced"
|
||||
|
||||
want "fmt.Scanf"
|
||||
want "fmt.Printf"
|
||||
!want "fmt.Println"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type T int
|
||||
|
||||
func main() {
|
||||
var x T
|
||||
x.Hello()
|
||||
}
|
||||
|
||||
func (T) Hello() { fmt.Println("hello") }
|
||||
func (T) Goodbye() { fmt.Println("goodbye") }
|
||||
|
||||
func unreferenced() {}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Test of -test flag.
|
||||
|
||||
deadcode -test -filter=example.com example.com/p
|
||||
|
||||
want "Dead"
|
||||
!want "Live1"
|
||||
!want "Live2"
|
||||
|
||||
want "ExampleDead"
|
||||
!want "ExampleLive"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- p/p.go --
|
||||
package p
|
||||
|
||||
func Live1() {}
|
||||
func Live2() {}
|
||||
func Dead() {}
|
||||
|
||||
-- p/p_test.go --
|
||||
package p_test
|
||||
|
||||
import "example.com/p"
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
p.Live1()
|
||||
}
|
||||
|
||||
func ExampleLive() {
|
||||
p.Live2()
|
||||
// Output:
|
||||
}
|
||||
|
||||
// A test Example function without an "Output:" comment is never executed.
|
||||
func ExampleDead() {
|
||||
p.Dead()
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
# Test of -whylive flag.
|
||||
|
||||
# The -whylive argument must be live.
|
||||
|
||||
!deadcode -whylive=example.com.d example.com
|
||||
want "function example.com.d is dead code"
|
||||
|
||||
# A fully static path is preferred, even if longer.
|
||||
|
||||
deadcode -whylive=example.com.c example.com
|
||||
want " example.com.main"
|
||||
want " static@L0004 --> example.com.a"
|
||||
want " static@L0009 --> example.com.b"
|
||||
want " static@L0012 --> example.com.c"
|
||||
|
||||
# Dynamic edges are followed if necessary.
|
||||
# (Note that main is preferred over init.)
|
||||
|
||||
deadcode -whylive=example.com.f example.com
|
||||
want " example.com.main"
|
||||
want "dynamic@L0006 --> example.com.e"
|
||||
want " static@L0017 --> example.com.f"
|
||||
|
||||
# Degenerate case where target is itself a root.
|
||||
|
||||
!deadcode -whylive=example.com.main example.com
|
||||
want "example.com.main is a root"
|
||||
|
||||
# Test of path through (*T).m method wrapper.
|
||||
|
||||
deadcode -whylive=example.com/p.live example.com/p
|
||||
want " example.com/p.main"
|
||||
want "static@L0006 --> example.com/p.E.Error"
|
||||
want "static@L0010 --> example.com/p.live"
|
||||
|
||||
# Test of path through (I).m interface method wrapper (thunk).
|
||||
|
||||
deadcode -whylive=example.com/q.live example.com/q
|
||||
want " example.com/q.main"
|
||||
want "static@L0006 --> example.com/q.E.Error"
|
||||
want "static@L0010 --> example.com/q.live"
|
||||
|
||||
# Test of path through synthetic package initializer,
|
||||
# a declared package initializer, and its anonymous function.
|
||||
|
||||
deadcode -whylive=example.com/q.live2 example.com/q
|
||||
want " example.com/q.init"
|
||||
want "static@L0000 --> example.com/q.init#1"
|
||||
want "static@L0016 --> example.com/q.init#1$1"
|
||||
want "static@L0015 --> example.com/q.live2"
|
||||
|
||||
# Test of path through synthetic package initializer,
|
||||
# and a global var initializer.
|
||||
|
||||
deadcode -whylive=example.com/r.live example.com/r
|
||||
want " example.com/r.init"
|
||||
want "static@L0007 --> example.com/r.init$1"
|
||||
want "static@L0006 --> example.com/r.live"
|
||||
|
||||
-- go.mod --
|
||||
module example.com
|
||||
go 1.18
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
func main() {
|
||||
a()
|
||||
println(c, e) // c, e are address-taken
|
||||
(func ())(nil)() // potential dynamic call to c, e
|
||||
}
|
||||
func a() {
|
||||
b()
|
||||
}
|
||||
func b() {
|
||||
c()
|
||||
}
|
||||
func c()
|
||||
func d()
|
||||
func e() {
|
||||
f()
|
||||
}
|
||||
func f()
|
||||
|
||||
func init() {
|
||||
(func ())(nil)() // potential dynamic call to c, e
|
||||
}
|
||||
|
||||
-- p/p.go --
|
||||
package main
|
||||
|
||||
func main() {
|
||||
f := (*E).Error
|
||||
var e E
|
||||
f(&e)
|
||||
}
|
||||
|
||||
type E int
|
||||
func (E) Error() string { return live() }
|
||||
|
||||
func live() string
|
||||
|
||||
-- q/q.go --
|
||||
package main
|
||||
|
||||
func main() {
|
||||
f := error.Error
|
||||
var e E
|
||||
f(e)
|
||||
}
|
||||
|
||||
type E int
|
||||
func (E) Error() string { return live() }
|
||||
|
||||
func live() string
|
||||
|
||||
func init() {
|
||||
f := func() { live2() }
|
||||
f()
|
||||
}
|
||||
|
||||
func live2()
|
||||
|
||||
-- r/r.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
|
||||
var x = func() int {
|
||||
return live()
|
||||
}()
|
||||
|
||||
func live() int
|
||||
Reference in New Issue
Block a user