mcpField
A
ast-grep-practice

ast-grep-practice

github.com/mizchiテスト・品質41664 回インストール1 週間前に更新
リポジトリ

ast-grep-practice について

ast-grep-practiceは、一般的なリンターで表現できない構造的なコードルールをast-grepで作るためのスキルです。ルールのテスト、修正の安全性、CIへの組み込み方も扱います。

主な機能

  1. 01ast-grepの適用判断
  2. 02構造ルールの設定
  3. 03テスト先行のルール開発
  4. 04安全な自動修正
  5. 05CIスキャンとテスト

活用シーン

  1. 01プロジェクト固有のASTパターン検査
  2. 02非推奨APIの検出と移行
  3. 03CI前のカスタムルール検証
  4. 04CIへの構造チェック追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mizchi/skills --skill ast-grep-practice

インストール後、Claude Code で/ast-grep-practiceを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
SKILL-ja.md
references/
cli.md
kind-catalog.md
rule-yaml.md
testing.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 :未検出(無効なコードに一致しない)
ワークフロー
  1. rule-tests/ の下にテストファイルを書く(Red)
  2. rules/ の下にルールを書く(Green)
  3. ast-grep test --skip-snapshot-tests で検証する
  4. ast-grep test -U でスナップショットを生成する
  5. スナップショットをレビューしてコミットする

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)
公式

コメント