株式会社Hacobuのテクノロジー本部に所属しているFEのtuskhsと申します。
この記事は株式会社Hacobu Advent Calendar 2024の14日目の記事になります。
はじめに
同じ構文で記述されているコードがプロジェクトのあちこちに記述してあるときに手作業で直さずコマンドで対象ファイルを一括で直せたらなぁと思ったことはないでしょうか。
AST(抽象構文木)について理解を深めるとそんな夢のようなことが出来るのですが、恥ずかしながら私は今回初めて知りました....
フロントエンドエンジニアで同じ境遇の方に触ってもらえることを目的として少し学習したことを紹介できればと思います。
そもそもAST(抽象構文木)とは何か?
簡単に説明するとコードの文法構造を木の枝葉状にしたもののことを言います。
たとえば以下のようなシンプルな関数があったとします。
const add = (a, b) => a + b
これをASTで表現すると以下のようになります。
add
を定義する内容(変数名やArrow関数であること、またその内部実装など)が木の枝葉のようにぶら下がっているのが確認できますね。
Esprima - parserで変換できるので興味がある方は試してみてください。
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"id": null,
"params": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Identifier",
"name": "b"
}
},
"generator": false,
"expression": true,
"async": false
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
ASTは上記のように文法構造情報が含まれているのでgrep置換した時より高度な条件を含んだ置換が可能になります。
また、実行したコードが残るので再配布の容易さや実行した内容の証跡にもなるという副次的な効果にも期待できます。
メリットについても理解していただいたところで、実際に多くの方が使うであろうjscodeshiftというライブラリについて話を進めたいと思います。
jscodeshiftとは
jscodeshiftはMetaが開発しているJavaScript、TypeScript向けのツールキットです。
内部ではRecastというJavaScriptのASTを探索・操作するためのライブラリを利用しており、ソースコードをASTに置換して探索し対象のノードがあった場合に書き換えを行い元のソースコード状に置換し直すという仕組みになっています。
なお、この時Recastでは先ほど紹介したEsprimaと同じASTに置換しているようです。
リファクタリング例
今回は前述のadd
関数を以下のようなaddOne
関数に変えるようなリファクタリングをしてみたいと思います。
// src/index.js
const addOne = (a) => a + 1
jscodeshiftでのリファクタリングにおける便利サービスとしてcodemodが提供しているCodemod Studioというものがあります。
これを利用すると対象コードについてASTを表示しつつ動作するかテストしながらwebエディターで開発することができます。
Codemod Studioを利用して今回の要件を満たすコードを書いてみました。(gpt-4oを利用した自動生成もしてもらえるらしいです)
// transform.js
export default function transform(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
let dirtyFlag = false;
// "add"という変数宣言をしている箇所を探索
root.find(j.VariableDeclarator, { id: { name: 'add' } }).forEach((path) => {
const init = path.node.init;
// アロー構文を使っているかどうかをチェック
if (j.ArrowFunctionExpression.check(init)) {
const params = init.params;
const body = init.body;
// 引数が2つあり body が Binary Expression であるかをチェック
if (params.length === 2 && j.BinaryExpression.check(body)) {
const left = body.left;
const right = body.right;
// 使っている演算子が"+"であるかをチェック
if (
body.operator === '+' &&
j.Identifier.check(left) &&
j.Identifier.check(right)
) {
// "addOne"に変数名を変更
path.node.id.name = 'addOne';
// 引数を1つだけ取るようにして"a + 1"と計算するようにbodyを変更
init.params = [params[0]];
init.body = j.binaryExpression('+', left, j.literal(1));
dirtyFlag = true;
}
}
}
});
return dirtyFlag ? root.toSource() : undefined;
}
簡易的に設定したテストは通っていそうですね。
コードを実装し終えたら以下からダウンロードしてから
自分で使いたいプロジェクト配下でjscodeshiftをインストールしコマンドを叩くだけです。
tsやtsxファイルにも適用したい場合には-parser
オプションが必要です。
$ npx jscodeshift -t transform.js index.js
注意点
- 予期しない動作を引き起こす可能性があるため最初は対象を絞ってテスト実行することをお勧めします。
- 会社のプロジェクトなど影響の大きいプロジェクトで実行する場合には変更前後でリグレッションが起きないかのテストもしっかり行いましょう!
さいごに
ASTを利用するフロントエンドでのリファクタリングについて紹介してみました。
プロジェクト全体に与える影響が大きいだけに慎重に進める必要はありますが、一括で狙ったコードをリファクタリングできるのは魅力的ですね。
使いこなすために今後もより深く学習していこうと思います。
ここまで読んでいただきありがとうございました。