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 (main in 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:

  • CALLS and REFERENCES (always)
  • INSTANTIATES and INHERITS (when --classes is 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 walk
  • include_classes: Whether to walk INSTANTIATES and INHERITS edges
  • exclude_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-code for automation or dead_code_from_graph() for custom workflows
  • CI-ready: --fail-on-found and --format json integrate 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:

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 →