Dead Code Analysis: The Best Detection Tools by Language (2026)

Raghav Chamadiya13 min read

dead code analysis · dead code detection tools · find dead code · unused code detection · unused dependencies · knip vs ts-prune

On this page

Dead code analysis means finding code and dependencies you can delete without changing behavior. The right tool depends on the language: Vulture or deadcode for Python, Knip for JavaScript and TypeScript, Go's deadcode command, IntelliJ inspections or PMD for Java, Roslyn analyzers for C#. For repos that mix languages, use a graph-based tool, then review every finding before deleting.

LanguageStart withAlso worth knowingFinds unused dependencies?
PythonVulturedeadcode (has --fix)No, use deptry
JavaScript / TypeScriptKnipts-prune (unused exports only)Yes, Knip does
Godeadcodestaticcheck U1000go mod tidy covers most of it
JavaIntelliJ "Unused declaration" inspectionPMD Unused* rules, UCDetectorNo
C#Roslyn rules IDE0051 / IDE0052IDE and ReSharper inspectionsNo
Rustthe compiler's dead_code lintcargo-udeps, cargo-machete
Mixed-language reporepowise get_dead_codethe per-language tools aboveUnused monorepo packages

Scroll the table sideways to see every column.

Unreachable files, unused exports and zombie packages all need different heuristics, and a tool that flags one well may miss the others. Below I go language by language, then cover unused dependencies, then the cleanup workflow that keeps false positives under control.

We build repowise, so weigh our entry accordingly. The measurements we publish about it, with their methods, are on the benchmarks page.

Types of dead code

Dead code is code you can delete without changing behavior. That sounds simple until you look at a real repo. Static analyzers, compilers, and IDE inspections all disagree on what counts as dead, because they make different assumptions about entry points, reflection, config files, and generated code.

Unreachable files

An unreachable file is a file that no runtime path can import or load. In a JS app, this might be a utility module left behind after a refactor. In Python, it might be a helper that nothing imports. In a monorepo, the same file can be reachable from one package and dead from another.

Tools that read one file at a time miss this, because only a project graph shows which files can be reached from an entry point.

Unused exports

Unused exports are the easiest place to start. These are functions, classes, types, or constants that other files never import.

Knip focuses on exported values and types across files; it does not report unused locals inside a file. Its docs also define unused files as project files that are not resolved from entry files, which is the right model for codebase-level cleanup. (knip.dev)

Zombie packages

Zombie packages are dependencies that still sit in package.json, requirements.txt, or similar manifests even though the code no longer imports them. An unused dependency still costs build time, adds security surface and makes upgrades noisier. The section on unused dependencies below covers the tools that find them.

How modern detectors work

Older detectors scanned text with a few syntax heuristics, while the better tools build a graph of the project.

Confidence scoring

A good dead code tool should tell you how sure it is. Vulture attaches a confidence value to every finding, from 60% for unused functions and attributes up to 100% for unreachable code, and --min-confidence 100 limits the report to the findings it is certain about. (pypi.org) That flag gives you a short list you can act on before you review the less certain findings.

repowise's get_dead_code is built around the same idea: findings are tiered high, medium or low confidence, and they sit on top of graph, git and context layers, so a finding can be paired with ownership, history, and dependency impact. (repowise.dev)

Cross-language graph traversal

The main reason dead code gets tricky is that modern repos are not single-language. A TypeScript frontend calls a Python API, which shells out to Go, which loads config from YAML. A file can look unused until you include generated code, dynamic imports, and framework conventions.

Every per-language tool below stops at its language boundary. That is fine for a single-language repo and a real gap in a polyglot one.

Python: Vulture and deadcode

Vulture

Vulture is the classic Python dead-code detector. It uses Python's AST to find unused classes, functions, methods, variables, imports, attributes and unreachable code. Version 2.16 shipped in March 2026 and needs Python 3.9 or newer. (pypi.org)

Reasons teams still use it:

  • It is lightweight and easy to run in CI.
  • Confidence values let you start with the safe 100% findings.
  • --make-whitelist turns today's false positives into a file you check in, so the next run only shows new findings.

