# How to Add Support for a New Programming Language to the Code-Graph-RAG Parser System

> Easily add a new programming language to the Code-Graph-RAG parser. Learn how to extend constants, language specifications, and custom handlers for seamless integration.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: how-to-guide
- Published: 2026-08-16

---

**Adding a new language to Code-Graph-RAG requires extending three tightly-coupled components: language constants in [`constants.py`](https://github.com/vitali87/code-graph-rag/blob/main/constants.py), language specifications in [`language_spec.py`](https://github.com/vitali87/code-graph-rag/blob/main/language_spec.py), and optionally a custom handler in `parsers/handlers/`.**

The Code-Graph-RAG parser system is designed to ingest codebases, build graph representations, and support retrieval-augmented generation workflows. To add support for a new programming language—whether it's Rust, Go, Zig, or a domain-specific language—you need to integrate with its **tree-sitter grammar** and define how the parser extracts **fully-qualified names (FQNs)**, detects **scopes**, and resolves **imports**. This guide walks through the complete implementation based on the actual source code in `vitali87/code-graph-rag`.

## The Three Core Components of Language Support

Understanding the architecture helps you know where each change belongs. The system separates concerns into three layers:

| Component | Purpose | Location |
|-----------|---------|----------|
| **Language constants** | File extensions, node-type sets, tree-sitter queries | [`codebase_rag/constants.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants.py) |
| **Language specifications** | FQN extraction rules and module path mapping | [`codebase_rag/language_spec.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/language_spec.py) |
| **Language handlers** | Runtime quirks the generic base cannot handle | `codebase_rag/parsers/handlers/<language>.py` |

Most languages can reuse the generic `BaseLanguageHandler`. You only need a custom handler when the language has unique semantics for class methods, exports, or implicit parameters.

## Step 1: Add Language Constants to [`constants.py`](https://github.com/vitali87/code-graph-rag/blob/main/constants.py)

Start by extending the `SupportedLanguage` enum with your new language. Every constant that follows uses this enum value as its key.

```python

# codebase_rag/constants.py

from enum import Enum, auto

class SupportedLanguage(Enum):
    PYTHON = auto()
    JS = auto()
    RUST = auto()
    # ... existing languages ...

    FOO = auto()  # Your new language

```

Next, define the file extensions and node-type sets. These must match the **actual node type strings** from the tree-sitter grammar:

```python

# File extensions that trigger this language handler

FOO_EXTENSIONS = (".foo", ".foos")

# Node types that create a new naming scope (module, class, namespace)

FQN_FOO_SCOPE_TYPES = ("module", "class_definition", "namespace")

# Node types that define callable entities

FQN_FOO_FUNCTION_TYPES = ("function_definition", "method_definition")

# Parser node-type sets for entity detection

SPEC_FOO_FUNCTION_TYPES = frozenset({"function_definition", "lambda"})
SPEC_FOO_CLASS_TYPES = frozenset({"class_definition"})
SPEC_FOO_MODULE_TYPES = frozenset({"module"})
SPEC_FOO_CALL_TYPES = frozenset({"call", "invocation"})
SPEC_FOO_IMPORT_TYPES = frozenset({"import_statement", "import_from_statement"})

```

If your language needs **custom tree-sitter queries** to locate functions, classes, or call sites, add them as string literals:

```python
FOO_FUNCTION_QUERY = """
(function_definition
    name: (identifier) @name) @function
"""

FOO_CLASS_QUERY = """
(class_definition
    name: (identifier) @name) @class
"""

FOO_CALL_QUERY = """
(call
    function: (identifier) @name) @call
"""

```

These queries follow standard tree-sitter syntax with capture names (`@name`, `@function`) that the parser uses to extract identifiers.

## Step 2: Create the FQN Specification

The **fully-qualified name (FQN)** system converts tree-sitter nodes into unique, hierarchical identifiers like `mypackage.submodule.MyClass.my_method`. In [`codebase_rag/language_spec.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/language_spec.py), you define two critical functions:

- `_foo_get_name(node)` — extracts the declared name from a node
- `_foo_file_to_module(file_path, repo_root)` — converts file paths to module parts

```python

# codebase_rag/language_spec.py

from pathlib import Path
from tree_sitter import Node  # type: ignore

from .constants import SupportedLanguage as cs
from .models import FQNSpec, LanguageSpec

def _foo_get_name(node: Node) -> str | None:
    """Extract the identifier name from a tree-sitter node."""
    name_node = node.child_by_field_name("name")
    if name_node and name_node.text:
        return name_node.text.decode(cs.ENCODING_UTF8)
    return None

def _foo_file_to_module(file_path: Path, repo_root: Path) -> list[str]:
    """Convert 'src/core/utils.foo' relative to repo root into ['src', 'core', 'utils']."""
    try:
        relative_path = file_path.relative_to(repo_root)
        # Remove extension and split into path components

        return list(relative_path.with_suffix("").parts)
    except ValueError:
        # File outside repo root — use filename only

        return [file_path.stem]

```

With these helpers, construct the `FQNSpec` and register it:

```python
FOO_FQN_SPEC = FQNSpec(
    scope_node_types=frozenset(cs.FQN_FOO_SCOPE_TYPES),
    function_node_types=frozenset(cs.FQN_FOO_FUNCTION_TYPES),
    get_name=_foo_get_name,
    file_to_module_parts=_foo_file_to_module,
)

LANGUAGE_FQN_SPECS[cs.SupportedLanguage.FOO] = FOO_FQN_SPEC

```

The `FQNSpec` dataclass (defined in [`codebase_rag/models.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/models.py)) centralizes how the **FQN resolver** traverses the AST upward to build qualified names.

