抽象構文木で検索するast-grepの使い方 ast-grep検索の基本

従来のgrepとは異なり「コードの構造」を理解して検索・置換を行うast-grepの基本的な使い方を紹介します。

発行

著者 杉浦 有右嗣 フロントエンド・エンジニア
抽象構文木で検索するast-grepの使い方 シリーズの記事一覧

はじめに

ast-grepは、コードの構造を理解して検索・置換などを行えるCLIツールです。

従来のgrepが文字列の部分一致で検索するのに対して、ast-grepはAST(Abstract Syntax Tree)を使った意味論的な検索ができるようになっています。

ASTは、人間が書いたコードをトークン化して構造化したもので、コードの意味論的なエッセンスを表現します。これによって、コメント内の文字列と実際のコードを区別したり、関数の定義と呼び出しを区別したりできます。改行やスペースなど、コードの意味を変えないものも無視できます。

ちょっと抽象的な説明でわかりにくいかもしれませんが、後述する実際の使い方をみると腑に落ちると思います。

ast-grepは実行可能なバイナリが配布されていて、簡単にインストールできます。

インストール方法

# homebrewを使う場合
brew install ast-grep

# npmを使う場合
npm i @ast-grep/cli -g

インストールするとast-grepコマンドが使えるようになり、エイリアスとして短縮形のsgコマンドも利用できるようになります。

従来のgrepとの違い

なぜast-grepが便利なのかを理解するために、まずは従来のgrepとの違いを見てみましょう。

従来のgrepコマンドやその派生コマンド(agrgなども)は、文字列を「部分一致」で検索するツールです。

たとえば、このようなコードがあるとします。

t.js

const getName = () => {
  // ...
};

// Use `getName` internally
const getNameFull = () => {
  const name = getName();

  // ...
};

上記のコードではgetNameという文字列が繰り返し現れますが、それぞれ意味が異なります。

  • getName関数の定義
  • コメント中で言及されたgetName関数
  • getNameFull関数の接頭辞として
  • 別の関数の中でのgetName関数の呼び出し

このコードに対して、grep 'getName' t.jsで検索すると、こうなります。

実行結果

