Skip to content

Dead code

dead_code

Dead code detection via AST caller analysis.

Enumerates all symbols in a package and flags those with zero callers after applying smart exemptions (dunders, tests, decorators, protocols, overrides, __all__ exports).

Example::

Text Only
>>> from axm_ast.core.analyzer import analyze_package
>>> from axm_ast.core.dead_code import find_dead_code, format_dead_code
>>> pkg = analyze_package(Path("src/mylib"))
>>> dead = find_dead_code(pkg)
>>> print(format_dead_code(dead))

DeadSymbol dataclass

An unreferenced symbol detected by dead code analysis.

Source code in packages/axm-ast/src/axm_ast/core/dead_code.py
Python
@dataclass(frozen=True, slots=True)
class DeadSymbol:
    """An unreferenced symbol detected by dead code analysis."""

    name: str
    module_path: str
    line: int
    kind: str  # "function", "method", "class"

find_dead_code(pkg, *, include_tests=False)

Detect unreferenced symbols across a package.

Algorithm
  1. Enumerate all functions and classes across all modules.
  2. For each symbol, check if it has any callers or references.
  3. Apply exemptions (dunders, tests, exports, decorators, entry points, etc.).
  4. For methods, check override chains.
  5. Also scan a sibling tests/ directory for callers.
  6. Detect lazy imports inside function bodies.

.. warning:: Reference matching is by name only. Liveness is decided against a single global set[str] of referenced names, so a dead symbol that shares its name with a live, distinct symbol elsewhere is wrongly considered referenced and omitted from the result (a false negative). This is an intrinsic tree-sitter limitation — no type or scope inference is performed — and mirrors the homonym ambiguity documented on :func:~axm_ast.core.callers.find_callers. Symbols reported as dead are therefore high-confidence; truly-dead symbols that are homonymous with a live one may be silently missed.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package from analyze_package().

required
include_tests bool

If True, also scan modules inside tests/ directories. Defaults to False.

False

Returns:

Type Description
list[DeadSymbol]

List of dead symbols, sorted by module path then line number.

Source code in packages/axm-ast/src/axm_ast/core/dead_code.py
Python
def find_dead_code(
    pkg: PackageInfo,
    *,
    include_tests: bool = False,
) -> list[DeadSymbol]:
    """Detect unreferenced symbols across a package.

    Algorithm:
        1. Enumerate all functions and classes across all modules.
        2. For each symbol, check if it has any callers or references.
        3. Apply exemptions (dunders, tests, exports, decorators,
           entry points, etc.).
        4. For methods, check override chains.
        5. Also scan a sibling ``tests/`` directory for callers.
        6. Detect lazy imports inside function bodies.

    .. warning:: Reference matching is **by name only**. Liveness is decided
        against a single global ``set[str]`` of referenced names, so a dead
        symbol that shares its name with a live, distinct symbol elsewhere is
        wrongly considered referenced and **omitted from the result** (a false
        negative). This is an intrinsic tree-sitter limitation — no type or
        scope inference is performed — and mirrors the homonym ambiguity
        documented on :func:`~axm_ast.core.callers.find_callers`. Symbols
        reported as dead are therefore high-confidence; truly-dead symbols that
        are homonymous with a live one may be silently missed.

    Args:
        pkg: Analyzed package from ``analyze_package()``.
        include_tests: If ``True``, also scan modules inside ``tests/``
            directories. Defaults to ``False``.

    Returns:
        List of dead symbols, sorted by module path then line number.
    """
    dead: list[DeadSymbol] = []

    test_pkg = _load_test_package(pkg.root)
    all_refs = _gather_all_refs(pkg, test_pkg)

    entry_points = _load_entry_point_symbols(pkg.root)

    # Also exempt framework-detected entry points (decorators, test_, __main__).
    from axm_ast.core.flows import find_entry_points

    for ep in find_entry_points(pkg):
        entry_points.add(ep.name)

    namespace_modules = find_namespace_modules(pkg)

    ctx = _ScanContext(
        entry_points=entry_points,
        all_refs=all_refs,
        extra_pkg=test_pkg,
        namespace_modules=namespace_modules,
    )

    for mod in pkg.modules:
        # Skip test files — they are consumers, not targets.
        path_name = mod.path.name
        if path_name.startswith("test_") or path_name == "conftest.py":
            continue
        if not include_tests and _is_in_tests_dir(mod.path):
            continue

        dead.extend(_scan_functions(mod, pkg, ctx))
        dead.extend(_scan_classes(mod, pkg, ctx))

    dead.sort(key=lambda d: (d.module_path, d.line))
    return dead

find_namespace_modules(pkg)

Find modules that are imported as namespace objects somewhere in pkg.

A module is considered a "namespace import" when it appears as: - from pkg import mod where mod resolves to a module file - import pkg.mod (bare module import)

Public symbols in such modules may be accessed via attribute access (mod.func()) and would not show up in a direct caller search.

Source code in packages/axm-ast/src/axm_ast/core/dead_code.py
Python
def find_namespace_modules(pkg: PackageInfo) -> set[Path]:
    """Find modules that are imported as namespace objects somewhere in *pkg*.

    A module is considered a "namespace import" when it appears as:
    - ``from pkg import mod`` where *mod* resolves to a module file
    - ``import pkg.mod`` (bare module import)

    Public symbols in such modules may be accessed via attribute access
    (``mod.func()``) and would not show up in a direct caller search.
    """
    mod_stems: dict[str, Path] = {mod.path.stem: mod.path for mod in pkg.modules}
    return {
        mod_stems[s]
        for mod in pkg.modules
        for s in _iter_namespace_stems(mod)
        if s in mod_stems
    }

format_dead_code(results)

Format dead code results as human-readable grouped output.

Groups results by module path, then lists each dead symbol with its line number and kind.

Parameters:

Name Type Description Default
results list[DeadSymbol]

List of dead symbols from find_dead_code().

required

Returns:

Type Description
str

Formatted string suitable for terminal display.

Source code in packages/axm-ast/src/axm_ast/core/dead_code.py
Python
def format_dead_code(results: list[DeadSymbol]) -> str:
    """Format dead code results as human-readable grouped output.

    Groups results by module path, then lists each dead symbol
    with its line number and kind.

    Args:
        results: List of dead symbols from ``find_dead_code()``.

    Returns:
        Formatted string suitable for terminal display.
    """
    if not results:
        return "✅ No dead code detected."

    # Group by module.
    groups: dict[str, list[DeadSymbol]] = {}
    for sym in results:
        groups.setdefault(sym.module_path, []).append(sym)

    parts: list[str] = [f"💀 {len(results)} dead symbol(s) found:\n"]

    for mod_path, symbols in groups.items():
        parts.append(f"  📄 {mod_path}")
        for sym in symbols:
            parts.append(f"    L{sym.line:>4d}  {sym.kind:<10s}  {sym.name}")
        parts.append("")

    return "\n".join(parts)