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::

>>> 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
@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.

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
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.

    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)

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

    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

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
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)