ast_edit — the AST structural engine
ast_edit is the structural editing strategy of the fs module —
the second engine behind Fs::edit, alongside the
hashline replace engine. Where edit anchors on line numbers and
content hashes, ast_edit rewrites the file’s syntax tree directly: it
matches a code pattern structurally and replaces each match with a template.
Fs::edit(&self, args: serde_json::Value) -> Result<Vec<EditResult>, String> // with the `ast` argument
ast_edit(metadata, FsAstEdit) -> Result<Vec<EditResult>, String> // the underlying free function
Use the AST engine when your change is structural — renaming a function’s signature across many call sites, wrapping every argument list in a helper, updating a repeated idiom — rather than a literal text swap. For plain text replacements, prefer the hashline engine.
The model: pattern → template
Each rewrite operation is an AstEditOp:
a pat (pattern) and an out (replacement template).
- Parse.
patis parsed as source code in the file’s language. A pattern that does not parse is reported as a per-file warning; the op is skipped for that file rather than aborting the whole run. - Match. The pattern is matched structurally against the file’s tree-sitter syntax tree. Matching is about shape, not text: whitespace and formatting differences do not break a match.
- Rewrite. Each matched subtree’s source text is replaced by
out. Metavariables captured inpatare substituted intoout. - Repeat. Multiple ops are applied in order, and for Rust, ast-grep also derives a statement-level contextual pattern so top-level and nested statements both match. The tree is re-parsed between derived patterns.
The whole set of matched files is gathered first, sorted, and deduplicated —
order of the caller’s paths list does not carry intention here, unlike the
replace engine.
Metavariables
Patterns support ast-grep metavariables:
| Metavariable | Matches |
|---|---|
$NAME |
Exactly one node. |
$$$NAME |
Zero or more nodes (for example, an argument list). |
Metavariable identity is enforced: the same metavariable must capture identical text in every occurrence for a match to succeed.
Critical gotcha: a metavariable captures a whole node — it never matches a substring inside a literal. To change the text of a string, match the whole literal including its quotes:
pat: console.log("$M") // ✅ matches, captures the whole string node
out: console.log("[$M]")
pat: Hello, $X! // ❌ a bare phrase is not a structurally valid snippet
Likewise, string literal patterns must include their quotes: "$M", not $M.
Patterns must be complete, structurally valid source snippets for the
target language.
Resolving which files to rewrite
The paths argument accepts any mix of:
- Files — explicit paths, rewritten if the language is supported.
- Directories — walked recursively; every file with a supported grammar is
included. Symlinks are never followed (they could escape the root or form a
cycle), and VCS/vendored subtrees (
.git,node_modules) are skipped. - Globs — expanded with gitignore-awareness, then validated.
Every resolved path is validated against the path guards and must have a
supporting tree-sitter grammar. Nonexistent explicit paths are silently skipped.
The result is sorted and deduplicated, then capped at max_files
(DEFAULT_MAX_FILES, 1000);
if the cap is reached, a note is appended warning you to narrow paths.
As with write/edit, a file that declares itself machine-generated is never
rewritten.
What you get back
The result is one EditResult per file that was actually
modified by at least one rewrite, plus files that produced warnings (a pattern
failed to parse, or no match was found) so you see the feedback instead of
silence. Searched files with no changes and no warnings are not reported.
The shape is identical to the replace engine’s output: fresh hash tag, the
1-based first_changed_line, warnings, and a unified diff when the file
changed.
Errors
Unlike the replace engine, ast_edit returns a plain error string. A failure
names the failing file and aborts the run — there is no caller-intended order to
preserve, so no applied/skipped distinction. Top-level errors occur when:
pathsis empty, oropsis empty.- A
patis empty or duplicated. - A path is denied by the guards, a glob fails, or a file cannot be read or written back.
Example
A complete runnable example is provided at
examples/fs/ast_edit.rs.
It writes a small source file, then uses Fs::edit with an ast argument to
rename a function everywhere it appears — including across a directory tree.