ast-grep matched and rewrote 9 nullable-call sites across 5 files in the TypeScript compiler's source
The pattern binds to the parse tree, not the text, so it skips every look-alike sitting inside a comment, a string, or a call with the wrong number of arguments.
A sed backreference matches the pattern until it breaks across a line
Code written before optional chaining existed is full of one shape:
check that a property exists, then call it.
obj.val && obj.val(). The modern replacement is
one operator, obj.val?.(), and a large codebase
carries the old shape hundreds of times, spelled slightly
differently at each site. Real occurrences split the two halves
across a line break, the way this one does:
const result = obj.val &&\n obj.val().1
A sed substitution catches the single-line form with a
backreference:
sed -E 's/([a-zA-Z_.]+) && \1\(\)/\1?.()/'. It
has no notion of the second half moving to the next line. It has no
notion of the difference between the pattern sitting in working
code and the same characters sitting inside a string or a comment
that happens to quote the old style. It has no way to tell a
two-argument call from a zero-argument one without a longer,
more fragile expression. Every character on the page counts the
same to it.
ast-grep is a Rust command-line tool, built on the tree-sitter
parsing library, that searches and rewrites code by matching the
shape of its parse tree instead of matching characters, also
invocable by the shorter alias sg.2
In the
release sitting at the top of the project's changelog as of this
writing, sg is being deprecated: the alias collides
with the pre-existing Linux sg command (for
setgroups), installed by default on distributions such
as Arch.34 Both the native
binary and its npm shim now print a warning to stderr whenever
sg is invoked, on every platform, and both keep
working while they do.5
The project's own docs already teach every example with the long
form.1
$PROP matched nine real calls in the TypeScript compiler's source
Generalize the pattern with a metavariable:
$PROP && $PROP(). A metavariable starts with
$ and matches exactly one node in the parse tree, one
expression, whatever its own internal shape. A repeated name like
$PROP must match an identical subtree at every
occurrence, the way a backreference matches identical text, except
the identity check is structural rather than character-by-character.
It also carries information a text match cannot. It is
argument-count aware, so the pattern does not fire on a call with a
different number of arguments. It is lexically aware too, so it
does not match the same characters sitting inside a comment or a
string literal.6
Run it against a fresh clone of the TypeScript compiler's own
source, at commit cc5c6e2d, and it finds nine real
matches spread across five files: one in
src/compiler/moduleSpecifiers.ts, two each in
src/harness/harnessIO.ts,
src/services/services.ts,
src/services/stringCompletions.ts, and
src/tsserver/nodeServer.ts. Each $PROP
bound to a real property chain, not a bare name:
os.homedir, host.getCustomTransformers,
ts.sys.tryEnableSourceMapsForHost, among others.
Applying the rewrite non-interactively changes all nine:
$ ast-grep --pattern '$PROP && $PROP()' --lang ts src
[... 9 matches ...]
$ ast-grep -p '$PROP && $PROP()' -r '$PROP?.()' --lang ts -U src
Applied 9 changes
$ git diff --stat
5 files changed, 9 insertions(+), 9 deletions(-)
-U) applies all 9, confirmed
by git diff --stat.7
Every hunk is exactly the transform the pattern promises and
nothing else. In src/tsserver/nodeServer.ts,
(os.homedir && os.homedir()) || becomes
(os.homedir?.()) ||. In
src/services/services.ts,
host.getCustomTransformers &&
host.getCustomTransformers()
becomes host.getCustomTransformers?.(). No line
outside those nine changed.7
None of the nine spans a line break; every real site in this
checkout sits on one line. The pattern would still bind across a
break if one existed, since it matches the shape of the tree and
not the text: that is a separate, documented property, not
something this run happened to demonstrate.1
One detail matters as much as the pattern itself: the argument has
to be single-quoted. A double-quoted
"$PROP && $PROP()" lets the shell expand
$PROP to an empty string before ast-grep ever sees the
pattern, the one place its own syntax collides with a text tool it
is not.1
A YAML rule renames the definition and every call, and nothing else
A one-shot --rewrite is still a manual action, run
once, watched by a person reading the diff. A check meant to be
reused, as a repeated codemod or a house lint rule enforced in CI,
moves into a YAML rule file, run with ast-grep scan.
Given a file that defines a function and calls it:
def foo(x):
return x + 1
y = foo(2)
print(y)
a rule file with two patterns, one for the definition and one for
the call, renames foo to baz only where
the tree says it is that function's name or that function's call
site:
id: change_def
language: Python
rule:
pattern: |
def foo($X):
$$$S
fix: |-
def baz($X):
$$$S
---
id: change_param
rule:
pattern: foo($X)
fix: baz($X)
ast-grep scan -r change_func.yml test.py
renames the definition and its call site, leaving
return x + 1 and print(y) untouched.8
A plain sed 's/foo/baz/g' cannot make that
distinction. It treats foo as a string wherever it
appears. It would rewrite the same characters inside a comment,
inside an unrelated identifier that merely contains those letters,
or inside a string literal that happens to quote it. Its regex
engine has no way to tell any of those apart from the real call.
The YAML rule matches by AST role instead, a function definition's
name, a call expression's callee, not by substring.8
The
$$$S in the pattern is the zero-or-more-node
metavariable, so the rule matches whether the function body is one
line or ten.6
The rewrite is also indentation-aware in a way substitution cannot be. A fix template's own indentation composes additively with wherever the match lands in the surrounding code. The same fix text comes out at a different depth depending on whether it lands at the top level or inside a nested block, and line-oriented substitution has no concept of a replacement re-indenting itself relative to where it lands in the tree.8
A rule can flag console.error unless it sits inside a catch block
ast-grep's rules also compose relationally. inside,
has, precedes, and follows
each take a nested sub-rule, so a match can depend on where it sits
in the tree, not only on its own shape.9
CodeRabbit, an AI code-review product with no stake in ast-grep,
ships exactly this as a worked example for its own custom review
rules: flag console.error(...) calls unless they sit
inside a catch_clause.
id: no-console-except-error
rule:
any:
- pattern: console.error($$$)
not:
inside:
kind: catch_clause
No single grep or sed invocation states
that condition, because neither tool has a concept of "inside" at
all. They see lines, not containment. A regex can find every
console.error call. Separating the ones sitting in
error handling from the ones that are not requires tracking clause
type and nesting across however many lines separate them. That is
the tree-structural awareness a line-based tool does not carry.9
ast-grep's own comparison page concedes it stops at syntax
The project does not oversell its reach. Its own comparison page
states plainly that ast-grep operates "only on the syntactic
level," with no type information and no data-flow analysis: it
cannot tell whether $X.foo() is being called on an
actual Foo instance, only that the shape matches.11
The same page concedes that GritQL is "generally more powerful," at
a steeper learning-curve cost, and that Semgrep carries "advanced
features like equivalence and deep-semgrep" that ast-grep does not
attempt, trading some of that power for being, in the project's own
words, "very performant" using "a real parser." It grants the same
trade to Comby, which it calls "more generic" precisely because
Comby skips per-language parsers altogether. That comes at the cost
of being "not aware of the syntax and semantics," unable to handle
an indentation-sensitive language like Python the way ast-grep's
fix templates do.11
That generality is a real advantage for a language none of these
tools parses, but not one for TypeScript or Python.
An independent account sharpens the same ceiling into a concrete failure mode for AI coding agents specifically. Matching by shape finds the right call sites, but the tool still cannot follow an import graph, a barrel export, or a dynamic import. It has no way to flag a stale match, and it hands back unranked, untyped hits the agent still has to interpret on its own.12 Its author built a competing product to cover that gap, a real conflict worth naming. But the underlying claim, that ast-grep does not do cross-file or semantic resolution, is exactly what the project's own comparison page already concedes.1211
The project's own FAQ catalogues where the syntax match gets
brittle in practice. A pattern has to parse as standalone code on
its own: a bare JSON fragment like "key": "$VAL" is
not legal JSON by itself and needs a context workaround
to match at all. The CLI and the online playground can disagree on
the identical pattern, purely from a tree-sitter parser-version
difference plus a UTF-8-versus-UTF-16 encoding difference that
changes how each recovers from a parse error, worth knowing before
prototyping a rule in the playground and shipping it from the CLI
unchecked. A $$$ capture is lazy in a specific,
documented way, so an ambiguous pattern like
foo($$$A, b, $$$C) against foo(a, c, b, b, c)
stops $$$A at the first b rather than
trying every split, which can silently mis-capture. And there is no
built-in prefix or suffix syntax: matching every identifier that
starts with use, the way React hook names do, needs a
separate constraints block with a regex bolted onto
the pattern, not something the pattern syntax expresses on its
own.13
The changelog ships every few weeks, and two outside companies trust it in production
Adopting a rewrite tool is also a bet on upkeep. ast-grep's own changelog has landed a release every two to six weeks for more than a year: 0.44.1 on 4 July 2026, 0.44.0 on 21 June 2026, back through 0.39.2 on 27 July 2025. 0.45.0 is already in flight on the project's main branch as of this writing, three weeks after the last dated release.3 Most of any given release is dependency bumps opened by an automated bot, Renovate, with genuine fixes and features landing alongside them every cycle, a maintenance signal rather than a stall.3 The project lists one author in its own build manifest,14 plus that bot-driven dependency pipeline and a stream of community pull requests: not a large team, but not an unattended one either.
Two companies with no stake in the project have put it into production. Codemod, a separate codemod-tooling company, uses ast-grep as the deterministic front end of an Axios-to-fetch migration pipeline, ahead of an LLM-driven transform step. On their own monorepo, 7 of 16 attempted call-site replacements, 43.75 percent, went through fully automatically, and 81.25 percent needed zero or only minor manual fixes.15 CodeRabbit, the review product already cited for its containment rule, lets customers write their own review checks as ast-grep YAML directly in a configuration file.10 Neither report is a large sample, and neither company publishes a user count, but both are working systems, built by outsiders who had every reason to reach for something else if the tool had not held up.
ast-grep earns its place for exactly the job: a rewrite or a lint rule that has to know it is looking at code, not text, applied at codebase scale without a human checking every hit by hand. It is not a substitute for a tool that reasons about types or data flow, and it is not the right layer for an agent that needs to follow an import graph.11
Sources
- ast-grep docs · Quick Start
- ast-grep/ast-grep · GitHub repository
- ast-grep/ast-grep · CHANGELOG.md
- ast-grep/ast-grep · Issue #56, "Command name alias: ast-grep and sg"
- ast-grep/ast-grep · PR #2843, "feat: deprecate the sg command"
- ast-grep docs · Pattern Syntax
- microsoft/TypeScript · commit cc5c6e2d (2026-07-23), tested directly with ast-grep 0.45.0
- ast-grep docs · Rewrite Code
- ast-grep docs · Rule Object Reference
- CodeRabbit docs · ast-grep Instructions
- ast-grep docs · Tool Comparison
- agentmako · "grep, ripgrep, ast-grep, and what AI coding agents actually need"
- ast-grep docs · FAQ
- ast-grep/ast-grep · Cargo.toml (workspace manifest)
- Codemod · "From Axios → Fetch with Codemod 2.0"