| name | ast-grep |
| description | AST-based code search and rewrite via tree-sitter patterns. Use instead of Grep/Edit when a change spans many sites or must respect code structure: structural matching, multi-file batch rewrites, or context-aware queries (e.g. "unwrap inside impl blocks"). Not for a single-file or single-symbol rename โ use Edit for that.
|
| allowed-tools | Bash(mise exec -- ast-grep *), Read, Glob |
ast-grep
Matches code by parsed AST, not text. Supports metavariable capture, relational rules, and in-place rewrite.
All commands must be run through mise exec -- since ast-grep is installed via mise:
Commands
# Pattern search
mise exec -- ast-grep run -p '$X.unwrap()' -l rust .
# Rewrite: always preview first, then apply with -U
mise exec -- ast-grep run -p '$X.lock().unwrap()' -l rust .
mise exec -- ast-grep run -p '$X.lock().unwrap()' -r '$X.lock().expect("lock poisoned")' -l rust -U .
# Structural search with YAML rules (for relational/composite logic)
mise exec -- ast-grep scan --inline-rules 'id: name
language: rust
rule:
pattern: $X.unwrap()
inside:
kind: impl_item
stopBy: end' .
# JSON output for programmatic use
mise exec -- ast-grep run -p '$X.unwrap()' -l rust --json .
# Debug: see how ast-grep parses your pattern or target code
mise exec -- ast-grep run -p 'PATTERN' -l rust --debug-query=cst # concrete syntax tree
mise exec -- ast-grep run -p 'PATTERN' -l rust --debug-query=pattern # metavar detection
Pattern syntax
| Syntax | Meaning |
|---|
$VAR | Single named node. Reuse = must match identically ($A == $A matches x == x only) |
$$VAR | Single unnamed node (operators, punctuation) |
$$$ | Zero or more nodes (variadic args, statements) |
$_ prefix | Non-capturing (each occurrence matches independently) |
Patterns must be valid parseable code. Bare .unwrap() is an ERROR โ use $X.unwrap().
Use --selector KIND to disambiguate patterns that parse as the wrong node type. Provide surrounding context in the pattern and select the sub-node you actually want: mise exec -- ast-grep run -p 'struct S { pub $N: $T }' --selector field_declaration -l rust .
YAML rule structure
Rules combine three categories:
- Atomic:
pattern, kind, regex, nthChild
- Relational:
has, inside, precedes, follows โ always add stopBy: end
- Composite:
all, any, not, matches
rule:
all:
- kind: function_item
- has:
kind: await_expression
stopBy: end
- not:
has:
kind: try_expression
stopBy: end
Use constraints in YAML rules to filter metavariable text by regex:
rule:
pattern: $X.$METHOD()
constraints:
METHOD:
regex: "^(unwrap|expect)$"
Key Rust node kinds
Use --debug-query=cst to discover kinds at runtime. Non-obvious mappings:
| Kind | Note |
|---|
function_item | All fn declarations (pub, async, const, etc.) |
impl_item | Both inherent and trait impls |
method_call_expression | x.foo() โ distinct from call_expression (foo()) |
macro_invocation | println!(...), vec![...] |
await_expression | expr.await |
try_expression | The ? operator |
function_modifiers | Contains async, unsafe, const keywords |
visibility_modifier | pub, pub(crate), etc. |
Shell escaping
Use single-quoted strings for inline rules to avoid $ expansion. If you must double-quote, escape as \$VAR, \$\$\$.