## Step 3: Register the Language Specification

The `LanguageSpec` dataclass bundles all constants for runtime access. Populate it with the values defined in Step 1 and add it to `LANGUAGE_SPECS`:

```python
LANGUAGE_SPECS[cs.SupportedLanguage.FOO] = LanguageSpec(
    language=cs.SupportedLanguage.FOO,
    file_extensions=cs.FOO_EXTENSIONS,
    function_node_types=cs.SPEC_FOO_FUNCTION_TYPES,
    class_node_types=cs.SPEC_FOO_CLASS_TYPES,
    module_node_types=cs.SPEC_FOO_MODULE_TYPES,
    call_node_types=cs.SPEC_FOO_CALL_TYPES,
    import_node_types=cs.SPEC_FOO_IMPORT_TYPES,
    import_from_node_types=cs.SPEC_FOO_IMPORT_TYPES,
    # Optional: only if you defined custom queries

    function_query=cs.FOO_FUNCTION_QUERY,
    class_query=cs.FOO_CLASS_QUERY,
    call_query=cs.FOO_CALL_QUERY,
)

```

The extension-to-spec mapping happens automatically at module import time via the loop at the bottom of [`language_spec.py`](https://github.com/vitali87/code-graph-rag/blob/main/language_spec.py):

```python
for _config in LANGUAGE_SPECS.values():
    for _ext in _config.file_extensions:
        _EXTENSION_TO_SPEC[_ext] = _config

```

This ensures that discovering a `.foo` file immediately yields the correct `LanguageSpec`.

## Step 4: Implement a Custom Handler (When Needed)

Many languages work with the default `BaseLanguageHandler` implementation. Create a custom handler only when you need to override:

- **`is_class_method(node)`** — detect methods bound to classes
- **`is_export_inside_function(node)`** — handle export visibility rules
- **`extract_function_name(node)`** — non-standard name extraction
- **`build_function_qualified_name(node, ...)`** — custom FQN construction logic

Create [`codebase_rag/parsers/handlers/foo.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/handlers/foo.py):

```python
from tree_sitter import Node  # type: ignore

from .base import BaseLanguageHandler
from ... import constants as cs


class FooLanguageHandler(BaseLanguageHandler):
    """
    Language-specific handler for Foo (.foo files).
    Overrides default behavior for class method detection.
    """

    def is_class_method(self, node: Node) -> bool:
        """
        Foo uses an explicit 'self' parameter as first argument
        to indicate instance methods, similar to Python.
        """
        if node.type not in cs.FQN_FOO_FUNCTION_TYPES:
            return False
        
        # Check first parameter list child for 'self' identifier

        for child in node.children:
            if child.type == "parameters":
                params = child.named_children
                if params and params[0].type == "identifier":
                    return params[0].text == b"self"
        return False

    def is_export_inside_function(self, node: Node) -> bool:
        """
        Foo allows 'export' statements inside functions for
        dynamic module re-exports. Default handler assumes
        exports are always module-level.
        """
        parent = node.parent
        while parent:
            if parent.type in cs.FQN_FOO_FUNCTION_TYPES:
                return True
            parent = parent.parent
        return False

```

The `BaseLanguageHandler` (defined in [`codebase_rag/parsers/handlers/base.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/handlers/base.py)) provides sensible defaults for most operations. Study existing handlers like [`python.py`](https://github.com/vitali87/code-graph-rag/blob/main/python.py) and [`rust.py`](https://github.com/vitali87/code-graph-rag/blob/main/rust.py) for patterns.

## Step 5: Register the Handler in the Dispatch Registry

The parser selects handlers via a registry mapping `SupportedLanguage` values to handler classes. In [`codebase_rag/parsers/handlers/registry.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/handlers/registry.py) (or directly in [`function_ingest.py`](https://github.com/vitali87/code-graph-rag/blob/main/function_ingest.py) if no separate registry exists):

```python
from .python import PythonLanguageHandler
from .rust import RustLanguageHandler
from .js_ts import JsTsLanguageHandler
from .foo import FooLanguageHandler  # Your new handler

from ... import constants as cs

HANDLER_REGISTRY: dict[cs.SupportedLanguage, type[BaseLanguageHandler]] = {
    cs.SupportedLanguage.PYTHON: PythonLanguageHandler,
    cs.SupportedLanguage.JS: JsTsLanguageHandler,
    cs.SupportedLanguage.RUST: RustLanguageHandler,
    # ... existing entries ...

    cs.SupportedLanguage.FOO: FooLanguageHandler,
}

```

The dispatch occurs in [`codebase_rag/parsers/function_ingest.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/function_ingest.py) when processing each file:

```python
from .handlers.registry import HANDLER_REGISTRY

def get_handler_for_file(file_path: Path, spec: LanguageSpec) -> BaseLanguageHandler:
    handler_class = HANDLER_REGISTRY.get(spec.language, BaseLanguageHandler)
    return handler_class()

```

## Step 6: Test Your Implementation

Add test coverage in `codebase_rag/tests/` to prevent regressions. The existing test suite provides language-agnostic infrastructure:

| Test File | Validates |
|-----------|-----------|
| [`test_fqn_resolver.py`](https://github.com/vitali87/code-graph-rag/blob/main/test_fqn_resolver.py) | FQN extraction for various AST patterns |
| `test_callable_flow_tracing_*.py` | Call graph construction |

Create a minimal test file in your new language:

```python

# codebase_rag/tests/test_foo_parsing.py

from pathlib import Path
import pytest

from codebase_rag.language_spec import LANGUAGE_SPECS, get_spec_for_extension
from codebase_rag.constants import SupportedLanguage


def test_foo_extension_registration():
    """Verify .foo files map to the correct language spec."""
    spec = get_spec_for_extension(".foo")
    assert spec.language == SupportedLanguage.FOO
    assert ".foo" in spec.file_extensions


def test_foo_fqn_resolution(tmp_path):
    """Test FQN extraction on a sample Foo file."""
    sample_code = '''
module mymodule;

class MyClass {
    function myMethod(self) {
        helper();
    }
}
'''
    test_file = tmp_path / "src" / "mymodule.foo"
    test_file.parent.mkdir(parents=True)
    test_file.write_text(sample_code)
    
    # Run through the actual parser pipeline

    from codebase_rag.parsers.function_ingest import ingest_file
    
    result = ingest_file(test_file, repo_root=tmp_path)
    
    # Assert expected qualified names were extracted

    fqns = {entity.fqn for entity in result.entities}
    assert "src.mymodule.MyClass.myMethod" in fqns

```

## Summary

Adding a new programming language to Code-Graph-RAG involves these key steps:

- **Extend [`constants.py`](https://github.com/vitali87/code-graph-rag/blob/main/constants.py)** with the `SupportedLanguage` enum value, file extensions, node-type sets, and optional tree-sitter queries
- **Define `FQNSpec`** in [`language_spec.py`](https://github.com/vitali87/code-graph-rag/blob/main/language_spec.py) with name extraction and module path mapping functions
- **Register `LanguageSpec`** to bundle all constants for runtime lookup
- **Create a custom handler** only when the language has unique semantics the base handler cannot infer
- **Add to `HANDLER_REGISTRY`** to enable dispatch based on file extension
- **Write tests** using existing language-agnostic test infrastructure

The modular architecture separates tree-sitter grammar details from FQN resolution logic and runtime behavior, making most language additions straightforward configuration work.

## Frequently Asked Questions

### What tree-sitter grammar do I need for a new language?

You need a tree-sitter grammar compiled to a shared library or Python bindings that the parser can load. The `codebase_rag` system uses the standard tree-sitter Python API. Install the grammar package (e.g., `tree-sitter-go`, `tree-sitter-zig`) and ensure it's importable. The node type strings in your `*_NODE_TYPES` constants must exactly match the grammar's node type names.

### Can I add a language without creating a custom handler?

Yes—most languages work with the default `BaseLanguageHandler`. You only need a custom handler when your language has unusual semantics: implicit `self` parameters, export visibility modifiers, name mangling, or non-standard scoping rules. Start without one, run the test suite, and add overrides only for failing cases.

### How do I debug FQN resolution for my new language?

Use the [`test_fqn_resolver.py`](https://github.com/vitali87/code-graph-rag/blob/main/test_fqn_resolver.py) infrastructure with a minimal source file. Add `print()` statements to your `_get_name` and `file_to_module_parts` functions, or set breakpoints in [`codebase_rag/language_spec.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/language_spec.py) where `LANGUAGE_FQN_SPECS` is accessed. The FQN builder traverses parent nodes upward—verify your `scope_node_types` correctly identifies all nodes that introduce a new naming scope.

### Why does my new language not trigger despite correct constants?

Check three things: (1) the file extension appears in `LANGUAGE_SPECS[<language>].file_extensions`, (2) the `SupportedLanguage` enum value is used consistently across all three files, and (3) Python has imported your new module so the `LANGUAGE_SPECS[...] = ` assignment executes. The extension-to-spec mapping builds at import time—any import errors or circular dependencies prevent registration.