【Node.js】Esprima / Escodegen / Estraverseを試してみた

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