概要
メジャーなJavascriptトランスパイラであるBabelの簡単なプラグイン作成を通して、ASTの操作を簡単に体験する記事です。簡単なプラグインの実装サンプルと、トランスパイラの知識を得るために参考にした記事をまとめています。
複雑なプラグインの作成方法やトランスパイラの設定方法等には触れていません。
対象読者
ビルドやトランスパイラの存在は知っているが、内部処理については強く意識したことがないくらいの人に向けて書いています。Javascriptのビルドやトランスパイラなどの前提知識について0から書いた記事ではないですが、それらの知識を得るために参考になる記事のリンクを貼っているので良ければ読んでみてください。
トランスパイラとは
詳細はここでは省略しますが学習するには以下の記事がとても綺麗にまとめられていて読みやすかったです。
Babel Pluginを作ってみる
複数行あるコードからconsole.log(...)を除去するプラグインを実際に作ってみます。
実行環境作り
AST Explorerを使うと楽に動作を確認できます。この記事内の変換対象コードと変換処理を貼り付けて動作をみてみてください。
AST Explorerを開いて画面上部のTransformでbabel v7を選択してください。
画面左上が変換対象コードの入力欄、右上がAST、左下に変換処理(プラグインのコード)の入力欄、右下に変換後のコードが表示されます。
変換対象コード
以下のコードを変換します。
console.log("Christmas is just around the corner.");
const greeting = "Merry Christmas";
今回は理解しやすさを優先してシンプルな関数にしています。
(このサンプルだと正規表現でも簡単に除去できますが、console.log(...)内で()がネストしているような正規表現で引っ掛けにくいパターンでも、ASTからであれば構文を壊すことなく安全に編集できる部分がAST操作の良さです。)
AST
Babelによってestreeの仕様に従ったASTが作成されます。
今回の変換対象コードのAST全量を書くと行数が多すぎるので、行数・文字数〜などの記載位置の属性は省略しています。
{
"type": "File",
"program": {
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"computed": false,
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [
{
"type": "StringLiteral",
"value": "Christmas is just around the corner."
}
]
}
},
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "greeting"
},
"init": {
"type": "StringLiteral",
"value": "Merry Christmas"
}
}
],
"kind": "const"
}
]
}
}
program.body属性の配列にExpressionStatement(式)とVariableDeclaration(変数宣言)2つのnodeにそれぞれ。console.log(...)とgreetingの変数宣言が含まれています。
更にExpressionStatementの配下にCallExpression(関数呼出)というnodeが含まれており、今回はここからconsole.log(...)へアクセスします。
変換処理
console.log()が含まれるノードを特定できたので、ASTの形を変える処理を書きます。
ASTに適用する処理は変換関数からVisitorオブジェクトを返して処理登録します(Visitorパターンになっています)。
Visitorオブジェクトに持たせることができる属性はestreeに記載されているnodeと対応しています。
色々な書き方ができますが、今回はCallExpressionからconsole.log(...)を対象に取ります。
visitor: {
CallExpression(path) {
const callee = path.get("callee");
// console.log(...) を検出
const isConsoleLog =
callee.isMemberExpression() &&
callee.get("object").isIdentifier({ name: "console" }) &&
callee.get("property").isIdentifier({ name: "log" });
if (isConsoleLog) {
path.remove(); // その呼び出しを削除
}
},
},
CallExpressionnode内のcallee属性の条件で絞り込み、NodePath<CallExpression>.remove()で削除します。
変換結果
console.log(...)が削除されます。
const greeting = "Merry Christmas";
感想
トランスパイラを使ったコード変換は、正規表現では対応しきれない対象を取ったり、言語の構文を壊すことなく安全に変換をかけることができる手段です。
私はコードを一斉変換する際など、生成AIに変換処理そのものを依頼するのではなく、変換するための簡単なスクリプトやツール作成を依頼することがよくあります。
そういったツール作成をAIに依頼する際、手札の一つに持っておくとどこかで役立つかなと思い今回触れてみました。
昨今メジャーなNextJSでは、今回取り上げたBabelではなくSWCが利用されており、SWCのプラグインはRustで作ることになります(SWCのASTはRustの構造体で生成されるため)。
私はRustに慣れが無いですが、ASTの概念とそれを操作する方法の概要さえ頭にあれば、AIが作成したSWCプラグインの正しさも概ね判断できました。
普段利用してるビルドツールによらず、とりあえず自分が馴染みのある言語でトランスパイラプラグインを作って・読んでみるのも価値がありそうです。
もう少し詳しく知るために
トランスパイラについてより詳しく理解するために私が読んだものを紹介しておきます。
Babelのプラグインとして操作する方法を一通り学ぶにはBabel Handbookを見ることをおすすめします。
その後、メジャーなプラグインを読んでみるとより理解が深まります。@babel/preset-envに含まれているメジャーなプラグインが変換の目的や変換前後のイメージが持ちやすくてオススメです。babel-plugin-transform-arrow-functionsなどシンプルなものから手を付けると読みやすい。
サンプルコード
ローカル環境だけで動かせるサンプルコードです。
babel-helper-plugin-utilsや@babel/typesを利用していたり、inputのお題を複雑にしてまいますが、基本的にやっていることはこの記事の内容と同じです。
別ディレクトリに同じ処理をするSWCのプラグインも置いているのでよければ覗いてみてください。SWCのASTはコードのフォーマット情報を保持しないので、変換後のファイルのフォーマットが異なっているのがちょっと面白い。
参考ドキュメント・参照ドキュメント
