この記事について
AST(抽象構文木)について知る機会があったので、自分で試したことや考えたことを記事にしてみました。
- ASTとは?
- JavaScriptをASTを扱う基本的な方法
- ASTの活用について
AST(抽象構文木)とは
AST(抽象構文木)とは、コードから言語の意味に関係ある情報のみを取り出し、木構造で表現したものです。「言語の意味に関係ある情報」とは演算子や式などプログラムの実行に影響する要素で、意味に関係ない情報はコメントやスペースなどがあります。
JavaScriptでASTを扱ってみる
JavaScriptのコードをASTにパースするライブラリはいくつかあるようですが、その中のesprimaを使って試してみます。
esprimaでコードをASTに変換する
次のような単純なコードをASTに変換してみます。
const answer = 1 + 2;
esprimaでコードパースするには次のようにします。
const esprima = require('esprima');
const ast = esprima.parseScript('const answer = 1 + 2;');
どのようなASTになったのか見てみましょう。
{
type: 'Program',
body: [
VariableDeclaration {
type: 'VariableDeclaration',
declarations: [
VariableDeclarator {
type: 'VariableDeclarator',
id: Identifier { type: 'Identifier', name: 'answer' },
init: BinaryExpression {
type: 'BinaryExpression',
operator: '+',
left: Literal { type: 'Literal', value: 1, raw: '1' },
right: Literal { type: 'Literal', value: 2, raw: '2' }
}
}
],
kind: 'const'
}
],
sourceType: 'script'
}
const
での変数宣言や、識別子であるanswer
、+
演算子とその左辺と右辺にリテラルがあるのを見て取れます。ASTパーサーを使うことでコードをこのようなオブジェクトに変換することができます。
しかしここで終わってはASTを活用するイメージが沸かないと思うので、次はこの木構造を走査し、その一部を変更してみます。
estraverseでASTを横断的に走査、変更する
estraverseでASTを走査したり、変更することができます。
まずはtraverse
関数で先程パースしたASTを走査してみましょう。
const estraverse = require('estraverse');
let depth = 0;
estraverse.traverse(ast, {
enter: function (node, parent) {
console.log(' '.repeat(depth) + '→ : ' + node.type);
depth++;
},
leave: function (node, parent) {
depth--;
console.log(' '.repeat(depth) + '← : ' + node.type);
}
});
enter
はノードに入る時に呼び出され、leave
はノードを出る時に呼び出されます。これを実行すると次のようになります。
→ : Program
→ : VariableDeclaration
→ : VariableDeclarator
→ : Identifier
← : Identifier
→ : BinaryExpression
→ : Literal
← : Literal
→ : Literal
← : Literal
← : BinaryExpression
← : VariableDeclarator
← : VariableDeclaration
← : Program
先程の木構造に沿ってノードへ出入りしているのが確認できます。
次はreplace
関数でASTの一部を変更してみましょう。
const repracedAst = estraverse.replace(ast, {
enter: function (node) {
if (node.type === estraverse.Syntax.BinaryExpression) {
node.right = {
...node.right,
value: 3,
raw: '3',
};
return node;
}
}
});
ASTはこれまでと同様にconst answer = 1 + 2;
をパースしたものです。この+
演算している部分の右辺を3
に変更してみます。
init: BinaryExpression {
type: 'BinaryExpression',
operator: '+',
left: Literal { type: 'Literal', value: 1, raw: '1' },
right: { type: 'Literal', value: 3, raw: '3' }
}
最初のASTと比較すると、right
の値が2
から3
に変わっています。
最後に、このASTをJavaScriptのコードに戻してみましょう。
escodegenでASTをコードに戻す
escodegenはASTからコードを生成します。
const escodegen = require('escodegen');
const newCode = escodegen.generate(repracedAst);
repracedAst
は先程+
演算の右辺を2
から3
に変えたものです。このASTから生成されたコードは次のようになります。
- const answer = 1 + 2;
+ const answer = 1 + 3;
ASTへの変更が反映され、1 + 2
から1 + 3
になっています。
どのようなことに使えそうか
さて、ここまでコードからASTへの変換、ASTの走査と変更、ASTからコードへの変換をやってきましたが、実際にどのような形で利用できるでしょうか。通常の開発で直接使うことは少ないと思いますが、静的解析やトランスパイラーで活用されているようです。
どんな場合に使えそうか考えてみました。
例1)変更が意味のあるものか検証する
ASTにはコメントやスペースなど意味に関係ない情報は含まれません。変更前後のASTを比較することで意味のある変更がおこなわれたかを検証し、テストをする/しないなどの判断に活用できるかもしれません。
例2)危険なコードが含まれていないか検証する
eval
のような文字列をコードとして実行する関数を使う場合、危険なコードが含まれていないか検証するために使えるかもしれません。例えばホワイトリストで特定の関数のみ許可するようなことはできそうです。
例3)追加の処理を挿入する
power-assertでは追加の情報を表示するためにASTを使っているようです。
テストやデバッグを支援する目的で、情報を収集したり表示したりするようなコードを追加することは考えられます。
まとめ
ASTについて知る機会があったので、esprimaを使って実際に試したことを記事にしました。積極的に活用する機会は少ないかも知れませんが、開発の基盤になるようなところで利用されているので、ASTについて知ることで新しい視点や考え方が出てきそうです。