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@fixturedefined inDEFAULT_ROOT_DECORATORSwithincodebase_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_pointsin theDeadCodeConfigobject
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_NODESselects every node whose qualified name starts with the project prefix (functions, methods, classes, and modules)CYPHER_DEAD_CODE_RELSpulls 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.pyto identify where execution begins. - Graph retrieval uses optimized Cypher queries (
CYPHER_DEAD_CODE_NODESandCYPHER_DEAD_CODE_RELS) to pull all callable symbols and their relationships from Neo4j. - Reachability analysis performs a multi-source BFS via
_walk()indead_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →