How Incremental Parsing Handles Cross-Language Symbol Collisions in Code-Graph-RAG
The Code-Graph-RAG indexer prevents cross-language symbol collisions during incremental parsing by maintaining a persistent module qualified name (QN) to file path map, rehydrating it from the existing graph before each run, and automatically disambiguating conflicting basenames by appending file extensions.
When indexing polyglot codebases containing files like shapes.rs and shapes.cpp, the cgr tool must ensure that symbols from different languages remain distinct even when only a subset of files is reparsed. The solution centers on the _disambiguate_module_qn function in codebase_rag/parsers/definition_processor.py, which leverages a rehydrated module_qn_to_file_path dictionary to preserve symbol uniqueness across incremental updates.
The Challenge of Cross-Language Name Collisions
During a full build, the indexer processes every source file simultaneously, allowing the disambiguation routine to see all files sharing the same basename. The system can immediately detect that shapes.rs and shapes.cpp both map to the module qualified name proj.shapes and resolve the conflict accordingly.
However, incremental runs present a unique problem. The GraphUpdater.run method (around line 1265 in codebase_rag/graph_updater.py) only reparses files whose hashes have changed. If a new shapes.cpp is added while shapes.rs remains untouched, the incremental parser would see only the new file and might incorrectly assign it the bare module QN proj.shapes, potentially overwriting the existing Rust module node.
How the Disambiguation Mechanism Works
The _disambiguate_module_qn Function
The core logic resides in definition_processor.py (lines 84–95). When processing a file, the routine first checks if the target module QN already exists in self.module_qn_to_file_path, a dictionary that maps qualified names to their canonical file paths:
def _disambiguate_module_qn(self, module_qn: str, file_path: Path) -> str:
# Two files that share a basename but differ by extension (foo.py / foo.cpp)
# strip to the same module qn. Append the extension to the later one so their
# module nodes and all derived class/method qns stay distinct instead of
# colliding under the qualified_name constraint.
existing = self.module_qn_to_file_path.get(module_qn)
if existing is None or existing == file_path:
return module_qn
return (
f"{module_qn}{cs.SEPARATOR_DOT}{file_path.suffix.lstrip(cs.SEPARATOR_DOT)}"
)
If the QN is already owned by a different file, the function appends the new file’s extension (e.g., transforming proj.shapes into proj.shapes.cpp), ensuring that derived symbols like classes and methods inherit unique qualified names.
Persistent Module Map Rehydration
To prevent newly added files from "stealing" bare QNs from unchanged siblings, the incremental pipeline rehydrates the module_qn_to_file_path map from the existing graph before re-parsing begins. The GraphUpdater clears in-memory entries only for files that are actually being reparsed; all other entries persist, allowing _disambiguate_module_qn to detect collisions against files processed in previous runs.
Practical Example: Adding a C++ Sibling to a Rust Module
The test suite in codebase_rag/tests/test_incremental_cross_language_collision.py validates this behavior (lines 66–95). Consider a scenario where a Rust file is indexed first, followed by the addition of a C++ counterpart:
# Example: Incremental addition of a C++ sibling next to an existing Rust module
temp_repo = Path("/tmp/project")
(temp_repo / "shapes.rs").write_text(_RS) # Rust file indexed first
store = _StatefulIngestor()
_index(store, temp_repo, force=False) # Full index → proj.shapes
# Add C++ sibling and force reparse
(temp_repo / "shapes.cpp").write_text(_CPP)
os.utime(temp_repo / "shapes.cpp", (future, future))
_index(store, temp_repo, force=False) # Incremental run
modules = _shapes_modules(store)
assert modules["shapes.rs"] == "proj.shapes"
assert modules["shapes.cpp"].endswith(".cpp") # Disambiguated QN
In this workflow, the first index assigns proj.shapes to the Rust file. During the incremental update, the rehydrated map contains the existing entry, so the C++ file receives proj.shapes.cpp instead of colliding with the established node.
Handling File Deletion and Replacement
The same test file (lines 101–126) verifies the delete-then-add scenario. When the original file is removed from the repository and a new file with the same basename but different extension is added, the new file correctly receives the bare module QN. This behavior mirrors a clean build, ensuring consistent symbol naming even as the codebase evolves.
Summary
- Persistent mapping: The
module_qn_to_file_pathdictionary is rehydrated from the existing graph before incremental runs, preserving knowledge of unchanged files. - Extension-based disambiguation: The
_disambiguate_module_qnfunction indefinition_processor.pydetects collisions and appends file suffixes to create unique qualified names. - Incremental safety: By only clearing entries for reparsed files, the system prevents newly added siblings from overwriting established symbols in other languages.
- Validated behavior: The
test_incremental_cross_language_collision.pysuite confirms that bothshapes.rsandshapes.cppcoexist asproj.shapesandproj.shapes.cpprespectively.
Frequently Asked Questions
What causes cross-language symbol collisions in incremental parsing?
Cross-language collisions occur when two source files share the same basename but use different extensions (e.g., utils.py and utils.rs). During a full index, the parser sees both files and can disambiguate them. During incremental parsing, however, the parser might only see the new file, risking an overwrite of the existing module node if the system does not maintain state from previous runs.
How does the Code-Graph-RAG indexer maintain symbol uniqueness across languages?
According to the source code in codebase_rag/parsers/definition_processor.py, the indexer maintains a module_qn_to_file_path mapping that tracks which file owns each module qualified name. When _disambiguate_module_qn detects that a desired QN is already assigned to a different file path, it appends the file extension to the new file's module name, creating distinct nodes like proj.shapes and proj.shapes.cpp.
What happens when a file is deleted and replaced with a different language version?
When the original file is deleted and a new file with the same basename is added, the incremental parser updates the module_qn_to_file_path map to reflect the deletion. The new file then claims the bare module QN (e.g., proj.shapes), matching the behavior of a clean build. The test suite confirms this behavior in test_incremental_cross_language_collision.py (lines 101–126).
Where is the incremental parsing logic orchestrated in the codebase?
The high-level orchestration occurs in codebase_rag/graph_updater.py around line 1265, where the GraphUpdater.run method manages the incremental update cycle. This component determines which files require reparsing and ensures that the module QN mapping is cleared only for those specific files, preserving the rest of the graph state for collision detection.
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 →