How class_ingest/relationships.py Extracts Class Hierarchies and Inheritance in code-graph-rag
The class_ingest/relationships.py module in the vitali87/code-graph-rag repository orchestrates inheritance extraction by delegating parent class discovery to language-specific helpers, storing results in a mutable mapping, and emitting INHERITS or IMPLEMENTS graph edges with support for deferred resolution of cross-file dependencies.
The relationships.py file sits at the core of the class-ingest pipeline in code-graph-rag, a multi-language code analysis tool that builds queryable knowledge graphs from source code. When tree-sitter parses a class or enum declaration, this module translates syntactic inheritance constructs into persistent graph relationships that power downstream analysis like method override detection and architectural queries.
Overview of the Inheritance Extraction Pipeline
The extraction process follows three coordinated phases: parent discovery, mapping storage, and edge emission. Each phase handles language-specific nuances while maintaining a unified graph schema across C++, C#, Java, Python, TypeScript/JavaScript, PHP, Rust, Dart, and other supported languages.
Phase 1: Identify Parent Classes via Delegation
The entry point create_class_relationships does not parse AST nodes directly. Instead, it delegates to extract_parent_classes from parent_extraction.py, which contains language-specific logic for walking inheritance syntax:
- C#:
split_csharp_basesseparates the single base class from implemented interfaces - Java:
extract_java_superclasshandlesextendsclauses - Python:
extract_python_superclassesresolves generic bases and import aliases - TypeScript/JavaScript:
extract_js_ts_heritage_parentsprocessesextends, mixins, andimplements - C++:
extract_cpp_parent_bases(called directly forCPP_CLASS_TYPES) captures base specifiers that may reference headers not yet parsed
This delegation pattern keeps relationships.py focused on graph construction while parent_extraction.py evolves independently with new language support.
Phase 2: Record Inheritance Mappings for Incremental Rebuilds
Parent class lists are stored in a mutable dictionary passed through the call chain:
class_inheritance[class_qn] = parent_classes
This mapping at line 44 of relationships.py enables incremental ingestion workflows. When re-indexing changed files, the system can rebuild affected portions of the class hierarchy without reparsing the entire codebase.
Phase 3: Emit Relationship Edges with Deferred Resolution
The core complexity lies in deciding when to create edges. The implementation at lines 57-133 branches on two factors: the target language and whether deferral is explicitly requested.
C++ Cross-File Deferral
C++ base classes frequently reside in separate headers. Rather than fail or emit incomplete edges, the code creates DeferredCppInherit records at lines 65-75:
DeferredCppInherit(
written_name=base_name,
guess_qn=guessed_qualified_name,
namespace_path=current_namespace
)
A later pass (resolve_deferred_cpp_inherits) resolves these against the complete symbol table once all headers are registered.
Immediate Edge Creation for Other Languages
For non-C++ languages where parent symbols are resolvable in the current translation unit, create_inheritance_relationship emits edges directly at lines 92-101:
ingestor.ensure_relationship_batch(
(child_node_type, cs.KEY_QUALIFIED_NAME, child_qn),
cs.RelationshipType.INHERITS,
(parent_type, cs.KEY_QUALIFIED_NAME, parent_qn),
{cs.KEY_BASE_INDEX: base_index},
)
The base_index property preserves declaration order—critical for correct Method Resolution Order (MRO) calculations in languages like Python and C++.
Interface Implementation Handling
Classes and enums that implement interfaces trigger extract_implemented_interfaces at lines 103-133. The same deferral pattern applies: DeferredInherit with RELATIONSHIP_TYPE.IMPLEMENTS for unresolved symbols, or direct create_implements_relationship calls when symbols are known.
Resolving Node Types for Inheritance Edges
A subtle challenge: when emitting an INHERITS edge, the graph store requires the parent's node label (Class, Interface, Enum, etc.). The helper get_node_type_for_inheritance at lines 39-45 implements this resolution:
- Query the function registry for the parent's qualified name
- Default to
NodeType.CLASSif unregistered - Pass the resolved type to
ensure_relationship_batch
This lookup ensures edges connect to nodes with correct labels even when the parent definition is encountered later in the parse order.
Code Examples
Typical Pipeline Invocation
This simplified excerpt shows how the structural ingest pass calls create_class_relationships:
from codebase_rag.parsers.class_ingest.relationships import create_class_relationships
def ingest_class(node, module_qn: str, language: cs.SupportedLanguage):
class_qn = f"{module_qn}.{safe_decode_text(node)}"
create_class_relationships(
class_node=node,
class_qn=class_qn,
module_qn=module_qn,
node_type=cs.NodeLabel.CLASS,
is_exported=False,
language=language,
class_inheritance=inheritance_dict, # Mutable: stores parent mappings
ingestor=graph_ingestor, # Implements IngestorProtocol
import_processor=import_proc, # Resolves import-relative names
resolve_to_qn=resolver, # Converts names to qualified form
function_registry=func_registry, # Symbol table for node type lookup
)
Manual Edge Creation for Testing
For unit tests or synthetic graph construction, edges can be created directly:
from codebase_rag.parsers.class_ingest.relationships import create_inheritance_relationship
from codebase_rag.constants import NodeLabel
create_inheritance_relationship(
child_node_type=NodeLabel.CLASS,
child_qn="my_pkg.Foo",
parent_qn="my_pkg.Base",
function_registry=func_registry, # Resolves "my_pkg.Base" → NodeLabel.CLASS
ingestor=graph_ingestor,
base_index=0, # First (or only) base class
)
Key Files in the Class Ingest System
| File | Purpose | Critical For |
|---|---|---|
class_ingest/relationships.py |
Orchestrates INHERITS, IMPLEMENTS, EXPORTS edges; deferred resolution |
Multi-language edge emission with cross-file support |
class_ingest/parent_extraction.py |
Language-specific AST walking for parent discovery | Handling C++, Java, Python, TS/JS, PHP, Rust, Dart syntax |
class_ingest/utils.py |
Shared helpers: find_child_by_type, safe_decode_text |
Robust AST navigation |
constants.py |
SupportedLanguage, RelationshipType, NodeLabel enums |
Type-safe graph schema |
services.py |
IngestorProtocol interface |
Decoupled graph store backends |
Summary
class_ingest/relationships.pyserves as the bridge between tree-sitter ASTs and the code-graph database, converting inheritance syntax into queryable edges- Delegation to
parent_extraction.pyisolates language-specific parsing logic from graph construction concerns - Deferred edge creation solves the cross-file reference problem, particularly for C++ headers and incremental ingestion scenarios
base_indexpreservation maintains declaration order for correct method resolution semantics- Registry-based node type resolution ensures edges connect to properly labeled nodes regardless of parse order
Frequently Asked Questions
How does code-graph-rag handle C++ multiple inheritance across header files?
The system creates DeferredCppInherit records that capture the written base name, a guessed qualified name, and namespace context. These records are stored during the initial parse and resolved in a subsequent pass once all headers are registered, as implemented at lines 65-75 of relationships.py.
What prevents duplicate INHERITS edges when re-ingesting modified files?
The class_inheritance dictionary passed through the call chain tracks parent mappings per qualified class name. Incremental ingestion workflows use this mapping to rebuild only affected hierarchy subgraphs, avoiding re-emission of edges for unchanged parent relationships.
Why does base_index matter for inheritance edges?
The base_index property (stored in relationship properties at line 100) preserves the declaration order of base classes. This ordering is semantically significant for Method Resolution Order in Python and virtual base resolution in C++, enabling accurate downstream analysis of method overrides and diamond inheritance patterns.
How does the system distinguish between class inheritance and interface implementation?
create_class_relationships branches based on AST node type and language semantics. For C#, split_csharp_bases explicitly separates the single base class from interfaces. For other languages, extract_implemented_interfaces identifies interface clauses, and the code emits IMPLEMENTS edges (via DeferredInherit or create_implements_relationship) rather than INHERITS edges at lines 103-133.
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 →