SuperAPI AI Directory

ast-grep-practice

General & other

View repository

Updated 10-15 21:02

About this skill

ast-grep Practice

Use ast-grep to supplement patterns that cannot be expressed with general-purpose lint tools (ESLint, oxlint, Biome, clippy, etc.). Always prefer reproducible static rules over natural language prompts.

Installation

# npm (プロジェクトローカル推奨)
npm install -D @ast-grep/cli
npx ast-grep --help

# または cargo
cargo install ast-grep --locked

# または brew
brew install ast-grep

Package manager selection: Project package.json に packageManager If is configured, follow it (pnpm / yarn, etc.). If not, install locally with npm. Align CI to the same tool as well (mixing them splits the lockfile / binary reference path). Limit global installs to dev machines only, and always use local references in CI and repo scripts.

Quick Start

Verify operation with a minimal configuration:

mkdir -p rules rule-tests
cat > sgconfig.yml << 'EOF'
ruleDirs:
  - rules
testConfigs:
  - testDir: rule-tests
EOF

cat > rules/no-console-log.yml << 'EOF'
id: no-console-log
language: TypeScript
severity: warning
rule:
  pattern: console.log($$$ARGS)
message: console.log を残さない。
fix: ''
EOF

cat > rule-tests/no-console-log-test.yml << 'EOF'
id: no-console-log
valid:
  - logger.info('ok')
invalid:
  - console.log('debug')
  - "console.log('a', 'b')"
EOF

ast-grep test --skip-snapshot-tests  # テスト実行
ast-grep scan src/                    # プロジェクトスキャン

Principles

  • First check whether existing linters can cover it
  • Use ast-grep when a "structural pattern" is needed
  • Develop rules with TDD: test first → implement rule → CI integration
  • fix If you can write , write it. Prefer rules with autofix over detection-only rules

How to use it alongside existing linters

Case Tool
unused import, no-console, naming convention ESLint / oxlint / Biome
type error, unreachable code TypeScript compiler / clippy
formatting Prettier / Biome / rustfmt
Prohibition of specific function call patterns ast-grep
Detection and automatic rewriting of deprecated APIs ast-grep (fix)
Prohibited patterns within specific contexts ast-grep (inside/has)
Project-specific structural constraints ast-grep

Signs you should use ast-grep:

  • It cannot be expressed by configuring existing rules alone
  • It depends on AST parent-child or sibling relationships
  • Automatic rewriting (migration) is needed

Project configuration

sgconfig.yml

ruleDirs:            # 必須: ルールファイルの格納先
  - rules
testConfigs:         # 任意: テスト設定
  - testDir: rule-tests
utilDirs:            # 任意: 共有ユーティリティルール
  - rule-utils
languageGlobs:       # 任意: 非標準拡張子のマッピング (TS/JS/Python 等は不要)
  html: ['*.vue', '*.svelte', '*.astro']

Directory structure

project/
  sgconfig.yml
  rules/
    no-direct-env-access.yml
    prefer-result-type.yml
  rule-tests/
    no-direct-env-access-test.yml
    prefer-result-type-test.yml
    __snapshots__/
  rule-utils/
    is-async-function.yml

ast-grep scan は sgconfig.yml Run all rules in from the location where exists. ruleDirs Run all rules in .

Rule file structure

id: no-direct-env-access
language: TypeScript
severity: warning
rule:
  pattern: process.env.$KEY
  not:
    inside:
      kind: function_declaration
      has:
        pattern: getEnv
      stopBy: end
message: process.env を直接参照しない。getEnv() を経由する。
note: 環境変数の型安全なアクセスを保証するため。
fix: getEnv('$KEY')
files:
  - "src/**"
ignores:
  - "src/config.ts"

Field

Field Required Description
id Yes Rule identifier
language Yes Target language
rule Yes Match condition
severity No hint, warning, error
message No One-line description
note No Detailed description / fix guide
fix No Autofix template
constraints No Additional constraints on metavariables
transform No Text transformation of metavariables
files No Target glob
ignores No Exclusion glob
url No Documentation URL

Suppression comments

// ast-grep-ignore
someCode()

// ast-grep-ignore: no-direct-env-access
process.env.NODE_ENV

Notes on metavariables

