はじめに
JavaScriptのコードを一括で変換したり修正したい場合、正規表現などを使い置換しますか?
シンプルなケースであればそれでも問題ないですが、複雑な変換であればASTベースでコードを自在に変換できる**「jscodeshift」**が便利です。
jscodeshiftを利用すると、以下のようなことができます。
例) functionで書かれた関数をアロー関数に一括で変換
target/arrow-function/index.js
const fn = function() {
console.log("foo");
}.bind(this);
[1, 2, 3].map(function(v) {
return v * v;
});
Promise.resolve()
.then(function() {
console.log("foo");
})
.then(function() {
return 1;
});
↓
$ npx jscodeshift -t transforms/arrow-function.js target/arrow-function/index.js
transforms/arrow-function.jsは事前に準備
↓
target/arrow-function/index.js
const fn = () => {
console.log("foo");
};
[1, 2, 3].map(v => v * v);
Promise.resolve()
.then(() => {
console.log("foo");
})
.then(() => 1);
コマンドを実行しただけで、コードが一括で変換されました。
この記事ではjscodeshiftの紹介から、基本的な使い方までを紹介します。ASTの入門としても良いと思います。
コード変換の必要性
フロントエンド開発の中でコードを新しい書き方に変換したいケースに遭遇することは多々あると思います。
例えば
- JavaScriptの新しい構文の登場で、シンプルな記述に
- 不要になった記述を削除
- 新しいライブラリのAPIを使う
- ライブラリのブレイキングチェンジによるコードの修正
などなど、様々なケースがあり、これらを手動で行ったり、正規表現を使った検索や置換では限界があります。また、現状のコードが複数のパターンある場合はそれぞれの正規表現を作成して、別々に適用するのも手間です。
そんなときにjscodeshiftなどのASTベースのツールを使うことで、難しい変換処理をプログラマブルに簡単に行うことができます。
ASTについて
コードをプログラマブルに変換したい場合、ASTを使うことが多いと思います。
ASTはフロントエンド開発に触れる中で身近な存在になっています。例えばBabel、Prettier、ESLint、TypeScriptなどはASTをベースに作られています。
ASTとは、Abstract Syntax Treeの略で、日本語にすると抽象構文木です。
ソースコードを解析し、それを木構造で表現したものです。
例えばvar a = 0というコードは以下のような木構造で表現されます。
そしてJSONであれば以下のように表現されます。
つまりJavaScriptの世界においてはただのオブジェクト(JSON)なので、それをプログラマブルに扱い、変換することが可能になります。
どのようなASTが生成されるかは、AST Explorerを使うことで簡単に見れます。是非手元のコードで試してみてください。
Babel Plugin
JavaScriptコードを変換する代表例としてBabelのPluginを作るというアプローチもあります。
今日紹介するjscodeshiftとできることは基本的に同じですが、BabelのPluginはビルドプロセスでコンパイル時に毎回適用したいケースに向いています。例えばReactのJSXをJSに変換するのはBabelのPluginを使うことが多いです。
特定のタイミングで1回だけ実行して終わり。というユースケースではjscodeshiftを使う方が簡単だと思います。
それでは、ここからはjscodeshiftについて深ぼっていきます。
jscodeshiftとは
jscodeshiftとはFacebookが開発しているOSSで、複数のJavaScriptまたはTypeScriptのファイルに対して、ASTによる操作を簡単に行えるツールです。
ASTの操作などを行うrecastというライブラリをラップしたAPIを提供しています。またrecastが依存しているast-typesについても知っておく必要があります。
利用者は、変換処理が記述された自作またはOSSのtransform fileを用意して、jscodeshiftコマンドで変換対象のコードを指定し、リファクタリングを実行する流れになります。
例)
$ npx jscodeshift -t transform.js src
OSSとして様々なtransform fileが提供されています。
jscodeshiftを使ってみる
自分でtransform fileを作成する前に、OSSとして用意されているものを使ってみるところから始めましょう。今回はjs-codemodのjs-codemod/transforms/no-vars.jsを使います。
これは、varをconstまたはletに書き換えてくれるtransform fileです。
js-codemodはnpmにも公開されているので、それを使ってもいいのですが最新版がpublishされてないのでリポジトリをcloneします。
git clone https://github.com/cpojer/js-codemod.git
それではvarで書かれたコードを手元に用意しましょう。
index.js
for (var i = 0; i < 10; i++) {
}
var letItBe;
var shouldBeLet = 0;
var shouldBeConst = 0;
function mutate() {
shouldBeLet = 1;
}
var whileIterator = 10;
while (whileIterator > 0) {
whileIterator--;
}
あとは、jscodeshiftを使って変換を適用します。
$ npx jscodeshift -t js-codemod/transforms/no-vars.js index.js
見事に変換されました。
for (let i = 0; i < 10; i++) {
}
let letItBe;
let shouldBeLet = 0;
const shouldBeConst = 0;
function mutate() {
shouldBeLet = 1;
}
let whileIterator = 10;
while (whileIterator > 0) {
whileIterator--;
}
自分で変換処理を書く
jscodeshiftを活用していく際は、transform fileをプロジェクトや要件に合わせて自分で作成していくことになると思います。本記事ではtransform fileをJavaScriptファイルとして作成しますが、TypeScriptでも書くことが可能です。
transform file
transform fileは以下のように最大で3つの引数を受け取り、ソースコードを文字列として返す関数をモジュールとして作成するだけです
module.exports = function(fileInfo, api, options) {
// `fileInfo.source`をもとにASTでの変換操作を行う
// ...
// 変換操作した結果を返す
return source;
};
fileInfo
| Property | Description |
|---|---|
| path | ファイルのパス(string) |
| source | ソースコード(string) |
api
| Property | Description |
|---|---|
| jscodeshift | jscodeshiftが提供するAPIの参照 |
| j | 同上 |
| stats | --dry実行中にデータを収集するヘルパー |
| report | 渡された文字列を標準出力に出力する関数 |
options
全てのオプション情報が含まれます。ここで、プロジェクト固有の値も渡すことができます。
例)
$ jscodeshift -t transform.js src --foo=bar
optionsは{foo: 'bar'}を含んだオブジェクトになります。
戻り値
基本的にはソースコードを文字列として返しますが、戻り値に応じて変換のステータスが決められます。
- 入力と違う文字列を返す: OK
- 何も返さない: skipped
- 入力と同じ文字列を返す: unmodified
- エラー発生: error
jscodeshiftはこれらの結果に従って変換のステータスを処理します。
foo.bar()をfoo.baz()に変換
今回は「foo.bar()をfoo.baz()に変換」というシンプルな例を題材に自分でtransform fileを作成してみます。
対象のNodeを見つける
まずは、foo.bar()を検出します。検出したいコードがどんなAST NodeなのかはAST Explorerで確認します。
jscodeshift関数にsourceを渡すとCollectionオブジェクトが返ってきます。Collectionが持つfind、filter 、map、forEachなどの汎用的なAPIを利用することができます。
特定のNodeを取得する場合はfind関数が便利です。また、第2引数で具体的な条件も指定することができます。
今回はCallExpressionが対象なので、以下のようになります。
module.exports = function(fileInfo, { jscodeshift }, options) {
const j = jscodeshift;
const root = jscodeshift(fileInfo.source);
root
.find(j.CallExpression, {
callee: {
object: { name: "foo" },
property: { name: "bar" }
}
})
};
Nodeを置き換える
次は、foo.baz()への変換を行います。find関数で検出できたNodeに対して変換処理を記述します。
forEach関数のコールバックの第一引数のpathは、実際のASTをラップしてparentなどの追加情報を持つNodePathです。詳しい詳細はast-typesを参照してください。
変換については、今回replaceWith関数で別のNodeに置き換えます。
module.exports = function(fileInfo, { jscodeshift }, options) {
const j = jscodeshift;
// ...
.forEach(path => {
j(path).replaceWith(
// ...
);
});
};
新しいNodeの作成
replaceWith関数に渡すNodeは、ASTノードを簡単に作成できるast-typesのBuilderメソッドを利用します。jscodeshift経由で利用しましょう。メソッドはNodeのTypeを小文字始まりにしたものです。
module.exports = function(fileInfo, { jscodeshift }, options) {
const j = jscodeshift;
// ...
j.callExpression(
j.memberExpression(j.identifier("foo"), j.identifier("baz")),
path.value.arguments
)
// ...
};
成果物
最終的には以下のようなコードになりました。
transform.js
module.exports = function(fileInfo, { jscodeshift }, options) {
const j = jscodeshift;
const root = jscodeshift(fileInfo.source);
root
.find(j.CallExpression, {
callee: {
object: { name: "foo" },
property: { name: "bar" }
}
})
.forEach(path => {
j(path).replaceWith(
j.callExpression(
j.memberExpression(j.identifier("foo"), j.identifier("baz")),
path.value.arguments
)
);
});
return root.toSource();
};
今回の例では、正規表現で書いたほうが簡単でしたが、自分でtransform fileを作成してプログラマブルに変換する流れが分かったと思うので、次は少し複雑な変換を行います。
Object.assign()をスプレッド構文で置き換える
最後に、Object.assignで行っていたオブジェクトのコピーやマージをスプレッド構文で書き換えます。
js-codemodのrm-object-assignを参考にして実装していきます。
Object.assign({}, a);
Object.assign({ a: 1 }, b, { c: 3 });
上記のコードがスプレッド構文を使った下記のコードになるイメージです。
({
...a,
});
({
a: 1,
...b,
c: 3,
});
対象のNodeを見つける
まずは変換対象のNodeをfind関数で取得しましょう。
AST Explorerで以下のコードを見て、どんなASTか確認すると簡単ですね。
Object.assign({}, a);
module.exports = function(fileInfo, { jscodeshift }, options) {
const j = jscodeshift;
const root = jscodeshift(fileInfo.source);
root
.find(j.CallExpression, {
callee: { object: { name: "Object" }, property: { name: "assign" } },
arguments: [{ type: "ObjectExpression" }]
})
};
※ Object.assign({}, ...b);など、SpreadElementを引数に渡しているコードだとNodeが持つ情報が違うのでサンプルコードではfilter処理をしていますが今回は割愛します
スプレッド構文で置き換える
あとは、取得したCallExpressionをスプレッド構文に置き換えれば完成です。
取得できた各Nodeに対して特定の変換処理を実装していきます。今回はrmObjectAssignCall関数を実装していきます。
module.exports = function(fileInfo, { jscodeshift }, options) {
// ...
const rmObjectAssignCall = path =>
j(path).replaceWith();
root
.find(j.CallExpression, {
callee: { object: { name: "Object" }, property: { name: "assign" } },
arguments: [{ type: "ObjectExpression" }]
})
.forEach(rmObjectAssignCall);
return root.toSource();
};
スプレッド構文のコードをAST Explorerで見ると、どんなNodeに置き換えればいいか分かりますね。
({
a: 1,
...b
});
ObjectExpressionを作成していきます。
AST Nodeを作成するBuilderに何を渡すといいのかはドキュメントがないので、ast-typesのコードを見て確認しましょう。
module.exports = function(fileInfo, { jscodeshift }, options) {
// ...
const rmObjectAssignCall = path => {
const properties = path.value.arguments.reduce(
(allProperties, { ...argument }) => {
if (argument.type === "ObjectExpression") {
const { properties } = argument;
return [...allProperties, ...properties];
}
return [...allProperties, { ...j.spreadProperty(argument) }];
},
[]
);
return j(path).replaceWith(j.objectExpression(properties));
};
// ...
};
成果物
最終的には以下のようなコードになりました。
transform.js
module.exports = function(fileInfo, { jscodeshift }, options) {
const j = jscodeshift;
const root = jscodeshift(fileInfo.source);
const rmObjectAssignCall = path => {
const properties = path.value.arguments.reduce(
(allProperties, { ...argument }) => {
if (argument.type === "ObjectExpression") {
const { properties } = argument;
return [...allProperties, ...properties];
}
return [...allProperties, { ...j.spreadProperty(argument) }];
},
[]
);
return j(path).replaceWith(j.objectExpression(properties));
};
root
.find(j.CallExpression, {
callee: { object: { name: "Object" }, property: { name: "assign" } },
arguments: [{ type: "ObjectExpression" }]
})
.forEach(rmObjectAssignCall);
return root.toSource();
};
まとめ
- JavaScriptのコードをリファクタリングしたいときに便利なASTベースのツール
jscodeshiftについて紹介しました - OSSとして公開されている
transform fileを利用したり、自作して活用していきます - 自作する時は、AST Explorerを使っていきます
- ドキュメントがあまり用意されていないので、TypeScriptの型定義を都度参照していくのが良さそうです
テスト
今回はjscodeshiftのテストについては触れられませんでした。
同僚の@pirosikickがテストについて書いてくれたので、是非こちらも合わせて読んでください。
参考リンク
- https://github.com/facebook/jscodeshift
- https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation
- https://github.com/sejoker/awesome-jscodeshift
- https://github.com/cpojer/js-codemod
- https://github.com/reactjs/react-codemod
- https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jscodeshift
- https://github.com/benjamn/ast-types
- https://github.com/benjamn/recast