Using ast-grep for Structural Search and Replace by AST Pattern in code-graph-rag
The code-graph-rag repository provides a Python wrapper around ast-grep-py that enables fast, language-agnostic structural code transformation using AST patterns instead of fragile regex.
This guide explains how to leverage the built-in ast-grep integration in vitali87/code-graph-rag to perform precise code searches and automated refactors across multiple programming languages. The implementation wraps the native ast-grep-py library with LangChain-compatible tooling for AI-driven code manipulation.
Core Architecture
The ast-grep integration consists of four primary components working together:
| Component | Implementation | Purpose |
|---|---|---|
| AstGrepService | codebase_rag/tools/ast_grep_service.py |
Loads source files, builds language-specific AST roots, and executes pattern matches |
| AstGrepTier | codebase_rag/parsers/ast_grep_tier.py |
Registers per-extension language IDs and loads pattern configuration files |
| Dependency Detection | codebase_rag/utils/dependencies.py |
Runtime check via has_ast_grep() for optional ast_grep_py availability |
| LangChain Tools | codebase_rag/main.py lines 1648-1653 |
create_structural_search_tool() and create_structural_editor_tool() factories |
The has_ast_grep() function in codebase_rag/utils/dependencies.py ensures graceful degradation—if the optional dependency is missing, structural tools fall back to no-op implementations rather than crashing.
How AST Pattern Matching Works
Unlike regex that matches raw text, ast-grep operates on the Abstract Syntax Tree. This eliminates false positives from string literals, comments, or formatting variations.
The workflow in codebase_rag follows this sequence:
- Project Discovery:
AstGrepService._iter_source_files()walks the repository, filtering files by supported extensions - Language Mapping: Extensions map to ast-grep language IDs via
constants/ast_python.py,constants/ast_php.py, and related modules - Pattern Execution:
AstGrepService._find_all()traversesSgRootnodes to yield matchingSgNodeobjects - Transformation:
SgNode.replace_with()preserves formatting while applying replacements
Setting Up Structural Search
Initialize the service with your project root, then execute patterns using JSON/YAML AST descriptors:
from codebase_rag.tools.ast_grep_service import AstGrepService
service = AstGrepService(project_root="/path/to/your/repo")
# Pattern: find function definitions lacking docstrings
pattern = """
{
"type": "FunctionDef",
"children": [
{
"type": "body",
"children": [
{
"type": "Expr",
"constraint": "not Str"
}
]
}
]
}
"""
matches = service.search(language="python", pattern=pattern)
for file_path, line, col in matches:
print(f"{file_path}:{line}:{col} — function missing docstring")
The search() method at line 147-152 in ast_grep_service.py returns tuples of (file_path, line_number, column_offset) for every match.
Performing Structural Replacement
Replace matched nodes while preserving surrounding formatting and indentation:
from codebase_rag.tools.ast_grep_service import AstGrepService
service = AstGrepService(project_root="/path/to/your/repo")
# Find all print() calls
search_pattern = """
{
"type": "Call",
"children": [
{
"type": "func",
"constraint": "Str == 'print'"
}
]
}
"""
# Replacement template with interpolation
replacement = "logger.info({{args}})"
service.replace(
language="python",
pattern=search_pattern,
replacement=replacement,
dry_run=True # Set False to apply changes
)
The replace() method (lines 187-197) uses SgNode.replace_with() from the underlying library. The {{args}} placeholder injects original call arguments into the replacement.
Integrating with LangChain Agents
Expose structural capabilities to LLM agents through standardized tool interfaces:
from codebase_rag.main import (
create_structural_search_tool,
create_structural_editor_tool,
AstGrepService
)
service = AstGrepService(project_root="/path/to/repo")
search_tool = create_structural_search_tool(service)
editor_tool = create_structural_editor_tool(service)
# Tool invocation from agent prompts
results = search_tool.run("""{
"language": "python",
"pattern": "{ \\"type\\": \\"ImportFrom\\", \\"children\\": [{ \\"type\\": \\"module\\", \\"constraint\\": \\"Str == 'os'\\" }] }"
}""")
edit_result = editor_tool.run("""{
"language": "python",
"pattern": "{ \\"type\\": \\"Call\\", \\"children\\": [{ \\"type\\": \\"func\\", \\"constraint\\": \\"Str == 'print'\\" }] }",
"replacement": "logging.info({{args}})"
}""")
These factory functions at lines 1648-1653 in codebase_rag/main.py return Tool objects compatible with LangChain's agent framework.
Adding Support for New Languages
Extend language coverage by modifying three locations:
- Register the language ID in
codebase_rag/constants/ast_nodes.py:
# Example: adding Ruby support
AST_GREP_LANGUAGES = {
"py": "python",
"js": "javascript",
"rb": "ruby", # new entry
}
- Create pattern configuration at
parsers/ast_grep_patterns/ruby.yaml:
ast_grep_id: ruby
default_patterns:
- name: class_definition
pattern: |
class $NAME
$BODY
end
- The
AstGrepTierclass auto-discovers new configs on startup—no service restart required.
Pattern Syntax Reference
ast-grep patterns use structural variables (prefixed with $) and constraints:
| Syntax | Meaning | Example |
|---|---|---|
$NAME |
Capture any node into variable | class $NAME: captures class names |
^$NAME |
Anchor to start of pattern | ^$FUNC matches top-level functions |
constraint: "Str == 'value'" |
String literal equality | Match specific identifiers |
constraint: "not Type" |
Negation | Exclude nodes of given type |
Complex patterns nest type and children fields to match hierarchical AST structures precisely.
Summary
- AstGrepService in
codebase_rag/tools/ast_grep_service.pyprovides the core search/replace API - Language-agnostic operation spans Python, PHP, JavaScript, Rust, Go, and extensible to others
- AST-level matching eliminates regex false positives from strings and comments
- LangChain integration via
create_structural_search_tool()andcreate_structural_editor_tool()enables LLM-driven refactoring - Graceful degradation through
has_ast_grep()detection incodebase_rag/utils/dependencies.py - Extensible architecture via
AstGrepTierpattern discovery incodebase_rag/parsers/ast_grep_tier.py
Frequently Asked Questions
What makes ast-grep better than regex for code search?
AST patterns match the semantic structure of code rather than text surface. A regex searching for print( hits string literals containing that text; ast-grep's {"type": "Call", "children": [{"type": "func", "constraint": "Str == 'print'"}]} only matches actual function calls. This precision is essential for safe automated refactoring.
Does the code-graph-rag implementation require ast-grep CLI installation?
No. The repository uses ast-grep-py, a Python-native binding. The has_ast_grep() utility checks for this package at runtime. If absent, structural tools return empty results rather than failing—allowing deployments to function without the optional dependency.
How do I preview changes before applying them?
Pass dry_run=True to AstGrepService.replace(). The method returns a list of proposed (file_path, original_snippet, replacement_snippet) tuples without writing to disk. Set dry_run=False to execute the transformation.
Can I use this with languages not officially listed?
Yes. Add the file extension-to-language ID mapping in constants/ast_nodes.py, create a YAML config in parsers/ast_grep_patterns/, and ensure the underlying ast-grep-py binary supports the language. The AstGrepTier loader auto-detects new configurations on the next service initialization.
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 →