# How FileEditor Performs AST‑Based Surgical Code Patching in code‑graph‑rag

> Discover how FileEditor uses AST parsing and byte-level offsets for precise, language-aware code patching in code-graph-rag, eliminating line-number guesswork for efficient edits.

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

---

**`FileEditor` uses tree‑sitter AST parsing and byte‑level node offsets to apply precise, language‑aware code edits without line‑number guessing.**

The `FileEditor` tool in the **code‑graph‑rag** repository enables retrieval‑augmented generation systems to modify source files with surgical precision. By anchoring edits to **AST node boundaries** rather than fragile line numbers, it ensures patches survive reflows, comment changes, and surrounding code motion. This article breaks down the four‑stage pipeline implemented across [`tools/file_editor.py`](https://github.com/vitali87/code-graph-rag/blob/main/tools/file_editor.py) and its supporting modules.

## Parse Stage: From Source Bytes to Tree‑Sitter AST

Every edit begins with **deterministic parsing**. The `codebase_rag.utils.source_extraction` module wraps tree‑sitter to return a syntax tree where each node carries exact **byte start/end offsets** into the original file.

```python
from codebase_rag.utils.source_extraction import TreeSitterParser

parser = TreeSitterParser.for_language("python")
tree = parser.parse_bytes(source_bytes)  # Root node spans entire file

```

Unlike regex or line‑based approaches, this guarantees that structural boundaries—function bodies, class definitions, import blocks—are identified correctly regardless of formatting style.

## Locate Stage: Resolving Qualified Names to AST Nodes

With the AST in hand, `FileEditor` delegates node lookup to `codebase_rag.graph_updater`. The `find_node_by_qualified_name` method traverses the hierarchy using scoped identifiers like `my_pkg.my_mod.my_func`.

```python
from codebase_rag.graph_updater import GraphUpdater

updater = GraphUpdater(ast_cache)
target_node = updater.find_node_by_qualified_name("utils.helper.process_data")

# Returns: node with .start_byte and .end_byte attributes

```

This resolution is **cache‑aware**: the `BoundedASTCache` stores parsed trees indexed by file path, so repeated edits to the same file avoid redundant parsing.

## Diff Stage: Byte‑Level Patch Generation

The core surgical operation lives in [`tools/file_editor.py`](https://github.com/vitali87/code-graph-rag/blob/main/tools/file_editor.py). The `apply_patch_to_file` method:

1. Extracts the original source slice using `node.start_byte` and `node.end_byte`
2. Generates a `difflib.unified_diff` between old and new text
3. Applies the diff in‑place while preserving surrounding whitespace and comments

```python
from tools.file_editor import FileEditor

editor = FileEditor(project_root="/path/to/project")

result = editor.replace_code_surgically(
    qualified_name="utils.helper.process_data",
    new_source="""
def process_data(items):
    # Optimized path

    return [x.value for x in items if x.active]
"""
)

```

Because the patch boundaries derive from AST offsets, the edit **never drifts**—even if imports are added above or docstrings reformatted below the target function.

## Verify Stage: Post‑Edit AST Validation

`FileEditor` re‑patches defensively. After writing the file, it:

- Re‑parses via `source_extraction.parse_file`
- Verifies the modified node's type matches expectations
- Rolls back and raises on syntax corruption

```python

# Simplified validation logic from file_editor.py

new_tree = parser.parse_file(file_path)
new_node = find_corresponding_node(new_tree, qualified_name)
assert new_node.type == original_node.type, "AST structure corrupted"

```

This atomicity ensures the codebase remains valid after every operation.

## Practical Examples

### Rename a Function Implementation

```python
editor.replace_code_surgically(
    qualified_name="analytics.metrics.compute_score",
    new_source="""
def compute_score(predictions, labels):
    return f1_score(labels, predictions, average="weighted")
"""
)

```

The AST node for `compute_score` is located, its entire definition replaced, and the file re‑parsed to confirm the function still exists.

### Insert an Import Statement

```python
editor.edit_file(
    file_path="src/main.py",
    before_node="import os",  # AST node reference, not line number

    new_source="import pathlib\n",
)

```

`before_node` resolves to an actual `import` node; the insertion offset is computed from `node.start_byte`, ensuring correct placement even with multiple import groups.

### Delete an Obsolete Constant

```python
editor.replace_code_surgically(
    qualified_name="config.LEGACY_TIMEOUT",
    new_source="",  # Empty replacement = deletion

)

```

The AST node spanning the constant definition is excised precisely, leaving surrounding assignments intact.

## Key Source Files

| File | Purpose |
|------|---------|
| [`tools/file_editor.py`](https://github.com/vitali87/code-graph-rag/blob/main/tools/file_editor.py) | `FileEditor` class, `apply_patch_to_file`, high‑level edit API |
| [`codebase_rag/utils/source_extraction.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/utils/source_extraction.py) | Tree‑sitter parsing, byte‑offset extraction |
| [`codebase_rag/graph_updater.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/graph_updater.py) | Node lookup by qualified name, cache invalidation |
| [`codebase_rag/graph_loader.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/graph_loader.py) | Bulk AST loading into `BoundedASTCache` |
| [`codebase_rag/types_defs.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/types_defs.py) | `ASTCacheProtocol`, node type definitions |

## Summary

- **AST parsing** via tree‑sitter eliminates line‑number fragility
- **Byte‑level offsets** from `source_extraction` anchor every edit
- **`GraphUpdater`** resolves qualified names to concrete nodes
- **`apply_patch_to_file`** generates and applies unified diffs surgically
- **Re‑parsing validation** guarantees syntactic integrity after changes
- **Cache invalidation** keeps the RAG index synchronized with file state

## Frequently Asked Questions

### What makes AST‑based patching "surgical"?

Traditional tools rely on line numbers or pattern matching, which break when code is reformatted. AST‑based patching uses **byte offsets tied to syntax nodes**, so edits target exact structural boundaries—functions, classes, imports—regardless of surrounding changes.

### Does FileEditor support languages other than Python?

The architecture is language‑agnostic through tree‑sitter. `TreeSitterParser.for_language()` accepts any grammar the tree‑sitter ecosystem provides. The `code‑graph‑rag` repository currently optimizes for Python, but the `source_extraction` module is designed for multi‑language extension.

### How does FileEditor handle patch conflicts?

Edits are atomic: the file is read, modified, and written within a single operation. If re‑parsing fails, the original file is restored. Concurrent modifications are not handled—`FileEditor` assumes single‑process access or external file locking.

### What happens to the RAG index after a file edit?

`GraphUpdater` invalidates affected entries in `BoundedASTCache` and triggers incremental re‑indexing. Subsequent retrievals see the updated AST immediately, keeping the knowledge graph consistent with the source codebase.