Ast-Grep
Use this skill for ast-grep project setup, rule authoring, rule debugging, and CLI workflows that go beyond a single structural query.
Routing
- Use
exec_commandfor ast-grep queries, project scans, rule tests, debug output, and rewrite previews. - Use
ast-grep scanfor project rules andast-grep testfor rule tests. - Use
ast-grep run --pattern=... --rewrite=... --json=compact --color=neverfor dry-run rewrite previews. Review every replacement before applying changes. - Use
ast-grep run --debug-querywhen a query matches an unexpected syntax node. - Use
--kindto match node kinds. Ast-grep supports ESQuery-style compound selectors and pseudo-selectors in supported releases. - Keep this skill read-only unless the user explicitly authorises applying a rewrite.
Quick Start
- In VT Code, prefer
vtcode dependencies install ast-grepbefore suggesting system package managers. - External install routes such as Homebrew, Cargo, npm, pip, MacPorts, or Nix are fallback options when the user explicitly wants a system-managed install.
- After installation, validate availability with
ast-grep --help. - On Linux, prefer the full
ast-grepbinary name oversgbecausesgmay already refer tosetgroups. - When running CLI patterns with shell metavariables like
$PROP, use single quotes so the shell does not expand them before ast-grep sees the pattern. - A good first rewrite example is optional chaining, for example rewriting
$PROP && $PROP()to$PROP?.().
Command Overview
ast-grep run: ad-hoc query execution and one-off rewrites.ast-grep scan: project rule scanning.ast-grep new: scaffold and rule generation.ast-grep test: rule-test execution.ast-grep lsp: editor integration via language server.
Built-In Languages
- ast-grep ships many built-in languages. Common aliases include
bash,c,cc/cpp,cs,css,ex,go/golang,html,java,js/javascript/jsx,json,kt,lua,md/markdown,php,py/python,rb,rs/rust,swift,ts/typescript,tsx, andyml. --lang <alias>and YAMLlanguage: <alias>use those built-in aliases. File-system scans infer language from built-in extensions unless the project overrides them.- Pass
--lang <alias>to the CLI when extension inference is insufficient. - Use
languageGlobswhen the repository needs a different extension mapping than ast-grep’s built-in defaults.
How Ast-Grep Works
- ast-grep accepts several query formats: pattern queries, YAML rules, and programmatic API usage.
- The core pipeline is parse first, match second. Tree-Sitter builds the syntax tree, then ast-grep’s Rust matcher finds the target nodes.
- The main usage scenarios are search, rewrite, lint, and analyze.
- ast-grep processes many files in parallel and is built to use multiple CPU cores on larger codebases.
- In VT Code, run ast-grep through
exec_commandand use this skill for YAML authoring, rule tests, rewrites, and API-level work.
Project Scaffolding
- A scan-ready ast-grep project needs workspace
sgconfig.ymlplus at least one rule directory, usuallyrules/. rule-tests/andutils/are optional scaffolding thatast-grep newcan create for rule tests and reusable utility rules.- If the repository already has
sgconfig.ymlandrules/, prefer working with the existing layout instead of recreating scaffolding. - Use
ast-grep newwhen the repository does not have ast-grep scaffolding yet. - Use
ast-grep new rulewhen the scaffold exists and the task is creating a new rule plus optional test case.
sgconfig.yml
sgconfig.ymlis the project-level ast-grep config file, not a rule file. Treat it like the repository root for rule discovery, tests, parser overrides, and embedded-language behavior.ruleDirsis required and is resolved relative to the directory containingsgconfig.yml.testConfigsis optional and configures ast-grep test discovery. Each entry needstestDir;snapshotDiris optional and otherwise defaults to__snapshots__under thattestDir.utilDirsdeclares directories for global utility rules shared across multiple rule files.languageGlobsremaps files to parsers and takes precedence over ast-grep’s default extension mapping, which is useful for similar-language reuse like TS -> TSX or C -> Cpp.customLanguagesregisters project-local parsers.libraryPathcan be one relative library path or a target-triple map,extensionsis required,expandoCharis optional, andlanguageSymboldefaults totree_sitter_{name}.languageInjectionsis experimental. Each entry needshostLanguage,rule, andinjected.- Use dynamic
injectedcandidates when the rule captures$LANGand the embedded language must be chosen from a list such ascss,scss, orless. - Raw ast-grep project discovery walks upward from the current working directory until it finds
sgconfig.yml, and--config <file>overrides that discovery with an explicit root config path. ast-grep scanrequires project config and will error if nosgconfig.ymlis found.ast-grep runcan still search without project config, though it also benefits from discovered config for things likecustomLanguagesandlanguageGlobs.ast-grep scan --inspect summaryis the quickest way to confirm which project directory and config file ast-grep actually selected during discovery.- ast-grep also recognizes a home-directory
sgconfig.ymlas a global fallback config. XDG config directories are not part of this behavior. - Keep
sgconfig.ymlauthoring on the skill path and pass the project config to ast-grep commands where needed.
Rule Catalog
- Use the ast-grep catalog as inspiration when the user wants existing example rules, not as something to copy blindly.
- Start from examples in the same language family when possible.
- Read catalog markers as hints about rule complexity:
- simple pattern examples are good starting points
Fixmeans the example includes a rewrite pathconstraints,labels,utils,transform, andrewritersmean the example depends on more advanced rule features
- When adapting a catalog example, translate it to the current repository’s language, style, and safety constraints instead of preserving the example verbatim.
- Prefer the bundled skill workflow when the user asks to explain, adapt, or combine catalog examples.
VT Code Bundled Rules
VT Code ships a set of curated ast-grep rules under rules/ with matching tests under rule-tests/. Run them with
vtcode check ast-grep. The bundled rules are organised by language:
Python (rules/python/)
no-print: flagsprint()calls in production codeno-walrus-source: flags walrus operators that harm readabilityno-unnecessary-list: flagslist(...)wrapping an already-list expressionno-identity-check-with-type: flagstype(x) is Tin favor ofisinstance(x, T)optional-to-union: flagsOptional[X]in favor ofX | Noneprefer-dict-get: flagsif k in d: d[k]in favor ofd.get(k)prefer-generator-expression: flags list comprehensions passed tosum/any/all/min/maxprefer-isinstance-tuple: flagsisinstance(x, A) or isinstance(x, B)in favor ofisinstance(x, (A, B))
Rust (rules/rust/)
no-unsafe-fn-without-unsafe: flagsunsafe fnbodies that contain nounsafeblockavoid-duplicate-export: flagspub usewhenpub modalready exposes the moduleno-iterator-for-each: flags.iter().for_each()in favor offorloopsno-redundant-closure: flags|x| foo(x)in favor offoodirectlylet-chain-candidate: flags nestedifthat could be collapsed withlet-chainsno-chars-enumerate: flags.chars().enumerate()when.char_indices()is more idiomaticno-alloc-digit-count: flags digit-count loops that allocate instead of using repeated divisionprefer-iterator-sum: flags manual accumulator loops in favor of.sum()prefer-retain-over-filter-collect: flags.filter().collect()on aVecin favor of.retain()prefer-unwrap-or-default: flags.unwrap_or(Default::default())in favor of.unwrap_or_default()
Kotlin (rules/kotlin/)
no-var: flags mutablevardeclarationsno-println: flagsprintln/printcallsno-lateinit: flagslateinit varusageno-unsafe-cast: flagsascasts without null-safeas?no-unnecessary-let: flagsletblocks that add no valueprefer-is-empty: flags.count() == 0in favor of.isEmpty()prefer-data-class: flags classes that should bedata classclean-architecture-imports: flags imports that violate clean architecture layer boundaries
Ruby (rules/ruby/)
no-path-traversal: flags string concatenation inFile.join/Pathnamethat may cause traversalprefer-action-over-filter: flagsbefore_filter/after_filterin favor ofbefore_action/after_actionprefer-symbol-over-proc: flagsProc.newwith a symbol when(&:method)is cleaner
TypeScript (rules/typescript/)
no-await-in-promise-all: flagsawaitinsidePromise.all()arrays (defeats parallelism)no-console-except-error: flagsconsole.log/debug/warn/info/trace(allowsconsole.errorin catch blocks)no-debugger: flagsdebuggerstatementsno-unnecessary-boolean-literal-compare: flagsx === trueorx === falseno-useless-promise-resolve: flagsreturn Promise.resolve(...)in async functionsprefer-array-flat-map: flags.map(fn).flat()in favor of.flatMap(fn)prefer-nullish-coalescing: flags||in assignments/returns where??is more preciseuse-logical-assignment: flags$A = $A || $Bin favor of$A ||= $Bprefer-optional-chaining: flagsa && a.bin favor ofa?.bno-return-in-forEach: flagsreturninside.forEach()callbacks (does not return from caller)no-array-delete: flagsdelete arr[i]in favor of.splice()
TSX (rules/tsx/)
avoid-jsx-short-circuit: flags{cond && <Elem />}in favor of{cond ? <Elem /> : null}(prevents rendering0)no-nested-links: flags<a>elements nested inside other<a>elements (invalid HTML)no-unnecessary-usestate-type: flagsuseState<string>('hello')when TypeScript can infer the typerename-svg-attribute: flags hyphenated SVG attributes likestroke-linecapin favor of camelCasestrokeLinecap
Examples (rules/examples/)
no-console-log: starter rule scoped to__ast_grep_examples__/for scaffold validation
Rust Catalog Highlights
- Avoid duplicated exports: a Rust lint-style rule can detect
pub use foo::Bar;in the same source file that already exposespub mod foo;. Treat this as API-surface cleanup, not a mechanical rewrite. The rule usesallto combine apub use $A::$B;pattern withinside: { kind: source_file }and ahascheck forpub mod $A;withstopBy: end:
yamlid: avoid-duplicate-export language: Rust severity: warning message: Item re-exported via `pub use` when `pub mod` already exposes the module. rule: all: - pattern: "pub use $A::$B;" - inside: kind: source_file - has: pattern: "pub mod $A;" stopBy: end
- Beware
chars().enumerate(): the Rust catalog rewrite from$A.chars().enumerate()to$A.char_indices()is valid when the code needs byte offsets instead of character indexes. Do not apply blindly if the caller intentionally wants character positions:
yamlid: no-chars-enumerate language: Rust severity: warning message: Use `.char_indices()` instead of `.chars().enumerate()` when byte offsets are needed. rule: pattern: "$A.chars().enumerate()" fix: "$A.char_indices()"
- Count
usizedigits without allocation: the catalog rewrite from$NUM.to_string().chars().count()to$NUM.checked_ilog10().unwrap_or(0) + 1is a good Rust-specific performance cleanup when the target is known to be an integer digit count. Do not over-apply if the expression is part of a more general formatting pipeline:
yamlid: no-alloc-digit-count language: Rust severity: info message: Count integer digits without heap allocation. rule: pattern: "$NUM.to_string().chars().count()" fix: "$NUM.checked_ilog10().unwrap_or(0) + 1"
- Unsafe function without unsafe block: the Rust catalog’s
function_itemrule that requiresunsafemodifiers but rejects bodies containingunsafe_blockis a good review rule for redundantunsafemarkers. It is diagnostic-oriented and should usually stay a scan rule, not an automatic rewrite. The rule useskind: function_itemwithhascheckingfunction_modifiersviaregex: "^unsafe"andnotrejecting bodies containingunsafe_block:
yamlid: no-unsafe-fn-without-unsafe language: Rust severity: warning message: Unsafe function contains no `unsafe` block. rule: all: - kind: function_item - has: kind: function_modifiers regex: "^unsafe" - not: has: kind: unsafe_block stopBy: end
- Rust 2024 let-chain candidate: the catalog’s nested
if/if letdetection rule usesutilsto define reusable matchers for sole-child statements, no-elseifexpressions, and no-elseif letexpressions. The root rule matches anifwhose block contains only anotherifstatement, suggesting the two can be collapsed into a single let-chain. Keep this as ahint-severity suggestion because let-chains require Rust 2024 edition:
yamlid: let-chain-candidate language: Rust severity: hint message: Nested `if`/`if let` can be collapsed into a Rust 2024 let-chain. utils: sole-child: all: - nthChild: 1 - nthChild: { position: 1, reverse: true } if-no-else: kind: if_expression not: { has: { field: alternative, kind: else_clause } } if-let-no-else: matches: if-no-else has: { field: condition, kind: let_condition } sole-inner-if-stmt: kind: expression_statement matches: sole-child has: { matches: if-no-else } sole-inner-if-let-stmt: kind: expression_statement matches: sole-child has: { matches: if-let-no-else } rule: matches: if-no-else has: field: consequence kind: block has: { matches: sole-inner-if-stmt } any: - matches: if-let-no-else - has: field: consequence kind: block has: { matches: sole-inner-if-let-stmt }
- Rewrite
indoc!macro: the catalog example that removesindoc! { r#"..."# }wrappers is a rewrite-oriented example. Keep it on the CLI skill path because the replacement is formatting-sensitive and should be reviewed interactively before broad apply. The CLI pattern isast-grep --pattern ‘indoc! { r#"$$$A"# }’ --rewrite ‘$$$A’. - Adapt these rules to the repository’s Rust style before using them directly. In VT Code, preserve existing lint policy, public API conventions, and the project’s bias against unnecessary rewrites.
TypeScript Catalog Highlights
- TypeScript vs TSX matters: keep
.tsand.tsxrules separate unless the repository intentionally parses.tsas TSX throughlanguageGlobs. Do not assume one pattern works unchanged across both parsers. - Find import file without extension: good scan rule for ESM codebases that require explicit local file extensions on static or dynamic imports. It is policy-dependent, so only use it where the runtime or bundler actually requires explicit extensions.
- XState v4 to v5 migration: strong example of multi-rule YAML with
utils,transform, and multi-document configs. Keep this sort of migration on the CLI skill path and review the generated diff instead of treating it as a one-line rewrite. - No
awaitinsidePromise.all([...]): good rewrite rule when the awaited expression is directly inside the array literal. Keep the rewrite narrow so it does not change intentionally sequential logic hidden behind helper calls. - No console except allowed cases: good scan rule for client-facing TypeScript, but it is repository-policy dependent. Adapt the allowed methods and environments before enabling it broadly.
- Find import usage or identifiers: these examples are useful for repository analysis and dependency cleanup, not just linting. They are often better treated as search/report rules than rewrite rules.
- Switch Chai
shouldtoexpect: a useful migration example, but it is test-framework-specific and should be applied only where Chai is actually in use. - Speed up barrel imports: strong
rewriters/transform.rewriteexample for splitting one import into many direct imports. Keep it on the CLI skill path because path conventions, default-vs-named exports, and formatting policy vary by repository. - Missing Angular
@Component()decorator: good example of labels plus pattern-objectcontextandselector. Keep framework-specific rules tied to actual framework usage in the repository. - Logical assignment operators: a compact rewrite example for
$A = $A || $Bto$A ||= $B, but only apply it where the project’s JS target and lint policy allow ES2021 operators. - Adapt TypeScript catalog rules to the repository’s module system, framework stack, transpilation target, and lint policy before using them directly.
TSX Catalog Highlights
- TSX vs TypeScript matters for parsing: JSX-bearing patterns should stay on the TSX parser unless the repository
intentionally routes
.tsthrough TSX withlanguageGlobs. - Unnecessary
useState<T>primitives: good cleanup rewrite foruseState<string|number|boolean>($A)when the initializer already gives TypeScript enough information to infer the state type. Bundled asrules/tsx/no-unnecessary-usestate-type.yml. - Avoid
&&short-circuit in JSX: good React-facing rewrite from{cond && <View />}to{cond ? <View /> : null}when the left side can evaluate to renderable falsy values like0. Bundled asrules/tsx/avoid-jsx-short-circuit.yml. - Rewrite MobX component style: useful migration example when
observer(() => ...)hides React hook linting from tooling. Keep it on the CLI skill path because naming, export shape, and component conventions vary by repository. - Avoid unnecessary React hooks: good diagnostic rule for
use*functions that do not actually call hooks. Treat it as a review rule first, because renaming or de-hooking can be API-affecting. - Reverse React Compiler: clearly rewrite-oriented and intentionally opinionated. Keep it on the CLI skill path and only use it when the user explicitly wants that de-memoization behavior.
- Avoid nested links: good accessibility and correctness scan rule for JSX trees. Bundled as
rules/tsx/no-nested-links.yml. - Rename SVG attributes: strong TSX rewrite example for hyphenated SVG attribute names such as
stroke-linecaptostrokeLinecap. Keep it reviewable because generated markup can be formatting-sensitive. Bundled asrules/tsx/rename-svg-attribute.yml. - Adapt TSX catalog rules to the repository’s React version, JSX runtime, lint rules, framework conventions, and browser-support target before using them directly.
YAML Catalog Highlights
- YAML scan rules are useful for configuration-policy checks where the repository needs to flag specific keys or values rather than rewrite source code.
- The catalog host/port example is a simple message-oriented rule that matches either
host: $HOSTorport: $PORTand attaches a diagnostic. Treat it as a starting point for config validation, not a complete policy by itself. - For YAML rules, be explicit about whether the repository cares about the key name, the value, or both. If both matter
together, move from separate
anypatterns to a more structured rule before relying on the result. - Keep YAML config checks repository-specific. Hard-coded values like
8000are only useful when they reflect an actual project policy.
Ruby Catalog Highlights
- Key Ruby tree-sitter node kinds for pattern authoring:
callfor method calls (e.g.$OBJ.method),method_callfor keyword-style calls (e.g.puts "hello"),blockfor{{ }}blocks,do_blockfordo...endblocks,symbolfor:nameliterals,assignmentfor variable assignments,methodfor method definitions,classfor class definitions,iffor conditionals,unlessfor negative conditionals,casefor case/when,whileanduntilfor loops,returnfor return statements,yieldfor yield calls,superfor super calls,selffor self references. - Ast-grep parses Ruby patterns with its bundled Ruby parser. Use
ast-grep run --lang ruby --debug-querythroughexec_commandto inspect surprising matches. - Ruby’s
$VARmeta-variable syntax works directly because$is a valid Ruby global variable prefix. NoexpandoCharoverride is needed. - Rails
*_filterto*_action: useful migration rewrite for older Rails controllers. The catalog rule uses atransformwithreplaceto swap_filterfor_actionon the captured$FILTERmeta-variable. The pattern uses$$$ACTIONto capture all arguments after the filter name. Keep it on the CLI skill path because framework version, controller style, and review expectations vary by repository:
yamlid: migration-action-filter language: Ruby rule: any: - pattern: before_filter $$$ACTION - pattern: after_filter $$$ACTION - pattern: around_filter $$$ACTION has: pattern: $FILTER kind: identifier fix: template: $FILTER_ACTION $$$ACTION transform: FILTER_ACTION: source: $FILTER replace: regex: _filter$ by: _action
- Prefer symbol over proc: good Ruby cleanup rewrite for cases like
.select { |v| v.even? }to.select(&:even?). The catalog rule constrainsITERtomap|select|eachviaregex, and matches the block pattern$LIST.$ITER { |$V| $V.$METHOD }. The fix uses$LIST.$ITER(&:$METHOD)syntax. Only apply where the shorthand remains readable and matches local Ruby style. Extend theITERregex to coverreject,find_all,detect,any?,all?,none?,countwhen appropriate. - Path traversal detection in Rails: good security-oriented scan rule for
Rails.root.join,File.join, orsend_filefed by variables. Usesanywith three patterns andseverity: hintbecause this is a detection rule, not proof of exploitability. The surrounding validation path still matters. AdviseFile.basename()or allowlist validation as remediation. - For bare block fragments like
{ |$V| $V.$METHOD }ordo |$V| $V.$METHOD end, wrap in the enclosing method call and useselector: callto match the outer call. For symbol-to-proc, match the enclosing method call directly with$LIST.$ITER(&:$METHOD). - Adapt Ruby catalog rules to the repository’s Rails version, Ruby style guide, and security posture before using them directly.
Python Catalog Highlights
- Key Python tree-sitter node kinds for pattern authoring:
function_definitionfor functions,callfor function calls,import_statementandimport_from_statementfor imports,assignmentfor assignments,decorated_definitionfor decorated functions/classes,with_statementfor context managers,try_statementfor try/except,if_statementfor conditionals,for_statementfor loops,return_statementfor returns,async_function_definitionfor async functions,awaitfor await expressions,typefor type annotations,subscriptfor generic types likeOptional[T],list_comprehensionfor list comprehensions,argument_listfor function arguments,keyword_argumentfor keyword arguments,conditional_expressionfor ternary expressions,assert_statementfor assertions. - Ast-grep parses Python patterns before matching them. Use
ast-grep run --lang python --debug-queryto inspect the parsed query when metavariables or incomplete fragments behave unexpectedly. - Python’s
$VARmeta-variable syntax works directly because$is not a valid Python identifier prefix in expression context. NoexpandoCharoverride is needed. - OpenAI SDK migration: useful multi-rule migration example for legacy
openaiPython client code, but keep it on the CLI skill path because imports, client lifetime, response shapes, and surrounding application logic often need repository-specific review. The migration uses three rules separated by---: import rewrite (import openaitofrom openai import Client), client initialization (openai.api_key = $KEYtoclient = Client($KEY)), and completion method (openai.Completion.create($$$ARGS)toclient.completions.create($$$ARGS)). - Prefer generator expressions: good example of narrowing a rewrite to contexts like
any(...),all(...), orsum(...)where generator expressions are clearly valid. Do not generalize it to every list comprehension. The constraint-based variant usesconstraintsto restrict$FUNCtoany|all|sumand$LISTtolist_comprehensionkind, then strips brackets with asubstringtransform:
yamlid: prefer-generator-in-builtins language: python rule: pattern: $FUNC($LIST) constraints: FUNC: regex: ^(any|all|sum)$ LIST: kind: list_comprehension transform: INNER: substring: source: $LIST startChar: 1 endChar: -1 fix: $FUNC($INNER)
- Walrus operator in
ifstatements: useful paired-rule rewrite example, but only apply it where the repository targets Python 3.8+ and the style guide accepts assignment expressions. This is a multi-rule YAML usingfollowsandprecedesrelational operators. The first rule rewrites theifto use:=, the second deletes the preceding assignment:
yamlid: use-walrus-operator language: python rule: follows: pattern: context: $VAR = $$$EXPR selector: expression_statement pattern: "if $VAR: $$$B" fix: |- if $VAR := $$$EXPR: $$$B --- id: remove-walrus-source language: python rule: pattern: $VAR = $$$EXPR kind: expression_statement precedes: pattern: "if $VAR: $$$B" fix: ‘’
- Remove async function: strong
rewritersexample for strippingasyncand innerawait, but treat it as high-risk migration work because it changes call semantics and often requires broader control-flow review. Usesrewritersto stripawaitfrom inside the body before removing theasynckeyword:
yamlid: remove-async language: python rule: pattern: context: ‘async def $FUNC($$$ARGS): $$$BODY’ selector: function_definition rewriters: remove-await-call: pattern: ‘await $$$CALL’ fix: $$$CALL transform: REMOVED_BODY: rewrite: rewriters: [remove-await-call] source: $$$BODY fix: |- def $FUNC($$$ARGS): $REMOVED_BODY
- Pytest fixture refactors: good example of
utils-driven context matching for fixture rename or type-hint updates. Usesutilsto define reusable context matchers likeis-fixture-function(function following a@pytest.fixturedecorator) andis-test-function(function whose name starts withtest_). Keep it tied to real pytest usage so similarly named non-test code is not swept in. Optional[T]toT | Noneand recursive union rewrites: useful typing-modernization examples, but only where the repository targets Python 3.10+ and static typing policy actually prefers PEP 604 unions. The simple variant usescontextandselectorto disambiguateOptional[$T]as a generic type:
yamlid: optional-to-union language: python rule: pattern: context: ‘a: Optional[$T]’ selector: generic_type fix: $T | None
The recursive variant handles nested Union and Optional types using multiple rewriters that call each other,
transforming deeply nested expressions like Optional[Union[List[Union[str, dict]], str]] into
List[str | dict] | str | None.
- SQLAlchemy
mapped_columnto annotatedMapped[...]: useful ORM migration example, but keep it on the CLI skill path because ORM version, model style, and nullable semantics need review. Usesrewritersto filter outStringpositional args andnullable=Truekeyword args from the argument list, then wraps the result inMapped[str | None]. printdetection: usekind: callwithhas: { field: function, pattern: print }to matchprint()calls. Scope withfilesto exclude test directories and scripts where console output is acceptable. Forlogging.debug()or similar, useregex: ^(debug|info|warning)$on the function field inside alogging.attribute access.- f-string preference: use
kind: callwithhas: { field: function, pattern: $FN }andconstraintsrestricting$FNto^(str|int|float|repr)$to find type-conversion calls that could be f-string expressions. This is a suggestion rule, not an enforcement rule, because some conversions are intentional type coercion. - List comprehension vs
map/filter: pattern$LIST = list(map($FUNC, $ITER))can be rewritten to$LIST = [$FUNC($X) for $X in $ITER]when$FUNCis a simple lambda or single-argument call. Keep it on the CLI skill path because readability depends on the complexity of$FUNC. dict.getwith default: pattern$D[$KEY]inside atry_statementwithexcept KeyErrorcan often be rewritten to$D.get($KEY)or$D.get($KEY, $DEFAULT). Usekind: subscriptwithinsideto scope within the try body. Treat as review material because some dict access patterns intentionally propagateKeyError.- Assert vs unittest assertions: pattern
assert $EXPR == $VALcan be rewritten toself.assertEqual($EXPR, $VAL)in unittest contexts, or left as-is in pytest contexts. Usefilesto scope by test framework convention. isinstancetuple consolidation: patternisinstance($X, $A) or isinstance($X, $B)can be rewritten toisinstance($X, ($A, $B)). This is a safe autofix when bothisinstancecalls check the same variable.- Adapt Python catalog rules to the repository’s Python version floor, framework stack, typing policy, async model, and migration scope before using them directly.
Kotlin Catalog Highlights
- Clean-architecture import checks: good scan-rule example for enforcing architectural boundaries with
filesplus import-path constraints. Treat it as repository-policy enforcement rather than a universal Kotlin rule. - The Kotlin catalog example is diagnostic-oriented, not rewrite-oriented. Keep it on the scan path because import-boundary violations usually need design review instead of blind mutation.
- File-scoped package constraints are the point of the example: adapt the
filesglob and package regexes to the repository’s actual module layout before relying on the result. - Ast-grep parses Kotlin patterns with its bundled Kotlin parser. Use
ast-grep run --lang kotlin --debug-querythroughexec_commandto inspect surprising matches. - Unsafe cast detection (
$EXPR as $TYPE): good warning-level scan rule for catching runtime ClassCastException risks. The safe castas?is a different AST node, so this pattern does not false-positive on safe casts. Treat as review material; some casts are intentionally unsafe after exhaustivewhenorischecks. varvsvalpreference: usekind: property_declarationwithhas: { field: property_delegate, pattern: var }to match mutable property declarations. A naivevar $NAME: $TYPEpattern may over-match in contexts where the parser attaches different node structure. Thekindplushasplusfieldapproach is more robust.printlndetection: use ananycomposite to cover Kotlin’s top-levelprintln($$$ARGS), Java’sSystem.out.println($$$ARGS), andSystem.err.println($$$ARGS). Scope withfilesto exclude test directories where console output is acceptable.isEmpty()preference: straightforward rewrite rule from$X.size == 0or$X.length == 0to$X.isEmpty(). Also cover$X.count() == 0and$X.size <= 0. This is a safe autofix because Kotlin’sisEmpty()is semantically equivalent for standard collections and strings.lateinitdetection: patternlateinit var $NAME: $TYPEis a direct structural match. Useseverity: infobecauselateinitis sometimes justified in dependency injection and test setup contexts. Teams should adjust severity to match their policy.- Unnecessary
letblocks: pattern$RECEIVER.let { $PARAM -> $BODY }catches explicit named-parameterletcalls. This does not match the implicititform ($RECEIVER.let { $BODY }) because the parser structures those differently. Focus on the named-parameter variant as the more egregious anti-pattern. - Data class candidates: use
kind: class_declarationwithhas: { kind: primary_constructor, has: { kind: class_parameter } }to find classes with constructor parameters. This is a suggestion rule, not an enforcement rule, because classes with inheritance or behavior should remain regular classes. - Key Kotlin tree-sitter node kinds for pattern authoring:
class_declarationfor classes,property_declarationfor val/var properties,function_declarationfor functions,primary_constructorfor primary constructors,class_parameterfor constructor parameters,import_declarationfor imports,call_expressionfor function calls,as_expressionfor cast expressions,lambda_expressionfor lambdas,when_expressionfor when blocks. - Kotlin tree-sitter parses
$EXPR as $TYPEasas_expressionand$EXPR as? $TYPEas a variant with the?token attached, so a pattern targetingaswill not matchas?. This makes cast-direction rules safe from false positives on safe casts. - Kotlin’s
?.let { }safe-call form is parsed differently from.let { }dot-call form. Rules targeting one will not match the other. Useanywith both patterns when both forms should be flagged. - Adapt Kotlin catalog rules and the rules above to the repository’s package naming, architecture boundaries, Android-vs-server structure, coroutine usage, and lint ownership before using them directly.
Java Catalog Highlights
- Unused local variable detection: useful educational example for
hasplus orderedallplusprecedes, but prefer the project’s established linter or IDE for real unused-variable enforcement because Java variable scopes are broader than the sample rule covers. The rule usesallto guarantee that the meta-variable$IDENTis captured by the firsthasclause before thenot/precedescheck runs. Without that ordering, the meta-variable would not be available for the later comparison:
yamlid: no-unused-vars language: java rule: kind: local_variable_declaration all: - has: has: kind: identifier pattern: $IDENT - not: precedes: stopBy: end has: stopBy: end any: - { kind: identifier, pattern: $IDENT } - { has: { kind: identifier, pattern: $IDENT, stopBy: end } } fix: ‘’
Treat matches as review candidates, not conclusive unused-variable proofs. Java variable scopes are broader than this sample covers, and the project’s established linter or IDE is usually a better fit for real unused-variable enforcement.
- Field declarations of type
String: good structural scan example showing whyfield_declarationplushas: { field: type }is more robust than a naive pattern when modifiers and annotations are present. A naiveString $F;pattern fails because it ignores modifiers and annotations. A$MOD String $F;pattern also fails because tree-sitter does not consider$MODa valid modifier and produces anERRORnode. The structural rule approach works regardless of how many modifiers or annotations precede the type:
yamlid: find-field-with-type language: java rule: kind: field_declaration has: field: type regex: ^String$
Use this kind plus has plus field plus regex pattern whenever a naive code pattern fails because Java modifiers,
annotations, or access qualifiers change the surface syntax. The field: type constraint targets the semantic type
child of the declaration, not the raw text, so it is robust against private static final String, @Nullable String,
or other decorated forms.
- The Java catalog examples are primarily search/diagnostic material, not high-confidence autofix rules. Keep them review-oriented unless the repository explicitly wants ast-grep-based cleanup instead of compiler or linter diagnostics.
- Adapt Java catalog rules to the repository’s package conventions, annotation usage, style tooling, and existing static-analysis stack before using them directly.
HTML Catalog Highlights
- HTML parser reuse for framework templates: useful when Vue, Svelte, Astro, or similar files are mostly HTML, but keep
parser caveats in mind because framework-specific control flow or frontmatter may require a custom language instead.
Use
languageGlobsinsgconfig.ymlto parse.vue,.svelte, or.astrofiles as HTML when the framework syntax is minimal enough for the HTML parser. - Key HTML node kinds for pattern authoring:
elementfor full HTML elements,tag_namefor tag names,attribute_namefor attribute names,attribute_valuefor attribute values,textfor text content, andcommentfor HTML comments. Use these withkindto match specific HTML structures without writing full pattern syntax. - Matching elements by tag name: use
kind: elementwithhas: { field: tag_name, pattern: $TAG }to match elements by their tag name. For regex-based tag matching (e.g. all heading tags), usekind: tag_namewithregex: "^h[1-6]$"andinside: { kind: element }. - Matching elements by attribute: use
kind: elementwithhas: { kind: attribute_name, regex: "^class$" }to find elements with a specific attribute. To also match the attribute value, add a nestedhason the attribute node to captureattribute_value. - Scoping with
insideandstopBy: HTMLinsidewithstopBy: { kind: element }scopes matches to the nearest enclosing element. This is essential for avoiding cross-element matches in deeply nested HTML. Theinside-tagutility pattern from the catalog demonstrates wrappinginsidewithkind: elementandhasto capture the enclosing tag name, then usingconstraintsto restrict which tags match. - Ant Design Vue
visibletoopen: good framework-specific attribute rewrite using enclosing-tag checks plus constraints. The pattern useskind: attribute_namewithregex: :visibleto match the attribute,insideto find the enclosingelement,hasto capture thetag_name, andconstraintsto restrict to specific components (a-modal|a-tooltip). Keep it on the CLI skill path because framework version and component set must be confirmed first. - i18n key extraction: useful template rewrite example for wrapping static text while skipping mustache expressions.
Uses
kind: textwithpattern: $Tto capture text content,not: { regex: ‘{{.*}}’ }to skip mustache interpolation, andfix: "{{ $(‘$T’) }}"to wrap the text. Keep it reviewable because real projects usually need key naming, dictionary updates, and whitespace policy beyond the raw rewrite. - Attribute rewrite patterns: HTML attribute rewrites commonly use
kind: attribute_nameto match the target attribute,insideto find the parent element, andconstraintsto narrow by attribute name regex. For renaming attributes (e.g.visibletoopen), match the attribute name node and usefixto replace it. - Text content patterns: use
kind: textto match raw text nodes inside elements. Combine withinside: { kind: element, has: { field: tag_name, pattern: $TAG } }to scope text matching to specific elements. Usenotto exclude text containing interpolation syntax. - HTML comment patterns: use
kind: commentto match HTML comments. Combine withregexto find comments containing specific text patterns like TODO, FIXME, or deprecated notices. - HTML
<script>and<style>content is parsed as embedded JavaScript and CSS respectively. Search inside these regions withlang: javascriptorlang: cssrules. For custom embedded languages (e.g. TypeScript in<script lang="ts">), configurelanguageInjectionsinsgconfig.yml. - Adapt HTML catalog rules to the repository’s template framework, parser limitations, i18n workflow, and component-library version before using them directly.
Go Catalog Highlights
- Problematic
deferwith nested function calls: strong Go-specific scan example for catching cases where deferred arguments are evaluated immediately instead of at function exit. In Go,deferevaluates arguments when the defer statement is encountered, not when the deferred function runs. This is particularly problematic with assertion libraries in tests:
yamlid: problematic-defer-call language: go rule: pattern: context: ‘{ defer $A.$B(t, failpoint.$M($$$)) }’ selector: defer_statement
Treat matches as correctness and test-reliability review items. The fix is wrapping in a closure:
defer func() { require.NoError(t, failpoint.Disable("...")) }(). Adapt to the repository’s test conventions before
enabling broadly.
- Function declarations by name pattern: good example of using
kindplushasplusregexwhen a meta-variable pattern cannot express the naming constraint directly. A plainTest$_pattern fails because it is not valid syntax; use a YAML rule instead:
yamlid: test-functions language: go rule: kind: function_declaration has: field: name regex: Test.*
Useful for test discovery, migration targeting, or repository audits where meta-variable patterns are too limited.
- Contextual matching for function calls: Go’s tree-sitter grammar parses
fmt.Println($A)as a type conversion, not a call expression, because Go syntax allows both. Use a contextual pattern withselector: call_expressionthrough the CLI skill path viaexec_command:
yamlid: match-function-call language: go rule: pattern: context: ‘func t() { fmt.Println($A) }’ selector: call_expression
Use this pattern whenever a plain call-expression pattern under-matches or parses as a conversion in Go.
- Package-import detection: useful search/scan rule for dependency auditing, compliance checks, or migration prep. Adapt the import regex to the repository’s actual dependency boundaries instead of hard-coding example packages:
yamlid: match-package-import language: go rule: kind: import_spec has: regex: github.com/golang-jwt/jwt
- Problematic JSON tags with
-,: high-signal security-oriented scan rule for Go struct tags. When a struct field has a JSON tag starting with-,, it can be unexpectedly unmarshaled with the-key, bypassing the developer’s intent to omit the field. This is a real unmarshaling footgun, not just a style preference:
yamlid: unmarshal-tag-is-dash severity: error message: Struct field can be decoded with the `-` key because the JSON tag starts with a `-` but is followed by a comma. rule: pattern: ‘`$TAG`’ inside: kind: field_declaration constraints: TAG: regex: json:"-,.+
Treat matches as actionable security review items. The fix is using just - without a comma: json:"-".
- Adapt Go catalog rules to the repository’s Go version, test conventions, package layout, security posture, and existing static-analysis tooling before using them directly.
Cpp Catalog Highlights
- Reuse Cpp rules for C only when the repository intentionally parses C sources as Cpp via
languageGlobs; do not assume mixed C/C++ projects want that parser tradeoff by default. - Ast-grep bundles C++ parser support. Use
--lang cppfor ad-hoc CLI patterns andlanguage: Cppin YAML rules. Runast-grep run --lang cpp --debug-querythroughexec_commandto inspect surprising matches. - Format-string vulnerability rewrite: strong security-oriented example for
fprintf/sprintf-style calls missing an explicit format string. Usesconstraintswith regex on$PRINTFand kind on$VARto distinguish vulnerable calls from safe ones. The fix inserts"%s"as the format argument:
yamlid: fix-format-string language: cpp rule: pattern: $PRINTF($S, $VAR) constraints: PRINTF: regex: ^sprintf|fprintf$ VAR: not: { kind: string_literal } not: { kind: concatenated_string } fix: $PRINTF($S, "%s", $VAR)
Keep it reviewable because real C/C++ codebases may prefer safer API migrations (e.g. snprintf) over mechanical "%s"
insertion in some contexts.
- Struct inheritance matching: useful example of AST-shaped pattern authoring in C++. A shortened surface pattern like
struct $SOMETHING: $INHERITSproduces anERRORnode because tree-sitter-cpp requires the full syntactic form. Use the complete pattern with the body block:
yamlid: find-struct-inheritance language: cpp rule: pattern: struct $NAME : $BASE { $$$BODY; }
This matches structs that use inheritance via base class clauses. The full struct ... : ... { ... } shape is required
for the parser to produce a valid struct_specifier node instead of an ERROR node.
- Adapt Cpp catalog rules to the repository’s C-vs-C++ parser choice, security posture, libc usage, and coding-standard expectations before using them directly.
C Catalog Highlights
- Parsing C as Cpp can reduce duplicated rule authoring, but only use that route when the repository intentionally opts
into the parser tradeoff with
languageGlobs; do not blur C and C++ semantics by default. - Match function calls in C with contextual patterns: tree-sitter-c parses code fragments differently depending on
surrounding syntax. A bare
test($A)becomesmacro_type_specifier, whiletest($A);becomesexpression_statement -> call_expression. Usecontextplusselector: call_expressionthrough the CLI skill path viaexec_command:
yamlid: match-function-call language: c rule: pattern: context: $M($$$); selector: call_expression
- Rewrite method-style calls to function calls: useful migration example for C codebases that emulate methods with
structs or function pointers. Uses
transformwithreplaceto derive a conditional comma from$$$ARGS:
yamlid: method_receiver language: c rule: pattern: $R.$METHOD($$$ARGS) transform: MAYBE_COMMA: replace: source: $$$ARGS replace: ‘^.+’ by: ‘, ‘ fix: $METHOD(&$R$MAYBE_COMMA$$$ARGS)
Keep it on the CLI skill path because it changes calling conventions and may affect ownership, pointer semantics, or naming policy.
- Yoda-condition rewrite: clearly style-driven and repository-policy-sensitive. Uses
constraintsto restrict$Btonumber_literalandinsideto scope withinif_statement:
yamlid: may-the-force-be-with-you language: c rule: pattern: $A == $B inside: kind: parenthesized_expression inside: {kind: if_statement} constraints: B: { kind: number_literal } fix: $B == $A
Treat it as optional rewrite material only where the project explicitly prefers constant-on-the-left comparisons.
- Adapt C catalog rules to the repository’s parser choice, macro usage, pointer conventions, coding style, and safety policy before using them directly.
Markdown Catalog Highlights
- Markdown became a first-class language in ast-grep 0.43. Query with
--lang mdorlang: markdownin YAML rules. - Queryable node kinds include
atx_headingfor ATX-style headings,fenced_code_blockfor fenced code blocks, andlist_itemfor list items. - Combine node kinds with compound selectors for broader sweeps, for example
atx_heading, fenced_code_blockmatches both headings and code blocks. - Markdown parsing is powered by
tree-sitter-mdand still has known parsing bugs and edge cases. Use it for inspection, indexing, outline extraction, and lightweight automation rather than critical rewrites. - Ast-grep infers a language from recognised file extensions during path scans. Pass
--lang mdfor stdin or whenever extension inference is unavailable. - Adapt Markdown catalog rules to the repository’s documentation conventions, heading hierarchy, and content structure before using them directly.
JavaScript API Highlights
- Use
@ast-grep/napionly when rule YAML and CLI commands are insufficient. The programmatic API suits computed replacements, ordered-match logic, cross-node inspection, or edit orchestration that would be awkward in pure rule syntax. - Core objects are
SgRootandSgNode:parse(Lang.<X>, source)creates the tree,root()returns the root node, andfind/findAll/ traversal / refinement / edit APIs live onSgNode. Matcherinputs can be pattern strings, numeric kind ids, orNapiConfigobjects. Prefer patterns orNapiConfigunless there is a concrete reason to drop to raw kind ids.getMatchandgetMultipleMatchesexpose captured metavariables, butreplacedoes not interpolate metavariables for you. Build replacement strings explicitly in JavaScript from matched nodes before callingcommitEdits.- Prefer the CLI path for ordinary query, scan, test, and rewrite flows. Use NAPI only when the task is genuinely programmatic.
registerDynamicLanguageand extra language packages exist, but that path is still experimental. Prefer established parsers and repo-native tooling unless dynamic-language support is actually needed.
Python API Highlights
- Use
ast-grep-pywhen the task needs programmatic AST traversal or computed edits but a Python host environment is a better fit than JavaScript or Rust. Prefer it only after rule YAML and direct CLI commands stop being a good fit. - Core objects are again
SgRootandSgNode:SgRoot(source, language)parses the source,root()returns the root node, and search, refinement, traversal, and edit APIs live onSgNode. findandfind_allsupport either direct rule keyword arguments or a config object. Prefer keyword-rule searches for simple cases and config objects when constraints or utility rules make the query more expressive.get_match,get_multiple_matches, and__getitem__expose captured metavariables.__getitem__is useful when you want a stricter access pattern and are willing to let missing captures raise instead of returningNone.replaceandcommit_editsgenerate source edits, but they do not interpolate metavariables for you. Build replacement text explicitly from matched nodes before applying edits.- Use the Python API only for genuinely programmatic transformations, rather than ordinary CLI queries or rewrites.
NAPI Performance Highlights
- NAPI is not automatically faster than host-language traversal. Performance usually comes from reducing Rust↔JavaScript FFI crossings and letting ast-grep do more work per boundary crossing.
- Prefer
parseAsyncoverparsewhen many parse jobs can benefit from Node’s libuv thread pool and the main JS thread is already busy. - Prefer
findAllover manual recursive traversal in JavaScript. One bulk Rust-side search is usually cheaper than repeatedkind(),children(), and recursion calls across the FFI boundary. - Prefer
findInFileswhen scanning many files and you can use its file-path-oriented search model. It avoids unnecessary round-tripping source strings through JavaScript and can parallelize work in Rust threads. findInFileshas a callback-completion caveat: its returned promise can resolve before all callbacks run. If completion ordering matters, track callback counts explicitly before treating the scan as finished.- Apply these performance tips only when scale justifies them. For small inputs or one-off transformations, simpler synchronous code is often the better tradeoff.
Rule Essentials
- Start rule files with
id,language, and rootrule. - Treat the root
ruleas a rule object that matches one target AST node per result. - A rule object still needs a positive anchor. In practice, start with
patternorkind;regexis a filter, not a sufficient root rule by itself. - Atomic rules are
pattern,kind,regex,nthChild, andrange. - Use atomic fields such as
pattern,kind,regex,nthChild, andrangefor direct node checks. - On the CLI, use
--kindalone to match by node type or combine it with a pattern to filter matches by node kind. - Use relational fields such as
inside,has,follows, andprecedeswhen the match depends on surrounding nodes. - Use composite fields such as
all,any,not, andmatchesto combine sub-rules or reuse utility rules. kindvalues support ESQuery-style pseudo-selectors (:has(),:not(),:is(),:nth-child()) for matching nodes by descendant structure, exclusion, alternatives, or sibling position without writing separate relational rules.- Rule object fields are effectively unordered and conjunctive; if matching becomes order-sensitive, rewrite the logic
with an explicit
allsequence instead of assuming YAML key order matters. languagecontrols how patterns parse. Syntax that is valid in one language can fail in another.
Rule Cheat Sheet
- Atomic rules check properties of one node. Start here when a single syntax shape is enough.
pattern,kind, andregexare the common atomic fields.patterncan also be an object withcontext,selector, and optionalstrictness.- Pattern objects are for invalid, incomplete, or ambiguous snippets.
contextis required;selectorpicks the real target node inside that context;strictnesstunes how literally the pattern matches. - Use pattern objects when the bare snippet would parse as the wrong node kind, such as JavaScript class fields or Go/C call expressions inside ambiguous fragments.
kindis usually a plain node kind name, but ast-grep 0.42+ supports ESQuery-style pseudo-selectors inkindstrings. Use:has(selector)or:has(> selector)to match nodes containing descendants or direct children,:not(selector)to exclude nodes,:is(selector, ...)for or-logic in compound selectors, and:nth-child(An+B)or:nth-child(An+B of selector)for positional matching. These pseudo-selectors also work in CLI selector values.- ast-grep 0.43+ further expands
kindwith compound selector operators:A > B(direct child),A B(descendant),A + B(immediate sibling),A ~ B(general sibling), andA, B(either). This syntax works in YAML rulekindfields and the CLI--kind/-kflag. It is ESQuery-style, not full ESQuery: class selectors, attribute selectors, and wildcard selectors are not supported. - Separate
kindandpatternchecks do not change how the pattern is parsed. If parse shape is the problem, switch to one pattern object withcontextandselector. regexmatches the whole node text. Reach fornthChildwhen position among named siblings matters andrangewhen the match must be limited to a known source span.- Regex syntax follows Rust
regex, not PCRE. Do not assume look-around or backreferences are available, and usually pairregexwithkindorpatternso the expensive text check only runs on the right node shapes. nthChildaccepts a number, anAn+Bstring, or an object withposition,reverse, andofRule. Counting is 1-based and only considers named siblings.rangematches by source position with 0-basedlineandcolumn;startis inclusive andendis exclusive.- Relational rules describe structure around the target node. Use
inside,has,follows, andprecedeswhen the match depends on ancestors, descendants, or neighboring nodes. - Read relational rules as: target node relates to surrounding node. The top-level rule still matches the target; the relational subrule matches the surrounding node that filters it.
- Relational subrules can themselves use
pattern,kind, composites, and captures. Those captures can still be referenced later infix, which is a practical way to extract surrounding syntax while keeping the target node as the match. - Add relational
fieldwhen the surrounding node matters by semantic role, not just by shape.fieldonly applies toinsideandhas. - Add
stopBywhen ancestor or sibling traversal must continue past the nearest boundary instead of stopping early. The default isneighbor,endsearches to the boundary, and a rule object stop is inclusive. insidemeans the target is somewhere under a matching ancestor,hasmeans the target node contains a matching descendant,followsmeans the target comes after a matching sibling or prior node, andprecedesmeans it comes before one.- Composite rules combine checks for the same target node. Use
allfor explicit conjunction,anyfor alternatives,notfor exclusions, andmatchesto delegate to a utility rule. allandanystill operate on one target node. They combine sub-rules, not multiple matched nodes.allis the ordered composite. Use it when later checks depend on captures established by earlierpatternmatches, because YAML rule-object field order is not guaranteed.anyis for alternatives, not for "collect all matching nodes". If one node cannot satisfy every branch at once,allis the wrong operator even if the surrounding structure feels plural.- Nested composites still evaluate one node at a time. A rule like
has: { all: [...] }means "has one child satisfying every listed rule", not "has one child for each listed rule". - When you need "a node that has X child and has Y child", write
all: [ { has: ... }, { has: ... } ]on the outer target instead of putting incompatible checks inside one nestedall. - Utility rules keep repeated logic out of the main rule body. Use file-local
utilsfor one config file and global utility-rule files when multiple rules in the project need the same building block. - Local utility rules live under the current file's
utilsmap, are only visible in that one config file, inherit the file's language, and cannot define their own separateconstraints. - Global utility rules live in separate files discovered through
utilDirs. They must declareidandlanguage, and can only use the utility-safe fields:id,language,rule,constraints, and localutils. - Local utility names must be unique inside one file. A local utility can shadow a global utility with the same name, so
check the nearest file first when
matchesseems to resolve unexpectedly. - Utility rules can call other utility rules through
matches, including recursive structural tricks like nested-parentheses matching. Avoid cyclicmatchesdependency graphs, because ast-grep does not allow recursive cycles there. - Self-reference through relational structure such as
insideorhasis different from cyclicmatchesreuse and is allowed when the AST traversal still makes progress. - Switch from a single
patternto a rule object when you need positional constraints, role-sensitive matching, reusable sub-rules, or several structural conditions on one node. - Rule-object fields are logically equivalent to an
allacross those fields, but not to an orderedall. Keep explicitallwhen capture order matters; use rule-object style when the checks are independent and flatter indentation helps readability.
ESQuery-Style Kind Selectors
Ast-grep supports ESQuery-style selectors in YAML rule kind fields and the CLI --kind or -k flag. Ast-grep parses
the selector internally.
Relationship Selectors
- Child selector (
>): matches a direct child node.kind: call_expression > identifieris equivalent tokind: identifierwithinside: { kind: call_expression }. - Descendant selector (space): matches a descendant node.
kind: call_expression identifieris equivalent tokind: identifierwithinside: { kind: call_expression, stopBy: end }. - Adjacent sibling selector (
+): matches the next sibling node.kind: decorator + method_definitionis equivalent tokind: method_definitionwithfollows: { kind: decorator }. - Following sibling selector (
~): matches any following sibling node.kind: decorator ~ method_definitionis equivalent tokind: method_definitionwithfollows: { kind: decorator, stopBy: end }.
Comma Selector
- Comma-separated selectors are converted to
any.kind: identifier, numberis equivalent toany: [{ kind: identifier }, { kind: number }].
Pseudo-classes
:has(selector): matches a node if it has a descendant matching the inner selector.kind: function_declaration:has(return_statement)matches function declarations that contain a return statement. Use>inside:hasto match a direct child:kind: expression_statement:has(> call_expression).:not(selector): negates the inner selector.kind: identifier:not(number)matches identifiers that are not numbers.:is(selector, ...): accepts comma-separated selectors and is converted toany.kind: :is(identifier, number)matches either identifiers or numbers. Can be combined with relationship selectors:kind: call_expression > :is(identifier, number).:nth-child(An+B): maps to ast-grep'snthChildrule.kind: array > number:nth-child(2n+1)matches odd-numbered number elements in arrays.:nth-child(An+B of selector): supportsofsyntax for filtering.kind: array > :nth-child(1 of number)matches the first number element in an array.:nth-last-child(position): equivalent tonthChildwithreverse: true.kind: array > number:nth-last-child(1)matches the last number element in an array.
Compound Selectors
Compound selectors are combined with all. kind: function_declaration:has(return_statement):not(generator_function)
is equivalent to
all: [{ kind: function_declaration }, { has: { kind: return_statement, stopBy: end } }, { not: { kind: generator_function } }].
Examples
yaml# Match identifiers that are direct children of call expressions kind: call_expression > identifier # Match any identifier or number node kind: identifier, number # Match function declarations containing return statements kind: function_declaration:has(return_statement) # Match identifiers that are not numbers kind: identifier:not(number) # Match either identifiers or numbers kind: :is(identifier, number) # Match the first number element in an array kind: array > :nth-child(1 of number) # Match odd-indexed elements kind: array > number:nth-child(2n+1) # Combine with pattern: match fn declarations that have return statements pattern: "fn $NAME() {}" kind: function_item:has(return_statement) # C++: match class definitions that have virtual methods kind: class_specifier:has(virtual_function_specifier) # C++: match template function declarations kind: template_declaration > function_definition # C++: match delete expressions (potential memory management issues) kind: delete_expression # Python: match function definitions with decorators kind: decorated_definition > function_definition
Current Limitations
- Class selectors like
.bodyare tokenized but rejected as unsupported. - Supported pseudo-classes are only
:has,:not,:is,:nth-child, and:nth-last-child. :has(...),:not(...), andof ...parse a single complex selector, not a comma selector list.:is(...)is the one pseudo-class that accepts comma-separated selector lists.- Identifiers can include letters, digits,
_and-, but cannot start with a digit.
Config Cheat Sheet
- Basic info keys define the rule itself. Use
idfor the unique rule name,languagefor the parser target,urlfor rule documentation, andmetadatafor custom project data. Native JSON includesmetadataonly when the scan uses--include-metadata. - One YAML file can hold multiple rules when you separate documents with
---. - Finding keys define what gets matched.
ruleis the core matcher,constraintsnarrows meta-variable captures, andutilsholds reusable helper rules that you call throughmatches. utilscan be purely local to the current file or can supplement global utility-rule files loaded throughutilDirs. Keep shared building blocks global only when multiple rule files genuinely need them.constraintsruns afterrulematched, only targets single meta variables like$ARG, and is a poor fit insidenot.- Patching keys define reusable fixes. Use
transformto derive new meta-variables before replacement,fixfor either a string replacement or atemplateobject withexpandStart/expandEnd, andrewriterswhen the transformation is too complex for one inlinefix. - Linting keys define what scan results report. Use
severity,message,note, andlabelsfor diagnostics, thenfilesandignoresto scope where the rule applies. - Severity levels are
error,warning,info,hint, andoff.hintis the default severity in ast-grep project scans. - An
error-severity match makesast-grep scanexit with status 1. Inspect the native human-readable or JSON findings before treating that status as an execution failure. severity: offdisables the rule during scanning.notesupports Markdown but cannot interpolate meta variables.- Use scan flags such as
--error,--warning,--info,--hint, and--offto override named rule severities for one CLI run. Use--filterto select configured rule identifiers. - Choosing severity for custom rules:
error: correctness bugs, security vulnerabilities, or patterns that should always fail CI. Examples:no-debugger,no-array-delete,no-return-in-foreach,no-await-in-promise-all.warning: code smells, style violations, or patterns that are usually wrong but may have valid exceptions. Examples:no-iterator-for-each,no-console-except-error,avoid-duplicate-export,no-chars-enumerate.info: informational findings that are useful for code review but not necessarily wrong. Examples:no-alloc-digit-count,optional-to-union,no-walrus-source.hint: suggestions and style preferences. This is the default. Examples:prefer-optional-chaining,prefer-nullish-coalescing,use-logical-assignment,let-chain-candidate.off: disabled rules. Useful for rules that are temporarily turned off or only enabled in specific contexts.
- Source suppression uses
ast-grep-ignorecomments.ast-grep-ignoresuppresses all rules for the same line or following lineast-grep-ignore: rule-idsuppresses one rule- comma-separated rule ids suppress multiple specific rules
- next-line suppression only works when there is no preceding AST node on that same comment line
- Example:
let x = dangerous_call(); // ast-grep-ignore: no-dangerous-calls - Example:
// ast-grep-ignore: no-console-log, no-debuggeron the line above
- File-level suppression requires the suppression comment on the first line plus an empty second line.
- Example:
// ast-grep-ignore: no-console-logas the very first line of the file, followed by an empty line.
- Example:
unused-suppressionis a built-in hint-style rule with autofix for stale ignore directives, but it only appears in fullscanruns when ast-grep is not filtering or disabling rules through CLI narrowing flags. Override its severity on the CLI with--error unused-suppressionto make stale suppressions fail CI.labelskeys must come from meta variables already defined by the rule orconstraints.filessupports either plain globs or object entries. Use object syntax when you need options likecaseInsensitiveglob matching.ignoresruns beforefiles. Both are relative to thesgconfig.ymldirectory, and the glob should not start with./.- Rule-level
ignoresis different from CLI--no-ignore: the CLI flag changes global ignore-file behavior, while YAMLignoresonly filters files for that rule. - JSON output only includes rule
metadatawhen the ast-grep run enabled metadata output, for example via--include-metadata. - Parameterized utility rules (experimental, ast-grep 0.42+) let global utility files declare
argumentsso callers pass rule objects into a reusable template viamatches. Arguments are mandatory, are full rule objects (not strings), and meta-variables captured inside the utility stay private unless explicitly exported by the argument rules. This feature is experimental and its API may change. - Keep config authoring on the ast-grep skill path. Run query, scan, and test commands through
exec_command.
Transformation Objects
transformbuilds new strings from captured meta variables beforefixruns.- Each
transformentry introduces a new variable name without a leading$. Inside the transform object,sourcestill points at an existing capture or prior transform result using the normal$VARform. - Later transforms can consume variables created by earlier transforms, so transform order matters when you are stacking multiple string operations.
- Transforms are evaluated in declaration order. A transform that references a variable created by an earlier transform
in the same
transformblock will see the already-transformed value. - Transforms only run after the rule matches. If the rule does not match, no transforms execute and no
fixis applied.
replace
replaceuses a Rust regex over one meta variable.sourcemust be$VARstyle,replaceis the regex,byis the replacement text, and regex capture groups can be reused inby.- Regex capture groups are only available inside the
replacefield of thereplacetransform and can only be referenced from thebyfield of that same transform. Regularregexrules do not expose those capture groups. - Rust regex syntax applies: no look-around, no backreferences in the traditional PCRE sense, but capture groups
()work and are referenced as$1,$2, etc. inby.
yaml# Strip leading underscore from a variable name transform: CLEAN_NAME: replace: replace: "^_" by: "" source: $VAR # Extract domain from email using capture group transform: DOMAIN: replace: replace: "^[^@]+@(.+)$" by: "$1" source: $EMAIL # String-form (ast-grep 0.38.3+) transform: CLEAN_NAME: replace($VAR, replace="^_", by="")
substring
substringslices a meta variable by character index with inclusivestartCharand exclusiveendChar. Negative indexes count from the end, and slicing is based on Unicode characters rather than raw bytes.substringbehaves like Python string slicing, so omit either bound when the slice should stay open-ended.
yaml# Remove first and last character (e.g., strip quotes) transform: UNQUOTED: substring: startChar: 1 endChar: -1 source: $STR # Get first 3 characters transform: PREFIX: substring: endChar: 3 source: $ID # Remove first character only transform: NO_PREFIX: substring: startChar: 1 source: $NAME # String-form (ast-grep 0.38.3+) transform: UNQUOTED: substring($STR, startChar=1, endChar=-1)
convert
convertchanges identifier-style casing throughtoCase. Common outputs arelowerCase,upperCase,capitalize,camelCase,snakeCase,kebabCase, andpascalCase.- Use
separatedByto control howconvertsplits words before rebuilding the target case. Supported separators include dash, dot, space, slash, underscore, andCaseChange. CaseChangesplits at transitions such asastGrep,ASTGrep, orXMLHttpRequest, which matters when converting mixed acronym identifiers.- When
separatedByis omitted, all known separators are used. This is usually the right default.
yaml# Convert camelCase to snake_case transform: SNAKE_NAME: convert: toCase: snakeCase source: $CAMEL # Convert only by underscore, preserving camelCase within segments transform: KEBAB_FROM_UNDERSCORE: convert: toCase: kebabCase separatedBy: [underscore] source: $UNDERSCORE_NAME # Convert PascalCase to camelCase (using CaseChange separator) transform: CAMEL: convert: toCase: camelCase separatedBy: [CaseChange] source: $PASCAL # String-form (ast-grep 0.38.3+) transform: SNAKE_NAME: convert($CAMEL, toCase=snakeCase)
Chaining Transforms
- Later transforms can consume variables created by earlier transforms. This is the standard way to build multi-step string pipelines.
- Transform order is the declaration order in the YAML
transformmap.
yaml# Pipeline: strip prefix, then convert case transform: RAW_NAME: replace: replace: "^get" by: "" source: $METHOD_NAME SNAKE: convert: toCase: snakeCase source: $RAW_NAME # Input: "getUserName" -> RAW_NAME="UserName" -> SNAKE="user_name"
Conditional Separators from Multi-Capture
- Use
replacetransforms for conditional punctuation or whitespace when a multi-capture may be empty. The common pattern is derivingMAYBE_COMMAor similar from$$$ARGSso the extra separator only appears when matches exist.
yaml# Add comma only when there are arguments rule: pattern: "foo($$$ARGS)" transform: MAYBE_COMMA: replace: replace: ".+" by: ", " source: $ARGS fix: "bar($MAYBE_COMMA$newArg)" # If $$$ARGS matched "a, b" -> MAYBE_COMMA=", " -> "bar(, newArg)" # If $$$ARGS matched nothing -> MAYBE_COMMA="" -> "bar(newArg)"
String-Form Transforms
- String-form transforms such as
replace(...),substring(...),convert(...), andrewrite(...)are valid shorthand in ast-grep 0.38.3+. - Prefer object form when compatibility or debugging clarity matters.
- String-form syntax:
operator($SOURCE, key1=value1, key2=value2). - Array values use
[item1, item2]syntax inside the string form.
Rewriters
rewritersis an experimental feature for advanced multi-node rewrites. Prefer ast-grep’s API instead when the YAML starts carrying too much control flow or state.- Rewriters allow replacing multiple sub-nodes with different fixes in one rule. The normal
fixreplaces one matched node at a time;rewritersplustransform.rewritehandle the one-to-many case. - The three-step workflow is:
- Define
rewritersat the YAML rule root. Each rewriter needsid,rule, andfix. Optional fields areconstraints,transform, andutils. - Apply the rewriter to a metavariable via
transformusing therewriteoperator.rewriterslists which rewriter ids to try;sourcepoints at the metavariable whose sub-nodes are rewritten. - Use the resulting transformed metavariable in the outer
fix.
- Define
- Concrete example converting Python
dict(a=1, b=2)to{‘a’: 1, ‘b’: 2}:- Define a rewriter that matches
keyword_argumentnodes and rewrites$KEY=$VALto’$KEY’: $VAL. - Apply it to
$$$ARGScaptured fromdict($$$ARGS)viatransform: { LITERAL: { rewrite: { rewriters: [dict-rewrite], source: $$$ARGS } } }. - Use
fix: ‘{ $LITERAL }’on the outer rule to wrap the rewritten arguments in braces.
- Define a rewriter that matches
- Multiple rewriters can be listed in one
transform.rewritecall. Each sub-node is transformed by the first matching rewriter in declaration order. If two rewriters could match the same node, only the one that appears earlier in therewriterslist is applied. Order matters. joinBycontrols how transformed sub-nodes are stitched together. By default, sub-nodes are replaced in-place preserving original separators. SetjoinByto a string like’ + ‘or’\n’to override the joiner.- A rewriter can call other rewriters from the same
rewriterslist inside its owntransformsection, enabling multi-pass rewrite pipelines. - Meta variables captured inside one rewriter do not leak to sibling rewriters or the outer rule. Rewriter-local
transformvariables andutilsare also scoped to that one rewriter. - String-form shorthand
rewrite(rewriters, source, joinBy?)is valid in newer ast-grep versions, but prefer object form when compatibility or debugging clarity matters. - For simple pattern-to-pattern rewrites, use
ast-grep run --pattern=... --rewrite=... --json=compact --color=neverthroughexec_commandto preview replacements. Do not apply them without explicit authorisation. - For advanced rewrite operations using
rewriters,transform.rewrite,joinBy, orFixConfigwithexpandStartorexpandEnd, use a YAML rule through the CLI skill path.
Pattern Syntax
- Pattern code must be valid code that tree-sitter can parse.
- Patterns match syntax trees, so a query can match nested expressions instead of only top-level text.
$VARmatches one named AST node.$$$ARGSmatches zero or more AST nodes in places like arguments, parameters, or statements.- Reusing the same captured name means both occurrences must match the same syntax.
- Prefixing a meta variable with
_disables capture, so repeated$_Xoccurrences do not need to match the same content. $$VARcaptures unnamed nodes when named-node matching is too narrow.- If a short snippet is ambiguous, move to an object-style pattern with more
contextplusselectorinstead of guessing.
Pattern Parsing Deep Dive
- Pattern creation has four stages: preprocess meta-variable text when the language needs a custom
expandoChar, parse the snippet, choose the effective node, then detect meta variables inside that effective node. - Invalid pattern code usually fails because a meta variable is standing in for syntax that the parser treats as an
operator or keyword. Patterns like
$LEFT $OP $RIGHTor{ $KIND foo() {} }should become rule objects using parseable code pluskind,regex,has, or other rule fields. - Incomplete or ambiguous snippets can appear to work only because tree-sitter recovered from an error. Treat that as
best-effort behavior, not a stable contract across ast-grep upgrades, and prefer valid
contextplusselector. - The default effective node is the leaf node or the innermost node with more than one child. Override it with
selectorwhen the real match should be a statement instead of the inner expression, especially forfollowsandprecedes. - Meta variables are detected only when the whole AST node text matches meta-variable syntax. Mixed text like
obj.on$EVENT, lowercase names like$jq, or string-content fragments do not become meta variables. $$VARcaptures unnamed nodes such as operators when the grammar exposes them only as anonymous tokens.$$$ARGSis lazy: it stops before the next node that satisfies the rest of the pattern.- When pattern behaviour is surprising, inspect the parsed tree and effective node with
ast-grep run --debug-query; in the Playground, use the pattern view for the same questions.
Pattern Core Concepts
- ast-grep is structural search, not plain text search.
patternmatches syntax-tree shape, whileregexis the escape hatch when node text itself matters. - Tree-Sitter gives ast-grep a concrete syntax tree, not a stripped-down abstract syntax tree. That CST detail is why punctuation and modifiers can still matter even when matching stays syntax-aware.
- Named nodes carry a
kind; unnamed nodes are punctuation or literal tokens. Meta variables match named nodes by default, and$$VARis the opt-in when unnamed nodes matter. kindbelongs to the node itself.fieldbelongs to the parent-child relationship, so use relationalhasorinsidewithfieldwhen role matters more than raw node kind.- Pseudo-selectors extend
kindmatching with CSS/ESQuery-style combinators. They are string-level refinements on node kind names, not separate rule fields, so they compose naturally with pattern and relational rules. - A node is significant to ast-grep when it is named or has a
field. Trivial nodes can still matter for exact matching, so do not assume every important token has its own named node.
Match Algorithm
- The default strictness is
smart. Every node you spell out in the pattern is respected, but unnamed nodes in the target code can be skipped. - Unnamed nodes written in the pattern are not skipped. A shorter pattern like
function $A() {}can matchasync function, whileasync function $A() {}requiresasyncto be present. - Use strictness to tune what ast-grep may skip during matching.
cst: skip nothingsmart: default, skip unnamed nodes in code onlyast: skip unnamed nodes on both sidesrelaxed: also skip commentssignature: ignore text and compare mostly named-node kinds
- This explains why quote differences can disappear under
ast, comments can disappear underrelaxed, and even different callee text can match undersignature. - Set strictness with the ast-grep CLI
--strictnessflag or a YAML pattern-objectstrictnessfield. Checkast-grep run --helpfor the flags supported by the installed release.
Custom Languages
- Use custom language support when the parser exists in tree-sitter form but ast-grep does not ship it as a built-in language.
- The basic workflow is:
- install
tree-sitterCLI and obtain the grammar - compile the parser as a shared library
- register it in workspace
sgconfig.ymlundercustomLanguages
- install
- Prefer
tree-sitter build --output <lib>to compile the dynamic library. If the installed tree-sitter is too old forbuild, useTREE_SITTER_LIBDIRwithtree-sitter testas the fallback path. - Reusing a parser library built by Neovim is valid when it already matches the grammar/version you need.
- Register
libraryPath,extensions, and optionalexpandoCharinsgconfig.yml.expandoCharmatters when$VARis not valid syntax in the target language and must be rewritten to a parser-friendly prefix. - Use
tree-sitter parse <file>to inspect parser output when the custom grammar or file association is unclear. - Ast-grep CLI queries can use a custom language after the local project config is in place. Keep setup, compilation, and debugging on this skill path.
Language Injection
- ast-grep can search embedded languages inside a host document. Built-in injection already covers HTML with CSS in
<style>and JavaScript in<script>. - Use
languageInjectionsinsgconfig.ymlwhen the embedded language is project-specific, such as CSS inside styled-components or GraphQL inside tagged template literals. - A
languageInjectionsentry needshostLanguage, arule, andinjected. Theruleshould capture the embedded subregion with a meta variable such as$CONTENT. - The
$CONTENTmeta variable in the injection rule designates which portion of the host match should be parsed as the injected language. Without it, ast-grep cannot identify the embedded region. - Typical patterns are
styled.$TAG\$CONTENT`for CSS-in-JS andgraphql`$CONTENT`` for GraphQL template literals. - ast-grep parses the extracted subregion with the injected language, not the parent document language. That is why CSS patterns can match inside JavaScript once injection is configured.
- Use
languageGlobswhen the whole file should be parsed as a different or superset language. UselanguageInjectionswhen only a nested region inside the file changes language. - Use
ast-grep scan --inspect=summarythroughexec_commandto inspect project configuration. Designing or debugginglanguageInjectionsstays on this skill path.
Injection Config Examples
yaml# CSS-in-JS (styled-components) languageInjections: - hostLanguage: js rule: pattern: styled.$TAG`$CONTENT` injected: css # GraphQL tagged template literals - hostLanguage: js rule: pattern: graphql`$CONTENT` injected: graphql # SQL tagged template literals - hostLanguage: js rule: pattern: sql`$CONTENT` injected: sql
Dynamic Injected Language
- Use dynamic
injectedcandidates when the rule captures$LANGand the embedded language must be chosen from a list such ascss,scss, orless. - This is useful for framework-specific template directives where the language tag is part of the matched syntax.
Injection vs. languageGlobs vs. customLanguages
languageGlobsremaps entire files to a different parser. Use it when the file extension does not match ast-grep's built-in mapping (e.g., parsing.tsfiles as TSX).languageInjectionsextracts a sub-region of a file and parses it as a different language. Use it for embedded languages like CSS in JS or SQL in template literals.customLanguagesregisters a new tree-sitter parser for a language ast-grep does not ship. Use it when the target language has a tree-sitter grammar but is not built in.- All three are configured in
sgconfig.ymland consumed automatically byast-grep scan.
FAQ Highlights
- If a pattern fragment fails, the usual fix is to provide more valid
contextand then narrow the real target withselector. This is the standard workaround for subnodes like JSON pairs or class fields that are not standalone code. - If a rule behaves strangely, reduce it to the smallest repro, confirm whether it is matching an expression or a
statement, and use
allto make rule order explicit when later checks depend on earlier meta-variable captures. - CLI and Playground can disagree because parser versions and text encodings differ. Run
ast-grep run --debug-query, then compare the parsed AST or CST before assuming the rule is wrong. - Meta variables must occupy one whole AST node.
use$HOOKand similar prefix/suffix patterns will not work; capture the full node and narrow it withconstraints.regexinstead. Use$$VARfor unnamed nodes, and remember that$$$MULTIis lazy. - Do not combine separate
kindandpatternrules to force a different parse shape. Use one pattern object withcontextandselectorso the parser sees the intended node kind. - ast-grep rules are single-language. Share coverage across related languages by parsing both with the superset via
languageGlobs, or keep separate rules when the AST differences matter. - ast-grep does not provide scope, type, control-flow, data-flow, taint, or constant-propagation analysis. If the task needs those, switch tools instead of stretching rule syntax.
Find & Patch
- ast-grep rewrites are still find first, patch second.
ruleplus optionalconstraintsfinds the target,transformderives replacement strings, andfixpatches the final text. - The simple workflow rewrites one matched node at a time. When one node must expand into multiple outputs, use
rewritersplustransform.rewriteinstead of forcing everything into one inlinefix. transform.rewritelets you run sub-rules over a matched meta-variable, generate one fix per sub-node, and join the results withjoinBy.transform.rewriteis still experimental. It rewrites descendants of the captured source, prevents overlapping rewriter matches, prefers higher-level AST matches first, and for one node only applies the first matching rewriter in the declared order.- This is the right model for list-style rewrites such as exploding a barrel import into multiple single imports,
converting
dict(a=1, b=2)to{‘a’: 1, ‘b’: 2}, or transforming heterogeneous lists where each element type needs a different rewrite rule. transform.rewritehas three important behavioral properties: (1) it rewrites descendants of the captured source metavariable, not the source itself; (2) overlapping rewriter matches are prevented so each sub-node is rewritten at most once; (3) higher-level AST matches are preferred before nested ones, and for one node only the first matching rewriter in declaration order is applied.- Use
joinBywhen the rewritten sub-nodes must be stitched with a different separator than the original source text. For example,joinBy: "\n"converts comma-separated imports into newline-separated direct imports. - For simple pattern-to-pattern rewrites, run
ast-grep run --pattern ... --rewrite ... --json=compactthroughexec_commandto preview replacements without applying them. - For FixConfig rewrites with range expansion, write a YAML rule and run
ast-grep scanthroughexec_command. - For advanced
transform.rewrite,rewriters,joinBy, and multi-pass transform operations, use the CLI skill path viaexec_command.
Rewrite Essentials
- Use
ast-grep run --pattern ... --rewrite ...for one-off rewrites. - Use YAML
fixin rule files for reusable rewrites that should live with the rule. - Use
--interactiveto review rewrite hunks before applying them. - Use
--update-allor-Uonly when the user clearly wants non-interactive apply behavior. - Meta variables captured in
patterncan be reused infix. - String
fixis raw replacement text, not a parsed Tree-Sitter pattern. Meta variables can appear anywhere in the replacement string. fixindentation is preserved relative to the matched source location, so multiline rewrites must be authored with deliberate indentation.- Non-matched meta variables become empty strings in rewritten output.
- If appended uppercase text would be parsed as part of a meta variable name, use transforms instead of writing
$VARNamedirectly. - Use
transform.rewritewhen a matched list must be rewritten element-by-element before the outerfixruns. - Use
joinByto control how rewritten list items are stitched together, for example newline-joined imports in a barrel-import rewrite. - Use
FixConfigwhen replacing only the matched node is not enough, especially for deleting list items or key-value pairs that also need a surrounding comma removed. - In
FixConfig,templateis the replacement text andexpandStart/expandEndwiden the rewritten range to consume commas, brackets, or other surrounding trivia outside the target node. - Use YAML
FixConfigfor rewrites that need range expansion, then preview the rule withast-grep scanbefore applying changes.
FixConfig Examples
Delete a key-value pair and its trailing comma from a YAML-like structure:
yamlrule: kind: pair has: field: key regex: Remove fix: template: '' expandEnd: regex: ','
Preview this rule with ast-grep scan --rule <rule-file> --json=compact.
Delete an array element and its surrounding comma, expanding both start and end:
yamlfix: template: '' expandStart: regex: ',' stopBy: line expandEnd: regex: ','
Replace a function call argument while consuming surrounding whitespace:
yamlrule: pattern: foo($ARG) fix: template: 'bar($ARG)' expandStart: kind: '(' expandEnd: kind: ')'
Preview this rule with ast-grep scan --rule <rule-file> --json=compact.
- Keep advanced
transformandrewritersin the skill-driven CLI workflow.
Run Command Basics
ast-grep -p 'foo()'andast-grep run -p 'foo()'are equivalent.runis the default subcommand.ast-grep rundefaults to searching.when no path is provided and can search multiple paths in one invocation.--globsincludes or excludes paths and overrides ignore-file behavior. Prefix a glob with!to exclude, and let later globs win when multiple patterns match.--no-ignorechanges which ignore sources ast-grep respects. The supported categories arehidden,dot,exclude,global,parent, andvcs.--followmakes ast-grep traverse symlinks. Expect loop or broken-link errors to surface directly from the CLI when the filesystem is invalid.
Scan Command Basics
ast-grep scandefaults to searching.when no path is provided and can search multiple paths in one invocation.--config <file>points scan at a projectsgconfig.ymlroot.--rule <file>runs one YAML rule file without project setup and conflicts with--config.--inline-rules '...'runs one or more inline YAML rules without creating a file on disk. Separate multiple rules with YAML---. It conflicts with--rule.--filter <regex>narrows project-config scan to matching rule ids and conflicts with--rule.--include-metadataincludes rule metadata in JSON output.
Test Command Basics
ast-grep testvalidates rule tests from the ast-grep project config.- Rule test files are YAML with
id,valid, andinvalid.validcases should produce no issue;invalidcases should produce at least one issue. - Ast-grep’s test output distinguishes four outcomes:
reported: invalid code correctly reportsvalidated: valid code correctly stays quietnoisy: valid code reported unexpectedlymissing: invalid code was not reported
--config <file>points test execution at a specific ast-grep root config.--test-dir <dir>narrows where test YAML files are discovered.--snapshot-dir <dir>changes the snapshot directory name from the default__snapshots__.--filter <glob>narrows which rule test cases run.--skip-snapshot-testschecks test validity without snapshot-output assertions.--include-offincludesseverity: offrules during test runs.--update-allgenerates or refreshes snapshot baselines, usually under__snapshots__/.--interactiveis for selective snapshot updates after rule or test changes.- Snapshot tests cover output details such as spans, labels, or message rendering in addition to simple valid/invalid
matching, so
--skip-snapshot-testsis useful while a rule is still evolving.
Other Commands
ast-grep new [project|rule|test|util]scaffolds a project or individual items. Common flags are--lang,--yes,--base-dir, and an optional itemNAME.ast-grep lspstarts the language server and accepts an optional--config <file>.ast-grep completions [shell]generates shell completion scripts forbash,elvish,fish,powershell, orzsh.ast-grep helpandast-grep --helpare the authoritative command-discovery entry points when the exact subcommand or flags are in doubt.
CLI Modes
--interactiveis for reviewing rewrite hunks one-by-one; ast-grep’s interactive controls arey,n,e, andq.--json=pretty|stream|compactselects ast-grep JSON output for inspection or shell pipelines.prettyis the default if a style is not specified.- Raw ast-grep JSON match objects include fields such as
text,range,file,lines, optionalreplacement, optionalreplacementOffsets, and optionalmetaVariables. Scan-mode rule matches add fields likeruleId,severity,message, and optionalnote. - ast-grep JSON positions are zero-based for line, column, and byte offsets. Keep that convention in mind when translating payloads into editor-facing or user-facing locations.
--json=streamemits one JSON object per line and is the better fit for large pipelines;prettyandcompactemit one JSON array and are easier to inspect but less streaming-friendly.--json=<STYLE>must use the equals-sign form.--json streamis parsed as plain--jsonplus an extra positional argument, not as--json=stream.--stdinis for piping code into ast-grep. It conflicts with--interactive.ast-grep run --stdinrequires an explicit--langbecause stdin has no file extension for language inference.ast-grep scan --stdinonly works with one single rule via--rule/-r.--stdinonly activates when the flag is present and ast-grep is not running in a TTY.--heading=auto|always|neveronly changes the human-readable text layout. It does not affect native JSON output.--color=auto|always|ansi|nevercontrols terminal colouring. Prefer--color=neverfor machine-readable output.--format=github|sarifis for CI reporting pipelines.--report-style=rich|medium|shortonly changes ast-grep’s human-readable diagnostics.--error,--warning,--info,--hint, and--offoverride rule severities for one scan run.--inspect entityis the direct CLI way to inspect each rule’s final enabled severity, including overrides and project-config effects.unused-suppressioncan also have its severity overridden on the CLI.--inspect=summary|entityemits file and rule discovery diagnostics to stderr without changing the actual match results.--threads <NUM>controls approximate parallelism.0keeps ast-grep’s default heuristics.-C/--contextshows symmetric surrounding lines.-A/--afterand-B/--beforeare asymmetric alternatives and conflict with--context.ast-grep runexits0when at least one match is found and1when no matches are found.ast-grep scanexits1when at least one error-severity rule matches and0when no rules match.
API Escalation
- Do not force complex transformations into rule syntax when the task needs arbitrary AST inspection or computed replacements.
- Escalate to ast-grep’s library API when the task needs conditional replacement logic, counting or ordering matched nodes, per-node patch generation, or replacement text computed from matched content and surrounding nodes.
- Node.js NAPI is the main experimental API surface today. The common entry points are
parse,kind, andpattern, and the main objects areSgRootandSgNode. - In NAPI,
parse(Lang.<X>, source)returnsSgRoot,root()returnsSgNode, and traversal/search APIs likefind,findAll,field,parent,children,matches,inside,has,replace, andcommitEditslive onSgNode. NapiConfigis the programmatic equivalent of rule YAML forfind/findAll, andFindConfigis the config shape for file-based searching.- Python bindings expose the same general model with
SgRoot(src, language)plusSgNodemethods for rule checks, traversal, searching, and edit generation. - JS language-specific objects like
js.parse(...)are deprecated; prefer the unified NAPI functions withLang.JavaScript. - Rust
ast_grep_coreis the lowest-level and most efficient option, but also the heaviest lift. - Applying ast-grep
fixthrough the JS/Python APIs is still experimental, so prefer generating explicit patches in code when reliability matters. - If the target language has no suitable JS/Python parser path for the desired automation, prefer a Rust implementation or another repo-native AST approach instead of overcomplicating ast-grep rules.
Use exec_command For
ast-grep --helpast-grep newast-grep new ruleast-grep scan -r <rule.yml> <path>ast-grep scan --rule <file> <path>ast-grep scan --inline-rules '...' <path>ast-grep run --pattern <pattern> --rewrite <rewrite>ast-grep run --jsonast-grep run --stdin --lang <lang>ast-grep run --no-ignore hidden --followast-grep run --inspect summary --threads 4ast-grep run --context 2ast-grep scan --format sarif --report-style shortast-grep scan --error=rule-idast-grep scan --stdin --rule <rule.yml>ast-grep test --config sgconfig.yml --filter 'rust/*'ast-grep test --test-dir rule-tests --snapshot-dir __snapshots__ast-grep test --include-off --update-allast-grep lspast-grep completionsast-grep new project --base-dir . --yesast-grep new rule my-rule --lang rustast-grep help- ast-grep GitHub Action setup
- ast-grep programmatic API experiments and library examples
- System package-manager install commands when the user explicitly wants them
sg new- Rewrite or apply flows
- Interactive ast-grep flags
- Raw ast-grep color control such as
--color never transform,rewrite,joinBy, orrewriters- Non-trivial
sgconfig.ymlauthoring or debugging - Rule authoring tasks that need direct ast-grep CLI iteration
Read More
- Read references/project-workflows.md for concise
exec_commandexamples covering ast-grep search, scan, test, debugging, configuration inspection, and rewrite previews.


