🔍

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)を表し、リスト内のすべてのサブルールがマッチする場合にのみノードをマッチさせます。このルールでは、以下のすべての条件を満たすノードをマッチさせます。

  1. kind: arrow_function

    • kindTree-sitter の AST ノード種類名でマッチングします
    • ここではアロー関数を対象とします
  2. has: pattern: await $EXPR + stopBy: end

    • has は子孫ノードとのマッチングで、子孫ノードがマッチする場合にノードをマッチさせます
    • pattern はコードパターンに基づいて単一のASTノードをマッチさせます
    • ここでは関数内に await を含む($EXPR は任意の式にマッチ)ことを表します
    • stopBy: end は検索の停止条件で、デフォルトneighborだとノードの1レベル下までしかマッチしませんが、end により関数の終端まで検索します
  3. 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. tech blog

Discussion