Where it struggles:

  • dynamic imports and getattr
  • decorators and framework magic (Django views, FastAPI routes, Celery tasks, pytest fixtures)
  • anything reached only from config or templates

Treat Vulture as a triage tool: run it, read the list and confirm each candidate before you delete anything.

deadcode

deadcode is a newer Python tool aimed at whole-codebase checks that Ruff and flake8 do not do (they only look at unused locals). It reports unused variables, functions, classes, methods, attributes, imports, empty files and unreachable blocks under DCxxx codes, and it can delete what it finds with deadcode . --fix, or show the diff first with --fix --dry. (github.com)

I would use --dry first every time. Auto-fix is only as good as the tool's view of your entry points, and Python hides a lot of entry points.

JavaScript and TypeScript: Knip, and where ts-prune stands

Knip

Knip is the strongest open source choice for JS/TS dead-code cleanup. It finds unused files, unused exports and unused dependencies in one pass, and it ships 150+ plugins for frameworks and tools like Next.js, Vite, Vitest, Jest, Nx, Storybook and GitHub Actions, so it knows where their entry points live. (knip.dev)

Why I like it:

  • It knows the difference between unused files and unused exports.
  • It can remove unused files and exports with auto-fix when you are ready. (knip.dev)
  • It handles monorepos and workspaces. (knip.dev)

The tradeoff is the one that comes with any smart static analyzer: you must configure entry files correctly. Miss an entry point and Knip reports used code as unused. Make entry files too broad and you hide dead code. (knip.dev)

ts-prune

ts-prune is simpler. It reads your tsconfig and prints unused exports, nothing else. (npmjs.com) Its README now says the project "has been resurrected after a long hiatus" and is kept working because many projects still depend on it. (github.com) So it is maintained, but in a keep-it-running way.

I would not start a new codebase on it. Knip covers exports plus files plus dependencies. ts-prune still makes sense when you only want an unused-export list and already have other tools for the rest.

Go: deadcode and staticcheck

deadcode

The Go team published deadcode in December 2023 (go install golang.org/x/tools/cmd/deadcode@latest). It starts from each main package and uses Rapid Type Analysis, a whole-program analysis, to work out which functions can ever be reached. Everything else is reported as unreachable.

Two limits matter. It needs a main entry point, so for a library you run it with -test and let your tests be the entry points. And it cannot see functions called only from assembly or through go:linkname. (go.dev)

staticcheck U1000

Staticcheck's U1000 check reports unused functions, types, fields and constants. It was rewritten in Staticcheck 2023.1 with fewer false positives, including fixes for generics. It treats every exported package-level name as used, which keeps it quiet on libraries, and it also runs inside golangci-lint as the unused linter. (staticcheck.io)

Use U1000 in CI for the per-package view and deadcode occasionally for the whole-program view, because the two checks answer different questions.

Java: IDE inspections, PMD and UCDetector

Java has no single standard CLI for whole-program dead code, so most teams combine an IDE inspection, a CI rule set and sometimes an Eclipse plugin.

  • IntelliJ's "Unused declaration" inspection works best when you run it project-wide (Code, Analyze Code, Run Inspection by Name) as well as in the editor. You can tell it which annotations mark entry points, which matters for Spring, JAX-RS and test frameworks. (JetBrains)
  • PMD rules like UnusedPrivateMethod, UnusedPrivateField, UnusedLocalVariable, UnusedFormalParameter and UnusedAssignment run in CI on every build. They only cover private and local code, which is exactly why they are safe to enforce. (pmd.github.io)
  • UCDetector is an open source Eclipse plugin that finds unused public code and suggests narrowing visibility. It is old (2.0.0 is the latest release on SourceForge), but for an Eclipse shop it still answers the "is this public method used anywhere" question.

Reflection, dependency injection and serialization are the usual false positives in Java. Tell the tool about your framework annotations before trusting the list.

C#: Roslyn analyzers

