抽象構文木で検索するast-grepの使い方 ast-grep検索の基本
従来の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コマンドやその派生コマンド(agやrgなども)は、文字列を「部分一致」で検索するツールです。
たとえば、このようなコードがあるとします。
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としての使い方もできるようです。
興味を持った方は、ぜひドキュメントを読んで試してみてください。