ブラウザ上で動くjavascriptエディターを作っている関係で,
エラー/ワーニングを表示するために,ユーザーが作ったjavascriptの構文解析をする必要がありました.
Lintで全部網羅できれば良かったのですが,独自のワーニングを出したかったりもしたので,構文解析をやりました.
構文解析なんて触れる機会がなかったので,どういうものなのか から調べてみました.
##構文解析について
AST(Abstract Syntax Tree)というらしいです.
たとえば,下のコードを構文解析すると
/**
* comment
*/
function printTips() {
var myHeading = document.querySelector('h1');
myHeading.textContent = 'Hello world!';
}
こういうjsonを作ることができます
{
"type": "Program",
"start": 0,
"end": 135,
"body": [
{
"type": "FunctionDeclaration",
"start": 20,
"end": 134,
"id": {
"type": "Identifier",
"start": 29,
"end": 38,
"name": "printTips"
},
"generator": false,
"expression": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 41,
"end": 134,
"body": [
{
"type": "VariableDeclaration",
"start": 45,
"end": 90,
"declarations": [
{
"type": "VariableDeclarator",
"start": 49,
"end": 89,
"id": {
"type": "Identifier",
"start": 49,
"end": 58,
"name": "myHeading"
},
"init": {
"type": "CallExpression",
"start": 61,
"end": 89,
"callee": {
"type": "MemberExpression",
"start": 61,
"end": 83,
"object": {
"type": "Identifier",
"start": 61,
"end": 69,
"name": "document"
},
"property": {
"type": "Identifier",
"start": 70,
"end": 83,
"name": "querySelector"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"start": 84,
"end": 88,
"value": "h1",
"raw": "'h1'"
}
]
}
}
],
"kind": "var"
},
{
"type": "ExpressionStatement",
"start": 93,
"end": 132,
"expression": {
"type": "AssignmentExpression",
"start": 93,
"end": 131,
"operator": "=",
"left": {
"type": "MemberExpression",
"start": 93,
"end": 114,
"object": {
"type": "Identifier",
"start": 93,
"end": 102,
"name": "myHeading"
},
"property": {
"type": "Identifier",
"start": 103,
"end": 114,
"name": "textContent"
},
"computed": false
},
"right": {
"type": "Literal",
"start": 117,
"end": 131,
"value": "Hello world!",
"raw": "'Hello world!'"
}
}
}
]
}
}
],
"sourceType": "module"
}
このjsonのことをASTというようです
ちょっとしたコードなのに,構文解析するとものすごく長いjsonになってしまいます.
小さく分解して考えるとよく分かるのですが,例えば,
function printTips() {
...
}
ここの部分こんな感じに解析されます
{
"type": "FunctionDeclaration",
"start": 20,
"end": 134,
"id": {
"type": "Identifier",
"start": 29,
"end": 38,
"name": "printTips"
},
"generator": false,
"expression": false,
"params": [],
"body": {
...
}
}
start
とかend
はコードの中の何文字目から何文字目ががいとうするか という情報です.
構文解析後のjsonから
-
FunctionDeclaration
(関数定義)が20〜134文字目の間でされている - id(=関数名)は29〜28文字目にかかれている"printTips"
- "params"(=引数)はなし
- "body"の中は
...
といった情報を読み取ることができます.
これを使ってこの関数が呼ばれてるけどどこにも定義されてないよ?とかの確認ができるわけです.
また,逆にこのjsonからコードを組み立てることもできて,
最近のminifyとかbabelとかの変換系は一度ASTにして,それを再度コードに直すことで実行内容を変えずにコードを変換しているらしいです.
##すごく役に立つサイト
AST exploer
いろいろなライブラリで構文解析した結果をUIで確認できます.
自分がやりたいことがどのライブラリでできるかな?と探すのにちょうどよかったです.
あと,ASTが複雑になりがちなので,デバッグでもビジュアライズしたくて活用させてもらいました.
##実際に使ってみた
いろんなのを試して見たところ,構文解析のライブラリはacornを使いました,
https://github.com/acornjs/acorn
ほかのライブラリだと,正しいjsじゃないと解析できないのですが,
acornはlooseという機能があって,正しくないjsでもとりあえず解析してみたよ!ってのができてよかったです.
(もちろん,正しくないjsなので正しくないASTが出てきます笑)
また,acornにはwalkというライブラリがあって,再帰的にASTを確認するのに役に立ちます
// <script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/5.5.3/acorn.js"></script>
// <script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/5.5.3/acorn_loose.js"></script>
// <script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/5.5.3/walk.js"></script>
let code = "...";
let ast;
try{
ast = acorn.parse(code, {ecmaVersion: 8}); //構文解析をしてみる(パースできないとエラー)
}catch(err){
ast = acorn.parse_dammit(code, {ecmaVersion: 8}); //構文解析をしてみる(パースできなくても無理やりパースしてエラーにならない)
}
function findAwaitKeyword(node, st, ancestors) {
// ...
}
acorn.walk.ancestor(ast, {
AwaitExpression: findAwaitKeyword
});
acorn.walk
も使い方が簡単で,上の書き方でAwaitExpression
が見つかったらfindAwaitKeyword
関数が呼ばれるというjsらしい設計でした.
引数はnode
が見つけたASTの本体(今回ならAwaitExpression
のノード),stがスタックトレースみたいな,node
がコードのどの関数のどの部分にかかれているかという情報です.
ancestors
はよくわからなくて.ほとんどst
と同じでした.何が違うんだろう・・・?
##やってみて
構文解析なんて言語を作る人がすごく頑張ってやれる ぐらいのイメージだったので,こんな簡単に手が出せる様になっていて驚きました.
構文解析したくなることはほとんどないと思いますが,babelやらwebpackやらでお世話になっているので,一度ソースを見てみるのも楽しそうです