JavaScript ASTを始める最初の一歩
何かJavaScriptのソースコードを機械的にチェックするためのツールを作りたいという場合に、JavaScriptのASTというものを触る必要が出てくると思います。
この記事では、その取っ掛かりとなる案内を簡単にまとめたものです。
ASTとは
AST(Abstract Syntax Tree)はコードをパースした抽象構文木のこと。 JavaScriptの場合はJavaScriptオブジェクト(JSON)として表現されます。
コード:
var a = 1;
AST:
{
  "range": [
    0,
    10
  ],
  "type": "Program",
  "body": [
    {
      "range": [
        0,
        10
      ],
      "type": "VariableDeclaration",
      "declarations": [
        {
          "range": [
            4,
            9
          ],
          "type": "VariableDeclarator",
          "id": {
            "range": [
              4,
              5
            ],
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "range": [
              8,
              9
            ],
            "type": "Literal",
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}
ASTのコミュニティ標準
ESTreeというデファクトスタンダードがあります(ES6までは定義されている)
経緯: [2015-02] 最近のJavaScript AST標準化の動き | Web Scratch
- token、- range、コメントのいち情報周りの扱いがパーサによって異なる
- ESTreeで定義されているASTの範囲内ならどのパーサでも殆ど同じ
- 注意: LiteralやGeneratorに関してパーサ間で解釈が異なるため議論中です
- Proposal to potentially incorporate changes to existing nodes · Issue #120 · estree/estree
- ポータブルな実装をしようとした時にハマるかもしれないので注意
 
- 注意: 
パーサ
- Babylon
- Babelで使われてるAcorn派生のパーサ
- まだ仕様として入るか分からないStageの構文も対応している
- 注意: 逆にESTree仕様外の拡張もしているため、ESTree互換ではないです。
 
- Esprima
- Acorn
- espree
- ESLintで使われてるパーサ
- AcornベースでEsprimaのtokenとの互換性を持っているパーサ
 
どのパーサもいろんなツールで使われてる実績があります。 Esprimaで試して、何か足りないならAcorn -> Babylonという感じでやるのが良いと思います。
大きく分けると以下の2系統になっています。
- Esprima
- Acorn
BabylonはEStreeの仕様外について積極的にサポートしているため、 それらが必要な場合はBabylonを使うが、そうでないなら他のパーサの方が安定しています。
関連ツール
- AST explorer
- AST見るのに便利
 
よくある流れ
- パーサでコードをパースしてASTにする
- ASTをtraverseして処理(変換、チェック)する
- estraverse、ast-types、babel-traverse
- estree-walker、esrecurse
- どのライブラリもほぼおなじものを持っている
 
- ASTからJSのコードを生成する(チェックのみの場合は不要)
- escodegen、babel-generatorを使う
- ジェネレータはパーサと合わせたツールが必要
- Esprimaならescodegen、babelならbabel-generator
 
Parser、Traverser、Generatorは基本的にセットで同じ系統のツールを使う形になります。
そういうのを考えるのが面倒なので、そこを抽象化するライブラリを作ったりしていました。 コンセプトの証明的なものなので、実用的に使う場合は未対応の部分があると思います。(維持コストが高い) コントリビュートは歓迎です。
例) evalの中身を解析したい
evalの中を取り出して安全に評価したいというケース
evalの中身
eval("var a = 20")
のようにパースして、evalの中身を取り出すことが可能。
- 何かを防ぐという仕組みから連想できるものはホワイトリスト
- ホワイトリストでevalの中で使えるプロパティの名前などを制限する
- 未知のプロパティを見つけらたらその弾く?
- ブラックリストだと何を防止すればいいのかが難しい
解析のフェーズ
適当な考え方(正しいのかは自信ないけど)
- evalのなかを取り出す
- evalのなかのコードをパースする
- 安全な部分だけを取り出す
- 安全な部分で評価する
安全な部分 が何かを決める必要がありそう。 安全でないことが分かったら実行は諦める。
というような考え方でやるのがよくあるケースだと思います。
おわりに
上記のようなチェックツールを書くには、まずESLintのルールを見てみるのが参考になるはずです。
書き換えをしたい場合はそのまま書き換えるとASTのTree内で不整合が起きて大変になります。 jscodeshiftやast-typesなどのライブラリが使えないか検討してみてください。
また、同様の機能を持つツールがどういうライブラリを使っているかを調べてみるのが近道になるはずです。 例えば、モジュールbundleツールのrollupはmagic-stringというライブラリで文字列の操作をしていることが分かります。
現在のASTの状況はESTreeより先のデファクトがないため新しい構文を扱おうとすると色々考えることが出てきます。 そういった時にはast-sourceのような抽象層が必要になってくるかもしれません。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。
 
 
    
    