ast-grep-practice について
ast-grep-practiceは、一般的なリンターで表現できない構造的なコードルールをast-grepで作るためのスキルです。ルールのテスト、修正の安全性、CIへの組み込み方も扱います。
主な機能
- 01ast-grepの適用判断
- 02構造ルールの設定
- 03テスト先行のルール開発
- 04安全な自動修正
- 05CIスキャンとテスト
活用シーン
- 01プロジェクト固有のASTパターン検査
- 02非推奨APIの検出と移行
- 03CI前のカスタムルール検証
- 04CIへの構造チェック追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mizchi/ skills --skill ast-grep-practiceインストール後、Claude Code で/ast-grep-practiceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdSKILL-ja.mdreferences/cli.mdkind-catalog.mdrule-yaml.mdtesting.md詳細
ast-grep の実践
汎用リンターツール(ESLint、oxlint、Biome、clippy など)では表現できないパターンを、ast-grep で補完します。自然言語のプロンプトより、再現可能な静的ルールを常に優先してください。
インストール
# npm (project-local recommended)
npm install -D @ast-grep/cli
npx ast-grep --help
# or cargo
cargo install ast-grep --locked
# or brew
brew install ast-grep
パッケージマネージャーの選択:プロジェクトの package.json に packageManager が設定されている場合は、それに従ってください(pnpm / yarn など)。設定されていない場合は、npm を使ってローカルにインストールしてください。CI でも同じツールを使用してください(ツールを混在させると、ロックファイルとバイナリの解決パスが分かれてしまいます)。グローバルインストールは開発マシンに限り、CI とリポジトリ内のスクリプトでは必ずローカルの参照を使用してください。
クイックスタート
動作確認に必要な最小限の設定:
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: Do not leave console.log behind.
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 # run tests
ast-grep scan src/ # scan project
原則
- まず、既存のリンターでルールに対応できるか確認します
- 「構造的なパターン」が必要な場合は ast-grep を使用します
- TDD でルールを開発します:テストを先に作成 → ルールを実装 → CI に統合
- 可能であれば
fixを記述します。検出のみのルールより、自動修正できるルールを優先してください
ast-grep と既存のリンターの選び方
| ケース | ツール |
|---|---|
| 未使用のインポート、no-console、命名規則 | ESLint / oxlint / Biome |
| 型エラー、到達不能なコード | TypeScript コンパイラー / clippy |
| フォーマット | Prettier / Biome / rustfmt |
| 特定の関数呼び出しパターンの禁止 | ast-grep |
| 非推奨 API の検出と書き換え | ast-grep (fix) |
| 特定のコンテキスト内にある禁止パターン | ast-grep (inside/has) |
| プロジェクト固有の構造上の制約 | ast-grep |
ast-grep が適しているケース:
- 既存ルールの設定では表現できない
- 親子や兄弟の AST 関係に依存する
- 自動書き換え(移行)が必要
プロジェクトの設定
sgconfig.yml
ruleDirs: # required: directories holding rule files
- rules
testConfigs: # optional: test configuration
- testDir: rule-tests
utilDirs: # optional: shared utility rules
- rule-utils
languageGlobs: # optional: mapping for non-standard extensions (unnecessary for TS/JS/Python etc.)
html: ['*.vue', '*.svelte', '*.astro']
ディレクトリ構成
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 を含むディレクトリを起点として、ruleDirs 内のすべてのルールを実行します。
ルールファイルの構造
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: Do not reference process.env directly. Go through getEnv().
note: Ensures type-safe access to environment variables.
fix: getEnv('$KEY')
files:
- "src/**"
ignores:
- "src/config.ts"
フィールド
| フィールド | 必須 | 説明 |
|---|---|---|
id | はい | ルール識別子 |
language | はい | 対象言語 |
rule | はい | マッチ条件 |
severity | いいえ | hint、warning、error |
message | いいえ | 1 行の説明 |
note | いいえ | 詳細な説明/移行ガイド |
fix | いいえ | 自動修正テンプレート |
constraints | いいえ | メタ変数に対する追加の制約 |
transform | いいえ | メタ変数のテキスト変換 |
files | いいえ | 対象の glob |
ignores | いいえ | 除外する glob |
url | いいえ | ドキュメントの URL |
抑制コメント
// ast-grep-ignore
someCode()
// ast-grep-ignore: no-direct-env-access
process.env.NODE_ENV
メタ変数の落とし穴
パターンマッチングに関する注意点:
$OBJ.$PROPは ドットアクセスのみ にマッチします。obj['key'](ブラケットアクセス)にはマッチしません$VARは AST ノード 1 つにのみマッチします$$$VARSは AST ノード 0 個以上にマッチします(可変長引数、複数のステートメントなど)$_はワイルドカードです(キャプチャしません)。同じ名前でも異なる内容にマッチすることがあります- メタ変数はノード全体を占める必要があります:
obj.on$EVENTと"hello $NAME"は機能しません
fix(自動修正)
修正を付けるかどうかの判断
fix は便利ですが、自動的に適用されるため、意味を変えてしまう可能性があります。次の場合は修正を付けず(検出のみ)、対応してください。
- 書き換えによって型安全性が変わる場合(例:
as any→as unknownは型推論の結果を変えます) - 副作用や評価順序が変わる可能性がある場合(短絡評価の動作、例外が発生するタイミング)
- コンテキストに依存するため、正しい置き換えが一意に定まらない場合(例:引数の順序を入れ替える API 移行。レビューが必須です)
- 同じステートメント内の他の式と絡み合った削除になる場合
迷った場合は fix を使わず、手動で移行する手順を note に記載してください。「すべて置き換えても安全」と確信できる場合に限り、fix を付けてください。
基本
rule:
pattern: console.log($ARG)
fix: logger.info($ARG)
fix テンプレート内では、メタ変数をそのまま使用できます。マッチしなかったメタ変数は空文字列になります。
削除
rule:
pattern: console.log($$$ARGS)
fix: ''
fix: '' は一致したノードを削除します。空行が残る場合があることに注意してください。文末の ; や末尾のカンマも削除したい場合は、必ず expandEnd と組み合わせてください(下記の「範囲の拡張」を参照)。空行が残っても問題ない場合は、expandEnd は不要です。目安として、後続でフォーマッター(Prettier など)が実行されるプロジェクトでは、空行は自動的に整理されるため expandEnd は不要です。フォーマッターが実行されない場合は、expandEnd をおすすめします。
any: で複数のパターンをまとめる場合の修正
any: の各分岐で同じ修正テンプレートを使える場合は、1つのルールにまとめて問題ありません。
rule:
any:
- pattern: $ARR.filter($P).length === 0
- pattern: $ARR.filter($P).length == 0
fix: '!$ARR.some($P)' # metavariables shared across both branches + identical template
分岐ごとに修正内容が異なる場合は、必ず別々のルールに分けてください(any: 内で分岐ごとに修正内容を指定することはできません)。たとえば、=== 0 → !some() と !== 0 → some() は分ける必要があります。目的が同じ場合でも、分割してかまいません(IDを *-empty / *-nonempty のように揃えると読みやすくなります)。
複数行
rule:
pattern: |
def foo($X):
$$$S
fix: |-
def bar($X):
$$$S
インデントは元のコード内の位置を基準に維持されます。
範囲の拡張(FixConfig)
削除時に末尾のカンマなども含めたい場合:
fix:
template: ''
expandEnd:
regex: ','
CLI からのクイックな書き換え
ast-grep run --pattern 'oldFunc($$$ARGS)' --rewrite 'newFunc($$$ARGS)' --lang typescript .
# --update-all applies everywhere without confirmation
制約
メタ変数に追加の条件を指定します。サポートされているのは $ARG のみです($$$ARGS はサポートされていません)。ルールが一致した後に、一致結果を絞り込みます。
制約と構造制約(has/inside/not)の使い分け:
- メタ変数の内容を制約したい場合 →
constraints(例:$METHODがget/set/deleteのいずれかである) - パターンの外側または内側の構造を制約したい場合 →
has/inside/not/precedes/follows(例:特定の親の内側にある、または特定の子を持つ) - 具体的なリテラルをパターンに直接書ける場合は、それが最もシンプルです(
pattern: new Set($X)とすれば Set があることを保証できます)
rule 直下に pattern と has / not を並べて指定すると、AND として評価されます(パターンに一致し、かつ has が true)。pattern だけでは表現できない構造制約を追加するには、この形式を使います。
rule:
pattern: $OBJ.$METHOD($$$ARGS)
constraints:
METHOD:
regex: '^(get|set|delete)$'
OBJ:
kind: identifier
使用可能なフィールド:kind、regex、pattern
注:not 内の制約付きメタ変数は、期待どおりに動作しない場合があります。
transform
fix で使う前に、一致したメタ変数をテキストとして変換します。
replace(正規表現による置換)
transform:
NEW_NAME:
replace:
source: $NAME
replace: 'get(\w+)'
by: 'fetch$1'
fix: $NEW_NAME($$$ARGS)
substring
transform:
INNER:
substring:
source: $STR
startChar: 1
endChar: -1
負のインデックスは末尾から数えます。Python のスライスと同じセマンティクスです。
convert(大文字・小文字の変換)
transform:
SNAKE:
convert:
source: $NAME
toCase: snakeCase
separatedBy: [caseChange]
サポートされているケース:camelCase、snakeCase、kebabCase、pascalCase、upperCase、lowerCase、capitalize
rewrite(実験的)
リライタールールを使って、メタ変数内のノードを再帰的に書き換えます。
transform:
REWRITTEN:
rewrite:
source: $$$BODY
rewriters: [migrate-api-call]
joinBy: "\n"
utils(ユーティリティルール)
matches を使って、utilDirs の下で定義された共有ルールを参照します。
# 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 function lacks try-catch.
severity: warning
テスト
テストには2種類あります。混同しないでください。
- 分類テスト(
test --skip-snapshot-tests):valid/invalidの下に記載されたコードが正しく分類されるかだけを検証します。CI で実行するのはこちらです。 - スナップショットテスト(
test/test -U):無効なコードに対する一致位置と修正結果をスナップショットとして固定し、リグレッションを検出します。初回は-Uを使って生成し、その後、人がレビューしてください。CI の前に少なくとも一度実行してください。
テストファイルの形式
テストファイル内の id は、ルールファイルの id と一致させる必要があります。ファイル名は任意です(慣例:{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
テストの実行
# classification test (is valid/invalid correct?)
ast-grep test --skip-snapshot-tests
# generate / update snapshots
ast-grep test -U
# interactive snapshot review
ast-grep test --interactive
テスト結果のマーカー:
.:成功N:ノイジー(一致するべきでないコードに一致)M:未検出(無効なコードに一致しない)
ワークフロー
rule-tests/の下にテストファイルを書く(Red)rules/の下にルールを書く(Green)ast-grep test --skip-snapshot-testsで検証するast-grep test -Uでスナップショットを生成する- スナップショットをレビューしてコミットする
CI との連携
justfile
ast-grep-test:
ast-grep test
ast-grep-lint:
ast-grep scan
check: format-check typecheck ast-grep-lint test
GitHub Actions
ツールを開発環境に合わせます(プロジェクトで pnpm を使用している場合は CI でも pnpm を、npm を使用している場合は npm を使います)。
- uses: actions/setup-node@v7
with: { node-version: 24, cache: npm } # for pnpm projects: pnpm/action-setup@v6 + cache: pnpm
- run: npm ci # for 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
重大度と終了コード:
ast-grep scanは、error重大度の検出結果が少なくとも 1 件ある場合、デフォルトで 0 以外の値で終了します--errorを渡すと、warning/hintも 0 以外の値で終了する原因になります(CI で警告も失敗とする場合に使用します)--error=errorのように重大度を指定して、段階的に厳格化できます- 他のツールとの連携には、構造化された出力を得られる
--format jsonを使います
Kind 名の調べ方
Kind 名は、言語の Tree-sitter 文法によって異なります。
# AST dump (named nodes only, use these when writing rules)
ast-grep run --pattern 'YOUR_CODE' --lang typescript --debug-query=ast
# CST dump (all nodes, including anonymous tokens)
ast-grep run --pattern 'YOUR_CODE' --lang typescript --debug-query=cst
よく使われる kind の言語別カタログについては、references/kind-catalog.md を参照してください(TypeScript/Rust/Go/Python に対応しています)。
実用的なルールの例
TypeScript:as any キャストを禁止する(検出のみ、修正なし)
id: no-as-any
language: TypeScript
severity: error
rule:
pattern: $EXPR as any
message: as any disables the type system. Go through as unknown or a type guard.
note: |
Why no auto-fix: mechanically replacing `as any` → `as unknown` changes type
inference results and introduces new compile errors at call sites. Detection-only,
migrate manually.
as any のような型アサーションにマッチさせる場合、$EXPR as any は as_expression ノードで機能します。$EXPR は左辺全体にマッチするため、JSON.parse(raw) as any と (value as any) の両方にマッチします。
TypeScript:非推奨 API を書き換える
id: migrate-old-api
language: TypeScript
severity: error
rule:
pattern: oldClient.fetch($URL, $OPTS)
fix: newClient.request($URL, $OPTS)
message: oldClient.fetch is deprecated. Migrate to newClient.request.
特定の import を禁止する
id: no-lodash-import
language: TypeScript
severity: warning
rule:
pattern: import $_ from 'lodash'
message: Do not import lodash wholesale. Use lodash/xxx.
fix: import $_ from 'lodash/xxx' // TODO: fix the correct path
TypeScript:React コンポーネント内での直接的な fetch を禁止する
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: Do not fetch directly inside a component. Use hooks or a server action.
Rust:unwrap() を禁止する
id: no-unwrap
language: Rust
severity: warning
rule:
pattern: $EXPR.unwrap()
not:
inside:
kind: function_item
regex: '#\[test\]'
stopBy: end
message: Do not use unwrap() outside tests. Use ? or expect().
note: unwrap() panics, so avoid it in production code.
Rust:unsafe ブロックを検出する
id: flag-unsafe-block
language: Rust
severity: warning
rule:
kind: unsafe_block
message: unsafe block. Explain the safety rationale in a comment.
Rust:println! を log マクロに移行する
id: no-println-in-lib
language: Rust
severity: warning
rule:
pattern: println!($$$ARGS)
not:
inside:
kind: function_item
regex: 'fn main'
stopBy: end
message: Do not use println! in library code. Use log::info! etc.
fix: log::info!($$$ARGS)
files:
- "src/lib.rs"
- "src/**/mod.rs"
- "src/**/*.rs"
ignores:
- "src/main.rs"
- "src/bin/**"
Go:無視されたエラーを検出する
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: Do not ignore errors with _. Handle them appropriately.
Go:defer Close() の記述漏れを防ぐ
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: Add defer Close() immediately after os.Open.
Python:bare except を禁止する
id: no-bare-except
language: Python
severity: warning
rule:
kind: except_clause
not:
has:
kind: identifier
stopBy: neighbor
message: Do not use bare except. Specify the exception type.
Python:print() をロガーに移行する
id: no-print-in-src
language: Python
severity: warning
rule:
pattern: print($$$ARGS)
not:
inside:
kind: function_definition
regex: 'def main'
stopBy: end
message: Use logger instead of print().
fix: logger.info($$$ARGS)
files:
- "src/**"
参考資料
スキル内の詳細
- references/rule-yaml.md — すべてのルール YAML フィールド、評価順序、メタ変数のバインド範囲、
any:と修正の統合/分割、$$$ARGSの空マッチなど - references/testing.md — 分類テストとスナップショットテスト、複数行コードの表記、スナップショット操作
- references/cli.md — サブコマンドとフラグの概要、
--error/終了コード/--format json - references/kind-catalog.md — kind の言語別カタログ(TypeScript/Rust/Go/Python)
公式
- ast-grep ドキュメント:https://ast-grep.github.io/
- ルールリファレンス:https://ast-grep.github.io/reference/yaml.html
- sgconfig:https://ast-grep.github.io/reference/sgconfig.html
- プレイグラウンド:https://ast-grep.github.io/playground.html
- ルールカタログ:https://ast-grep.github.io/catalog/