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

> Learn how to detect dead code by walking call graph edges from entry points. Traverse your code's call graph using Neo4j and BFS to identify unreachable code.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: how-to-guide
- Published: 2026-08-13

---

**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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants/deadcode_roots.py) | Language-specific root definitions including `DEFAULT_ROOT_DECORATORS` and framework hook patterns |
| [`codebase_rag/cypher_queries.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/cypher_queries.py) | Cypher query constants `CYPHER_DEAD_CODE_NODES` and `CYPHER_DEAD_CODE_RELS` |
| [`codebase_rag/graph_loader.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/graph_loader.py) | `GraphQueryClient` for executing database queries |
| [`evals/dead_code.py`](https://github.com/vitali87/code-graph-rag/blob/main/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:

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

```python
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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants/deadcode_roots.py) and are matched by helper functions like `_is_nest_root()` and `_is_react_root()` in [`dead_code.py`](https://github.com/vitali87/code-graph-rag/blob/main/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`](https://github.com/vitali87/code-graph-rag/blob/main/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.