How to Detect Dead Code by Walking Call Graph Edges from Entry Points

You detect dead code by identifying entry points and framework hooks as roots, fetching the call graph from a Neo4j database, and performing a multi-source BFS traversal to mark reachable nodes; everything unreachable is flagged as dead code.

The vitali87/code-graph-rag repository implements a language-agnostic dead-code detector that traverses a Neo4j-backed call graph starting from runtime entry points. Unlike traditional static analysis, this approach traces actual execution paths across Python, Rust, Go, JavaScript, and other languages to identify truly unreachable symbols.

How Dead-Code Detection Works in Code-Graph-RAG

The system operates in three distinct phases: root identification, graph retrieval, and reachability traversal. According to the source code, this design replaces an earlier O(roots × graph) per-root BFS approach with two linear scans, eliminating timeout issues on large codebases.

Step 1: Identify Reachability Roots

Root detection happens in codebase_rag/dead_code.py (lines 12-36) through pattern matching and language-specific heuristics. The detector recognizes three categories of roots:

  • Root decorators – Framework annotations like @route, @task, and @fixture defined in DEFAULT_ROOT_DECORATORS within codebase_rag/constants/deadcode_roots.py

  • Language-specific entry points – Runtime hooks such as Rust trait methods, Java serialization hooks, C# attributes, NestJS component decorators (matched by _is_nest_root()), React lifecycle methods (_is_react_root()), and C++ operator overloads (_is_cpp_operator_root())

  • Explicit user configuration – Additional entry points supplied via config.entry_points in the DeadCodeConfig object

Helper predicates including _is_dunder(), _is_rust_runtime_root(), and _is_cpp_operator_root() filter these patterns to build the initial root set.

Step 2: Fetch the Call Graph

The engine retrieves the complete codebase graph using optimized Cypher queries defined in codebase_rag/cypher_queries.py:

  • CYPHER_DEAD_CODE_NODES selects every node whose qualified name starts with the project prefix (functions, methods, classes, and modules)
  • CYPHER_DEAD_CODE_RELS pulls all relationships that affect reachability: CALLS, REFERENCES, INSTANTIATES, INHERITS, DEFINES, DEFINES_METHOD, OVERRIDES, and IMPLEMENTS

These queries execute through the GraphQueryClient class from codebase_rag/graph_loader.py, returning raw Neo4j results that the Python layer converts into an adjacency map.

Step 3: Walk the Graph with Multi-Source BFS

The core reachability algorithm lives in dead_code.py. First, the _walk() function (lines 75-90) performs a multi-source breadth-first search starting from the root set, traversing CALLS and REFERENCES edges to populate a live set of reachable qualified names.

After the initial traversal, a fixed-point expansion loop (lines 60-86) repeatedly revives additional nodes:

  • Decorated closures that wrap entry points
  • Factory-class methods instantiated at runtime
  • Override chains in inheritance hierarchies

This proceeds until no new nodes are added, ensuring framework-specific dispatch mechanisms don't falsely report code as dead.

Key Implementation Files

File Purpose
codebase_rag/dead_code.py Core engine containing collect_dead_code(), dead_code_from_graph(), and the _walk() BFS implementation
codebase_rag/constants/deadcode_roots.py Language-specific root definitions including DEFAULT_ROOT_DECORATORS and framework hook patterns
codebase_rag/cypher_queries.py Cypher query constants CYPHER_DEAD_CODE_NODES and CYPHER_DEAD_CODE_RELS
codebase_rag/graph_loader.py GraphQueryClient for executing database queries
evals/dead_code.py Example evaluation script demonstrating invocation patterns

Practical Usage Example

Configure and run the detector using the DeadCodeConfig object to customize root detection and filtering:

from codebase_rag.dead_code import collect_dead_code, default_dead_code_config
from codebase_rag.graph_loader import GraphQueryClient

# Initialize the graph client

client = GraphQueryClient(...)

# Configure detection parameters

cfg = default_dead_code_config(
    include_tests=False,          # Exclude test files from analysis

    include_classes=True,         # Treat classes as standalone nodes

    exclude_patterns=("*/generated/*",)  # Skip generated code

)
cfg.entry_points = ("myproject.main",)   # Add explicit roots

# Execute dead-code detection

dead_rows = collect_dead_code(client, "myproject", cfg)

# Report results

for row in dead_rows:
    print(f"{row['qualified_name']} – {row['path']}:{row['start_line']}-{row['end_line']}")

Internally, dead_code_from_graph() constructs an adjacency map from the fetched relationships and executes the reachability analysis:

from collections import defaultdict

def dead_code_from_graph(nodes, rels, project_prefix, config):
    # Build adjacency for CALLS and REFERENCES edges

    adjacency = defaultdict(set)
    for src_label, src_qn, rel_type, dst_label, dst_qn in rels:
        if rel_type in {"CALLS", "REFERENCES"}:
            adjacency[src_qn].add(dst_qn)
    
    # Initialize roots from decorators and language-specific hooks

    roots = {...}  # See dead_code.py lines 5-100

    live = set(roots)
    
    # Multi-source BFS traversal

    _walk(roots, adjacency, live)
    
    # Fixed-point expansion for factories and overrides

    # ... (lines 60-86)

    
    return candidates - live  # Dead nodes

Summary

  • Root detection combines decorator patterns, language-specific runtime hooks, and user-supplied entry points defined in codebase_rag/constants/deadcode_roots.py to identify where execution begins.
  • Graph retrieval uses optimized Cypher queries (CYPHER_DEAD_CODE_NODES and CYPHER_DEAD_CODE_RELS) to pull all callable symbols and their relationships from Neo4j.
  • Reachability analysis performs a multi-source BFS via _walk() in dead_code.py, followed by fixed-point expansion for factories and override chains to eliminate false positives.
  • Performance optimization shifted from O(roots × graph) per-root traversals to two linear scans: one to fetch data, one to propagate reachability.

Frequently Asked Questions

What types of entry points does the detector recognize automatically?

The detector recognizes framework-specific decorators like @route and @task, language runtime hooks such as main or init functions, Rust trait methods, Java serialization hooks, C# attributes, NestJS components, and React lifecycle methods. These patterns live in codebase_rag/constants/deadcode_roots.py and are matched by helper functions like _is_nest_root() and _is_react_root() in dead_code.py.

How does the system handle dynamic code patterns like factories or inheritance?

After the initial BFS, the system runs a fixed-point expansion loop (lines 60-86 of dead_code.py) that repeatedly evaluates decorated closures, factory-class instantiations, and method override chains. This process continues until no new reachable nodes are discovered, ensuring that dynamically dispatched code isn't incorrectly flagged as dead.

Can I exclude specific files or directories from dead-code analysis?

Yes, pass exclusion patterns through the DeadCodeConfig object. The default_dead_code_config() function accepts an exclude_patterns parameter (e.g., ("*/generated/*", "*/tests/*")) to filter out generated code, test files, or any directory matching the supplied globs before the reachability analysis begins.

Why does the system use Neo4j instead of static AST parsing?

The Neo4j graph model captures cross-language relationships (CALLS, REFERENCES, OVERRIDES, IMPLEMENTS) across the entire codebase, enabling whole-program analysis that spans multiple languages and frameworks. This graph structure supports efficient reachability queries and fixed-point iterations that would be difficult to implement with traditional file-by-file AST traversal.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →