Esprima / Escodegen / Estraverse を用いて, javascript code -> AST (esprima) -> javascript code (escodegen) を試してみます。環境は OSX 10.10 / Node.js v4.0.0です。
Esprima
Esprimaは JavaScript Parserで AST (Abstract Syntax Tree) という抽象構文木を取得できる。
MozillaのSpiderMonkey Parser APIがこれまで実質的な標準だったが, 亜種が増えてきてこれ以上の乱立を防ぐために The ESTree Spec という標準化に向かったよう。[1]
ASTはaltJSやLintツールの開発など幅広く使われる。
ES2015(ES6)では, 改行可能な文字列リテラルの Template string が使える。
parseするコードには, せっかくなので ES6 でサポートされた Default parametersを使ってみる。
var esprima = require('esprima');
var code_src = `
// Default parameters (es6)
function multiply(a, b = 1) {
return a*b;
}
multiply(5);
`;
var ast = esprima.parse(code_src);
console.log(JSON.stringify(ast));
var tokens = esprima.tokenize(code_src);
console.log(JSON.stringify(tokens));
parseで取得したAST。(JSONLintで整形)
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "multiply"
},
"params": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"defaults": [
null,
{
"type": "Literal",
"value": 1,
"raw": "1"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Identifier",
"name": "b"
}
}
}
]
},
"generator": false,
"expression": false
},
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "multiply"
},
"arguments": [
{
"type": "Literal",
"value": 5,
"raw": "5"
}
]
}
}
],
"sourceType": "script"
}
tokenizeで tokenの一覧が取得できる。
[
{
"type": "Keyword",
"value": "function"
},
{
"type": "Identifier",
"value": "multiply"
},
{
"type": "Punctuator",
"value": "("
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": ","
},
{
"type": "Identifier",
"value": "b"
},
...
]
Escodegen
Esprimaで生成したASTを Escodegenで復元してみる。
var escodegen = require('escodegen');
var code_gen = escodegen.generate(ast);
console.log(code_gen);
結果は下記で, 元のcodeと一致していることを確認。
$ node escodegen-example.js
function multiply(a, b = 1) {
return a * b;
}
multiply(5);
ASTを根ノード (root node)から辿って, multiplyの戻り値を元の b から a*b に変更するように改変してみる。
var escodegen = require('escodegen');
var esprima = require('esprima');
var code_src = `
function multiply(a, b = 1) {
return b;
}
multiply(5);
`;
var ast = esprima.parse(code_src);
console.log(ast.body[0].body.body[0].argument);
// { type: 'Identifier', name: 'b' }
ast.body[0].body.body[0].argument = {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Identifier",
"name": "b"
}
}
console.log(ast.body[0].body.body[0].argument);
// { type: 'BinaryExpression',
// operator: '*',
// left: { type: 'Identifier', name: 'a' },
// right: { type: 'Identifier', name: 'b' } }
// function multiply(a, b = 1) {
// return a * b;
// }
var code_gen = escodegen.generate(ast);
console.log(code_gen);
// function multiply(a, b = 1) {
// return a * b;
// }
// multiply(5);
Estraverse
ASTを辿っていくのはかなり大変なので Estraverseを使ってみる。EstraverseはASTの traverseを行うツールである。
var esprima = require('esprima');
var estraverse = require('estraverse');
var code = `
function multiply(a, b = 1) {
return a*b;
}
`;
var ast = esprima.parse(code);
function get_node_info (node) {
switch (node.type) {
case 'Identifier':
return node.name;
case 'ExpressionStatement':
return node.body;
case 'FunctionDeclaration':
return node.id;
case 'Literal':
return node.value;
default:
return node.type;
}
}
estraverse.traverse(ast, {
enter: function (node, parent) {
console.log('[enter] ', node.type, ':', get_node_info(node))
},
leave: function (node, parent) {
console.log('[leave] ', node.type, ':', get_node_info(node))
}
});
enter は 根ノード (root node)から 葉ノード (leaf node)に向かって巡回していき, leave はその逆で葉ノードから根ノードに向かって巡回していく。
enterでは estraverse.VisitorOption.Skip / this.skip() で子ノード (child node)の探索を skipすることができる。
また, enter/leave共通で estraverse.VisitorOption.Break / this.break() で探索を止める。
$ node estraverse-example.js
[enter] Program : Program
[enter] FunctionDeclaration : { type: 'Identifier', name: 'multiply' }
[enter] Identifier : multiply
[leave] Identifier : multiply
[enter] Identifier : a
[leave] Identifier : a
[enter] Identifier : b
[leave] Identifier : b
[enter] BlockStatement : BlockStatement
[enter] ReturnStatement : ReturnStatement
[enter] BinaryExpression : BinaryExpression
[enter] Identifier : a
[leave] Identifier : a
[enter] Identifier : b
[leave] Identifier : b
[leave] BinaryExpression : BinaryExpression
[leave] ReturnStatement : ReturnStatement
[leave] BlockStatement : BlockStatement
[leave] FunctionDeclaration : { type: 'Identifier', name: 'multiply' }
[leave] Program : Program
おわりに
ASTをいじるのは楽しかったです。ESLintも使ってみたい。
今回の内容とは関係ないですが, Golangの Go 1.5 Bootstrap Plan がとても気になっていて, Goコンパイラ触ってみたいです。
[1] The power-assert Goes To The Next Scene
[2] JavaScript AST Walker