diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index 0c0b7ec394e..ac6d8067c16 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -45,6 +45,7 @@ var ( filterFlag = flag.String("filter", "", "report only packages matching this regular expression (default: module of first package)") generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files") + markerFlag = flag.Bool("marker", false, "include marker interface methods in the report") 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") @@ -299,6 +300,16 @@ func main() { } } + // Collect interfaces by package for marker method identification + interfacesByPkg := make(map[*ssa.Package][]*types.Interface) + for _, fn := range sourceFuncs { + pkg := fn.Package() + if _, exists := interfacesByPkg[pkg]; exists { + continue + } + interfacesByPkg[pkg] = getTopLevelInterfaces(pkg) + } + // Build array of jsonPackage objects. var packages []any for _, pkgpath := range slices.Sorted(maps.Keys(byPkgPath)) { @@ -334,10 +345,18 @@ func main() { continue } + // If the -marker flag is not set to true, + // marker methods should not be reported + marker := isMarkerMethod(fn, interfacesByPkg[fn.Package()]) + if marker && !*markerFlag { + continue + } + functions = append(functions, jsonFunction{ Name: prettyName(fn, false), Position: toJSONPosition(posn), Generated: gen, + Marker: marker, }) } if len(functions) > 0 { @@ -538,6 +557,79 @@ func cond[T any](cond bool, t, f T) T { } } +func getTopLevelInterfaces(p *ssa.Package) []*types.Interface { + var interfaces []*types.Interface + for _, member := range p.Members { + if typ, ok := member.(*ssa.Type); ok { + if intf, ok := typ.Type().Underlying().(*types.Interface); ok { + interfaces = append(interfaces, intf) + } + } + } + return interfaces +} + +// isMarkerMethod returns true if the function is a method that implements a marker interface. +// A marker interface method is defined by the following properties: +// - Is a method (i.e. has a receiver) +// - Its receiver type implements one of the top-level interfaces in its package +// - Is unexported +// - Has no params (other than the receiver) and no results +// - Has an empty function body +func isMarkerMethod(fn *ssa.Function, interfaces []*types.Interface) bool { + var ( + sig = fn.Signature + implements = func(intf *types.Interface) bool { + return types.Implements(fn.Signature.Recv().Type(), intf) + } + isFunctionEmpty = func(fun *ssa.Function) bool { + // SSA analyzes the source code + // if blocks is nil, it means it's an external (imported) function. + // This shouldn't be flagged as a marker method + if fun.Blocks == nil { + return false + } + + if len(fun.Blocks) != 1 { + return false + } + + blk := fun.Blocks[0] + if len(blk.Instrs) > 1 { + return false + } + + instr := blk.Instrs[0] + if _, ok := instr.(*ssa.Return); !ok { + return false + } + + return true + } + ) + + if isMethod := sig.Recv() != nil; !isMethod { + return false + } + if implementsInterface := slices.ContainsFunc(interfaces, implements); !implementsInterface { + return false + } + if isUnexported := !ast.IsExported(fn.Name()); !isUnexported { + return false + } + if hasNoParams := sig.Params() == nil; !hasNoParams { + return false + } + if hasNoResults := sig.Results() == nil; !hasNoResults { + return false + } + if isEmpty := isFunctionEmpty(fn); !isEmpty { + return false + } + + return true +} + // -- output protocol (for JSON or text/template) -- // Keep in sync with doc comment! @@ -546,6 +638,7 @@ 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 } diff --git a/cmd/deadcode/doc.go b/cmd/deadcode/doc.go index bd474248e55..8bb0ca613ce 100644 --- a/cmd/deadcode/doc.go +++ b/cmd/deadcode/doc.go @@ -43,6 +43,15 @@ 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. +The tool also does not report marker interface methods by default. +Marker interface methods are typically used to create compile-time constraints +to ensure that only specific types can implement a particular interface. +These methods have no other functionality (empty function body) and are never invoked. +Although marker interface methods are technically unreachable, removing them would break +the interface implementation. +Hence, the default behavior is to exclude them from the report. +Use the -marker 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 @@ -121,6 +130,7 @@ is static or dynamic, and its source line number. For example: Name string // name (sans package qualifier) Position Position // file/line/column of function declaration Generated bool // function is declared in a generated .go file + Marker bool // function is a marker interface method } type Edge struct { diff --git a/cmd/deadcode/testdata/marker.txtar b/cmd/deadcode/testdata/marker.txtar new file mode 100644 index 00000000000..43a028ce972 --- /dev/null +++ b/cmd/deadcode/testdata/marker.txtar @@ -0,0 +1,50 @@ +# Test of -marker functionality. + +# Suppresses marker methods by default + deadcode -filter= example.com +!want "T.isMarker" +want "R.isNotMarker" +!want "P.greet" +want "Q.greet" + +# Includes marker methods if flag is on + deadcode -marker -filter= example.com +want "T.isMarker" +want "R.isNotMarker" +!want "P.greet" +want "Q.greet" + +-- go.mod -- +module example.com +go 1.18 + +-- main.go -- +package main +import "fmt" + +// Marker method: implements interface, unexported, no params, no results, empty body +type Marker interface { + isMarker() +} +type T struct{} + +func (t *T) isMarker() {} + +// Not marker method: does not implement interface +type R struct {} +func (r *R) isNotMarker() {} + +// Ensure that it still reports valid interface methods +// when unused +type Greeter interface { + greet() +} +type P struct {} +func (p *P) greet() {fmt.Println("P: hello")} +type Q struct {} +func (q *Q) greet() {fmt.Println("Q: hello")} // this method should be reported + +func main () { + var p P + p.greet() +} \ No newline at end of file