بنقرة واحدة
ast-grep
Use for ast-grep project setup, rule authoring, rule debugging, and structural search workflows.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Use for ast-grep project setup, rule authoring, rule debugging, and structural search workflows.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Perform comprehensive codebase analysis and generate reports (usage: /analyze [full|security|performance])
Run a terminal command (usage: /command <program> [args...])
Review the current diff or selected files (usage: /review [--last-diff|--target <expr>|--file <path>|files...] [--style <style>])
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends VT Code's capabilities with specialized knowledge, workflows, or tool integrations.
Install VT Code skills into ~/.agents/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo, including private repos.
| name | ast-grep |
| description | Use for ast-grep project setup, rule authoring, rule debugging, and structural search workflows. |
| metadata | {"short-description":"Ast-grep project workflows","keywords":["inline-rules","new-rule","expandEnd","fix-config","string-fix","nthChild","stopBy","range-field","metadata-url","severity-off","include-metadata","caseInsensitive-glob","rule-order","kind-pattern","positive-rule","kind-esquery","static-analysis","tree-sitter-parser","pattern-yaml-api","search-rewrite-lint-analyze","textual-structural","ast-cst","named-unnamed","kind-field","ambiguous-pattern","effective-selector","meta-variable-detection","lazy-multi","strictness-smart","relaxed-signature","find-patch","rewrite-joinBy","replace-substring","toCase-separatedBy","rewriter","ruleDirs-testConfigs","libraryPath-languageSymbol","dynamic-injected","barrel-import","custom-language","TREE_SITTER_LIBDIR","language-injection","styled-components","language-alias","stdin","programmatic-API","napi-parse","python-api","meta-variables","optional-chaining","rule-catalog","walrus-operator","list-comprehension","isinstance-tuple"]} |
Use this skill for ast-grep project setup, rule authoring, rule debugging, and CLI workflows that go beyond a single structural query.
unified_search with action="structural" and workflow="scan" for read-only project scans. Use severities: ["error", "warning"] to filter findings by severity level and focus on actionable issues.unified_search with action="structural" and workflow="test" for read-only ast-grep rule tests.unified_search with action="structural" and workflow="rewrite" for dry-run rewrite previews. This runs ast-grep run --pattern=... --rewrite=... --json=compact --color=never and returns proposed replacements without applying them. Required fields: pattern, rewrite. Optional: lang, selector, strictness, globs, context_lines, max_results.debug_query on the public tool surface before falling back to raw ast-grep run --debug-query.kind on the public structural surface to match nodes by tree-sitter node kind (e.g. function_item, call_expression). kind supports ESQuery-style compound selectors like A > B, A + B, A ~ B, A, B, and pseudo-selectors like :has(), :not(), :is(), :nth-child(). kind can be used alone or combined with pattern.unified_exec only when the public structural surface cannot express the requested ast-grep flow.vtcode dependencies install ast-grep before suggesting system package managers.ast-grep --help.ast-grep binary name over sg because sg may already refer to setgroups.$PROP, use single quotes so the shell does not expand them before ast-grep sees the pattern.$PROP && $PROP() to $PROP?.().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.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, and yml.--lang <alias> and YAML language: <alias> use those built-in aliases. File-system scans infer language from built-in extensions unless the project overrides them.lang is passed through to ast-grep. VT Code also normalizes and infers a local subset it can pre-parse itself: Rust, Python, JavaScript, TypeScript, TSX, Go, Java, and Markdown.golang, jsx, cjs, mjs, cts, mts, py3, pyi, and mdx.languageGlobs when the repository needs a different extension mapping than ast-grep’s built-in defaults.sgconfig.yml plus at least one rule directory, usually rules/.rule-tests/ and utils/ are optional scaffolding that ast-grep new can create for rule tests and reusable utility rules.sgconfig.yml and rules/, prefer working with the existing layout instead of recreating scaffolding.ast-grep new when the repository does not have ast-grep scaffolding yet.ast-grep new rule when the scaffold exists and the task is creating a new rule plus optional test case.sgconfig.yml is 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.ruleDirs is required and is resolved relative to the directory containing sgconfig.yml.testConfigs is optional and configures ast-grep test discovery. Each entry needs testDir; snapshotDir is optional and otherwise defaults to __snapshots__ under that testDir.utilDirs declares directories for global utility rules shared across multiple rule files.languageGlobs remaps 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.customLanguages registers project-local parsers. libraryPath can be one relative library path or a target-triple map, extensions is required, expandoChar is optional, and languageSymbol defaults to tree_sitter_{name}.languageInjections is experimental. Each entry needs hostLanguage, rule, and injected.injected candidates when the rule captures $LANG and the embedded language must be chosen from a list such as css, scss, or less.sgconfig.yml, and --config <file> overrides that discovery with an explicit root config path.ast-grep scan requires project config and will error if no sgconfig.yml is found. ast-grep run can still search without project config, though it also benefits from discovered config for things like customLanguages and languageGlobs.ast-grep scan --inspect summary is the quickest way to confirm which project directory and config file ast-grep actually selected during discovery.sgconfig.yml as a global fallback config. XDG config directories are not part of this behavior.sgconfig.yml authoring on the skill path. VT Code’s public structural tool can consume an existing config through config_path, but it does not expose these top-level schema fields directly.Fix means the example includes a rewrite pathconstraints, labels, utils, transform, and rewriters mean the example depends on more advanced rule featuresVT 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 organized by language:
rules/python/)no-print: flags print() calls in production codeno-walrus-source: flags walrus operators that harm readabilityno-unnecessary-list: flags list(...) wrapping an already-list expressionno-identity-check-with-type: flags type(x) is T in favor of isinstance(x, T)optional-to-union: flags Optional[X] in favor of X | Noneprefer-dict-get: flags if k in d: d[k] in favor of d.get(k)prefer-generator-expression: flags list comprehensions passed to sum/any/all/min/maxprefer-isinstance-tuple: flags isinstance(x, A) or isinstance(x, B) in favor of isinstance(x, (A, B))rules/rust/)no-unsafe-fn-without-unsafe: flags unsafe fn bodies that contain no unsafe blockavoid-duplicate-export: flags pub use when pub mod already exposes the moduleno-iterator-for-each: flags .iter().for_each() in favor of for loopsno-redundant-closure: flags |x| foo(x) in favor of foo directlylet-chain-candidate: flags nested if that could be collapsed with let-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 a Vec in favor of .retain()prefer-unwrap-or-default: flags .unwrap_or(Default::default()) in favor of .unwrap_or_default()rules/kotlin/)no-var: flags mutable var declarationsno-println: flags println/print callsno-lateinit: flags lateinit var usageno-unsafe-cast: flags as casts without null-safe as?no-unnecessary-let: flags let blocks that add no valueprefer-is-empty: flags .count() == 0 in favor of .isEmpty()prefer-data-class: flags classes that should be data classclean-architecture-imports: flags imports that violate clean architecture layer boundariesrules/ruby/)no-path-traversal: flags string concatenation in File.join / Pathname that may cause traversalprefer-action-over-filter: flags before_filter / after_filter in favor of before_action / after_actionprefer-symbol-over-proc: flags Proc.new with a symbol when (&:method) is cleanerrules/typescript/)no-await-in-promise-all: flags await inside Promise.all() arrays (defeats parallelism)no-console-except-error: flags console.log/debug/warn/info/trace (allows console.error in catch blocks)no-debugger: flags debugger statementsno-unnecessary-boolean-literal-compare: flags x === true or x === falseno-useless-promise-resolve: flags return 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 || $B in favor of $A ||= $Bprefer-optional-chaining: flags a && a.b in favor of a?.bno-return-in-forEach: flags return inside .forEach() callbacks (does not return from caller)no-array-delete: flags delete arr[i] in favor of .splice()rules/tsx/)avoid-jsx-short-circuit: flags {cond && <Elem />} in favor of {cond ? <Elem /> : null} (prevents rendering 0)no-nested-links: flags <a> elements nested inside other <a> elements (invalid HTML)no-unnecessary-usestate-type: flags useState<string>('hello') when TypeScript can infer the typerename-svg-attribute: flags hyphenated SVG attributes like stroke-linecap in favor of camelCase strokeLinecaprules/examples/)no-console-log: starter rule scoped to __ast_grep_examples__/ for scaffold validationpub use foo::Bar; in the same source file that already exposes pub mod foo;. Treat this as API-surface cleanup, not a mechanical rewrite. The rule uses all to combine a pub use $A::$B; pattern with inside: { kind: source_file } and a has check for pub mod $A; with stopBy: 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
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()"
usize digits without allocation: the catalog rewrite from $NUM.to_string().chars().count() to $NUM.checked_ilog10().unwrap_or(0) + 1 is 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"
function_item rule that requires unsafe modifiers but rejects bodies containing unsafe_block is a good review rule for redundant unsafe markers. It is diagnostic-oriented and should usually stay a scan rule, not an automatic rewrite. The rule uses kind: function_item with has checking function_modifiers via regex: "^unsafe" and not rejecting bodies containing unsafe_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
if/if let detection rule uses utils to define reusable matchers for sole-child statements, no-else if expressions, and no-else if let expressions. The root rule matches an if whose block contains only another if statement, suggesting the two can be collapsed into a single let-chain. Keep this as a hint-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 }
indoc! macro: the catalog example that removes indoc! { 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 is ast-grep --pattern ‘indoc! { r#"$$$A"# }’ --rewrite ‘$$$A’..ts and .tsx rules separate unless the repository intentionally parses .ts as TSX through languageGlobs. Do not assume one pattern works unchanged across both parsers.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.await inside Promise.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.should to expect: a useful migration example, but it is test-framework-specific and should be applied only where Chai is actually in use.rewriters / transform.rewrite example 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.@Component() decorator: good example of labels plus pattern-object context and selector. Keep framework-specific rules tied to actual framework usage in the repository.$A = $A || $B to $A ||= $B, but only apply it where the project’s JS target and lint policy allow ES2021 operators..ts through TSX with languageGlobs.useState<T> primitives: good cleanup rewrite for useState<string|number|boolean>($A) when the initializer already gives TypeScript enough information to infer the state type. Bundled as rules/tsx/no-unnecessary-usestate-type.yml.&& 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 like 0. Bundled as rules/tsx/avoid-jsx-short-circuit.yml.observer(() => ...) hides React hook linting from tooling. Keep it on the CLI skill path because naming, export shape, and component conventions vary by repository.use* functions that do not actually call hooks. Treat it as a review rule first, because renaming or de-hooking can be API-affecting.rules/tsx/no-nested-links.yml.stroke-linecap to strokeLinecap. Keep it reviewable because generated markup can be formatting-sensitive. Bundled as rules/tsx/rename-svg-attribute.yml.host: $HOST or port: $PORT and attaches a diagnostic. Treat it as a starting point for config validation, not a complete policy by itself.any patterns to a more structured rule before relying on the result.8000 are only useful when they reflect an actual project policy.call for method calls (e.g. $OBJ.method), method_call for keyword-style calls (e.g. puts "hello"), block for {{ }} blocks, do_block for do...end blocks, symbol for :name literals, assignment for variable assignments, method for method definitions, class for class definitions, if for conditionals, unless for negative conditionals, case for case/when, while and until for loops, return for return statements, yield for yield calls, super for super calls, self for self references.sg for parsing. Use debug_query to inspect parse output when matching is surprising.$VAR meta-variable syntax works directly because $ is a valid Ruby global variable prefix. No expandoChar override is needed.*_filter to *_action: useful migration rewrite for older Rails controllers. The catalog rule uses a transform with replace to swap _filter for _action on the captured $FILTER meta-variable. The pattern uses $$$ACTION to 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
.select { |v| v.even? } to .select(&:even?). The catalog rule constrains ITER to map|select|each via regex, 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 the ITER regex to cover reject, find_all, detect, any?, all?, none?, count when appropriate.Rails.root.join, File.join, or send_file fed by variables. Uses any with three patterns and severity: hint because this is a detection rule, not proof of exploitability. The surrounding validation path still matters. Advise File.basename() or allowlist validation as remediation.{ |$V| $V.$METHOD } or do |$V| $V.$METHOD end, wrap in the enclosing method call and use selector: call to match the outer call. For symbol-to-proc, match the enclosing method call directly with $LIST.$ITER(&:$METHOD).function_definition for functions, call for function calls, import_statement and import_from_statement for imports, assignment for assignments, decorated_definition for decorated functions/classes, with_statement for context managers, try_statement for try/except, if_statement for conditionals, for_statement for loops, return_statement for returns, async_function_definition for async functions, await for await expressions, type for type annotations, subscript for generic types like Optional[T], list_comprehension for list comprehensions, argument_list for function arguments, keyword_argument for keyword arguments, conditional_expression for ternary expressions, assert_statement for assertions.sg.$VAR meta-variable syntax works directly because $ is not a valid Python identifier prefix in expression context. No expandoChar override is needed.openai Python 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 openai to from openai import Client), client initialization (openai.api_key = $KEY to client = Client($KEY)), and completion method (openai.Completion.create($$$ARGS) to client.completions.create($$$ARGS)).any(...), all(...), or sum(...) where generator expressions are clearly valid. Do not generalize it to every list comprehension. The constraint-based variant uses constraints to restrict $FUNC to any|all|sum and $LIST to list_comprehension kind, then strips brackets with a substring transform: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)
if statements: 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 using follows and precedes relational operators. The first rule rewrites the if to 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: ‘’
rewriters example for stripping async and inner await, but treat it as high-risk migration work because it changes call semantics and often requires broader control-flow review. Uses rewriters to strip await from inside the body before removing the async keyword: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
utils-driven context matching for fixture rename or type-hint updates. Uses utils to define reusable context matchers like is-fixture-function (function following a @pytest.fixture decorator) and is-test-function (function whose name starts with test_). Keep it tied to real pytest usage so similarly named non-test code is not swept in.Optional[T] to T | None and 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 uses context and selector to disambiguate Optional[$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.
mapped_column to annotated Mapped[...]: useful ORM migration example, but keep it on the CLI skill path because ORM version, model style, and nullable semantics need review. Uses rewriters to filter out String positional args and nullable=True keyword args from the argument list, then wraps the result in Mapped[str | None].print detection: use kind: call with has: { field: function, pattern: print } to match print() calls. Scope with files to exclude test directories and scripts where console output is acceptable. For logging.debug() or similar, use regex: ^(debug|info|warning)$ on the function field inside a logging. attribute access.kind: call with has: { field: function, pattern: $FN } and constraints restricting $FN to ^(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.map/filter: pattern $LIST = list(map($FUNC, $ITER)) can be rewritten to $LIST = [$FUNC($X) for $X in $ITER] when $FUNC is a simple lambda or single-argument call. Keep it on the CLI skill path because readability depends on the complexity of $FUNC.dict.get with default: pattern $D[$KEY] inside a try_statement with except KeyError can often be rewritten to $D.get($KEY) or $D.get($KEY, $DEFAULT). Use kind: subscript with inside to scope within the try body. Treat as review material because some dict access patterns intentionally propagate KeyError.assert $EXPR == $VAL can be rewritten to self.assertEqual($EXPR, $VAL) in unittest contexts, or left as-is in pytest contexts. Use files to scope by test framework convention.isinstance tuple consolidation: pattern isinstance($X, $A) or isinstance($X, $B) can be rewritten to isinstance($X, ($A, $B)). This is a safe autofix when both isinstance calls check the same variable.files plus import-path constraints. Treat it as repository-policy enforcement rather than a universal Kotlin rule.files glob and package regexes to the repository’s actual module layout before relying on the result.sg for parsing. Use debug_query to inspect parse output when matching is surprising. This is the same situation as C, C++, Ruby, and other extended languages.$EXPR as $TYPE): good warning-level scan rule for catching runtime ClassCastException risks. The safe cast as? 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 exhaustive when or is checks.var vs val preference: use kind: property_declaration with has: { field: property_delegate, pattern: var } to match mutable property declarations. A naive var $NAME: $TYPE pattern may over-match in contexts where the parser attaches different node structure. The kind plus has plus field approach is more robust.println detection: use an any composite to cover Kotlin’s top-level println($$$ARGS), Java’s System.out.println($$$ARGS), and System.err.println($$$ARGS). Scope with files to exclude test directories where console output is acceptable.isEmpty() preference: straightforward rewrite rule from $X.size == 0 or $X.length == 0 to $X.isEmpty(). Also cover $X.count() == 0 and $X.size <= 0. This is a safe autofix because Kotlin’s isEmpty() is semantically equivalent for standard collections and strings.lateinit detection: pattern lateinit var $NAME: $TYPE is a direct structural match. Use severity: info because lateinit is sometimes justified in dependency injection and test setup contexts. Teams should adjust severity to match their policy.let blocks: pattern $RECEIVER.let { $PARAM -> $BODY } catches explicit named-parameter let calls. This does not match the implicit it form ($RECEIVER.let { $BODY }) because the parser structures those differently. Focus on the named-parameter variant as the more egregious anti-pattern.kind: class_declaration with has: { 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.class_declaration for classes, property_declaration for val/var properties, function_declaration for functions, primary_constructor for primary constructors, class_parameter for constructor parameters, import_declaration for imports, call_expression for function calls, as_expression for cast expressions, lambda_expression for lambdas, when_expression for when blocks.$EXPR as $TYPE as as_expression and $EXPR as? $TYPE as a variant with the ? token attached, so a pattern targeting as will not match as?. This makes cast-direction rules safe from false positives on safe casts.?.let { } safe-call form is parsed differently from .let { } dot-call form. Rules targeting one will not match the other. Use any with both patterns when both forms should be flagged.has plus ordered all plus precedes, 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 uses all to guarantee that the meta-variable $IDENT is captured by the first has clause before the not/precedes check 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.
String: good structural scan example showing why field_declaration plus has: { field: type } is more robust than a naive pattern when modifiers and annotations are present. A naive String $F; pattern fails because it ignores modifiers and annotations. A $MOD String $F; pattern also fails because tree-sitter does not consider $MOD a valid modifier and produces an ERROR node. 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.
languageGlobs in sgconfig.yml to parse .vue, .svelte, or .astro files as HTML when the framework syntax is minimal enough for the HTML parser.element for full HTML elements, tag_name for tag names, attribute_name for attribute names, attribute_value for attribute values, text for text content, and comment for HTML comments. Use these with kind to match specific HTML structures without writing full pattern syntax.kind: element with has: { field: tag_name, pattern: $TAG } to match elements by their tag name. For regex-based tag matching (e.g. all heading tags), use kind: tag_name with regex: "^h[1-6]$" and inside: { kind: element }.kind: element with has: { kind: attribute_name, regex: "^class$" } to find elements with a specific attribute. To also match the attribute value, add a nested has on the attribute node to capture attribute_value.inside and stopBy: HTML inside with stopBy: { kind: element } scopes matches to the nearest enclosing element. This is essential for avoiding cross-element matches in deeply nested HTML. The inside-tag utility pattern from the catalog demonstrates wrapping inside with kind: element and has to capture the enclosing tag name, then using constraints to restrict which tags match.visible to open: good framework-specific attribute rewrite using enclosing-tag checks plus constraints. The pattern uses kind: attribute_name with regex: :visible to match the attribute, inside to find the enclosing element, has to capture the tag_name, and constraints to 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.kind: text with pattern: $T to capture text content, not: { regex: ‘{{.*}}’ } to skip mustache interpolation, and fix: "{{ $(‘$T’) }}" to wrap the text. Keep it reviewable because real projects usually need key naming, dictionary updates, and whitespace policy beyond the raw rewrite.kind: attribute_name to match the target attribute, inside to find the parent element, and constraints to narrow by attribute name regex. For renaming attributes (e.g. visible to open), match the attribute name node and use fix to replace it.kind: text to match raw text nodes inside elements. Combine with inside: { kind: element, has: { field: tag_name, pattern: $TAG } } to scope text matching to specific elements. Use not to exclude text containing interpolation syntax.kind: comment to match HTML comments. Combine with regex to find comments containing specific text patterns like TODO, FIXME, or deprecated notices.<script> and <style> content is parsed as embedded JavaScript and CSS respectively. Search inside these regions with lang: javascript or lang: css rules. For custom embedded languages (e.g. TypeScript in <script lang="ts">), configure languageInjections in sgconfig.yml.defer with nested function calls: strong Go-specific scan example for catching cases where deferred arguments are evaluated immediately instead of at function exit. In Go, defer evaluates arguments when the defer statement is encountered, not when the deferred function runs. This is particularly problematic with assertion libraries in tests:id: 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.
kind plus has plus regex when a meta-variable pattern cannot express the naming constraint directly. A plain Test$_ pattern fails because it is not valid syntax; use a YAML rule instead:id: 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.
fmt.Println($A) as a type conversion, not a call expression, because Go syntax allows both. Use a contextual pattern with selector: call_expression to disambiguate. Note: contextual patterns are pattern objects (context + selector inside pattern), which require the CLI skill path via unified_exec. The public structural surface’s selector field works with simple string patterns but does not support the context field:id: 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.
id: match-package-import
language: go
rule:
kind: import_spec
has:
regex: github.com/golang-jwt/jwt
-,: 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:id: 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:"-".
languageGlobs; do not assume mixed C/C++ projects want that parser tradeoff by default.sg for parsing. Use debug_query to inspect parse output when matching is surprising.fprintf/sprintf-style calls missing an explicit format string. Uses constraints with regex on $PRINTF and kind on $VAR to distinguish vulnerable calls from safe ones. The fix inserts "%s" as the format argument:id: 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 $SOMETHING: $INHERITS produces an ERROR node because tree-sitter-cpp requires the full syntactic form. Use the complete pattern with the body block:id: 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.
languageGlobs; do not blur C and C++ semantics by default.test($A) becomes macro_type_specifier, while test($A); becomes expression_statement -> call_expression. Use context plus selector: call_expression to disambiguate. Note: contextual patterns are pattern objects (context + selector inside pattern), which require the CLI skill path via unified_exec. The public structural surface's selector field works with simple string patterns but does not support the context field:id: match-function-call
language: c
rule:
pattern:
context: $M($$$);
selector: call_expression
transform with replace to derive a conditional comma from $$$ARGS:id: 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.
constraints to restrict $B to number_literal and inside to scope within if_statement:id: 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.
--lang md or lang: markdown in YAML rules.atx_heading for ATX-style headings, fenced_code_block for fenced code blocks, and list_item for list items.atx_heading, fenced_code_block matches both headings and code blocks.tree-sitter-md and still has known parsing bugs and edge cases. Use it for inspection, indexing, outline extraction, and lightweight automation rather than critical rewrites.lang=md from .md and .mdx file paths and globs, so structural queries over Markdown files do not always require an explicit lang argument.@ast-grep/napi only when rule YAML or VT Code’s public structural tool is not enough. The programmatic API is the right escalation path for computed replacements, ordered-match logic, cross-node inspection, or edit orchestration that would be awkward in pure rule syntax.SgRoot and SgNode: parse(Lang.<X>, source) creates the tree, root() returns the root node, and find / findAll / traversal / refinement / edit APIs live on SgNode.Matcher inputs can be pattern strings, numeric kind ids, or NapiConfig objects. Prefer patterns or NapiConfig unless there is a concrete reason to drop to raw kind ids.getMatch and getMultipleMatches expose captured metavariables, but replace does not interpolate metavariables for you. Build replacement strings explicitly in JavaScript from matched nodes before calling commitEdits.registerDynamicLanguage and extra language packages exist, but that path is still experimental. Prefer established parsers and repo-native tooling unless dynamic-language support is actually needed.ast-grep-py when the task needs programmatic AST traversal or computed edits but a Python host environment is a better fit than JavaScript or Rust. As with NAPI, prefer it only after rule YAML or VT Code’s structural/CLI path stops being a good fit.SgRoot and SgNode: SgRoot(source, language) parses the source, root() returns the root node, and search, refinement, traversal, and edit APIs live on SgNode.find and find_all support 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 returning None.replace and commit_edits generate source edits, but they do not interpolate metavariables for you. Build replacement text explicitly from matched nodes before applying edits.parseAsync over parse when many parse jobs can benefit from Node’s libuv thread pool and the main JS thread is already busy.findAll over manual recursive traversal in JavaScript. One bulk Rust-side search is usually cheaper than repeated kind(), children(), and recursion calls across the FFI boundary.findInFiles when 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.findInFiles has 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.id, language, and root rule.rule as a rule object that matches one target AST node per result.pattern or kind; regex is a filter, not a sufficient root rule by itself.pattern, kind, regex, nthChild, and range.pattern, kind, regex, nthChild, and range for direct node checks.kind is available as a first-class field alongside pattern. Use kind alone to match by node type without a pattern, or combine both to filter pattern matches by node kind.inside, has, follows, and precedes when the match depends on surrounding nodes.all, any, not, and matches to combine sub-rules or reuse utility rules.kind values 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.all sequence instead of assuming YAML key order matters.language controls how patterns parse. Syntax that is valid in one language can fail in another.pattern, kind, and regex are the common atomic fields. pattern can also be an object with context, selector, and optional strictness.context is required; selector picks the real target node inside that context; strictness tunes how literally the pattern matches.kind is usually a plain node kind name, but ast-grep 0.42+ supports ESQuery-style pseudo-selectors in kind strings. Use :has(selector) or :has(> selector) to match nodes containing descendants (or direct children) matching a selector, :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 selector values on the CLI and in VT Code's public structural surface.kind with compound selector operators: A > B (direct child), A B (descendant), A + B (immediate sibling), A ~ B (general sibling), and A, B (either). This syntax works in YAML rule kind fields and the CLI --kind / -k flag. It is ESQuery-style, not full ESQuery: class selectors, attribute selectors, and wildcard selectors are not supported.kind and pattern checks do not change how the pattern is parsed. If parse shape is the problem, switch to one pattern object with context and selector.regex matches the whole node text. Reach for nthChild when position among named siblings matters and range when the match must be limited to a known source span.regex, not PCRE. Do not assume look-around or backreferences are available, and usually pair regex with kind or pattern so the expensive text check only runs on the right node shapes.nthChild accepts a number, an An+B string, or an object with position, reverse, and ofRule. Counting is 1-based and only considers named siblings.range matches by source position with 0-based line and column; start is inclusive and end is exclusive.inside, has, follows, and precedes when the match depends on ancestors, descendants, or neighboring nodes.pattern, kind, composites, and captures. Those captures can still be referenced later in fix, which is a practical way to extract surrounding syntax while keeping the target node as the match.field when the surrounding node matters by semantic role, not just by shape. field only applies to inside and has.stopBy when ancestor or sibling traversal must continue past the nearest boundary instead of stopping early. The default is neighbor, end searches to the boundary, and a rule object stop is inclusive.inside means the target is somewhere under a matching ancestor, has means the target node contains a matching descendant, follows means the target comes after a matching sibling or prior node, and precedes means it comes before one.all for explicit conjunction, any for alternatives, not for exclusions, and matches to delegate to a utility rule.all and any still operate on one target node. They combine sub-rules, not multiple matched nodes.all is the ordered composite. Use it when later checks depend on captures established by earlier pattern matches, because YAML rule-object field order is not guaranteed.any is for alternatives, not for "collect all matching nodes". If one node cannot satisfy every branch at once, all is the wrong operator even if the surrounding structure feels plural.has: { all: [...] } means "has one child satisfying every listed rule", not "has one child for each listed rule".all: [ { has: ... }, { has: ... } ] on the outer target instead of putting incompatible checks inside one nested all.utils for one config file and global utility-rule files when multiple rules in the project need the same building block.utils map, are only visible in that one config file, inherit the file's language, and cannot define their own separate constraints.utilDirs. They must declare id and language, and can only use the utility-safe fields: id, language, rule, constraints, and local utils.matches seems to resolve unexpectedly.matches, including recursive structural tricks like nested-parentheses matching. Avoid cyclic matches dependency graphs, because ast-grep does not allow recursive cycles there.inside or has is different from cyclic matches reuse and is allowed when the AST traversal still makes progress.pattern to a rule object when you need positional constraints, role-sensitive matching, reusable sub-rules, or several structural conditions on one node.all across those fields, but not to an ordered all. Keep explicit all when capture order matters; use rule-object style when the checks are independent and flatter indentation helps readability.ast-grep supports ESQuery-style selectors in the kind field. This syntax works in YAML rule kind fields, the CLI --kind / -k flag, and VT Code's public structural kind parameter. The selector is written in the kind field and ast-grep parses it internally.
>): matches a direct child node. kind: call_expression > identifier is equivalent to kind: identifier with inside: { kind: call_expression }.kind: call_expression identifier is equivalent to kind: identifier with inside: { kind: call_expression, stopBy: end }.+): matches the next sibling node. kind: decorator + method_definition is equivalent to kind: method_definition with follows: { kind: decorator }.~): matches any following sibling node. kind: decorator ~ method_definition is equivalent to kind: method_definition with follows: { kind: decorator, stopBy: end }.any. kind: identifier, number is equivalent to any: [{ kind: identifier }, { kind: number }].: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 :has to 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 to any. 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's nthChild rule. kind: array > number:nth-child(2n+1) matches odd-numbered number elements in arrays.:nth-child(An+B of selector): supports of syntax for filtering. kind: array > :nth-child(1 of number) matches the first number element in an array.:nth-last-child(position): equivalent to nthChild with reverse: true. kind: array > number:nth-last-child(1) matches the last number element in an array.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 } }].
# 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
.body are tokenized but rejected as unsupported.:has, :not, :is, :nth-child, and :nth-last-child.:has(...), :not(...), and of ... parse a single complex selector, not a comma selector list.:is(...) is the one pseudo-class that accepts comma-separated selector lists._ and -, but cannot start with a digit.id for the unique rule name, language for the parser target, url for rule documentation, and metadata for custom project data that VT Code should preserve with the rule.---.rule is the core matcher, constraints narrows meta-variable captures, and utils holds reusable helper rules that you call through matches.utils can be purely local to the current file or can supplement global utility-rule files loaded through utilDirs. Keep shared building blocks global only when multiple rule files genuinely need them.constraints runs after rule matched, only targets single meta variables like $ARG, and is a poor fit inside not.transform to derive new meta-variables before replacement, fix for either a string replacement or a template object with expandStart / expandEnd, and rewriters when the transformation is too complex for one inline fix.severity, message, note, and labels for diagnostics, then files and ignores to scope where the rule applies.error, warning, info, hint, and off. hint is the default severity in ast-grep project scans.error findings make raw ast-grep scan exit non-zero; VT Code normalizes that CLI behavior into structured findings on the public scan path instead of surfacing a tool error. The scan summary includes a has_error_findings flag that is true when any error-severity rule matched.severity: off disables the rule during scanning. note supports Markdown but cannot interpolate meta variables.severities filter (a list of severity levels like ["error", "warning"]). When present, only findings matching one of the listed severities are returned. This filters the output after ast-grep runs; it does not override rule severities at the CLI level. Use this to focus on actionable findings in CI or to reduce noise from hint-level 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.ast-grep-ignore comments.
ast-grep-ignore suppresses all rules for the same line or following lineast-grep-ignore: rule-id suppresses one rulelet x = dangerous_call(); // ast-grep-ignore: no-dangerous-calls// ast-grep-ignore: no-console-log, no-debugger on the line above// ast-grep-ignore: no-console-log as the very first line of the file, followed by an empty line.unused-suppression is a built-in hint-style rule with autofix for stale ignore directives, but it only appears in full scan runs when ast-grep is not filtering or disabling rules through CLI narrowing flags. Override its severity on the CLI with --error unused-suppression to make stale suppressions fail CI.labels keys must come from meta variables already defined by the rule or constraints.files supports either plain globs or object entries. Use object syntax when you need options like caseInsensitive glob matching.ignores runs before files. Both are relative to the sgconfig.yml directory, and the glob should not start with ./.ignores is different from CLI --no-ignore: the CLI flag changes global ignore-file behavior, while YAML ignores only filters files for that rule.metadata when the ast-grep run enabled metadata output, for example via --include-metadata.arguments so callers pass rule objects into a reusable template via matches. 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.transform builds new strings from captured meta variables before fix runs.transform entry introduces a new variable name without a leading $. Inside the transform object, source still points at an existing capture or prior transform result using the normal $VAR form.transform block will see the already-transformed value.fix is applied.replace uses a Rust regex over one meta variable. source must be $VAR style, replace is the regex, by is the replacement text, and regex capture groups can be reused in by.replace field of the replace transform and can only be referenced from the by field of that same transform. Regular regex rules do not expose those capture groups.() work and are referenced as $1, $2, etc. in by.# 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 slices a meta variable by character index with inclusive startChar and exclusive endChar. Negative indexes count from the end, and slicing is based on Unicode characters rather than raw bytes.substring behaves like Python string slicing, so omit either bound when the slice should stay open-ended.# 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 changes identifier-style casing through toCase. Common outputs are lowerCase, upperCase, capitalize, camelCase, snakeCase, kebabCase, and pascalCase.separatedBy to control how convert splits words before rebuilding the target case. Supported separators include dash, dot, space, slash, underscore, and CaseChange.CaseChange splits at transitions such as astGrep, ASTGrep, or XMLHttpRequest, which matters when converting mixed acronym identifiers.separatedBy is omitted, all known separators are used. This is usually the right default.# 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)
transform map.# 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"
replace transforms for conditional punctuation or whitespace when a multi-capture may be empty. The common pattern is deriving MAYBE_COMMA or similar from $$$ARGS so the extra separator only appears when matches exist.# 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)"
replace(...), substring(...), convert(...), and rewrite(...) are valid shorthand in ast-grep 0.38.3+.operator($SOURCE, key1=value1, key2=value2).[item1, item2] syntax inside the string form.rewriters is 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.fix replaces one matched node at a time; rewriters plus transform.rewrite handle the one-to-many case.rewriters at the YAML rule root. Each rewriter needs id, rule, and fix. Optional fields are constraints, transform, and utils.transform using the rewrite operator. rewriters lists which rewriter ids to try; source points at the metavariable whose sub-nodes are rewritten.fix.dict(a=1, b=2) to {‘a’: 1, ‘b’: 2}:
keyword_argument nodes and rewrites $KEY=$VAL to ’$KEY’: $VAL.$$$ARGS captured from dict($$$ARGS) via transform: { LITERAL: { rewrite: { rewriters: [dict-rewrite], source: $$$ARGS } } }.fix: ‘{ $LITERAL }’ on the outer rule to wrap the rewritten arguments in braces.transform.rewrite call. 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 the rewriters list is applied. Order matters.joinBy controls how transformed sub-nodes are stitched together. By default, sub-nodes are replaced in-place preserving original separators. Set joinBy to a string like ’ + ‘ or ’\n’ to override the joiner.rewriters list inside its own transform section, enabling multi-pass rewrite pipelines.transform variables and utils are also scoped to that one rewriter.rewrite(rewriters, source, joinBy?) is valid in newer ast-grep versions, but prefer object form when compatibility or debugging clarity matters.workflow="rewrite" on the public structural surface to preview replacements without applying them. This runs ast-grep run --pattern=... --rewrite=... --json=compact --color=never and returns each match with its proposed replacement and replacementOffsets. The surface remains read-only; no files are modified.rewriters, transform.rewrite, joinBy, or FixConfig with expandStart/expandEnd, use the CLI skill path via unified_exec. VT Code’s public structural surface does not expose multi-rewriter or transform-pipeline behavior.$VAR matches one named AST node.$$$ARGS matches zero or more AST nodes in places like arguments, parameters, or statements.