Pitfalls of pattern matching:

  • $OBJ.$PROP は Dot access only matches.obj['key'](bracket access) does not match
  • $VAR matches a single AST node
  • $$$VARS matches zero or more AST nodes (variadic arguments, multiple statements, etc.)
  • $_ is a wildcard (does not capture). It can match different content even with the same name
  • A metavariable must occupy an entire node: obj.on$EVENT や "hello $NAME" does not work

fix (autofix)

How to decide whether to add fix

fix is convenient, but since it is applied automatically when added, there is a risk of changing semantics. In the following cases, do not add do not add it(Detection only):

  • Rewriting changes type safety (e.g., as any → as unknown changes the type inference result)
  • Side effects or evaluation order may change (presence/absence of short-circuit evaluation, timing of exception occurrence)
  • Context-dependent, so the correct replacement cannot be uniquely determined (e.g., swapping argument order in API migration; review is mandatory)
  • For deletion-type changes, the deletion target is entangled with other expressions in the same statement

If in doubt, write a "manual migration procedure" in a note without a fix. Only add a fix when you are confident that "replacing all occurrences is safe."

Basics

rule:
  pattern: console.log($ARG)
fix: logger.info($ARG)

Metavariables can be used as-is in the fix template. Metavariables that did not match become empty strings.

Deletion

rule:
  pattern: console.log($$$ARGS)
fix: ''

fix: '' deletes the matched node. However, blank lines may remain.If you also want to remove the statement terminator ; or trailing commas, always use expandEnd together(see "Range expansion" below). If leaving blank lines is acceptable, expandEnd is unnecessary. Criterion: if the project runs a formatter (such as Prettier) after deletion, blank lines are cleaned up automatically, so expandEnd is unnecessary; if not, expandEnd is recommended.

any: fix when bundling multiple patterns with

any: , they may be merged into one rule: same fix template can be used in each branch under

rule:
  any:
    - pattern: $ARR.filter($P).length === 0
    - pattern: $ARR.filter($P).length == 0
fix: '!$ARR.some($P)'   # 両分岐で共通のメタ変数 + 同一テンプレート

If the fix differs per branch, always split the rules(any: per-branch fixes cannot be written inside). Example: === 0 → !some()、!== 0 → some() should be a separate rule. Splitting rules is acceptable even for the same purpose (it is clearer if you align the ids with *-empty / *-nonempty , etc.).

Multiple lines

rule:
  pattern: |
    def foo($X):
      $$$S
fix: |-
  def bar($X):
    $$$S

Indentation is preserved to match the position in the original code.

Range expansion (FixConfig)

When you want to delete including trailing commas, etc.:

fix:
  template: ''
  expandEnd:
    regex: ','

Simple rewriting via CLI

ast-grep run --pattern 'oldFunc($$$ARGS)' --rewrite 'newFunc($$$ARGS)' --lang typescript .
# --update-all で確認なしに一括適用

constraints

Add extra conditions to metavariables.$ARG only ($$$ARGS is not allowed). Filtered after rule matching.

How to use constraints vs. structural constraints (has/inside/not):

  • If you want to add a condition to the contents of a metavariable → constraints(e.g., $METHOD が get / set / delete either)
  • If you want to add a condition to the structure outside/inside the pattern → has / inside / not / precedes / follows(e.g., inside a specific parent element, having a specific child element)
  • If you can write a concrete literal in the pattern itself, that is the simplest (pattern: new Set($X) ensures the Set exists)

rule , it is pattern と has / not directly under AND-evaluated (matches the pattern AND has is true).pattern alone in this form.

rule:
  pattern: $OBJ.$METHOD($$$ARGS)
constraints:
  METHOD:
    regex: '^(get|set|delete)$'
  OBJ:
    kind: identifier

Available fields: kind, regex, pattern

Note: not constrained metavariables inside may not work as expected.

transform

After text-converting the matched metavariable, use it in fix .

replace (regex replacement)

transform:
  NEW_NAME:
    replace:
      source: $NAME
      replace: 'get(\w+)'
      by: 'fetch$1'
fix: $NEW_NAME($$$ARGS)

substring (substring)

transform:
  INNER:
    substring:
      source: $STR
      startChar: 1
      endChar: -1

