Babel Plugin を作りながら AST と Babel を学ぶ

この記事は JavaScript Advent Calendar 2019 の 23日目の記事です。
前日の22日目は [Vue-CLI 4を使用したJavaScript開発環境構築(プロトタイプ版とプロジェクト版)](https://qiita.com/cube_3110/items/9b846ce027171d304066) でした。

今回は表題通り`Babel Plugin`を作りながら`AST`と[Babel](https://babeljs.io/)を学ぼうという記事です。

## AST とは？
まずは根幹である`AST`について軽く説明します。
`AST`は`Abstract Syntax Tree`の略で、日本語では抽象構文木などと呼ばれるものです。

`AST`はプログラムの構造を示したデータ構造体であり、JavaScriptでは`JSON`データの形で表現されることが一般的になり、基本的に仕様は、[ESTree](https://github.com/estree/estree) に準拠されています。

`AST`は`Babel`以外に [ES Lint](https://eslint.org/) や [webpack](https://webpack.js.org/) などにも使用されています。

実際に`AST`がどのようなものなのかを[AST explorer](https://astexplorer.net/)というサイトで簡単に確認することができます。
今回は`const a = 1`を`AST`の構造体にしてみました。

画面左側が実際の値、右側が`AST`の構造体になります。
これは [acorn](https://github.com/acornjs/acorn) というミニマムな JavaScript の parser により生成されたものです。

![スクリーンショット 2019-12-24 8.07.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108073/dc3e2140-9c47-49ab-ecfa-87caf69c23a0.png)

こちらがそのデータになります。

```json
{
  "type": "Program",
  "start": 0,
  "end": 11,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
```

後ほど詳しく説明します。
今は`AST`はこのような`JSON`なんだなというくらいの理解で大丈夫です。

## Babel とは？
続いて`Babel`について説明をします。
`Babel`とは JavaScript のコードを変換してくれる Compiler です。

元々`Babel`は`6 to 5`という名称でした、その名の通り ES6 から ES5 にコードを変換するだけのものでした。
しかし、その後さまざまな要望を得た機能が実装され、現在の`Babel`という名称になりました。

### Babelの機能
`Babel`は主に３つの機能を備えています。

- 構文変換
- Polyfill の提供
- ソースコードの変換

`Babel`は上記のような機能をもって、ブラウザでは使用できない最新の機能を書いた JavaScript や TypeScript を、指定したブラウザでも使用できるようにコードを変換します。

例えば `const a = () => {}` のようなアロー関数は、[@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-arrow-functions) によって処理され、このようなコードになります。

> https://babeljs.io/docs/en/babel-plugin-transform-arrow-functions
![スクリーンショット 2019-12-24 8.25.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108073/eb8eb955-66c5-1c41-38c6-b7b2c85bde98.png)

イメージとしてはこのようになります。
![スクリーンショット 2019-12-24 8.27.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108073/78e6a193-90db-33e1-44fc-c72aaa607717.png)

## Babel はコードをどのように変換するのか?
`Babel`の変換にはこのように3つの段階があります。


- Parsing
    - [@babel/parser](https://babeljs.io/docs/en/babel-parser) を用いて、ソースコードを`AST`に変換
- Transformation 
    - [@babel/traverse](https://babeljs.io/docs/en/babel-traverse)を用いて、`AST`を変換する、
- Code Generat 
    - [@babel/generator](https://babeljs.io/docs/en/babel-generator) を用いて` AST`をソースコードに変換する

図にするとこのようになります。
![スクリーンショット 2019-12-24 8.32.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108073/8fdf265e-7dfa-bfbc-29db-2eed7d9bce4d.png)

## 実際に Babel Plugin を作りながら AST を学ぶ
ここからは実際に`Babel Plugin`を作りながら`Babel`がどのように`AST`を駆使してコードを変換しているのか見ていきましょう。
今回は`const`と`let`をすべて`var`に置き換えるものを作成します。

### 前準備
必要なパッケージを事前に落としておきます。

```bash
npm i -D @babel/parser @babel/generator @babel/traverse
```

### 1. Parse
[@babel/parser](https://babeljs.io/docs/en/babel-parser) を使用して、ソースコードを`AST`に変換しましよう。

```javascript
// parser/index.js

const { parse } = require("@babel/parser");

// AST に変換
const ast = parse(`
const a = 1
`);

console.log(JSON.stringify(ast, null, 2));

```
このコードを実際に`.json`ファイルに出力します。

```bash
node node parser/index.js
```

実際に出力されたものはこちらです。

```json
{
  "type": "File",
  "start": 0,
  "end": 13,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 3,
      "column": 0
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 13,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 3,
        "column": 0
      }
    },
    "sourceType": "script",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 1,
        "end": 12,
        "loc": {
          "start": {
            "line": 2,
            "column": 0
          },
          "end": {
            "line": 2,
            "column": 11
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 7,
            "end": 12,
            "loc": {
              "start": {
                "line": 2,
                "column": 6
              },
              "end": {
                "line": 2,
                "column": 11
              }
            },
            "id": {
              "type": "Identifier",
              "start": 7,
              "end": 8,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 6
                },
                "end": {
                  "line": 2,
                  "column": 7
                },
                "identifierName": "a"
              },
              "name": "a"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 11,
              "end": 12,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 10
                },
                "end": {
                  "line": 2,
                  "column": 11
                }
              },
              "extra": {
                "rawValue": 1,
                "raw": "1"
              },
              "value": 1
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

```
これで`AST`に変換することができました！

### 2. Generate

続いては [@babel/generator](https://babeljs.io/docs/en/babel-generator) を用いて先ほど`AST`にしたデータをソースコードに変換します。

```javascript
// generator/index.js 

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;

// ソースコードを AST に変換
const ast = parse(`
const a = 1
`);

// generate の第一引数に AST を格納
console.log(generate(ast).code);
```
このコードを実行してみましょう。

```bash
node generator/index.js        
```

実行結果はこのようになります。

```js
const a = 1;
```

`AST`に変更される前と変わりないコードが生成されました、これは`AST`になにも変更を加えずにコードに戻したからです。

### 3. Travers
ここから本題である`const`と`let`をすべて`var`に置き換える作業を行います。
もう一度`1. Parse`で吐き出した`AST`を見てましょう。

```json
{
  "type": "File",
  "start": 0,
  "end": 13,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 3,
      "column": 0
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 13,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 3,
        "column": 0
      }
    },
    "sourceType": "script",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 1,
        "end": 12,
        "loc": {
          "start": {
            "line": 2,
            "column": 0
          },
          "end": {
            "line": 2,
            "column": 11
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 7,
            "end": 12,
            "loc": {
              "start": {
                "line": 2,
                "column": 6
              },
              "end": {
                "line": 2,
                "column": 11
              }
            },
            "id": {
              "type": "Identifier",
              "start": 7,
              "end": 8,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 6
                },
                "end": {
                  "line": 2,
                  "column": 7
                },
                "identifierName": "a"
              },
              "name": "a"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 11,
              "end": 12,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 10
                },
                "end": {
                  "line": 2,
                  "column": 11
                }
              },
              "extra": {
                "rawValue": 1,
                "raw": "1"
              },
              "value": 1
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

```

`VariableDeclaration`に`"kind": "const"` という値があるのが分かります。
ここがかなり怪しいので、冒頭にも紹介した JavaScript　の `AST`の仕様に相当する`EsTree`で`VariableDeclaration `を検索してみましょう。
[検索結果はこちら](https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#variabledeclaration)

このような検索結果コードが表示されています。

```javascript
extend interface VariableDeclaration {
    kind: "var" | "let" | "const";
}
```

どうやらこの`kind`の値を`var`にすればよさそうなので、これからASTを使って変換していきます。
今回は [@babel/traverse](https://babeljs.io/docs/en/babel-traverse) を使用していきます。

```javascript
// traverse/index.js

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;

// AST を変換
const ast = parse(`
const a = 1
let b = 2
`);

// AST を第一引数に、変更内容を第二引数にする
traverse(ast, {
  // 変更したい部分(今回は VariableDeclaration )
  VariableDeclaration(path) { 
    // kind を var に変更
    path.node.kind = "var";
  },
});

// // generate の第一引数に AST を格納し、コードを生成
console.log(generate(ast).code);
```

これを実行します。

```bash
 node traverse/index.js
```
実行結果はこちらになります。

```javascript
var a = 1;
var b = 2;
```
これで、`const`と`let`を`var`に変更することができました。

ほかにもこのようにすることで値の変更もできます。

```javascript
// traverse/index.js

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;

const ast = parse(`
const a = 1
let b = 2
`);

traverse(ast, {
  VariableDeclaration(path) {
    path.node.kind = "var";
  },
  VariableDeclarator(path) {
    if (path.node.id.name === "a") {
      path.node.id.name = "c";
      path.node.init.value = 3;
    }
  }
});

console.log(generate(ast).code);

```

実行。

```bash
 node traverse/index.js
```
実行結果。

```javascript
var c = 3;
var b = 2;
```

このように`AST`を駆使すれば正規表現では表現できないようなパターンも変更可能になります。

## 最後に
今回説明した部分は、`Babel Plugin`の肝となる部分になります。
`AST`と`Babel Plugin`の肝を理解すれば、 [@babel/plugin-transform-arrow-functions のコード](https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-arrow-functions/src/index.js) のようなプラグインがやっていることは案外簡単なことのように思えるかもしれません。

`AST`を使って便利ツールを作っていきましょう！！

今回使用したソースコードはこちらのレポジトリにあります。
https://github.com/sakit0/babel-plugin-demo