Verified 2026-09-27

Dead Code Detector: What a Static Analyzer Can Actually Prove

A single-file, zero-backend dead code detector for Python, JavaScript, and TypeScript — and an honest account of what its heuristics can and cannot establish.

Contents

    Dead code is the only class of defect that is invisible at runtime. Nothing crashes, no test fails, no alert fires — the code simply sits there, inflating every future read of the file. Removing it is a high-ratio maintenance win, perpetually deferred because proving a symbol unused costs more than ignoring it.

    The Dead Code Detector is a single-file browser tool that front-loads that proof. Paste a source file and it returns a risk-ranked deletion queue: unused exports, orphan functions, stale imports, unreachable statements, and transitively dead helpers. It runs entirely client-side with deterministic heuristics — no model, no network call, no upload. That constraint is the point, and it also sets the ceiling on what the tool can honestly claim.

    Three ways dead code hides

    Mature codebases rarely contain one obvious dead function. They contain three separate failure modes, each requiring a different detection strategy:

    1. Never-referenced symbols. A helper was written, its call site was refactored away, and the definition stayed. Tooling for this is old and settled — ESLint’s no-unused-vars and @typescript-eslint/no-unused-vars have flagged it for years, and Python’s vulture does the equivalent for .py files.
    2. Unreachable code. Statements after return, raise, throw, break, or continue inside the same block can never execute. ESLint’s no-unreachable and Pylint’s unreachable treat this as a correctness smell rather than a cleanup task.
    3. Transitive deadness. This is the one that defeats single-pass linters. If parseConfig is unused, then normalizeKeys — referenced only from inside parseConfig — is also dead, but a naive scan sees a reference and stays silent.

    What the detector actually measures

    The tool parses the pasted file without an AST library (it must run with zero dependencies), so the heuristics are token- and indentation-based rather than grammar-based:

    • Unused exports — export prefixes, export { … } lists, export default, module.exports / exports.x assignments, and Python __all__ entries plus module-level public names.
    • Orphan functions, methods, and classes — declared names with zero references outside their own declaration span.
    • Orphan types — TypeScript interface X and type X = aliases. TypeScript’s own noUnusedLocals covers the variable case; unused exported types usually survive because they are exported.
    • Dead-code cascade — a fixpoint pass that marks symbols referenced only from already-dead bodies as transitively dead. The pass deliberately never propagates through an export, because an exported symbol may be consumed by another file.
    • Unreachable statements — indentation-aware for Python, brace-depth-aware for JS/TS, stopping at dedent, }, case, default, else, except, and finally.
    • Statically dead branches — if False: / while False, if (false | 0 | null | undefined), and the else clause after if True.
    • Stale imports — Python import / from … import (with aliases and parenthesised multi-line forms), ESM default/named/namespace/import type, and CommonJS require including destructuring.

    Two implementation details matter for accuracy. First, a masking pass blanks out comment and string bodies before reference counting, so an identifier mentioned in a docstring does not register as a use — quoting the symbol in a comment is not a call site. Second, that same masking is used inversely: when a dead symbol’s name appears inside a string or comment (a dynamic reference, a getattr by name), the finding’s risk is escalated from low to high, because a static scan cannot follow a string into a lookup table.

    Each finding carries an estimated LOC cost, a source line range, and a risk level — high for decorated or explicitly exported declarations, medium for public Python names and methods, low for cascade hits. Reads flow into a grouped deletion queue, a line-map heat strip, and a copyable Markdown report.

    How to use it

    Open /tools/dead-code-detector/, pick the language hint, and paste one file. Press Scan (or Ctrl+Enter). The tool auto-scans the bundled Python sample on load. Work it top-down: high-risk items deserve a repository-wide grep before deletion; low-risk cascade items are safe once their root is gone. Copy the Markdown report into a cleanup PR’s description.

    Everything runs in the tab. The page makes no requests after load, which is why it is safe to paste proprietary code.

    Known limits

    This is a static, single-file heuristic — not a semantic analyzer, and not tsc with whole-program type information. Concretely:

    • Single-file scope. A symbol exported from the pasted file and consumed elsewhere will be flagged as a candidate. Treat export findings as “confirm with a repo-wide grep”, which is exactly why they are rated high-risk.
    • No type resolution. A reference through a computed member (obj[methodName]) is invisible to it.
    • No dynamic dispatch. Reflection, decorators that register handlers, and string-keyed registries cannot be followed.
    • Regex, not a parser. Pathological or unbalanced input degrades the tokenizer; the tool reports a parse warning rather than guessing silently.

    Tree shaking and bundlers remove a narrower, provable slice of unused code at build time; this tool is a pre-commit funnel that tells a human where to look, and it should be read that way. Python users can cross-check its output against ast-based analyzers like vulture.

    On the roadmap

    Dead Code Detector moves from Planned to Complete on the tools roadmap, the fourth of eighteen planned tools. Next in the Code Analysis category is the Code Churn Forecaster. If the heuristics misjudge a file, the failure mode is documented in-page under // known-limits.