Negative indices count from the end. Same as Python slicing.

convert (case conversion)

transform:
  SNAKE:
    convert:
      source: $NAME
      toCase: snakeCase
      separatedBy: [caseChange]

Supported cases: camelCase, snakeCase, kebabCase, pascalCase, upperCase, lowerCase, capitalize

rewrite (experimental)

Recursively rewrite nodes within metavariables using rewriter rules.

transform:
  REWRITTEN:
    rewrite:
      source: $$$BODY
      rewriters: [migrate-api-call]
      joinBy: "\n"

utils (utility rules)

utilDirs Reference the common rules defined in matches with

# rule-utils/is-async-function.yml
id: is-async-function
language: TypeScript
rule:
  any:
    - kind: function_declaration
      has:
        field: async
        regex: async
    - kind: arrow_function
      has:
        field: async
        regex: async
# rules/async-no-try-catch.yml
id: async-no-try-catch
language: TypeScript
rule:
  all:
    - matches: is-async-function
    - has:
        pattern: await $EXPR
        stopBy: end
    - not:
        has:
          kind: try_statement
          stopBy: end
message: async 関数に try-catch がない。
severity: warning

Test

There are two kinds of tests. Don't confuse them:

  • Classification tests (test --skip-snapshot-tests): valid / invalid Only checks whether the code listed in is classified correctly. This is the one to run in CI.
  • Snapshot testing (test / test -U): Fixes match positions for invalid code and fix application results as snapshots to detect regressions. Generate initially with -U , then human review thereafter. Run once before CI.

Test file format

Within the test file, id is in the rule file id must match. The filename is arbitrary (convention is {rule-id}-test.yml)。

# rule-tests/no-direct-env-access-test.yml
id: no-direct-env-access
valid:
  - getEnv('NODE_ENV')
  - "function setup() { return getEnv('PORT') }"
invalid:
  - process.env.NODE_ENV
  - process.env.PORT

Running tests

# 分類テスト (valid/invalid が正しいか)
ast-grep test --skip-snapshot-tests

# スナップショット生成・更新
ast-grep test -U

# スナップショット対話的レビュー
ast-grep test --interactive

Test results:

  • . : Path
  • N : Noisy (false positive — matches valid code)
  • M : Missing (false negative — does not match invalid code)

Workflow

  1. rule-tests/ Write a test file in (Red)
  2. rules/ Write the rule in (Green)
  3. ast-grep test --skip-snapshot-tests Verify with
  4. ast-grep test -U Generate snapshots with
  5. Review snapshot and commit

CI integration

justfile

ast-grep-test:
  ast-grep test

ast-grep-lint:
  ast-grep scan

check: format-check typecheck ast-grep-lint test

GitHub Actions

Match the dev environment and tools (if the project uses pnpm, use pnpm in CI too; if npm, use npm):

- uses: actions/setup-node@v4
  with: { node-version: 24, cache: npm }   # pnpm プロジェクトなら pnpm/action-setup@v4 + cache: pnpm

- run: npm ci   # pnpm なら pnpm install --frozen-lockfile

- name: ast-grep rule tests
  run: npx ast-grep test --skip-snapshot-tests

- name: ast-grep scan
  run: npx ast-grep scan --error

severity and exit codes:

  • ast-grep scan defaults to error Exits non-zero if there is even one severity
  • --error Adding makes it exit non-zero even for warning / hint (useful when you want warnings to fail CI too)
  • --error=error You can also specify severity like to tighten gradually
  • --format json for structured output (for integration with other tools)

How to find kind names

Kind names depend on the language's Tree-sitter grammar.

# AST ダンプ(名前付きノードのみ、ルール記述に使う)
ast-grep run --pattern 'YOUR_CODE' --lang typescript --debug-query=ast

# CST ダンプ(全ノード、anonymous トークン含む)
ast-grep run --pattern 'YOUR_CODE' --lang typescript --debug-query=cst

For a catalog of common kinds by language, see references/kind-catalog.md (covers TypeScript / Rust / Go / Python).

Practical rule examples

TypeScript: as any Banning casts (detection only, no fix)

id: no-as-any
language: TypeScript
severity: error
rule:
  pattern: $EXPR as any
