power-doctest

以前JavaScriptでdoctestを行う power-doctest を作った | Web Scratchという記事で紹介しましたが、

評価したい式; // => 期待する評価結果

と書くことで、これを以下のようなassertへ変換するツールを作っていました。

assert.equal(評価したい式, 期待する評価結果);

今回power-doctestをシンプルなものへと作りなおしました。

1.0未満のバージョンはツール自体に変換したコードの実行=>レポート表示の機能があったのですが、そこを削除して変換のみを行うように書き換えました。

実行機能はやっぱりとても複雑で、Nodeだけでも結構制御が大変なので、単純にassertに変換して後は他のツールと組み合わせて実行できるような形がいいかなと思いました。

使い方

azu/power-doctest

さきほども書いたように変換機能しかないのでライブラリとして使うのがいい気がしますが、単純なファイルを指定して変換するCLIの機能だけは入っています。

npm install -g power-doctest

example.jsというファイルを用意して

function sum(ary) {
    return ary.reduce(function (current, next) {
        return current + next
    }, 0);
}

var total = sum([1, 2, 3, 4, 5]);
total; // => 5

power-doctest /path/to/file.jsという感じでファイルを指定すると変換したコードををSourceMap付きで返してくれます。

$ power-doctest example/example.js
var assert = require('power-assert');
function sum(ary) {
    return ary.reduce(function (current, next) {
        return current + next;
    }, 0);
}
var total = sum([
    1,
    2,
    3,
    4,
    5
]);
assert.equal(assert._expr(assert._capt(total, 'arguments/0'), {
    content: 'assert.equal(total, 5)',
    line: 14
}), 5);
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbXX0

後は、これをただのJavaScriptとして実行すれば、power-assertを使った場合と同じような結果が得られます。

$ power-doctest example/example.js | node -p
AssertionError:   # at line: 14

  assert.equal(total, 5)
               |
               15

ブラウザでも同じように変換すれば動かすことができます。(この辺のSourceMapとかをもっと上手くやるためにはespowerifyのようなものが必要そう)

power-doctest example.js > powered-example.js
browserify -D -r power-assert powered-example.js > build.js

browser

ただのassertに変換するだけなので非同期テストも何もせずに対応できています。

ただ、今の所実行面のサポートがなんにもないので、なんか上手い方法が欲しくなりそうな感じですね。

仕組み

仕組み的にはASTを変換するということには全く変わりがないですが、変換の役割をはっきりさせたり、ASTを変換する部分以外を抽象化することで変換に集中しやすくするためのライブラリを書きました。

具体的な話をすると長くなるので細かい話は次回にして、次のようなライブラリをつくました。

ASTとコードを比較したり、ASTとASTを比較したりできるassertのようなライブラリです。 テストに使ってます。

import { parse } from "esprima";
import astEqual from "ast-equal"
var sourceCode = 'var a = "string";';
var actualAST = parse(sourceCode);
var expectedCode = 'var a = "string";';
var expectedAST = parse(expectedCode);

// AST === Code
astEqual(actualAST, expectedCode);
// AST === AST
astEqual(actualAST, expectedAST);

今回のAST変換でメインとなるライブラリですが、ASTはESTreeというデファクトが存在するので、本来どのパーサで作られたASTであるのかは気にする必要がありません。

しかし今回のようにコメントをassertに変換するAST変換関数はnode.trailingCommentsというようにESTreeでは標準化されたないコメントの位置に関する情報を使ったり、power-assertの変換をするespowerlocといったこちらも標準化されてない情報を使います。 (実際にはどのパーサも同じ情報をもっているのですが)

また、最近はesprima、espree、acorn、Babylonなど色々なパーサが存在するため、 ESTreeレベル(現在だとES6レベル)だと互換性はあるが、ES.nextやJSXといた拡張に対する扱いがあったりなかったりがパーサによって違います。

そのため、AST変換関数がそのパーサだったりを気にするのは本質的ではないなと思ったので、パーサやジェネレータを抽象化したazu/ast-sourceというライブラリを作りました。

結局はそのAST変換関数が受け付けるのはユーザが書いたコードであるためそれがパースできれば問題ないという方針で、自動的にパーサを切り替えたりするような仕組みが入ってます。(上手く動いてるのかまだ良くわかってない)

追記: escodegenやrecastなどESTree -> Codeがまだ完全ではないことが発覚した…

後はSourceMapを生成するジェネレータ部分の仕組みはいってるので簡単に扱えるようにする目的もあります(escodegenの使い方いつも忘れる)

power-doctestの本体とも言える、// =>assertに変換するAST変換関数です。

tagged template stringを使ったコードとAST Nodeを合成するtag関数です。

import {parse} from "esprima"
import toAST from "tagged-template-to-ast"
// AST
var nodeForInline = parse('"string"');
// コードでASTを合成する => 合成したASTを作れる
var astNode = toAST`var a = ${nodeForInline}`;
// astNode is the AST of `var a = "string";`

これを使うことで、AST変換する際にJSON的にNodeを色々書いたりしないで組み立てができるようになって意外と上手く動いてる感じです。

この辺の仕組みを使って書き換えたpower-doctest本体のコードは1/10ぐらいまで短くなりました。