ast-grep/claude-skill で Claude Code に AST ベースの検索を導入する
はじめに
grep では「try-catch がない async 関数」のような構造的な条件を表現できませんが、ast-grep/claude-skill を導入すると Claude が YAML ルールを生成し、1回のコマンドで条件に合致するコードを抽出できます。
こんな時におすすめです:
- 「〇〇がない関数」のような否定条件で検索したい
- 関数やクラスなどコードの構造単位で検索したい
- grep では表現が難しい複合条件で検索したい
本記事では、ast-grep/claude-skill を導入して検証した結果を紹介します。
前提条件
- ast-grep がインストールされていること
- Claude Code が使えること
導入方法
以下の手順で導入します。
https://github.com/ast-grep/claude-skill?tab=readme-ov-file#prerequisites
導入後、Claude Code がスキルを自動的に検出します。
検証:「try-catch がない async 関数を検索」
この検索条件を使い、従来の grep ベースの検索と ast-grep スキルを使った検索を比較しました。
try-catch がない async 関数の例と期待する結果を示します。
// マッチする(try-catch なし)
const fetchData = async () => {
const res = await fetch("/api/data");
return res.json();
};
// マッチしない(try-catch あり)
const fetchDataSafe = async () => {
try {
const res = await fetch("/api/data");
return res.json();
} catch (e) {
return null;
}
};
従来の方法(grep)
まず、ast-grep スキルを使わない場合の検索の流れを確認します。
「try-catch がない async 関数を検索して」という指示で、Claude Code は以下の流れで処理しました。
grep の課題
grep は「パターンに一致する行」を返すツールです。そのため:
- 「async 関数」と「その関数内の try-catch」という構造的な関係性を表現できない
- 関数の開始から終了までを 1 単位として扱えない
その結果、Claude Code は複数回の検索とファイル読み込みを繰り返し、try-catch の有無を自身で判断する必要がありました。
ast-grep スキルを使った方法
次に、同じ指示で ast-grep スキルを使った場合の動作を確認します。
スキルとして公開されているのは、ast-grep の YAML ルール作成ガイドです。スキルには以下が含まれています:
- YAML ルールの構文と書き方
- CLI コマンドの使用例
-
stopBy: endなどの重要なオプションの説明
Claude はこのガイドを参照し、ユーザーの検索条件を YAML ルールに変換し、ast-grep コマンドを生成します。
検索の流れ
Claude がスキルを参照し、以下の YAML ルールを生成しました:
id: async-arrow-no-trycatch
language: typescript
rule:
all:
- kind: arrow_function
- has:
pattern: await $EXPR
stopBy: end
- not:
has:
kind: try_statement
stopBy: end
このルールは ast-grep scan --inline-rules コマンドで使用されます。
このルールの意味:
all: ルールの論理積(AND)を表し、リスト内のすべてのサブルールがマッチする場合にのみノードをマッチさせます。このルールでは、以下のすべての条件を満たすノードをマッチさせます。
-
kind: arrow_function-
kindは Tree-sitter の AST ノード種類名でマッチングします - ここではアロー関数を対象とします
-
-
has: pattern: await $EXPR+stopBy: end-
hasは子孫ノードとのマッチングで、子孫ノードがマッチする場合にノードをマッチさせます -
patternはコードパターンに基づいて単一のASTノードをマッチさせます - ここでは関数内に
awaitを含む($EXPRは任意の式にマッチ)ことを表します -
stopBy: endは検索の停止条件で、デフォルトneighborだとノードの1レベル下までしかマッチしませんが、endにより関数の終端まで検索します
-
-
not: has: kind: try_statement+stopBy: end-
notはルールの否定で、単一のサブルールがマッチしない場合にノードをマッチさせます - ここでは関数内に try 文がないことを表します
- こちらも
stopBy: endにより関数の終端まで検索します
-
ast-grep を使った場合の流れを以下に図示します。
ast-grep の利点
grep では難しかった構造的な検索を、ast-grep は直接表現できます:
- 「async 関数」を 1 つの単位として認識できる
- 「その中に try-catch がない」という否定条件を直接表現できる
- 1 回のコマンドで条件に合致する関数を抽出できる
Claude Code がファイルを読み込んで判断する必要がなくなりました。
ルールの検証と拡張
生成されたルールの検証
ast-grep スキルを使う場合、検索に使用するルールは Claude が生成します。ユーザーが「async 関数を検索して」と指示した場合、Claude は適切なルールを生成しますが、AST node の kind の選択(function_declaration なのか arrow_function なのか、あるいはすべてなのか)は文脈によって判断されます。
今回の検証では、Claude は kind: arrow_function のみを指定したルールを生成しました。しかし「async 関数」には複数の形式があります:
| kind | 構文例 | 補足 |
|---|---|---|
function_declaration |
async function foo() {} |
名前付き関数宣言 |
arrow_function |
const foo = async () => {} |
アロー関数(変数への代入は別ノード) |
method_definition |
class C { async foo() {} } や { async foo() {} }
|
クラスメソッドやオブジェクトメソッド |
function_expression |
const foo = async function() {} |
匿名または名前付き関数式 |
すべての形式を網羅的に検索するには、以下のようにany で複数の関数形式を指定し、all で共通の条件を適用する必要があります。
id: async-function-no-trycatch
language: typescript
rule:
any:
- kind: arrow_function
- kind: function_declaration
- kind: function_expression
- kind: method_definition
all:
- has:
pattern: await $EXPR
stopBy: end
- not:
has:
kind: try_statement
stopBy: end
ast-grep は指定した構造に正確にマッチします。 この正確性により、意図した検索条件を確実に反映できます。ルールは Claude が生成するため、より厳密な検索を行うには、生成されたルールを確認することや、より詳細な仕様を Claude に渡すことが大切です。
まとめ
今回は ast-grep/claude-skill を紹介しました。
構造的なコード検索を行う機会がある方はぜひ試してみてください。
より高度な機能(ルールのテストやデバッグなど)を求める場合は、MCP Server もあるようなのでそちらも試してみると良さそうです。
関連リンク
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion