The Nightly Build.
Expert Tools

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:

search and rewrite, against microsoft/TypeScriptCONSOLE
$ 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(-)
Fig. 1 · The search finds 9 matches across 5 files; the non-interactive rewrite (-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:

test.pyPYTHON
def foo(x):
    return x + 1

y = foo(2)
print(y)
Fig. 2 · The starting file, before the rule runs.8

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:

change_func.ymlYAML
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)
Fig. 3 · 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.

.coderabbit.yaml ruleYAML
id: no-console-except-error
rule:
  any:
    - pattern: console.error($$$)
      not:
        inside:
          kind: catch_clause
Fig. 4 · A relational rule, cited by CodeRabbit's own documentation for its ast-grep integration.10

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.

Verdict

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

  1. ast-grep docs · Quick Start
  2. ast-grep/ast-grep · GitHub repository
  3. ast-grep/ast-grep · CHANGELOG.md
  4. ast-grep/ast-grep · Issue #56, "Command name alias: ast-grep and sg"
  5. ast-grep/ast-grep · PR #2843, "feat: deprecate the sg command"
  6. ast-grep docs · Pattern Syntax
  7. microsoft/TypeScript · commit cc5c6e2d (2026-07-23), tested directly with ast-grep 0.45.0
  8. ast-grep docs · Rewrite Code
  9. ast-grep docs · Rule Object Reference
  10. CodeRabbit docs · ast-grep Instructions
  11. ast-grep docs · Tool Comparison
  12. agentmako · "grep, ripgrep, ast-grep, and what AI coding agents actually need"
  13. ast-grep docs · FAQ
  14. ast-grep/ast-grep · Cargo.toml (workspace manifest)
  15. Codemod · "From Axios → Fetch with Codemod 2.0"