const getName = () => {
// Use `getName` internally
const getNameFull = () => {
  const name = getName();

結果としては、関数定義・コメント・関数呼び出しなど、すべてが部分一致で検索されてしまいます。

この挙動が便利な場面もありますが、純粋に定義場所を探したい場合などでは、意図しない結果が含まれている、ということになってしまいます。

rgなどのコマンドでは、正規表現を使うことも可能なため、ある程度までは対応できますが、それでも完璧ではありませんし、何より記述が煩雑になります。

補足:rgコマンド

Rustで書かれた検索ツール「ripgrep」のコマンドです。

ast-grepを使った検索

ast-grepの検索では、-pまたは--patternで、探したいパターンを指定します。

では、同じコードに対してsg -p 'getName' t.jsを実行してみます。

実行結果

t.js
1│const getName = () => {
7│  const name = getName();

コメント内の文字列や、完全に一致しない関数名は除外され、getName部分のみがマッチしているのがわかります。これが、構文ベースのパターンマッチです。ちなみに、コメントは構文木に含まれないので、検索対象にはなりません。

さらに、関数呼び出しのみを検索したい場合は、sg -p 'getName()' t.jsとします。

実行結果

t.js
7│  const name = getName();

このように、関数定義ではなく関数呼び出しのみも抽出できました。

上記の例では検索対象にt.jsのようにファイル名を指定していますが、特定のディレクトリ以下のファイル群を対象にしたい場合は、以下のようにディレクトリ名を指定します。このあたりの使い方はgrepと同様です。

ディレクトリを対象に検索

sg -p 'getName()' src

メタ変数を使う

ast-grepのパターンでは、$A$Bなどのメタ変数を使ってより発展した検索パターンを作成できます。

たとえば、こういうコードがあったとします。

t.js

function tryAstGrep() {
  console.log  ('Hello World')
}

const multiLineExpression = console
  .log('Also matched!')

// console.log(123) in comment
("console.log(123) in string"); // string
console.log();
console.log(a, b);

ここから、console.log("hey")のように、引数を1つだけ取っているものも探したいとします。

sg -p 'console.log($ARG)' t.jsとして検索します。なお、ここでは例として、メタ変数の名前を$ARGとしていますが、これは予約語ではないので$Aなどでもかまいません。

検索すると、このような結果になります。

実行結果

t.js
2│  console.log  ('Hello World')
5│const multiLineExpression = console
6│  .log('Also matched!')

console.log();は引数がないのでマッチせず、console.log(a, b);は引数が2つなのでマッチしません。また、先ほど確認したようにコメントもマッチしませんし、単なる文字列もマッチしません。

一方、console.log<ここにスペース>('Hello World')const multiLineExpression = console<ここに改行>.log("Also matched!");にはマッチします。

ASTとしては、スペースも改行も意味を持たないため、適切に検索ができるということがわかったでしょうか。

メタ変数はいくつ使ってもよいので、たとえばsg -p 'console.log($ARG1, $ARG2)'のように、任意の引数の数だけ指定して検索することもできます。

また、$$$という特別なメタ変数を使って、sg -p 'console.log($$$)' t.jsとして実行すると、引数の数に関わらず、すべてのconsole.log関数の呼び出しにマッチさせることもできます。sg -p 'console.log()'だと、明示的に引数が存在しないものだけを検索します。

このように、特定の引数パターンで呼び出されている箇所も簡単に特定できるため、リファクタリング時に「この引数は実際に使われている?」「常に同じ値が渡されていないか?」などの調査に便利というわけです。

メタ変数は$のあとに大文字のアルファベット(A-Z)、アンダースコア(_)、数字(1-9)が使えます。$だけ、数字だけは無効です。ハイフンは使えません。使える文字には注意してください。

  • 有効
    • $META, $META_VAR, $META_VAR1, $_, $_123
  • 無効
    • $invalid, $Svalue, $123, $KEBAB-CASE, $

対応言語とツール連携

ast-grepは多くの言語に対応していますが、.svelte.astroなどのファイルには現在対応していません。

また、CLIとしてだけでなく、さまざまな使い方ができます。

たとえば、npmの@ast-grep/napiパッケージからインストールすれば、JavaScriptのコードからも利用できます。

APIとしての利用

import { parse, Lang } from "@ast-grep/napi";

let source = `console.log("hello world")`;
const ast = parse(Lang.JavaScript, source); // 1. parse the source
const root = ast.root(); // 2. get the root
const node = root.find("console.log($A)"); // 3. find the node
node.getMatch("A").text(); // 4. collect the info
// "hello world"

懐かしいjQueryのようなAPIになっていて、直感的に使えます。

また、昨今のAIコーディングで役立つMCPも公開されています。

まとめ

ast-grepは、従来の文字列検索では難しかった「コードの構造を理解した検索・置換」を可能にするツールです。今回は比較的簡単な使い方だけを紹介しましたが、ast-grepの本領はまだこの先にあります。

たとえば、検索してマッチした部分を、書き換えることができます。

検索からの置換

# 古い構文: foo && foo() をモダンな構文: foo?.() に
$ sg -p '$A && $A()' -r '$A?.()'

検索パターン(-p)で見つけた箇所を、置換パターン(-r)で指定した形式に書き換えます。

検索・置換のほかにも、Linterとしての使い方もできるようです。

興味を持った方は、ぜひドキュメントを読んで試してみてください。

杉浦 有右嗣
フロントエンド・エンジニア

SIerとしてシステム開発の上流工程を経験した後、大手インターネット企業でモバイルブラウザ向けソーシャルゲーム開発を数多く経験した。2015年にピクセルグリッドへ入社し、フロントエンド・エンジニアとして数々のWebアプリ制作を手掛ける。2018年に大手通信会社に転職し、低遅延配信の技術やプロトコルを使ったプラットフォームの開発と運用に携わっていたが、2020年ピクセルグリッドに再び入社。プライベートでのOSS公開やコントリビュート経験を活かしながら、実務ではクライアントにとって、ちょうどいいエンジニアリングを日々探求している。2025年、ピクセルグリッドを退社。

Xにポストする Blueskyにポストする この記事の内容についての意見・感想を送る

全記事アクセス+月4回配信、月額880円(税込)

CodeGridを購読する

初めてお申し込みの方には、30日間無料でお使いいただけます