Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions cmd/deadcode/deadcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (

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")
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")
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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!
Expand All @@ -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 }
Expand Down
10 changes: 10 additions & 0 deletions cmd/deadcode/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 50 additions & 0 deletions cmd/deadcode/testdata/marker.txtar
Original file line number Diff line number Diff line change
@@ -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()
}