Back to Oh My Openagent

YAML rule reference — atomic, relational, composite, transform, fix

packages/shared-skills/skills/ast-grep/references/yaml-rules.md

4.11.011.1 KB
Original Source

YAML rule reference — atomic, relational, composite, transform, fix

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:


Skeleton

A single YAML file can hold multiple rules separated by ---.

yaml
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

Top-level fields

FieldRequiredDescription
idyesUnique identifier. Use kebab-case.
languageyesOne 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.
ruleyesThe matching logic. Object containing one or more atomic / relational / composite rules.
constraintsnoFilter on captured single-metavariables ($VAR, not $$$).
utilsnoLocal utility rules referenced by matches: in this file.
transformnoManipulate metavariable strings before fix.
fixnoString or FixConfig for auto-rewrite.
rewritersnoRewriter rules for the rewrite transform.
severitynohint | info | warning | error | off (default: hint).
messagenoConcise lint message. May reference $VAR capture text.
notenoDetailed markdown explanation (no $VAR interpolation).
labelsnoCustom diagnostic highlighting per-metavariable.
filesnoGlob include list.
ignoresnoGlob exclude list.
urlnoDoc link shown in editor diagnostics.
metadatanoFree-form data ignored by sg, useful for external tooling.

Atomic rules — match a single node

pattern

Match by structural pattern. The most common rule.

yaml
# 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

kind

Match by AST node type name. Tree-sitter grammar-specific.

yaml
rule:
  kind: call_expression

ast-grep 0.39+ supports limited ESQuery selectors:

yaml
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:

bash
sg run -p '$_' --lang ts --debug-query=cst src/foo.ts | head -40

regex

Match node text against a Rust regex. Whole-text match (no partial). Always combine with kind or pattern for performance.

yaml
rule:
  all:
    - kind: identifier
    - regex: '^[A-Z][a-z]+$'   # PascalCase

Inline flags work: (?i)apple, (?m)^foo. No look-around, no backreferences.

nthChild

Match by 1-based index among named siblings. Inspired by CSS :nth-child.

yaml
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

range

Match by character range. Useful for tooling that pinpoints a known location.

yaml
rule:
  range:
    start: { line: 0, column: 0 }
    end:   { line: 0, column: 11 }

Relational rules — match by relation to other nodes

All four take a sub-rule object plus optional stopBy and (for inside/has) field.

inside — target is inside parent/ancestor matching sub-rule

yaml
rule:
  pattern: this.$PROP
  inside:
    kind: class_body
    stopBy: end                # walk up to file root, default: neighbor

has — target has child/descendant matching sub-rule

yaml
rule:
  kind: function_declaration
  has:
    kind: throw_statement
    stopBy: end

precedes — target appears before sibling matching sub-rule

yaml
rule:
  kind: import_statement
  precedes:
    kind: function_declaration

follows — target appears after sibling matching sub-rule

yaml
rule:
  pattern: super($$$)
  follows:
    pattern: $X = $Y

stopBy

ValueBehavior
"neighbor" (default)Stop at immediate parent/child/sibling.
"end"Walk all the way to root / leaf / sequence boundary.
Rule objectStop when sub-rule matches (inclusive).

field

Specify the semantic role of the target inside its parent (e.g. name, body, value, key).

yaml
rule:
  kind: pair
  has:
    field: key
    regex: '^password$'

Composite rules — combine sub-rules

RuleMeaning
allAll sub-rules must match the same target node. Metavariables from all sub-rules merge.
anyAt least one sub-rule must match. Only metavars from the matched branch survive.
notInverse: target must NOT match the sub-rule.
matchesReference a utility rule by id.
yaml
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 all inside has. See references/pitfalls.md §10.


Implicit all — multiple rule fields

A rule object with multiple fields is treated as an implicit all:

yaml
# 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 filtering

After the main rule matches, additional checks on captured single metavariables:

yaml
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-rules

yaml
utils:
  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 fix

Operations: replace, substring, convert, rewrite.

replace — regex search/replace on a captured string

yaml
rule:
  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)

yaml
transform:
  INNER:
    substring:
      source: $WRAPPED
      startChar: 1
      endChar: -1

convert — case conversion

yaml
transform:
  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)

yaml
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

Transforms can chain

Later transforms can reference variables produced by earlier ones:

yaml
transform:
  KEBABED:
    convert: { source: $X, toCase: kebabCase }
  PREFIXED:
    replace:
      source: $KEBABED
      replace: '^'
      by: 'css-'
fix: $PREFIXED

fix — auto-rewrite

String form

yaml
fix: logger.log($$$ARGS)

# Empty string deletes the match
fix: ""

FixConfig form (for list-item deletion that needs to expand the range)

When deleting one item from a comma-separated list, you also need to remove the trailing comma. Use expandEnd:

yaml
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 transform

Top-level field defining one or more named rewriters:

yaml
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 highlighting

yaml
rule:
  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-rule

yaml
files:
  - '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):

yaml
files:
  - pattern: 'src/**/*.ts'
    case_sensitive: true

See also