message: as any は型システムを無効化する。as unknown 経由か型ガードを使う。
note: |
  自動 fix を付けない理由: `as any` → `as unknown` への機械置換は型推論結果が
  変わるため、呼び出し側で新たなコンパイルエラーを生む。検出のみにして手動移行。

as any When matching a "type assertion" like$EXPR as any が as_expression it works as a node.$EXPR matches the entire left-hand side, soJSON.parse(raw) as any also matches (value as any) also matches.

TypeScript: rewriting deprecated APIs

id: migrate-old-api
language: TypeScript
severity: error
rule:
  pattern: oldClient.fetch($URL, $OPTS)
fix: newClient.request($URL, $OPTS)
message: oldClient.fetch は廃止。newClient.request に移行する。

Banning specific imports

id: no-lodash-import
language: TypeScript
severity: warning
rule:
  pattern: import $_ from 'lodash'
message: lodash の全体 import を禁止。lodash/xxx を使う。
fix: import $_ from 'lodash/xxx' // TODO: 正しいパスに修正

TypeScript: banning direct fetch inside React components

id: no-fetch-in-component
language: TypeScript
severity: warning
rule:
  pattern: fetch($$$ARGS)
  inside:
    any:
      - kind: function_declaration
        has:
          field: return_type
          pattern: JSX.Element
      - kind: arrow_function
        inside:
          kind: variable_declarator
          regex: '^[A-Z]'
    stopBy: end
message: コンポーネント内で直接 fetch しない。hooks か server action を使う。

Rust: banning unwrap()

id: no-unwrap
language: Rust
severity: warning
rule:
  pattern: $EXPR.unwrap()
  not:
    inside:
      kind: function_item
      regex: '#\[test\]'
      stopBy: end
message: テスト以外で unwrap() を使わない。? か expect() を使う。
note: unwrap() は panic するため、本番コードでは避ける。

Rust: detecting unsafe blocks

id: flag-unsafe-block
language: Rust
severity: warning
rule:
  kind: unsafe_block
message: unsafe ブロック。安全性の根拠をコメントで示す。

Rust: migrating println! to the log macro

id: no-println-in-lib
language: Rust
severity: warning
rule:
  pattern: println!($$$ARGS)
  not:
    inside:
      kind: function_item
      regex: 'fn main'
      stopBy: end
message: ライブラリコードで println! を使わない。log::info! 等を使う。
fix: log::info!($$$ARGS)
files:
  - "src/lib.rs"
  - "src/**/mod.rs"
  - "src/**/*.rs"
ignores:
  - "src/main.rs"
  - "src/bin/**"

Go: detecting ignored errors

id: no-ignored-error
language: Go
severity: error
rule:
  kind: short_var_declaration
  has:
    kind: identifier
    regex: '^_$'
    field: left
  has:
    kind: call_expression
    field: right
    stopBy: end
message: エラーを _ で無視しない。適切にハンドリングする。

Go: preventing forgetting to Close with defer

id: defer-close-after-open
language: Go
severity: warning
rule:
  kind: short_var_declaration
  has:
    pattern: os.Open($PATH)
    field: right
    stopBy: end
  not:
    precedes:
      pattern: defer $_.Close()
      stopBy:
        kind: return_statement
message: os.Open の直後に defer Close() を入れる。

Python: banning bare except

id: no-bare-except
language: Python
severity: warning
rule:
  kind: except_clause
  not:
    has:
      kind: identifier
      stopBy: neighbor
message: bare except を使わない。具体的な例外型を指定する。

Python: migrating print() to a logger

id: no-print-in-src
language: Python
severity: warning
rule:
  pattern: print($$$ARGS)
  not:
    inside:
      kind: function_definition
      regex: 'def main'
      stopBy: end
message: print() ではなく logger を使う。
fix: logger.info($$$ARGS)
files:
  - "src/**"

References

Details within this skill

  • references/rule-yaml.md — All rule YAML fields, evaluation order, metavariable binding scope,any: + fix merging/splitting,$$$ARGS empty matches, etc.
  • references/testing.md — Classification tests vs snapshots, multi-line code notation, snapshot operation
  • references/cli.md — Subcommands and flags in general,--error / exit code / --format json
  • references/kind-catalog.md — kind catalog by language (TypeScript / Rust / Go / Python)

Official