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_bases separates the single base class from implemented interfaces
  • Java: extract_java_superclass handles extends clauses
  • Python: extract_python_superclasses resolves generic bases and import aliases
  • TypeScript/JavaScript: extract_js_ts_heritage_parents processes extends, mixins, and implements
  • C++: extract_cpp_parent_bases (called directly for CPP_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.CLASS if 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.py serves as the bridge between tree-sitter ASTs and the code-graph database, converting inheritance syntax into queryable edges
  • Delegation to parent_extraction.py isolates 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_index preservation 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:

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 →