For .NET, start with the analyzers that ship with the SDK. IDE0051 flags unused private members and IDE0052 flags private members that are written but never read. (Microsoft Learn) Set them to warning in .editorconfig and they run in every build. Public and internal code that nothing uses needs a solution-wide pass, which Rider or ReSharper can do.

Unused dependencies: the dead code in your manifest

A dependency nobody imports still gets installed, scanned and upgraded. These tools compare what your manifest declares with what your code imports.

EcosystemToolWhat it reportsStatus
JS / TSKnipUnused and unlisted dependencies, plus unused files and exportsActively maintained
JS / TSdepcheckUnused dependenciesNo longer maintained; its README recommends Knip
PythondeptryMissing (DEP001), unused (DEP002) and misplaced dev (DEP004) dependenciesWorks with pip, Poetry, PDM, uv and PEP 621
Rustcargo-udepsUnused crates, from compiler outputBuilds on stable, needs nightly to run
Rustcargo-macheteUnused crates, by scanning sourceFast and stable Rust, but imprecise by its own description
Gogo mod tidyRemoves modules nothing importsBuilt in

Scroll the table sideways to see every column.

If you still run depcheck in CI, switching to Knip is a cheap win: its maintainers say it "is no longer actively maintained" and point to Knip. (github.com)

For Rust, a common setup is cargo-machete in CI because it is fast, and cargo-udeps occasionally for the precise answer.

Mixed-language repos: repowise get_dead_code

Best for polyglot repos, large monorepos and teams that want dead-code findings with context.

Every tool above stops at its language. If your repo has a TypeScript frontend, a Python service and shared packages, you either run four tools and merge the results by hand, or you use something that reads the whole repo as one graph.

That second option is what we built get_dead_code in repowise for. It finds unreachable files, unused exports and unused monorepo packages from the dependency graph, tiers each finding by confidence, marks the ones that look safe to delete, and rolls them up by directory or owner. Because it is an MCP tool, a coding agent can ask for dead-code candidates in the same session as ownership, risk and dependency paths, which matters when a cleanup touches shared code. (repowise.dev) repowise is open source under AGPL-3.0 and self-hostable.

Inside their own languages, Knip and Vulture know framework conventions in more depth. I would use repowise for the cross-language view and the context, and keep the language tools in CI.

If you want a concrete output shape, the FastAPI dependency graph and FastAPI docs show what it produces on a real codebase, and live examples cover more repos.

Architecture map for repowise-dev/repowise with the toolbar filters All, Hot and Dead below the graphThe architecture map for repowise-dev/repowise. The Dead filter below the graph picks out the files the dead-code pass flagged, in place among the files that import them.

Dead Code Detection Comparison TableDead Code Detection Comparison Table

Dead-code detection in IDEs

Best for local edits and small cleanups.

IDE inspections are good feedback while you edit. They are not enough for repo-wide cleanup, especially when your app uses reflection, entry points are spread across config files, or you need reachability across packages. Use them for the file in front of you and a standalone tool for the audit.

Side-by-side comparison

ToolLanguageUnused exports / membersUnused filesUnused depsAuto-fixNotes
VulturePythonYesNoNoNoConfidence values, whitelist file
deadcodePythonYesEmpty filesNoYes (--fix)Whole-codebase checks Ruff lacks
deptryPythonNoNoYesNoMissing, unused, misplaced
KnipJS / TSYesYesYesYes150+ framework plugins
ts-pruneTSExports onlyNoNoNoMaintained to keep working
deadcode (Go)GoUnreachable functionsNoNoNoWhole-program, needs main or -test
staticcheck U1000GoYesNoNoNoExported names count as used
PMD Unused* rulesJavaPrivate and local onlyNoNoNoSafe to enforce in CI
IntelliJ inspectionJava, KotlinYesPartlyNoQuick-fixConfigure entry-point annotations
Roslyn IDE0051/52C#Private membersNoNoCode fixShips with the SDK
repowise get_dead_codeManyYesYesMonorepo packagesNoConfidence tiers, owner rollups, cross-language

