10/22の技術書典3に、東京ラビットハウスで「簡単JavaScript AST入門 ASTで1桁上の生産性 誰でも簡単にトランスパイラを作る時代」という同人誌を出します。ASTを知らない人でも簡単にソースコードをハックできるようになる入門&実用的な本です。今回の記事はその本を書いてる過程で生まれた物を記事にしたものです。

Babelプラグインはとても簡単に作れます。ここではちょっとした最適化プラグインを作ってみましょう。最適化というとコンパイラの教科書に載ってるような難しい何かを想像されるかもしれませんがそういう知識は不要です。そうBabelプラグインならね!

注意

babel7およびNode v8.6.0でのみ動作確認しています。

$ npm i babel-core@next -S
$ npm i babel-core -S

Babel7系ではTypeScriptに対応してたり美味しい点が多そうだし、せっかく新規で覚えるならBabel6よりは7系でしょう。まだベータ版なので@nextが必要ですが、正式版になれば@nextはいりません。

Babelプラグイン

Babelプラグインのひな形はとてもシンプルです。

const {transform} = require('babel-core')

const plugin = ({types: t}) => {
  return {
    visitor: {
        Program: (nodePath) => {
            console.log(nodePath.node)
        }
    }
  }
}

const source = '1 + 2'
const {code} = transform(source, {plugins: [plugin]})
console.log(code)

実質何もしないプラグインですが、これでも立派なbabelのプラグインです。Babelのプラグインは、単なるオブジェクトか、オブジェクトを返す関数です。ただしオブジェクトにはvisitorというオブジェクトが含まれている必要があります。ビジターパターンのビジターです。

以前書いたJavaScriptでASTを使ってコードをインジェクションしてみるでは単なるオブジェクトを使った事例でしたが、今回は関数化してあります。どちらでもプラグインとしては動作するのですが、利点としては引数にBabel系プラグインなどが渡ってくることです。({types: t})はbabel-typesプラグインに該当するものです (あとで詳しく説明します)。

nodePath.evaluate

visitor関数の第1引数はNodePath型のオブジェクトでevaluateというメソッドを持っています。これは静的に計算できるものは勝手に計算してくれるという超絶便利メソッドなのです。四則演算はもちろん、リテラルで初期化された変数を展開したりです。

const {confident, value} = nodePath.evaluate()

うまく評価 (evaluate) できていれば confidenttrue になります。

nodePath.replaceWith

NodePath型にはreplaceWithというASTを書き換えるメソッドがあります。ただし、evaluate.valueはASTではなくJSとしてのデータそのものなのでいったんASTに変換しなければなりません。

const plugin = ({types: t}) => {
  const toLiterals = {
    string: value => t.stringLiteral(value),
    number: value => t.numericLiteral(value),
    boolean: value => t.booleanLiteral(value),
    null: value => t.nullLiteral(),
  }

  const valueToLiteral = value => toLiterals[typeof value](value)
}

t.stringLiteral()などの関数は、babel-typesというパッケージに所属するメソッドです。ただしBabelプラグインでは引数にtypesという名前で渡されるので、わざわざ別途requireする必要がありません。これらの関数を使ってリテラルのASTに変換します。

nodePath.traverse

evaluateする対象のNodeはどう記述すればいいのでしょうか?Babelプラグインのvisitorオブジェクトにはexitという便利なラベルをダイレクトに指定できないという制約があります。

const plugin = ({types: t}) => {
  return {
    visitor: {
      exit: (nodePath) => {},
    }
  }
}
// --> Error: [BABEL] unknown: Plugins aren't allowed to specify catch-all enter/exit handlers. Please target individual nodes.

ところがNodePathにはtraverseという便利なメソッドがあります。自分のNodePathからtraverseを意図的に発生させられて、なおかつこの中ではenter, exitを普通に使えるのです。

const optimizePlugin = ({types: t}) => {
  const evaluateVisitor = {
    exit: (nodePath) => {
      // 全てのノードから離れる時の処理をここに記述する
    }
  }

  return {
    visitor: {
      Program: (nodePath) => {
        nodePath.traverse(evaluateVisitor)
      },
    }
  }
}

これなら通ります。

実際に変換してみる

const evaluateVisitor = {
  exit: (nodePath) => {
    // 一度変換したやつやそもそも変換できないものはいじらない
    if (t.isImmutable(nodePath.node)) {
      return
    }

    const {confident, value} = nodePath.evaluate()
    if (confident && typeof value !== 'object') {
      nodePath.replaceWith(valueToLiteral(value))
    }
  },
}

先頭の判定文が無いと無限に変換しつづけてエラーで落ちるまで帰ってきません。

また、valueがたとえば配列リテラルの時などはobjectが帰ってきますがスルーしています。

ここまでで完成です。

まとめ

  • Babelプラグインは簡単に作れる
  • Babelプラグインならちょっとした静的最適化は簡単に作れる

突っ込みとか、この記事がわかりやすい・わかりにくい、ASTに関して知りたい事とかあったら遠慮無くコメントやTwitterで話しかけて頂ければ幸いです。

10/22の技術書典3に、東京ラビットハウスで今回の記事のようなものをまとめあげた本を出します。良ければサークルチェックなどしていただければ嬉しいです。

質問や感想をいただければ本に反映できるかもしれません。よろしくお願いいたします。

全ソース

const {transform} = require('babel-core')

const optimizePlugin = ({types: t}) => {
  const toLiterals = {
    string: value => t.stringLiteral(value),
    number: value => t.numericLiteral(value),
    boolean: value => t.booleanLiteral(value),
    null: value => t.nullLiteral(),
  }

  const valueToLiteral = value => toLiterals[typeof value](value)

  const evaluateVisitor = {
    exit: (nodePath) => {
      if (t.isImmutable(nodePath.node)) {
        return
      }

      const {confident, value} = nodePath.evaluate()
      if (confident && typeof value !== 'object') {
        nodePath.replaceWith(valueToLiteral(value))
      }
    },
  }

  return {
    visitor: {
      exit: (nodePath) => {

      },
      Program: (nodePath) => {
        nodePath.traverse(evaluateVisitor)
      },
    }
  }
}

const source = `
const a = 1 + 2 * 3 / 4
console.log(a)
let b = a + 2
console.log(b)
`

const {code} = transform(source, {plugins: [optimizePlugin]})
console.log(code)

/* 結果:
const a = 2.5;
console.log(2.5);
let b = 4.5;
console.log(4.5);
*/

どうせならconst alet bの宣言も消してくれればいいのですがevaluateだけではそこまではしてくれませんが、NodePath型 (正確にはNodePathから参照できるBinding型) に変数の静的解析された情報があるので、少しのコードで不要な変数宣言の削除なんかもできてしまいます。

大切なことなので

10/22の技術書典3に、東京ラビットハウスで、ASTの本出します!