How to Detect Dead Code Using Code-Graph-RAG: A Complete Guide
To detect dead code with Code-Graph-RAG, build a call graph of your repository in Memgraph, then run cgr dead-code to find symbols unreachable from any entry point, test, or public API.
Dead code increases maintenance burden, compile times, and security surface area. Code-Graph-RAG solves this by combining Tree-sitter parsing with graph-based reachability analysis. This guide walks through the mechanism, CLI usage, Python API, and CI integration for the vitali87/code-graph-rag open-source tool.
How Dead Code Detection Works in Code-Graph-RAG
Code-Graph-RAG detects dead code through a four-stage pipeline implemented in codebase_rag/dead_code.py:
Stage 1: Graph Construction
The tool parses every source file with Tree-sitter, storing nodes (functions, methods, classes, modules) and edges (CALLS, REFERENCES, INSTANTIATES, INHERITS) in a Memgraph knowledge graph.
The parser lives in the codebase_rag package. Schema constants are defined in codebase_rag/constants.py.
Stage 2: Root Identification
Detection starts from roots—symbols guaranteed to be reachable. The _is_root function at line 91 in codebase_rag/dead_code.py combines multiple rule thunks to identify:
- Exported/public symbols
- Test symbols (unless disabled with
--no-include-tests) - Symbols with well-known framework decorators
- Dunder methods (
__init__,__call__, etc.) - Language-specific entry points (
mainin C/C++, Rust runtime hooks, Java serialization methods)
You can extend roots with custom entry points and decorators via CLI flags.
Stage 3: Reachability Walk
The private _walk helper at line 77 performs a breadth-first traversal following:
CALLSandREFERENCES(always)INSTANTIATESandINHERITS(when--classesis enabled)
All visited symbols are marked live.
Stage 4: Dead Code Report
After the walk, unvisited symbols are considered dead. The dead_code_from_graph function at line 90 filters results by user-provided --exclude globs and returns the final set.
Quick Start: CLI Workflow
Run dead code detection in three steps:
# 1. Start the Memgraph+Qdrant stack (once per machine)
cgr daemon up
# 2. Index your repository
cgr start --repo-path /path/to/project --update-graph --clean
# 3. Detect dead code
cgr dead-code
CLI Options for Dead Code Detection
The cgr dead-code command (implemented in evals/dead_code.py) supports fine-grained control:
| Option | Purpose |
|---|---|
-e / --entry-point <suffix> |
Treat symbols ending with value as roots (e.g., -e main) |
--decorator-root <name> |
Treat decorated symbols as roots (e.g., --decorator-root celery_app.task) |
--exclude <glob> |
Exclude files matching glob after the walk; quote globs with wildcards |
--include-tests / --no-include-tests |
Toggle test code as roots (default: on) |
--classes |
Include unreachable classes in report (default: off) |
--format json|table |
Choose output format |
--fail-on-found |
Exit status 1 if any dead code found (CI-friendly) |
-o / --output <file> |
Write report to file instead of stdout |
Practical CLI Examples
Find dead code excluding generated files, failing CI if found:
cgr dead-code \
--format json \
--output dead-code.json \
--fail-on-found \
--exclude '*_generated*'
Treat Flask route handlers as entry points:
cgr dead-code --decorator-root flask.app.route --decorator-root flask.blueprints.Blueprint.route
Include class reachability analysis:
cgr dead-code --classes --format table
Detecting Dead Code with the Python API
For programmatic access, import from codebase_rag.dead_code:
from codebase_rag.dead_code import dead_code_from_graph, default_dead_code_config
from codebase_rag.types_defs import DeadCodeConfig
# `ingestor` is a GraphQueryClient with loaded graph data
nodes, rels = ingestor.nodes, ingestor.rels
config: DeadCodeConfig = default_dead_code_config(
include_tests=False,
include_classes=False,
exclude_patterns=("*/generated/*", "*/vendor/*")
)
dead_symbols = dead_code_from_graph(
nodes,
rels,
project_prefix="mycompany.", # filter to your namespace
config=config
)
print("Dead symbols:", sorted(dead_symbols))
Key parameters in DeadCodeConfig:
include_tests: Whether test functions/methods seed the reachability walkinclude_classes: Whether to walkINSTANTIATESandINHERITSedgesexclude_patterns: Glob patterns applied post-analysis to filter results
CI/CD Integration
Integrate dead code detection into pipelines to prevent accumulation:
#!/bin/bash
set -e
cgr daemon up --detach
cgr start \
--repo-path . \
--update-graph \
--clean
cgr dead-code \
--fail-on-found \
--exclude '*/migrations/*' \
--exclude '*/tests/fixtures/*' \
--output dead-code-report.json
echo "No dead code detected"
The --fail-on-found flag ensures builds fail when unreachable code exists, enforcing cleanup before merge.
Key Source Files
| File | Role |
|---|---|
codebase_rag/dead_code.py |
Core engine: dead_code_from_graph, _is_root, _walk |
evals/dead_code.py |
CLI command implementation (cgr dead-code) |
codebase_rag/constants.py |
Graph schema constants (CALLS, REFERENCES, etc.) |
docs/guide/dead-code.md |
Official user documentation |
Summary
- Graph-first approach: Code-Graph-RAG uses Tree-sitter + Memgraph to model call relationships precisely
- Configurable roots: Entry points, tests, decorators, and public APIs define the live code boundary
- Fast analysis: Reachability runs entirely on the graph without re-parsing source files
- Multiple interfaces: Use
cgr dead-codefor automation ordead_code_from_graph()for custom workflows - CI-ready:
--fail-on-foundand--format jsonintegrate with GitHub Actions, GitLab CI, and Jenkins
Frequently Asked Questions
What programming languages does Code-Graph-RAG support for dead code detection?
Code-Graph-RAG supports any language Tree-sitter can parse. The CALLS, REFERENCES, INSTANTIATES, and INHERITS edges are language-agnostic, while language-specific root detection handles entry points like main in C/C++, Rust runtime hooks, and Java serialization methods automatically.
Can I exclude specific files or directories from dead code analysis?
Yes. Use the --exclude <glob> flag multiple times to filter paths after the reachability walk. Quote globs containing wildcards to prevent shell expansion: --exclude '*/node_modules/*' or --exclude 'build/**'.
Why does Code-Graph-RAG include test code as roots by default?
Tests often exercise code that no production entry point reaches. Including tests as roots prevents false positives on test utilities and fixtures. Disable with --no-include-tests to find production-only dead code.
How does dead code detection differ from a simple "unused function" linter?
Traditional linters perform static analysis on single files or limited cross-file scope. Code-Graph-RAG builds a complete call graph across your entire codebase including cross-module references, inheritance chains, and decorator-based registration—catching dead code that file-scoped tools miss.
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 →