Scroll the table sideways to see every column.

Cross-Language Dead Code GraphCross-Language Dead Code Graph

Cleanup playbook

Dead-code cleanup goes wrong when teams jump straight to deletion, so use a staged process. The find dead code guide walks through it on a real repo.

1. Start with one signal type

Pick either unused exports or unused files. Starting with one signal type keeps the review small enough to finish.

2. Sort by confidence

Delete obvious leaf code first:

  • private helpers no one imports
  • stale feature flags
  • abandoned modules with no dependents
  • dead packages with no runtime or build references

3. Check for framework entry points

Watch for:

  • route files
  • reflection-based lookups
  • CLI subcommands
  • plugin registries
  • test-only imports

4. Review ownership and history

Before you remove a shared module, check who owns it and how often it changes. A file nobody has touched in two years and a file that changed last week are different conversations, even if both look unused. repowise's git-intelligence layer shows hotspots and ownership next to the finding.

5. Remove in small patches

Small patches are easier to revert, and they make it obvious which change broke CI. If you use a tool with auto-fix, run it on one package or directory at a time.

6. Add guardrails

After cleanup:

  • keep the detector in CI
  • add a baseline or allowlist for known exceptions
  • review new dead-code hits in PRs

If you want the repo context around the cleanup, the FastAPI code health page shows how churn and complexity interact, and the ownership map for FastAPI shows the history that tells you who to ask before a refactor.

Final recommendation

If you need the shortest path:

  • Python: Vulture, plus deptry for packages
  • JS/TS: Knip
  • Go: staticcheck U1000 in CI, deadcode for a whole-program pass
  • Java: IntelliJ inspection project-wide, PMD in CI
  • C#: IDE0051 and IDE0052 as warnings
  • Mixed-language or context-heavy repos: repowise get_dead_code on top of the language tools

Whatever you pick, do not stop at a list of filenames. Pair findings with dependency paths, ownership and history before you delete.

To see it on your own code, paste a public repo URL on the homepage or run pip install repowise && repowise init locally. If the cleanup is a team effort, repowise for teams covers shared indexes and owner rollups.

How I checked

Tool status, versions and features come from each project's own README, docs or release notes, read on October 6, 2026. I did not run a head-to-head accuracy benchmark across these tools, so nothing here claims one finds more true dead code than another. False-positive rates depend heavily on how well you configure entry points.


For the full picture, see the cluster hub: How to prioritize technical debt (impact-per-effort).

FAQ

What is dead code analysis?

Dead code analysis finds code that can never run or is never used: unreachable functions, unused exports and private members, files nothing imports, and dependencies nothing references. Static tools do it by building a graph from entry points. Runtime tools, like coverage in production, find code that is reachable but never actually called.

What is the best dead code detection tool for Python?

Vulture is the standard starting point, with confidence values and a whitelist file for false positives. deadcode is a newer option that can also delete what it finds with `--fix`. For unused packages, add deptry. ([pypi.org](https://pypi.org/project/vulture/))

What is the best dead code detection tool for JavaScript and TypeScript?

Knip. It finds unused files, exports and dependencies in one pass, understands monorepos, and has plugins for most frameworks. ([knip.dev](https://knip.dev/))

Is ts-prune deprecated?

Not officially. Its README says it was revived after a long hiatus and is kept working because many projects depend on it. For new projects Knip covers more: files and dependencies as well as exports. ([github.com](https://github.com/nadeesha/ts-prune))

How do I find unused dependencies?

Use Knip for JavaScript and TypeScript, deptry for Python, cargo-machete or cargo-udeps for Rust, and `go mod tidy` for Go. Avoid depcheck in new setups, since its maintainers say it is no longer maintained.

How do I find dead code in Go?

Run `deadcode` from `golang.org/x/tools/cmd/deadcode` for whole-program reachability from your `main` packages, and staticcheck's U1000 (or golangci-lint's `unused` linter) in CI for unused code per package. ([go.dev](https://go.dev/blog/deadcode))