Pattern files use @@ delimiters with a required match mode and optional metavariable declarations:
@@
match: strict
metavar $obj: single
metavar $method: single
metavar $arg: single
@@
$obj.$method($arg)
Match modes (required - must specify one):
match: strict- Exact positional matching (no extra children allowed, ordered). Use for function calls, arrays.match: partial- Subset matching (ignores extra children, unordered). Use for object literals, JSX attributes.match: field- Declaration matching that ignores optional fields the pattern omits (decorators, annotations, modifier groups, return types), by aligning the pattern to a subsequence of the declaration's children. Use for decorated/annotated definitions.
The three modes behave differently when the source has extra children or different ordering:
Pattern: foo($a, $b)
| Source | strict | partial | field |
|---|---|---|---|
foo(1, 2) |
✓ match | ✓ match | ✓ match |
foo(1, 2, 3) |
✗ no match (extra arg) | ✓ match | ✓ match |
foo(2, 1) |
✓ match ($a=2, $b=1) | ✓ match | ✓ match |
For function calls, you typically want strict because foo(1, 2) and foo(1, 2, 3) are semantically different calls.
Pattern: { x: $X, y: $Y }
| Source | strict | partial | field |
|---|---|---|---|
{ x: 1, y: 2 } |
✓ match | ✓ match | ✓ match |
{ x: 1, y: 2, z: 3 } |
✗ no match | ✓ match | ✓ match |
{ y: 2, x: 1 } |
✗ no match (wrong order) | ✓ match | ✗ no match |
For object literals, you typically want partial because property order doesn't matter and you may not care about extra properties.
Pattern:
function $NAME() { $BODY }
| Source | strict | partial | field |
|---|---|---|---|
function foo() { return 1; } |
✓ match | ✓ match | ✓ match |
@decorator function foo() { return 1; } |
✗ no match | ✓ match | ✓ match |
function foo() { return 1; return 2; } |
✗ no match | ✓ match | ✗ no match |
For function definitions, field is ideal: it ignores decorators/attributes (which are in a different tree-sitter field) while still enforcing order within the function body.
| Mode | Extra children | Ordering | Best for |
|---|---|---|---|
strict |
Fail | Required | Function calls, arrays, exact structure |
partial |
Ignored | Ignored | Object literals, JSX attributes |
field |
Ignored (different field) | Required within fields | Definitions with decorators/attributes |
Each metavariable must be declared with a type:
single- matches exactly one AST node (which may be a compound expression)sequence- matches zero or more AST nodes
Example with sequence metavar:
@@
match: strict
metavar $class_name: single
metavar $body: sequence
@@
class $class_name { $body }
This matches a class with any number of members, binding $body to all children.
All metavariables used in the pattern body should be declared in the preamble.
The parser validates undeclared metavars for tokens like $NAME (uppercase after $).
You can use ... as a shorthand for an anonymous sequence metavariable. Each
ellipsis is treated as its own sequence binding internally:
@@
match: strict
metavar $NAME: single
@@
function $NAME(...) { ... }
Notes:
- Each
...matches zero or more nodes. - Ellipsis is not replaced when it looks like a spread operator (e.g.,
...$xor...args). - Sequence metavars (including
...) are not supported withmatch: partial. - On transform (strict mode), the source
...captures are preserved: a-line inside an...-bracketed pattern removes only the marked part and keeps the surrounding siblings (...is never emitted literally). - A bare
...cannot sit on a-or+line — it is a match-only construct, so it belongs on a context line. (An ellipsis nested in a marked expression, e.g.- old(...)/+ new(), is fine: the-applies to the whole expression, which is replaced wholesale.) A pattern that breaks this is rejected with an error.
A pattern body that contains multiple top-level statements (with no enclosing function/class wrapper in the pattern) matches any consecutive run of source children of the same shape, regardless of what surrounds them. The pattern fires anywhere a container's child sequence has the named statements as immediate successors:
@@
match: strict
metavar $X: single
@@
bar();
foo($X);
This matches bar(); foo(x); inside any function body, if/else branch, loop body, lambda body, or other statement-bearing block — at arbitrary nesting depth. The matched region is just those two statements (not the entire enclosing block), so a deletion like:
@@
match: strict
@@
bar();
- foo(x);
removes only the foo(x); line at every site where it immediately follows bar();, leaving surrounding statements untouched.
Notes:
- "Immediate succession" means no statements between the named ones in source. To allow intervening statements, write an explicit
...between them. - The semantic is positional: order matters and the statements must be adjacent. To match an unordered set (e.g. members of an object body), use
match: partial. - Single-statement patterns are unaffected; they match any AST node of the matching shape, as before.
Tree-sitter "extras" — comments, and in some grammars whitespace tokens — are filtered automatically when the pattern doesn't mention them. A pattern matches regardless of where comments appear in the source:
@@
match: strict
metavar $A: single
metavar $B: single
metavar $C: single
@@
{ a: $A, b: $B, c: $C }
This matches { a: 1, b: 2, /* note */ c: 3 } and { a: 1, b: 2, c: 3 } — comments are transparent.
To require a comment, include one in the pattern body. Source comments at the corresponding position must then match the pattern's comment text exactly:
@@
match: strict
metavar $A: single
metavar $B: single
metavar $C: single
@@
{ a: $A, b: $B, /* note */ c: $C }
This matches { a: 1, b: 2, /* note */ c: 3 } but not { a: 1, b: 2, c: 3 } — the pattern explicitly demands the comment.
Notes:
- Comments are matched by exact text; metavariables inside comment bodies are not interpolated.
- The behavior applies uniformly to
match: strict,match: partial, andmatch: field. - Whether
//and/* */are interchangeable depends on the grammar — some expose them as the same node type, others as distinct types. A pattern using one form may not match source using the other in grammars that distinguish them.
Use match: partial to enable subset matching for children. Each pattern child finds any matching source child (unordered), and extra source children are ignored:
@@
match: partial
metavar $X: single
@@
{ someField: $X }
This matches objects containing someField, regardless of other properties. For example, it matches { someField: 1, other: 2 } binding $X = 1.
Partial matching applies recursively to all nested structures:
@@
match: partial
metavar $X: single
@@
{ f1: { f2: $X } }
This matches { f1: { f2: 42, f3: "extra" }, other: true } binding $X = 42.
Notes:
match: partialmay backtrack to satisfy all bindings when multiple children could match.- Sequence metavars are rejected in
match: partial.
Use on $VAR in a section to match directly against a previously-bound node
instead of traversing the whole source tree. This restricts where the later
section can match:
@@
match: strict
metavar $class_name: single
metavar $body: single
@@
class $class_name { $body }
@@
match: strict
on $body
metavar $msg: single
@@
console.log($msg)
This finds console.log calls only inside class bodies, not at the top level:
section 1 binds $body to the class body node; section 2 uses on $body to
search within that node only.
on $VAR requires $VAR to be a single metavar (it scopes the section to
that one bound subtree). To iterate a sequence metavar element-by-element,
use foreach $VAR instead — the section's pattern is matched against, and its
-/+ transform applied to, each element in turn.
@@
match: strict
metavar $OBJ: single
@@
foo($OBJ)
@@
match: partial
on $OBJ
metavar $X: single
@@
{ someField: $X }
This matches foo({ someField: 1, other: 2 }):
- Section 1 matches
foo($OBJ), binding$OBJto{ someField: 1, other: 2 } - Section 2 uses
on $OBJto match directly against that object (not searching within it) - With
match: partial, it extracts$X = 1while ignoringother: 2
Example output:
Found 2 match(es):
line 5: console.log("hello")
$obj = console
$method = log
$arg = "hello"
line 12: Math.floor(x + 1)
$obj = Math
$method = floor
$arg = x + 1
Metavariables are pre-processed before parsing, so they work across all supported languages regardless of whether $name is valid syntax in that language.
When a multi-section pattern has no on $VAR directives, all sections are
conjunctive siblings: each section searches the source file independently,
and the rule fires only if every section finds at least one match. If any section
finds nothing, no transforms are applied and the source is left unchanged.
This is useful when two changes must always happen together — for example, renaming an import and updating all its call sites.
@@
match: strict
@@
- import { useAppSelector } from "app/hooks";
+ import { useUser } from "app/UserContext";
@@
match: strict
metavar $NAME: single
metavar $PARAM: single
@@
- const $NAME = useAppSelector(($PARAM) => $PARAM.users.user);
+ const { user: $NAME } = useUser();
Both sections must match somewhere in the file. If the import is absent the call sites are left alone; if there are no matching call sites the import is left alone. When both are present, both transforms are applied in a single pass.
A section with no -/+ lines acts as a guard: it must match somewhere in
the file but contributes no edits. Use this to make a transform conditional on
the presence of some other code.
@@
match: strict
@@
- foo()
+ bar()
@@
match: strict
@@
setupFoo()
foo() is only renamed if setupFoo() also appears somewhere in the file.
Each sibling section has its own independent metavar scope. The same name can be declared in multiple sections without conflict:
@@
match: strict
metavar $X: single
@@
- foo($X)
+ bar($X)
@@
match: strict
metavar $X: single
@@
- baz($X)
+ qux($X)
The $X in section 1 and the $X in section 2 are entirely independent
bindings.
Conjunctive sibling sections (no on $VAR) and outer+inner sections (with
on $VAR) cannot be mixed in the same pattern. Attempting to do so raises an
error at parse time.
Matcher.find and Matcher.transform handle single- and multi-section
patterns alike — there is no separate multi-section entry point.
In outer+inner mode (patterns that use on $VAR), metavariables share a
global scope across all sections. A metavar declared in the outer section
is automatically in scope for all inner sections — you do not need to (and must
not) re-declare it:
@@
match: strict
metavar $CLASS: single
metavar $BODY: single
@@
class $CLASS { $BODY }
@@
match: strict
@@
console.log($CLASS)
Here the inner section uses $CLASS (declared in the outer section) directly in its pattern body without re-declaring it. This matches only console.log calls whose argument is the same identifier as the enclosing class name.
Declaring a metavar in an inner section that was already declared in an outer section is an error:
@@
match: strict
metavar $X: single
@@
foo($X)
@@
match: strict
metavar $X: single (* ERROR: $X was already declared above *)
@@
bar($X)
This raises a Failure exception at parse time. Use the existing binding from the outer section instead.
When the same metavar appears in more than one position in a match pattern, all occurrences must bind to structurally identical nodes. This applies both within a single section and across sections:
@@
match: strict
metavar $X: single
@@
compare($X, $X)
This only matches calls where both arguments are identical (e.g. compare(a, a)), not compare(a, b).
Across sections, the outer binding constrains the inner match. In the console.log($CLASS) example above, only calls where the argument text matches the previously bound $CLASS will match.
Patterns can describe code transformations using -/+ line prefixes, similar
to unified diff syntax. Lines prefixed with - appear only in the match; lines
with + appear only in the replacement. Unprefixed lines (or lines with a
leading space) are context that appears in both.
Rename a function call:
@@
match: strict
metavar $MSG: single
@@
- console.log($MSG)
+ logger.info($MSG)
Applied to console.log("hello"), this produces logger.info("hello").
Metavariables carry their bound values from the match side to the replace side.
Context lines anchor the match while -/+ lines describe the change:
@@
match: strict
metavar $NAME: single
metavar $BODY: sequence
@@
- function $NAME() {
- $BODY
- }
+ const $NAME = () => {
+ $BODY
+ }
This converts traditional function declarations into arrow functions.
# Preview the diff (no file changes)
diffract apply patch.txt source.ts
# Apply changes in place
diffract apply --in-place patch.txt source.ts
# Apply across a directory
diffract apply --in-place --include '*.ts' patch.txt src/With match: strict, the entire matched node is replaced with the instantiated
template. This is the simplest mode and works well for renaming function calls,
swapping arguments, or restructuring expressions.
Swap arguments:
@@
match: strict
metavar $A: single
metavar $B: single
@@
- assertEqual($A, $B)
+ assertEqual($B, $A)
With match: partial, only the matched subset of children is affected. Source
children not mentioned in the pattern are preserved. This is useful for modifying
individual properties in object literals.
Rename a property:
@@
match: partial
metavar $V: single
@@
- { color: $V }
+ { colour: $V }
Source { color: "red", size: 10 } becomes { colour: "red", size: 10 } -
the size property is untouched.
Add a property:
@@
match: partial
metavar $V: single
@@
- { name: $V }
+ { name: $V, id: 0 }
Every object that has a name property gets an id: 0 added. Objects without
name are left alone.
Remove a property:
@@
match: partial
metavar $V: single
@@
- { name: $V, deprecated: true }
+ { name: $V }
Removes the deprecated property from objects that have both name and
deprecated: true.
match: field matches a declaration while ignoring the optional fields the
pattern omits. On transform it is surgical: the edit covers only the part
the -/+ lines mark, and the omitted optional fields — decorators,
annotations, modifiers, return types — are preserved.
@@
match: field
metavar $NAME: single
metavar $BODY: single
@@
- fun $NAME() { $BODY }
+ fun $NAME() { logCall(); $BODY }
against @Deprecated private fun f() { ... } keeps @Deprecated private and
rewrites the fun … { … } part. Marking the whole declaration (a body with
no context line) still replaces it whole — and then the ignored fields are
dropped, which the matcher warns about.
Transforms are surgical in all modes: the edit is localized to the -/+
lines, and everything else is preserved byte-for-byte. A pattern whose -/+
cover the whole match replaces the whole match (the degenerate single-region
case); a pattern that marks only a sub-part edits only that part.
| Mode | A sub-part -/+ edit |
Preserved |
|---|---|---|
strict |
edits the marked lines | context, ...-captured siblings |
partial |
edits the marked element(s), separator-aware | tolerated extra elements |
field |
edits the marked part of the declaration | ignored optional fields (decorators, modifiers, return types) |
The remaining footgun is marking the whole container in partial/field
mode (a body with no context line): that replaces it whole and drops the
tolerated extras / ignored fields, so the matcher warns (pattern_warnings).
To edit a sub-part, mark only that part (context lines, and partial's
unlisted elements, are preserved), or use foreach/on.
Write the + as bare content. A + line is spliced in place over what the
- matched, so it should carry only the new content — not the structural
indentation or the element separator the container already supplies. Against
{ color: red, size: 10 }, write + colour: $v (not + colour: $v,); the
surrounding indentation and commas are kept by the splice, and restating them
would double them.
When a sequence metavar is referenced in a + template, its elements are
rendered and substituted in place. Two independent knobs:
join $VAR by "<sep>"(a preamble directive) — the string placed between rendered elements (interpreting\n,\t,\\); default is empty.foreach $VAR(a following@@section) — a per-element transform: the section matches one element and its-/+lines rewrite it, and the results are joined. Without aforeach, elements render as their source text.
The sequence metavar must be declared metavar $VAR: sequence and appear on the
match side.
@@
match: strict
metavar $ELEMS: sequence
join $ELEMS by " && "
@@
- all([$ELEMS])
+ ($ELEMS)
all([x, y, z]) becomes (x && y && z).
The outer section captures the call and names the sequence of case pairs; a
foreach section describes how to rewrite each pair. The outer template
references $CASES, which is replaced by the joined per-element results (here
the default empty join).
@@
match: strict
metavar $TAG: single
metavar $CASES: sequence
@@
- matchExhaustive($TAG, { $CASES });
+ match($TAG)$CASES.exhaustive();
@@
match: strict
foreach $CASES
metavar $KEY: single
metavar $VAL: single
@@
- $KEY: $VAL
+ .with("$KEY", $VAL)
Given matchExhaustive(tag, { A: () => 1, B: () => 2 }); the result is
match(tag).with("A", () => 1).with("B", () => 2).exhaustive();.
A foreach element with an empty replacement (a - line, no +) deletes the
element and cleans up the adjacent separator — useful for removing a deprecated
property or unused argument from a list.
join/foreachtargetsequencemetavars that also appear on the match side.- A
foreachsection is a normal section (its ownmatch:mode and metavars); it cannot itself contain a nestedforeach.
- Every metavariable used on the
+side must also appear on the-or context side (so it has a value to substitute). - Ellipsis (
...) is only allowed on context or-lines so it is bound on the match side. Using...in a+-only line is an error. - Use
- ...only with strong anchors on surrounding context lines; otherwise it can match too broadly (including empty sequences) and remove more than intended. - Patterns without any
-/+lines are plain search patterns;transformreturns the source unchanged. - When multiple matches occur in the same file, edits are applied bottom-to-top to avoid offset invalidation. Overlapping edits are filtered (first in document order wins; ties by start position keep the longest edit).
(* Create a context once at program start and reuse it *)
let ctx = Diffract.Context.create () in
(* Parse and traverse *)
let tree = Diffract.parse_tree ~ctx ~language:"typescript" code in
Diffract.Tree.traverse (fun node ->
Printf.printf "%s: %s\n"
node.node_type
(Diffract.Tree.text tree.source node)
) tree.root
(* Pattern matching with concrete syntax. `find` returns composite_match list
(one entry per section per match); print each matched span. *)
let pattern_text = {|@@
match: strict
metavar $obj: single
metavar $method: single
@@
$obj.$method()|} in
let composites =
Diffract.Matcher.find ~ctx ~language:"typescript" ~pattern_text
~source_text:code
in
List.iter
(fun (c : Diffract.Matcher.composite_match) ->
List.iter
(fun (r : Diffract.Matcher.M.match_result) ->
Printf.printf "match: %s\n"
(String.sub code r.start_byte (r.end_byte - r.start_byte)))
c.sections)
composites
(* Apply a semantic patch. `transform` returns the rewritten source; render a
diff with Text_diff if it changed. *)
let patch = {|@@
match: strict
metavar $MSG: single
@@
- console.log($MSG)
+ logger.info($MSG)|} in
let transformed =
Diffract.Matcher.transform ~ctx ~language:"typescript" ~pattern_text:patch
~source_text:code
in
if transformed <> code then
print_string
(Diffract.Text_diff.generate_diff ~file_path:"source.ts" ~original:code
~transformed)
(* Multi-section, foreach, and conjunctive patches all go through the same
`transform` — there is no separate entry point. *)
let patch = {|@@
match: strict
metavar $TAG: single
metavar $CASES: sequence
@@
- matchExhaustive($TAG, { $CASES });
+ match($TAG)$CASES.exhaustive();
@@
match: strict
foreach $CASES
metavar $KEY: single
metavar $VAL: single
@@
- $KEY: $VAL
+ .with("$KEY", $VAL)|} in
print_string
(Diffract.Matcher.transform ~ctx ~language:"typescript" ~pattern_text:patch
~source_text:code)