【JavaScript】AST（抽象構文木）に触れてみる

## この記事について

AST（抽象構文木）について知る機会があったので、自分で試したことや考えたことを記事にしてみました。

- ASTとは？
- JavaScriptをASTを扱う基本的な方法
- ASTの活用について

## AST（抽象構文木）とは

AST（抽象構文木）とは、コードから言語の意味に関係ある情報のみを取り出し、木構造で表現したものです。「言語の意味に関係ある情報」とは演算子や式などプログラムの実行に影響する要素で、意味に関係ない情報はコメントやスペースなどがあります。

## JavaScriptでASTを扱ってみる

JavaScriptのコードをASTにパースするライブラリはいくつかあるようですが、その中の[esprima](https://github.com/jquery/esprima)を使って試してみます。

### esprimaでコードをASTに変換する

次のような単純なコードをASTに変換してみます。

```js:対象のコード
const answer = 1 + 2;
```

esprimaでコードパースするには次のようにします。

```js:コードをパースしてASTにする
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](https://github.com/estools/estraverse)でASTを走査したり、変更することができます。
まずは`traverse`関数で先程パースしたASTを走査してみましょう。

```js:パースした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の一部を変更してみましょう。

```js:パースした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](https://github.com/estools/escodegen)はASTからコードを生成します。

```js:ASTからコードを生成する
const escodegen = require('escodegen');

const newCode = escodegen.generate(repracedAst);
```

`repracedAst`は先程`+`演算の右辺を`2`から`3`に変えたものです。このASTから生成されたコードは次のようになります。

```diff_javascript
- const answer = 1 + 2;
+ const answer = 1 + 3;
```

ASTへの変更が反映され、`1 + 2`から`1 + 3`になっています。

## どのようなことに使えそうか

さて、ここまでコードからASTへの変換、ASTの走査と変更、ASTからコードへの変換をやってきましたが、実際にどのような形で利用できるでしょうか。通常の開発で直接使うことは少ないと思いますが、静的解析やトランスパイラーで活用されているようです。
どんな場合に使えそうか考えてみました。

### 例１）変更が意味のあるものか検証する

ASTにはコメントやスペースなど意味に関係ない情報は含まれません。変更前後のASTを比較することで意味のある変更がおこなわれたかを検証し、テストをする/しないなどの判断に活用できるかもしれません。

### 例２）危険なコードが含まれていないか検証する

`eval`のような文字列をコードとして実行する関数を使う場合、危険なコードが含まれていないか検証するために使えるかもしれません。例えばホワイトリストで特定の関数のみ許可するようなことはできそうです。

### 例３）追加の処理を挿入する

[power-assert](https://github.com/power-assert-js/power-assert)では追加の情報を表示するためにASTを使っているようです。
テストやデバッグを支援する目的で、情報を収集したり表示したりするようなコードを追加することは考えられます。

## まとめ

ASTについて知る機会があったので、esprimaを使って実際に試したことを記事にしました。積極的に活用する機会は少ないかも知れませんが、開発の基盤になるようなところで利用されているので、ASTについて知ることで新しい視点や考え方が出てきそうです。