はじめに
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