deadset-go finds dead code in a Go module and in the repositories that import it, and writes a report your CI can fail on.
It type-checks the module with its tests through golang.org/x/tools, its one run-time dependency, and reports without editing your code. It is in active development, so the report shape, exit codes and configuration keys can change between minor releases. It supports Linux only, needs Go 1.27.1 or later and is licensed under GPL-3.0-or-later.
deadset-go is built for a CI gate on dead code across a module, with every verdict from type information and the reference graph.
- It reports unused declarations, declarations only tests use, and tests of dead code.
- It reports exports only their own package uses, fields written and never read, and uncalled interface methods.
- Inside a function, it reports unused parameters, results and receivers, unreachable code and dead stores.
- It loads the consumers you name, so an export a downstream repository calls is never reported.
- Nine exemption classes hold back code that encoders,
fmt, templates or reflection reach. - The same tree gives the same report, as JSON, text, GitHub annotations, SARIF 2.1.0 or your own template.
Consider deadcode if you want the functions no call path from a main package reaches. It follows calls through func values, interface methods and reflection, and -whylive shows the path that keeps a function live.
go install github.com/cplieger/deadset-go/v2/cmd/deadset-go@latestdeadset-go loads packages with the go toolchain on your PATH and never downloads a module, so run go mod download in the module and in every consumer first. Then create a deadset.json at the module root that says whether the module is an application or a library, and run analyze there. Run it in a directory that no go.mod governs, and it analyzes every module below that directory in one report. A module listed in a go.work is loaded through that workspace, as go build loads it there, and the other modules that workspace lists below it are analyzed with it:
echo '{ "target": { "kind": "application" } }' > deadset.json
deadset-go analyze --report=deadset-report.jsonanalyze writes the JSON report to deadset-report.json and a text rendering beside it as deadset-report.json.txt. Take this main.go:
package main
import "fmt"
type counter struct {
count int
label string
}
func (c *counter) increment() { c.count++ }
func greet(name string) string { return "hello " + name }
func farewell(name string) string { return "bye " + name }
func main() {
c := &counter{label: "x"}
c.increment()
fmt.Println(greet("you"), c.count)
}The text rendering lists two findings:
main.go:7:2: field counter.label: field counter.label is written once and never read [certain] (DS1301)
main.go:14:6: function farewell: unexported function has no reference in the target [certain] (DS1002)
summary: 2 findings (0 allow, 0 warn, 2 deny), 1 deletable line, 0 suppressions in effect, 0 reasons recorded, 0 stale suppressions, 0 pending, 0 omitted
The run exits 1 because the report holds a finding at deny severity, and 0 when it holds none. Common next steps:
- Add
--format=sarifor--format=githubfor a SARIF file or GitHub annotations. Naming any format replaces the text rendering, so add--format=textto keep it. - Run
deadset-go explain farewellto see why one symbol is reported, retained, live or not reported. - To keep one declaration, put
//deadset:ignore DS1002 -- <reason>on the line above it. To accept today's findings, add--baseline-write=deadset-baseline.json.
A library's unused exports are reported only when a scope document lists its consumers. Put a scope.json in a folder that holds the library and its consumers, with each path relative to that file:
{
"target": { "path": "lib", "role": "target" },
"consumers": [{ "path": "app", "role": "consumer" }]
}Then run from that folder, with --target naming the library folder whose deadset.json says library:
deadset-go analyze --target=lib --scope=scope.json --report=deadset-report.jsondeadset-go is a standalone command, and its interface is seven verbs, the JSON report and the exit codes.
analyzewrites the report and exits with the verdict, andexplainanswers for one symbol.print-config,print-rootsandprint-retainedprint the resolved configuration with each setting's source, the root set, and every symbol an exemption held back.describeprints the analyzer, contract and schema versions and the conformance record as JSON, andversionprints the analyzer and contract versions.- The run exits 0 when no finding fails it and 1 when one does. It exits 2 on a usage error or a refused source-edit flag, and 3 when the analysis fails before a verdict. It exits 4 when a finding is about a symbol a cross-language edge names, which only the merge in deadset can settle.
The report follows the deadset contract. Configuration and invocation lists every flag, setting and suppression form.
- Source edits. Every verb only reports, and a flag whose name holds
fix,edit,deleteorrewriteexits with 2. The JSON report carries what an external codemod needs. - Network access. Consumers are local checkouts, and the toolchain runs with
GOPROXY=off. A module missing from the module cache then stops the run with exit 3 where a declared or host configuration needs it. - Runtime evidence. Coverage profiles and production logs are not inputs.
- Guesses. A kind ships only where its answer follows exactly from type information and the module graph.
- Other languages. deadset-ts analyzes TypeScript and JavaScript.
Conformance, platforms and non-goals explains each one.
deadset-go implements the deadset contract, which fixes the issue codes, the report schema and the exit codes. It answers all 214 of the contract's conformance fixtures that carry a Go rendering: 186 pass and 28 are pending a rule the contract has adopted and this release does not meet yet. Every report names that result.
- deadset-ts is the same analysis for TypeScript and JavaScript.
- deadset runs both analyzers as one command and merges their reports, resolving the references between Go and TypeScript code.
- Configuration and invocation lists every setting, verb, flag, suppression form and exit code.
- Issue kinds states what each code reports and what counts as a use.
- Exemption classes explains the nine reasons a symbol is held back.
- The encoding-reflection class lists what encoders and reflection retain.
- How the analysis decides covers the roots, consumers, build configurations and cgo files.
- Conformance, platforms and non-goals gives the contract version, the conformance result, the platforms and the performance budget.
See CONTRIBUTING.md.
This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.
This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.
GPL-3.0-or-later. See LICENSE.