JavaScript の文法を自分好みに変えてみる（null と undefined は友達の巻）

@reki2000 の[Javaの文法を自分好みに変えてみる（nullは友達の巻）](http://qiita.com/reki2000/items/07f85583eac499f09e2a)が面白い！と思ったので、JavaScript で同じことをやってみます。

>自分で javac をコンパイルして、好きな文法を追加した Java言語のソースを書いちゃうという話です。
>今回は Groovy などで見かける ?. という Null Safe Reference を導入してみました。似たものに Elvis Operator とよばれる ?: もあって、そのうちこれにも挑戦してみたいです。

この記事の成果は [shuhei/babel-plugin-null-safe-accessor](https://github.com/shuhei/babel-plugin-null-safe-accessor) という Babel プラグインとして利用できます。

## 方針

JavaScript には `null` だけでなく `undefined` もあるので、以下のような変換にしたいと思います。関数呼び出しの場合も便利に使えるように末尾の `()` も考慮します。

```js
// 新文法
var b = a?.b;
a?.b(c, d);

// 既存文法で表現した場合
var b = a == null ? a : a.b;
a == null ? a : a.b(c, d);
```

V8 などをいじってコンパイルするのはハードルが高いので、実行前の変換を考えます。JavaScript 文法の拡張には [Sweet.js](http://sweetjs.org/) というものがありますが、今回はすでに多くの方が使っている Babel でやってみようと思います。

Babel の JavaScript 変換の仕組みは大まかに言うと以下のようになります。

```
元のコード -> Parser -(AST)-> Transformers -(AST)-> Generator -> 出力されるコード
```

今回はまず Parser が新しい文法をパースできるようにする必要があります。Parser は Tokenizer を継承しており、Tokenizer がパースした token から AST を組み立てていきます。`?.` 用に questionDot という token と、それに対応する AST type を用意します。

`?.` の元になる `.`（プロパティへのアクセス）の AST 表現は以下の様なものです。

```
{
  "type": "MemberExpression",
  "computed": false,
  "object": { /* オブジェクト */ },
  "property": { /* プロパティ */ }
}
```

`?.` 用に MemberExpression と同様の構造の SafeMemberExpression という新しい AST type を用意し、Parser が解釈・出力できるようにします。その後 Transformer で SafeMemberExpression を上述の三項演算子の AST に変換します。

## Parser

現状の Babel 6 ではプラグインで新文法を追加することはできません。babel-plugin-syntax-* というような公式のプラグインがありますが、フラグを on にしているだけなのです。しょうがないので node_modules 以下のソースを編集していきます（後でモンキーパッチ化してモジュールに切り出します）。

まずは触る場所のあたりをつけるために、どこでエラーになるか見てみます。

```sh
npm init
npm install -D babel-core
```

```js:index.js
var babel = require('babel-core');

var code = 'var c = a?.b?.c;';
var result = babel.transform(code);
```

```sh
$ node index.js
/Users/shuhei/work/js/null-friendly-js/node_modules/babel-core/lib/transformation/file/index.js:547
      throw err;
      ^

SyntaxError: unknown: Unexpected token (1:10)
> 1 | var c = a?.b?.c;
    |           ^
    at Parser.pp.raise (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/location.js:22:13)
    at Parser.pp.unexpected (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/util.js:91:8)
    at Parser.pp.parseExprAtom (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:510:12)
    at Parser.pp.parseExprSubscripts (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:265:19)
    at Parser.pp.parseMaybeUnary (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:245:19)
    at Parser.pp.parseExprOps (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:176:19)
    at Parser.pp.parseMaybeConditional (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:158:19)
    at Parser.pp.parseMaybeAssign (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:121:19)
    at Parser.pp.parseMaybeConditional (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:163:28)
    at Parser.pp.parseMaybeAssign (/Users/shuhei/work/js/null-friendly-js/node_modules/babylon/lib/parser/expression.js:121:19)
```

`node_modules/babylon/lib/parser/expression.js` でエラーが出ていることがわかります。エラーの内容は、そんな token はありませんというもの。Token を増やす必要がありそうです。`node_modules/bablylon/lib/tokenizer/types.js` を見ると dot, doubleColon など token の一覧があります。ここに `?.` に対応する questionDot という token を追加します。

```js:node_modules/babylon/lib/parser/expression.js
types.questionDot = new TokenType("?.", { beforeExpr: true });
```

そして `?.` というコードに対して questionDot token を返すようにします。現在の babel ではパフォーマンスのため char code を使って文字を判断しているようですね。

```js:bablylon/lib/tokenizer/index.js
  Tokenizer.prototype.getTokenFromCode = function getTokenFromCode(code) {
    switch (code) {
      /* snip */
      case 63:
        var next = this.input.charCodeAt(this.state.pos + 1);
        if (next === 46) {
          // ?.
          this.state.pos += 2;
          return this.finishToken(_types.types.questionDot);
        } else {
          ++this.state.pos;return this.finishToken(_types.types.question);
        }
      /* snip */
    }
  };
```

ここまでで Tokenizer ができたので、今度は token から AST を作る方も修正します。`dot` から MemberExpression を作るところの横で、同じように `questionDot` から SafeMemberExpression を作ります。

```js:node_modules/babylon/lib/parser/expression.js
pp.parseSubscripts = function (base, startPos, startLoc, noCalls) {
  for (;;) {
    if (/* snip */) {
      /* snip */
    } else if (this.eat(_tokenizerTypes.types.questionDot)) {
      var node = this.startNodeAt(startPos, startLoc);
      node.object = base;
      node.property = this.parseIdentifier(true);
      node.computed = false;
      base = this.finishNode(node, "SafeMemberExpression");
    } /* snip */
  }
}
```

ここで試しに実行してみます。

```sh
$ node index.js
/Users/shuhei/work/js/null-friendly-js/node_modules/babel-core/lib/transformation/file/index.js:547
      throw err;
      ^

ReferenceError: unknown: unknown node of type "SafeMemberExpression" with constructor "Node"
    at CodeGenerator.print (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:69:13)
    at CodeGenerator.VariableDeclarator (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/generators/statements.js:292:10)
    at CodeGenerator._print (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:143:19)
    at CodeGenerator.print (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:88:10)
    at CodeGenerator.printJoin (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:178:12)
    at CodeGenerator.printList (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:243:17)
    at CodeGenerator.VariableDeclaration (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/generators/statements.js:275:8)
    at CodeGenerator._print (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:143:19)
    at CodeGenerator.print (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:88:10)
    at CodeGenerator.printJoin (/Users/shuhei/work/js/null-friendly-js/node_modules/babel-generator/lib/printer.js:178:12)
```

SafeMemberExpression の AST が Generator まで来ているので、パースがうまくいっていることがわかりますね！

## Transformer

次は Transformer を書きます。今回の変換の対象は以下です。

- メソッド呼び出し（callee が SafeMemberExpression）な CallExpression
- メソッド呼び出しでない SafeMemberExpression

書き方は [Babel plugin handbook](https://github.com/thejameskyle/babel-plugin-handbook) を参考に。`babel-template` を使えば AST を作るのも簡単ですね。

```js:index.js
var babel = require('babel-core');
var template = require('babel-template');

var buildReference = template('OBJECT == null ? OBJECT : OBJECT.PROPERTY');
var buildMethodCall = template('OBJECT == null ? OBJECT : OBJECT.METHOD(ARGUMENTS)');

function transformSafeMemberExpression(t) {
  var types = t.types;
  return {
    visitor: {
      SafeMemberExpression: function (path) {
        var safeReference = buildReference({
          OBJECT: path.node.object,
          PROPERTY: path.node.property
        });
        path.replaceWith(safeReference);
      },
      CallExpression: function (path) {
        var callee = path.node.callee;
        if (callee.type === 'SafeMemberExpression') {
          var safeMethodCall = buildMethodCall({
            OBJECT: callee.object,
            METHOD: callee.property,
            ARGUMENTS: path.node.arguments
          });
          path.replaceWith(safeMethodCall);
        }
      }
    }
  };
};

var code = [
  'var c = a?.b?.c;',
  'a?.b?.c(d, e);',
].join('\n');
var result = babel.transform(code, {
  plugins: [transformSafeMemberExpression]
});
console.log(result.code);
```

SafeMemberExpression なんてないよ、と言われてしまうので、ちょっと雑ですが以下のように AST type 定義に追加しておきます。

```js:index.js
var babelTypes = require('babel-types');

// For visitor validation.
babelTypes.TYPES.push('SafeMemberExpression');

// For node validation.
babelTypes.FLIPPED_ALIAS_KEYS['Expression'].push('SafeMemberExpression');
```

そして実行すると・・・

```sh
$ node index.js
var c = (a == null ? a : a.b) == null ? a == null ? a : a.b : (a == null ? a : a.b).c;
(a == null ? a : a.b) == null ? a == null ? a : a.b : (a == null ? a : a.b).c(d, e);
```

できました！同じチェックを何回もしているのが微妙ですが、ちゃんと変換できているようです。

## プラグイン化

上記のコード変更をうまいことモンキーパッチに変えて、プラグインモジュールを作りました。

[shuhei/babel-plugin-null-safe-accessor](https://github.com/shuhei/babel-plugin-null-safe-accessor)

普通の Babel プラグイン同様、以下のような感じで利用できます。

```sh
npm install -D babel-plugin-null-safe-accessor
```

```json:.babelrc
{
  "plugins": [
    "null-safe-accessor"
  ]
}
```

こちらでは、ついでに `a.b?()` のように関数/メソッドを null 安全に呼び出す SafeCallExpression も追加してあります。そのうち `?[]` にも対応してみたいですね。

## まとめ

- Java でできて JavaScript でできないことはない！（嘘）
- Babel に文法追加用の API はないけど、モンキーパッチすれば好きな文法を追加できる。