packages/shared-skills/skills/ast-grep/references/yaml-rules.md
Use this when you outgrow inline sg run -p ... patterns and need a reusable, testable rule. A YAML rule is the unit of work for sg scan. Drop one or more files in ruleDirs/ (configured via sgconfig.yml) and they get loaded automatically.
This page is the practical reference. The full upstream docs live at:
A single YAML file can hold multiple rules separated by ---.
id: no-console
language: TypeScript
severity: warning
message: "Avoid console.* in production"
note: |
Use a proper logger so we can route logs to stderr in production
and silence them in tests.
url: https://internal.docs/rules/no-console
rule:
pattern: console.$METHOD($$$ARGS)
fix: logger.$METHOD($$$ARGS)
constraints:
METHOD:
not:
regex: '^(error|warn)$'
files:
- 'src/**/*.ts'
ignores:
- 'src/**/*.test.ts'
metadata:
category: logging
| Field | Required | Description |
|---|---|---|
id | yes | Unique identifier. Use kebab-case. |
language | yes | One of: Bash, C, Cpp, CSharp, Css, Elixir, Go, Haskell, Html, Java, JavaScript, Json, Kotlin, Lua, Nix, Php, Python, Ruby, Rust, Scala, Solidity, Swift, TypeScript, Tsx, Yaml. Capitalized PascalCase is canonical, but lowercase often works. |
rule | yes | The matching logic. Object containing one or more atomic / relational / composite rules. |
constraints | no | Filter on captured single-metavariables ($VAR, not $$$). |
utils | no | Local utility rules referenced by matches: in this file. |
transform | no | Manipulate metavariable strings before fix. |
fix | no | String or FixConfig for auto-rewrite. |
rewriters | no | Rewriter rules for the rewrite transform. |
severity | no | hint | info | warning | error | off (default: hint). |
message | no | Concise lint message. May reference $VAR capture text. |
note | no | Detailed markdown explanation (no $VAR interpolation). |
labels | no | Custom diagnostic highlighting per-metavariable. |
files | no | Glob include list. |
ignores | no | Glob exclude list. |
url | no | Doc link shown in editor diagnostics. |
metadata | no | Free-form data ignored by sg, useful for external tooling. |
patternMatch by structural pattern. The most common rule.
# String form
rule:
pattern: console.log($MSG)
# Object form (when context is needed)
rule:
pattern:
context: 'class C { $FIELD = $INIT }'
selector: field_definition
strictness: relaxed # optional, default: smart
kindMatch by AST node type name. Tree-sitter grammar-specific.
rule:
kind: call_expression
ast-grep 0.39+ supports limited ESQuery selectors:
rule:
kind: call_expression > identifier # direct child
kind: call_expression + identifier # next sibling
kind: call_expression ~ identifier # following sibling
kind: call_expression identifier # descendant
To find the right kind, parse a known-good file:
sg run -p '$_' --lang ts --debug-query=cst src/foo.ts | head -40
regexMatch node text against a Rust regex. Whole-text match (no partial). Always combine with kind or pattern for performance.
rule:
all:
- kind: identifier
- regex: '^[A-Z][a-z]+$' # PascalCase
Inline flags work: (?i)apple, (?m)^foo. No look-around, no backreferences.
nthChildMatch by 1-based index among named siblings. Inspired by CSS :nth-child.
rule:
nthChild: 1 # first sibling
# Functional form
rule:
nthChild: 2n+1 # odd siblings
# With reverse and ofRule
rule:
nthChild:
position: 1
reverse: true # last
ofRule:
kind: function_declaration
rangeMatch by character range. Useful for tooling that pinpoints a known location.
rule:
range:
start: { line: 0, column: 0 }
end: { line: 0, column: 11 }
All four take a sub-rule object plus optional stopBy and (for inside/has) field.
inside — target is inside parent/ancestor matching sub-rulerule:
pattern: this.$PROP
inside:
kind: class_body
stopBy: end # walk up to file root, default: neighbor
has — target has child/descendant matching sub-rulerule:
kind: function_declaration
has:
kind: throw_statement
stopBy: end
precedes — target appears before sibling matching sub-rulerule:
kind: import_statement
precedes:
kind: function_declaration
follows — target appears after sibling matching sub-rulerule:
pattern: super($$$)
follows:
pattern: $X = $Y
stopBy| Value | Behavior |
|---|---|
"neighbor" (default) | Stop at immediate parent/child/sibling. |
"end" | Walk all the way to root / leaf / sequence boundary. |
| Rule object | Stop when sub-rule matches (inclusive). |
fieldSpecify the semantic role of the target inside its parent (e.g. name, body, value, key).
rule:
kind: pair
has:
field: key
regex: '^password$'
| Rule | Meaning |
|---|---|
all | All sub-rules must match the same target node. Metavariables from all sub-rules merge. |
any | At least one sub-rule must match. Only metavars from the matched branch survive. |
not | Inverse: target must NOT match the sub-rule. |
matches | Reference a utility rule by id. |
rule:
all:
- kind: call_expression
- pattern: $FN($$$ARGS)
- inside:
kind: function_declaration
stopBy: end
rule:
any:
- pattern: console.log($X)
- pattern: console.warn($X)
- pattern: console.error($X)
rule:
all:
- pattern: $E.unwrap()
- not:
inside:
kind: function_item
has:
kind: result_type
stopBy: end
rule:
matches: is-react-component
Composites apply to a single target. To express "node X has BOTH a number child AND a string child," use two relational rules at the top level, not
allinsidehas. Seereferences/pitfalls.md§10.
all — multiple rule fieldsA rule object with multiple fields is treated as an implicit all:
# These two are equivalent
rule:
pattern: this.$PROP
inside: { kind: class_body }
rule:
all:
- pattern: this.$PROP
- inside: { kind: class_body }
Use the explicit all array when capture order matters (rare, but possible with downstream transform).
constraints — post-match metavariable filteringAfter the main rule matches, additional checks on captured single metavariables:
rule:
pattern: function $NAME($$$P) { $$$B }
constraints:
NAME:
regex: '^[a-z][a-zA-Z0-9]*$' # camelCase only
not:
regex: '^_' # not starting with _
Constraints only apply to single metavars ($VAR), not multi ($$$VAR).
utils — local reusable sub-rulesutils:
is-literal:
any:
- kind: number
- kind: string
- kind: 'true'
- kind: 'false'
rule:
all:
- pattern: $X = $Y
- has:
matches: is-literal # references utils.is-literal
For utils accessible across multiple rule files, use utilDirs in sgconfig.yml and put each util in its own YAML file with id and language.
transform — manipulate captures before fixOperations: replace, substring, convert, rewrite.
replace — regex search/replace on a captured stringrule:
pattern: $OLD_FN($$$A)
constraints:
OLD_FN:
regex: '^debug_'
transform:
NEW_FN:
replace:
source: $OLD_FN
replace: '^debug_'
by: 'release_'
fix: $NEW_FN($$$A)
substring — character slicing (negative indices supported)transform:
INNER:
substring:
source: $WRAPPED
startChar: 1
endChar: -1
convert — case conversiontransform:
KEBAB:
convert:
source: $CAMEL
toCase: kebabCase # camelCase | snakeCase | kebabCase | pascalCase | upperCase | lowerCase | capitalize
separatedBy: [underscore] # optional: dash | dot | space | slash | underscore | caseChange
rewrite — apply other rewriter rules (experimental)rewriters:
- id: stringify
rule: { pattern: "'' + $A" }
fix: "String($A)"
rule:
pattern: stringify-all($EXPR)
transform:
REWRITTEN:
rewrite:
source: $EXPR
rewriters: [stringify]
joinBy: "\n"
fix: $REWRITTEN
Later transforms can reference variables produced by earlier ones:
transform:
KEBABED:
convert: { source: $X, toCase: kebabCase }
PREFIXED:
replace:
source: $KEBABED
replace: '^'
by: 'css-'
fix: $PREFIXED
fix — auto-rewritefix: logger.log($$$ARGS)
# Empty string deletes the match
fix: ""
When deleting one item from a comma-separated list, you also need to remove the trailing comma. Use expandEnd:
rule:
kind: pair
has:
field: key
regex: '^password$'
fix:
template: ''
expandEnd:
regex: ','
expandStart and expandEnd accept regex matching characters that should be absorbed into the rewrite range.
rewriters — sub-rule library for rewrite transformTop-level field defining one or more named rewriters:
rewriters:
- id: nullable-to-optional
rule: { pattern: $X | null }
fix: '$X | undefined'
- id: stringify
rule: { pattern: "'' + $A" }
fix: 'String($A)'
Used inside transform via the rewrite operation (see above).
labels — custom diagnostic highlightingrule:
pattern: $FN($$$ARGS)
labels:
FN:
style: primary
message: "this function shouldn't be called"
ARGS:
style: secondary
message: "with these arguments"
Editor extensions render the diagnostic with these labels. Defaults are usually fine.
files and ignores — file selection per-rulefiles:
- 'src/**/*.ts'
- 'lib/**/*.ts'
ignores:
- 'src/**/*.test.ts'
- '**/__generated__/**'
If omitted, the rule runs on every file matching its language. These globs override sgconfig.yml-level globs for this rule only.
Object form (rare):
files:
- pattern: 'src/**/*.ts'
case_sensitive: true
references/recipes.md — copy-paste rules by language.references/cli.md — sg scan, sg test.references/sgconfig.md — project-level configuration.