How Code-Graph-RAG Detects Dead Code Using Its Knowledge Graph

Code‑Graph‑RAG detects dead code by constructing a knowledge graph of symbols and relationships, then performing a breadth‑first search from identified entry points (roots) to mark reachable symbols; any symbol not reached is flagged as dead.

The vitali87/code-graph-rag repository provides a static analysis engine that represents your codebase as a rich knowledge graph. By traversing this graph from known entry points, the engine in codebase_rag/dead_code.py accurately identifies unreachable functions, methods, and classes across multiple programming languages.

How the Knowledge Graph Powers Dead Code Detection

Code‑Graph‑RAG models every symbol—functions, methods, classes, and modules—as nodes in a graph. The ingest stage (cgr_graph._capture) extracts these symbols and their interconnections, storing them as edges that capture relationships such as CALLS, REFERENCES, INSTANTIATES, INHERITS, DEFINES, OVERRIDES, and more.

Dead code detection treats this graph as a reachability problem. The algorithm determines which symbols are reachable from any root (entry point) in the project. Symbols that remain unvisited after the traversal constitute the dead code set.

Identifying Entry Points (Roots) with Language‑Aware Heuristics

The engine gathers entry points through a comprehensive set of language‑specific heuristics defined in codebase_rag/dead_code.py. These roots serve as the starting nodes for the reachability traversal.

Decorator and Annotation Detection

The _has_root_decorator function normalizes decorator names and matches them against predefined sets (DEFAULT_ROOT_DECORATORS). This catches framework entry points like Flask routes or pytest fixtures.

Exported and Protocol Symbols

Symbols marked with props.get(cs.KEY_IS_EXPORTED) are treated as public APIs. For Python, the engine also identifies typing.Protocol stubs as roots since they define interface contracts that external code may implement.

Language‑Specific Runtime Entry Points

The engine recognizes canonical entry points across languages:

  • Python: Dunder methods (_is_dunder), Enum hooks (PY_ENUM_HOOK_METHOD_NAMES), and framework patterns (NestJS @Injectable, React lifecycle methods via _is_react_root)
  • Go: main and init functions (GO_ROOT_FUNCTION_NAMES)
  • Rust: fn main() and trait methods (_is_rust_runtime_root)
  • C/C++: main, WinMain, DllMain, and operator overloads (_is_c_cpp_entry_root, _is_cpp_operator_root)
  • Java: Serialization hooks like readObject (_is_java_serialization_root)
  • C#: Attributes such as [Fact] and dispose patterns (_is_csharp_attribute_root, _is_csharp_dispose_root)
  • JavaScript/TypeScript: Well‑known symbols ([Symbol.iterator]) via _is_js_well_known_symbol_root

Custom Entry Points

Users can provide additional roots via the entry_points configuration parameter. The engine checks if a symbol’s fully‑qualified name ends with any user‑specified entry string.

The Dead Code Detection Algorithm

The dead_code_from_graph function implements a multi‑phase analysis pipeline.

Candidate Selection

The engine first builds a candidates set containing all symbols whose fully‑qualified name starts with the project prefix. By default, test symbols are filtered out via _is_test_symbol logic unless include_tests=True is set in the configuration.

Graph Traversal and Reachability

The core traversal occurs in the _walk function, which executes a breadth‑first search starting from the collected roots. The walker follows CALLS and REFERENCES edges (defined as _CALLS and _REFERENCES in the traversal set) to discover all transitively reachable symbols, which are added to the live set.

Closure and Factory Handling

After the initial pass, the engine performs a second phase to handle indirect relationships:

  • Closure roots: Any symbol connected via a DEFINES edge to an already‑live owner (such as decorator‑generated functions) is added to the roots and re‑traversed.
  • Factory classes and overrides: The algorithm iteratively expands factory‑class relationships and override hierarchies until no new symbols are discovered (lines 182–224 in dead_code.py).

Final Dead Set Calculation

The dead code set is computed as candidates – live. Optionally, the exclude_patterns configuration filters out generated files or specific paths before producing the final report.

CLI and Programmatic Usage

You can invoke the dead code detector via the command line or directly from Python.

Programmatic API

from codebase_rag.dead_code import dead_code_from_graph, default_dead_code_config
from codebase_rag.constants import NodeLabel, RelationshipType

# Assume `nodes` and `rels` come from a GraphQueryClient

project_prefix = "myproj."
config = default_dead_code_config(
    include_tests=False,
    include_classes=False,
    exclude_patterns=("*/generated/*",)
)

dead_qns = dead_code_from_graph(nodes, rels, project_prefix, config)

for qn in sorted(dead_qns):
    print(f"Dead: {qn}")

Command Line Interface

cgr dead-code \
  --target path/to/cgr/report \
  --project myproj \
  --include-tests \
  --exclude "**/generated/**"

The CLI writes results to dead_code.diff.json (or the path specified by --output), containing a sorted list of fully‑qualified dead symbol names.

Summary

  • Code‑Graph‑RAG constructs a knowledge graph where nodes are symbols and edges represent relationships like CALLS and REFERENCES.
  • The engine identifies roots using 15+ language‑specific heuristics in codebase_rag/dead_code.py, covering decorators, main functions, protocol stubs, and framework patterns.
  • A breadth‑first search (_walk) traverses from roots over CALLS and REFERENCES edges to mark live code.
  • Closure and factory passes handle indirect definitions and inheritance hierarchies in a second iterative phase.
  • The final dead set is the difference between project candidates and reachable symbols, optionally filtered by exclude patterns.

Frequently Asked Questions

What types of relationships does Code‑Graph‑RAG traverse to find reachable code?

The engine primarily traverses CALLS and REFERENCES edges during the initial reachability phase. In subsequent passes, it also considers DEFINES relationships to capture decorator‑generated functions and closure patterns, ensuring indirect references are not falsely flagged as dead.

How does Code‑Graph‑RAG handle dynamic entry points like dependency injection frameworks?

The root detection logic includes specific heuristics for frameworks such as NestJS (@Injectable, @Controller via _is_nest_root) and React (lifecycle methods via _is_react_root). These functions check for framework‑specific decorators, base classes, and metadata to identify symbols that serve as implicit entry points despite not being directly invoked in source code.

Can I exclude test files from dead code detection in Code‑Graph‑RAG?

Yes. By default, the _is_test_symbol logic filters out test files. You can control this behavior via the include_tests parameter in DeadCodeConfig or the --include-tests CLI flag if you want to treat test code as valid entry points for reachability analysis.

What file contains the core dead code detection logic?

The core algorithm resides in codebase_rag/dead_code.py. This file contains the dead_code_from_graph function, the _walk traversal implementation, and all language‑specific root detection heuristics. The CLI wrapper is located in evals/dead_code.py, which handles graph ingestion and configuration setup.

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 →