@babel/plugin-transform-instanceof を読む
まずは手近な実装として Babel が提供しているプラグインのうち、@babel/plugin-transform-instanceof を読んでみる。このプラグインは、ES6 の instanceof 演算子を変換するだけなので実装コードも短く読みやすい。
@babel/helper-plugin-utils
import { declare } from "@babel/helper-plugin-utils";
export default declare(api => {
api.assertVersion(7);
...
});
まずは、モジュールから export されている箇所だが、@babel/helper-plugin-utils から declare
関数が使われている。これは Web サイトにも解説があるとおり、Babel 7 と以前のバージョンの互換性を保つためのものだ。この declare
関数に渡された関数は、以下の修正が施されるように別関数にラップされて返される。
- 第 1 引数
api
(babel オブジェクト) がassertVersion()
メソッドを持つようになる - 第 2 引数
options
が常に渡されるようになる
Visitor オブジェクト
export default declare(api => {
return {
name: 'transform-instanceof',
visitor: {...}
};
});
export した関数からは visitor
プロパティをもつオブジェクトを返すようにする。visitor
プロパティに指定したオブジェクトのメソッドが AST のノードに応じて呼び出されるわけだが、このへんは Babel Plugin Handbook に詳しい。
instanceof 演算子に対応させる
visitor: {
BinaryExpression(path) {
const { node } = path;
if (node.operator === 'instanceof') {
...
}
}
}
instanceof
演算子を変換したいので、visitor オブジェクトは BinaryExpression
メソッドを実装し、path.node.operator
で演算子を判定する。ちなみに、この BinaryExpression
メソッドは、
visitor: {
BinaryExpression: {
enter() {...}
exit() {...}
}
}
のように、enter/exit を実装することもできる。
helper の利用
ここから、実際の変換処理に入る。
const helper = this.addHelper('instanceof');
export した関数が実行されるときの this
は PluginPass であり、この addHelper()
関数は以下のように、file
オブジェクトにそのまま委譲されている。
addHelper(name: string) {
return this.file.addHelper(name);
}
この addHelper() 関数の実装は割と複雑なのだが、動作は @babel/helpers に説明がある。
// The .addHelper function adds, if needed, the helper to the file
// and returns an expression which references the helper
理解できた範囲でまとめると、
- コアのプラグインは @babel/helpers を使って、変換に必要な関数を定義している
- たとえば、instanceof を実装する関数はこんな感じ
- 関数の実装など長めのコードを構築するには @babel/template が便利
addHelper() 関数のメモ
- ローカル変数の binding を管理する Scope
- path の unshiftContainer, pushContainer - body のようなコンテナノードの開始位置、終了位置にノードを追加
helper の利用 (2)
const isUnderHelper = path.findParent(path => {
return (
(path.isVariableDeclarator() && path.node.id === helper) ||
(path.isFunctionDeclaration() && path.node.id && path.node.id.name === helper.name)
);
});
if (isUnderHelper) {
return;
}
ここでは isUnderHelper
変数で「走査中のノードがヘルパー自体ではないか」を判定している。これは、たとえば、以下の変換後のコードを見れば理解できると思う。
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
_instanceof(this, String)
ここで、_instanceof
関数がヘルパーとして追加された関数だが、当然、この関数の中身の instanceof
演算子を変換してはいけない。なので、isUnderHelper
変数を使って判定する必要があるわけだ。
ノードの置き換え
path.replaceWith(t.callExpression(helper, [node.left, node.right]));
最後に、変換対象のノードを置き換える。
- @babel/types の callExpression() でヘルパー関数を呼び出すためのノードを生成
- path.replaceWith() によって対象ノードを置き換え
Discussion