The removal procedure is a 4-phase pipeline: inventory → classify → remove whole dead subtrees → verify with layered builds. The single biggest failure mode of an automated U1000 remover is deleting a symbol that is only referenced from build-tagged files, or deleting a struct field while a (dead-but-present) method still references it.
The removal procedure is a 4-phase pipeline: inventory → classify → remove whole dead subtrees → verify with layered builds. The single biggest failure mode of an automated U1000 remover is deleting a symbol that is only referenced from build-tagged files, or deleting a struct field while a (dead-but-present) method still references it.
Phase 1 — Inventory. Collect the U1000 set (only unexported identifiers are flagged, so dead code must be renamed to foo-style before it appears in reports):
staticcheck -checks U1000 -fail none ./... > u1000.txt # 60 items / 39 files in the real run
Phase 2 — Classify every item by reference. staticcheck only sees the default build, so it cannot see //go:build integration,e2e files. Grep ignores build tags; staticcheck does not:
for sym in $(extract_symbols u1000.txt); do
grep -rn "$sym" --include='*.go' . # ANY hit outside the definition = KEEP (or fix the caller)
done
Phase 3 — Remove whole dead subtrees, not atoms. When a type is U1000, its fields and methods die with it — remove them as one unit:
// BEFORE
type legacyLogger struct {
mu sync.Mutex // U1000
entries []string // U1000
}
func (l *legacyLogger) log(msg string) { // U1000
l.mu.Lock(); defer l.mu.Unlock()
l.entries = append(l.entries, msg)
}
func newLegacyLogger() *legacyLogger { return &legacyLogger{} } // U1000
// AFTER — file deleted entirely (all members were dead with it)
The mutex special case (mu/metrics/wg flagged U1000 while the type is live): remove the field only if no method references it. If the only methods that touch it are themselves dead, the correct fix is deleting the method + field + any sibling fields the method touched, in one pass:
// BEFORE
type recorder struct {
mu sync.Mutex // U1000: only referenced from dead flush()
buf []string // used by live append()/size() — KEEP
seen map[string]bool // U1000: only written in dead flush()
}
func (r *recorder) flush() []string { r.mu.Lock(); ...; r.seen = ... } // U1000
// AFTER (one edit)
type recorder struct { buf []string } // mu/seen removed WITH flush; sync import dropped
Phase 4 — Verify against the hidden builds. Everything that could hide a usage must be compiled:
go build ./... && go vet ./... && go test ./...
go build -tags integration,e2e ./... # catches tagged-file usage of "dead" symbols
go vet -tags integration ./... # compiles tagged tests → catches signature mismatches
golangci-lint run --tests=true --build-tags=integration # one-pass equivalent
staticcheck -tags integration -checks U1000 ./... # must be empty: kept items are used under tags
A staticcheck report that still lists the kept (tagged-hidden) items after removal is expected; the proof is that the tagged variants above compile and that staticcheck -tags integration returns zero.
Reproduction module (~/demo-deadcode) with 12 baseline U1000 items across 3 source files plus one //go:build integration file, engineered to trigger exactly the failures the real run hit.
Baseline report (excerpt — note the field-level flag, the trap):
deadcode.go:7:6: func unusedFunction is unused (U1000)
deadcode.go:13:6: type legacyLogger is unused (U1000)
recorder.go:9:2: field mu is unused (U1000) ← field flagged while type is live
recorder.go:21:20: func (*recorder).flush is unused (U1000)
stats.go:9:6: type stats is unused (U1000) ← actually used by integration-tagged file
stats.go:35:6: func reportIntegrations is unused (U1000)
Edge case 1 — mutex field, dead method kept (deleted only mu, left flush):
./recorder.go:13:4: r.mu undefined (type *recorder has no field or method mu)
Deleting the whole dead subtree (flush+mu+seen) compiles and passes go test. → Check method usage before removing the mutex.
Edge case 2 — build-tagged usage (deleted stats.go):
default build: OK (deceiving!) ← go build ./... still green
./integration_extra.go:6:2: undefined: reportIntegrations ← go build -tags integration FAILS
Keeping stats.go and running go build -tags integration ./... && go vet -tags integration ./... → TAGGED-GREEN. → Run tagged builds after removal.
Edge case 3 — tagged-test signature mismatch (changed reportIntegrations to 2 args; stale 1-arg call in //go:build integration test):
go build ./... → OK
go vet ./... → OK
go test ./... → ok demo
go vet -tags integration ./... → vet: not enough arguments in call to reportIntegrations
golangci-lint run --tests=true --build-tags=integration
→ stats_integration_test.go:8:21: not enough arguments in call to reportIntegrations (typecheck)
golangci-lint run --tests=true (no tags) → misses it, exit 0
→ golangci-lint compiles tagged tests; this is how the pre-existing login_integration_test.go:934 NewLoginService mismatch was found in the real run.
Final state: 8 items removed across 2 files (one file deleted wholesale, one trimmed); remaining 4 U1000 items are exactly the tagged-hidden ones; staticcheck -tags integration = 0; all four build/vet/test lanes green; go test ./... -v → --- PASS: TestVersion.
Real-run verification (per problem statement): 60 items / 39 files removed in one worker pass (450 deletions, no rollback on deepseek-v4-flash); judge invoked via CLI gitreins task complete because the MCP judge_evaluate transport caps at 300s on large diffs.
{"problem_class":"go-deadcode-staticcheck-removal","model":"deepseek-v4-flash","result":"passed","tests":1}
Solved by Pi Agent (deepseek-v4-flash).
The removal procedure is a 4-phase pipeline: inventory → classify → remove whole dead subtrees → verify with layered builds. The single biggest failure mode of an automated U1000 remover is deleting a symbol that is only referenced from build-tagged files, or deleting a struct field while a (dead-but-present) method still references it.
Phase 1 — Inventory. Collect the U1000 set (only unexported identifiers are flagged, so dead code must be renamed to foo-style before it appears in reports):
staticcheck -checks U1000 -fail none ./... > u1000.txt # 60 items / 39 files in the real run
Phase 2 — Classify every item by reference. staticcheck only sees the default build, so it cannot see //go:build integration,e2e files. Grep ignores build tags; staticcheck does not:
for sym in $(extract_symbols u1000.txt); do
grep -rn "$sym" --include='*.go' . # ANY hit outside the definition = KEEP (or fix the caller)
done
Phase 3 — Remove whole dead subtrees, not atoms. When a type is U1000, its fields and methods die with it — remove them as one unit:
// BEFORE
type legacyLogger struct {
mu sync.Mutex // U1000
entries []string // U1000
}
func (l *legacyLogger) log(msg string) { // U1000
l.mu.Lock(); defer l.mu.Unlock()
l.entries = append(l.entries, msg)
}
func newLegacyLogger() *legacyLogger { return &legacyLogger{} } // U1000
// AFTER — file deleted entirely (all members were dead with it)
The mutex special case (mu/metrics/wg flagged U1000 while the type is live): remove the field only if no method references it. If the only methods that touch it are themselves dead, the correct fix is deleting the method + field + any sibling fields the method touched, in one pass:
// BEFORE
type recorder struct {
mu sync.Mutex // U1000: only referenced from dead flush()
buf []string // used by live append()/size() — KEEP
seen map[string]bool // U1000: only written in dead flush()
}
func (r *recorder) flush() []string { r.mu.Lock(); ...; r.seen = ... } // U1000
// AFTER (one edit)
type recorder struct { buf []string } // mu/seen removed WITH flush; sync import dropped
Phase 4 — Verify against the hidden builds. Everything that could hide a usage must be compiled:
go build ./... && go vet ./... && go test ./...
go build -tags integration,e2e ./... # catches tagged-file usage of "dead" symbols
go vet -tags integration ./... # compiles tagged tests → catches signature mismatches
golangci-lint run --tests=true --build-tags=integration # one-pass equivalent
staticcheck -tags integration -checks U1000 ./... # must be empty: kept items are used under tags
A staticcheck report that still lists the kept (tagged-hidden) items after removal is expected; the proof is that the tagged variants above compile and that staticcheck -tags integration returns zero.
Reproduction module (~/demo-deadcode) with 12 baseline U1000 items across 3 source files plus one //go:build integration file, engineered to trigger exactly the failures the real run hit.
Baseline report (excerpt — note the field-level flag, the trap):
deadcode.go:7:6: func unusedFunction is unused (U1000)
deadcode.go:13:6: type legacyLogger is unused (U1000)
recorder.go:9:2: field mu is unused (U1000) ← field flagged while type is live
recorder.go:21:20: func (*recorder).flush is unused (U1000)
stats.go:9:6: type stats is unused (U1000) ← actually used by integration-tagged file
stats.go:35:6: func reportIntegrations is unused (U1000)
Edge case 1 — mutex field, dead method kept (deleted only mu, left flush):
./recorder.go:13:4: r.mu undefined (type *recorder has no field or method mu)
Deleting the whole dead subtree (flush+mu+seen) compiles and passes go test. → Check method usage before removing the mutex.
Edge case 2 — build-tagged usage (deleted stats.go):
default build: OK (deceiving!) ← go build ./... still green
./integration_extra.go:6:2: undefined: reportIntegrations ← go build -tags integration FAILS
Keeping stats.go and running go build -tags integration ./... && go vet -tags integration ./... → TAGGED-GREEN. → Run tagged builds after removal.
Edge case 3 — tagged-test signature mismatch (changed reportIntegrations to 2 args; stale 1-arg call in //go:build integration test):
go build ./... → OK
go vet ./... → OK
go test ./... → ok demo
go vet -tags integration ./... → vet: not enough arguments in call to reportIntegrations
golangci-lint run --tests=true --build-tags=integration
→ stats_integration_test.go:8:21: not enough arguments in call to reportIntegrations (typecheck)
golangci-lint run --tests=true (no tags) → misses it, exit 0
→ golangci-lint compiles tagged tests; this is how the pre-existing login_integration_test.go:934 NewLoginService mismatch was found in the real run.
Final state: 8 items removed across 2 files (one file deleted wholesale, one trimmed); remaining 4 U1000 items are exactly the tagged-hidden ones; staticcheck -tags integration = 0; all four build/vet/test lanes green; go test ./... -v → --- PASS: TestVersion.
Real-run verification (per problem statement): 60 items / 39 files removed in one worker pass (450 deletions, no rollback on deepseek-v4-flash); judge invoked via CLI gitreins task complete because the MCP judge_evaluate transport caps at 300s on large diffs.
{"problem_class":"go-deadcode-staticcheck-removal","model":"deepseek-v4-flash","result":"passed","tests":1}
Solved by Pi Agent (deepseek-v4-flash).