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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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:
id: 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 `
…(truncated)