articles
TSDoc と documentation tests
はじめに
この記事はTypeScript アドベントカレンダー7 日目の記事です。 昨日はmasanori_msl さんの記事でした。
TypeScript の Doc Comment の仕様として、TSDocがあります。 TSDoc 自体の仕様はinitial の draftも出ていない状況で、エコシステムが整いきっているとはまだ言い難い状況ですが、 すでに VSCode ではその Syntax Highlight に対応しているなど、今後は TSDoc が TypeScript の Doc Comment の標準的な仕様になるかと思われます。
上記の repository にリファレンス実装として Parser が提供されていたので、今回はその Parser を使って簡単なアプリケーションを書いてみました。
Rust のdocumentation testsや、Go のExamplesのように、 Doc Comment に埋め込んだ example コードを実行して、その正当性をチェックするツールです。
この記事では TSDoc について簡単に整理した上で、実際に作ったものの紹介をします。
作ったものはこちら
TSDoc
コードにコメントを書き、そこから API 等のドキュメントを生成する仕組みは多くの言語で幅広く採用されています。この記事ではその仕組みを Doc Comment
と呼びます。
TypeScript にはいくつかの Doc Comment の仕様がありますが、現在ですと、
Microsoft も含むチームで仕様の策定を進めているTSDocが今後標準的な位置付けになっていくかと思います。
TSDoc 自体は JSDoc に loosely based
とのことで、JSDoc でコメントを書いていたユーザーからすると、馴染みやすいものになっていると思います。
TSDocの repository では RFC に関する議論や、 その標準実装の Parser が提供されているだけで、この repository には documentation 生成ツールが含まれているわけではありません。 つまり実際に Doc Comment からなんらかの html や markdown の documentation を生成するためには、別のツールを使う必要があります。
api-extractorを使えば TSDoc に乗っ取った Documentation を生成することができますが、
api-extractor
は documentation 以外の範囲もカバーするツールなので、シンプルに documentation だけを生成したいといったユースケースに対応するようなツールは見つけることができませんでした
(もし知っている方がいらっしゃいましたら教えてください...)
documentation の生成を気楽に試すようなツールはまだありませんが、 前述の repository でTSDoc Playgroundという Web 上で TSDoc を試せるツールがあるので、そちらでおおまかな雰囲気をつかめるかと思います。
TSDoc の status としては、2019 年 12 月 7 日の時点で initial draft がまだ出ていない段階ですので、今後、細かいところは変わってくる可能性があります。 詳しくはREADME の Roadmapを参照してみてください。 TSDoc の詳細な Syntax については、現在はプロジェクトの Status として前述のような状態ですので、完全なドキュメントは見つけられませんでした。 ですが、api-extractorに参考になりそうなドキュメントを見つけましたので、 実際に Doc Comment を書く際には参照してると良いかもしれません。
tsdoc-testify
実際に作ったツールの紹介をします。
今回作ったツールは、TSDoc の@example
block に書かれたコードを整形し、jest
等で実行可能な test code として出力するものです。
Rust の documentation testsのようなことを実現するツールで、
example で書かれたコードが実際に valid なものかどうかをチェックすることができるようになります。
例えば、API の変更があった際の documentation のチェックや、簡単なものだと、example のコードに typo が紛れていないかのチェックをすることができます。
僕自身、documentation を 書かない 雑に書くことが多く、書いたとしてもそのメンテナンスが億劫になってしまうことがよくあり、そのチェックの手間を減らしたいと思ってこのツールを作成しました。
Getting Started
cli ツールとして publish しているので、npm or yarn で install してください。
$ npm install -g tsdoc-testify
TSDoc の Style でコメントを書いた .ts
のファイルを用意してください。今回は以下のようなファイルを用意します。
/**
* sum function
*
* @remarks
* demo
*
* @example
*
* ```
* import * as assert from "assert";
* import { sum } from "./sample";
*
* assert.equal(sum(2, 1), 3);
* ```
*
* @param a
* @param b
*/
export function sum(a: number, b: number) {
return a + b;
}
このファイルを sum.ts
という名前で保存し、以下のコマンドを実行してください。
$ tsdoc-testify --filepath path/to/sum.ts
filepath の正規表現を使いたいときは --fileMatch
のオプションを使ってください。
$ tsdoc-testify --fileMatch 'path/to/**/*.ts'
すると、sum.doctest.ts
というファイル名で同じディレクトリに以下のファイルが生成されていると思います。
// Code generated by "tsdoc-testify"; DO NOT EDIT.
import * as assert from "assert";
import { sum } from "./sum";
test("/Users/akito/workspace/tsdoc-testify/examples/sum.ts_0", () => {
assert.equal(sum(2, 1), 3);
});
あとはのコードを jest
で実行すると、 @example
のコードをチェックすることができます。
% yarn jest sum.doctest.ts
yarn run v1.19.2
$ /Users/akito/workspace/tsdoc-testify/node_modules/.bin/jest sum.doctest.ts
PASS examples/sum.doctest.ts
✓ /Users/akito/workspace/tsdoc-testify/examples/sum.ts_0 (1ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.999s
Ran all test suites matching /sum.doctest.ts/i.
✨ Done in 3.53s.
以上が基本的な使い方です。基本的に一つのソースファイルに対応して一つのテストが生成されます。
現在ですと、 import
のパスを修正する機能がないので、example 対象のコードを import { sum } from "./sum"
として import
読み込む必要があります。
(ここは修正する予定で、auto で import させるか path の修正の機能をつけるかする予定です。)
以下のように、複数の @example
で同じ module を import
していても、生成される test code では import
はまとめて出力されるので、
syntax としては valid なコードが吐き出されるかと思います(そうでなかったらバグなので、報告していただけると助かります)
/**
* sum function
*
* @remarks
* demo
*
* @example
*
* ```
* import * as assert from "assert";
* import { sum } from "./math";
*
* assert.equal(sum(2, 1), 3);
* ```
*
* @param a
* @param b
*/
export function sum(a: number, b: number) {
return a + b;
}
/**
* sub function
*
* @example
*
* ```
* import * as assert from "assert";
* import { sub } from "./math";
*
* assert.equal(sub(4, 5), -1);
* ```
* @param a
* @param b
*/
export function sub(a: number, b: number) {
return a - b;
}
generate される file
// Code generated by "tsdoc-testify"; DO NOT EDIT.
import * as assert from "assert";
import { sum, sub } from "./math";
test("/Users/akito/workspace/tsdoc-testify/examples/math.ts_0", () => {
assert.equal(sum(2, 1), 3);
});
test("/Users/akito/workspace/tsdoc-testify/examples/math.ts_1", () => {
assert.equal(sub(4, 5), -1);
});
Custom Tag
@exampleCaseName
デフォルトで、test の名前 (test の第一引数の文字列)は 生成元ファイル + 順番の番号
となっています。
これを、 @exampleCaseName
という inline tag
を用いて修正することができます。
/**
* sum function
*
* @remarks
* demo
*
* @example
* {@exampleCaseName custom case name}
*
* ```
* import * as assert from "assert";
* import { sum } from "./math";
*
* assert.equal(sum(2, 1), 3);
* ```
*
* @param a
* @param b
*/
export function sum(a: number, b: number) {
return a + b;
}
// Code generated by "tsdoc-testify"; DO NOT EDIT.
import * as assert from "assert";
import { sum } from "./math";
test("custom case name", () => {
assert.equal(sum(2, 1), 3);
});
@ignoreExample
@example
に擬似コードを使いたい場合や、そのままでは動かせないようなコードを書きたい場合、そのままテストコードとして生成されたら困るケースがあるかと思います。
その際は、 @ignoreExample
という inline tag
を用いて生成を skip させることができます。
/**
* sum function
*
* @remarks
* demo
*
* @example
* {@ignoreExample}
*
* ```
* import * as assert from "assert";
* import { sum } from "./math";
*
* 擬似コードなどを書きたい。
* assert.equal(sum(2, 1), 3);
* ```
*
* @param a
* @param b
*/
export function sum(a: number, b: number) {
return a + b;
}
これで、この @example
ブロックの生成は抑制されます。
実装
最後に簡単に実装について触れます。前述した通り、 tsdoc-testify
では tsdoc
の parser を使っています。
それ以外にも、 TypeScript の compiler API を用いて、ソースコード生成と、 import
等の整形を行っています。
TypeScript の Compiler API については他にもドキュメントが多くあるかと思うので、ここでは tsdoc
の Parser の使い方を紹介します。
comment を parse する
すぐ上で TypeScript の Compier API には触れないと言いましたが、実は tsdoc の parser を使って実際のファイルのコメントを取得するためには、まずは TypeScript の Compiler API を使って TypeScript の AST を取得する必要があります。(正確言うと、must ではないのですが、Compiler API を使った方が簡単に実現できます。)
先ほどの sum.ts
を parse して、AST を取得します。
import * as ts from "typescript";
import * as path from "path";
import * as fs from "fs";
const source = ts.createSourceFile(
filepath,
fs.readFileSync(filepath).toString(),
ts.ScriptTarget.ES2015
);
forEachChild
メソッドを call して、ts.SourceFile
の子の node を辿ります。
TS AST Viewer
で見ると確認できるかと思いますが、今回のケースでは FunctionDeclaration
の node が該当します。
forEachChild
の中で、 ts.isFunctionDeclaration
でチェックして、目的の node
だけを対象にします。
const commentRanges: ts.CommentRange[] = [];
const fullText = source.getFullText();
source.forEachChild(node => {
if (!ts.isFunctionDeclaration(node)) {
return false;
}
commentRanges.push(
...(ts.getTrailingCommentRanges(fullText, node.pos) || []) // <= これがTrivia API
);
});
console.log(commentRanges);
TypeScript の Compiler API には、Comment を扱うための API があります。TypeScript Deep Dive の Trivia APIを参照してみてください。 この API では、Comment の中身はパースできませんが、コメントがどの position から始まって、終わっているのかの range を取得することができます。
この range
と、 source code のファイル全体の文字列を tsdoc の parse に渡すことにより、comment の Node が取得できます。
const textRange = commentRanges[0];
const textRange = tsdoc.TextRange.fromStringRange(
fullText,
range.pos,
range.end
);
const config = new tsdoc.TSDocConfiguration();
const tsdocParser: tsdoc.TSDocParser = new tsdoc.TSDocParser(config);
const parserContext = tsdocParser.parseRange(textRange);
console.log(parserContext.docComment); // <= ここにparseされたコメントのNodeが入っている
tsdoc.TextRange.fromStringRange
に comment の range と source code の fullText を渡し、tsdoc のTextRange
を生成した上で、
TSDocParser
の parseRange
を呼び出します。
TSDocParser
には parseString
のメソッドもありますが、より実践的には TS Compiler API と組み合わせて、parseRange
を使った方が利便性が高いと思います。
より詳しい Example はtsdoc repository の api-demoを参照してください。
custom tag の handling
今回のケースの @ignoreExample
や、 @exampleCaseName
のように、custom な tag の handling も比較的簡単にできます。
TSDocConfiguration
を new
した上で、TSDocTagDefinition
を add すれば Node として handling してくれます。
const config = new tsdoc.TSDocConfiguration();
const exampleCaseName = new tsdoc.TSDocTagDefinition({
tagName: "@exampleCaseName",
syntaxKind: tsdoc.TSDocTagSyntaxKind.InlineTag
});
const ignoreCase = new tsdoc.TSDocTagDefinition({
tagName: "@ignoreExample",
syntaxKind: tsdoc.TSDocTagSyntaxKind.InlineTag
});
config.addTagDefinitions([exampleCaseName, ignoreCase]);
SyntaxKind は、用途に合わせて、 BlockTag
, InlineTag
, ModifierTag
を選択します。
この辺りも、前述のapi-demoのコードが参考になるかと思います。
まとめ
TSDoc についての紹介と、TSDoc の parser を使ったアプリケーションの紹介を行いました。 TSDoc 自体はまだまだ出てきたばかりの仕様ですが、将来的にはこれが TS のスタンダードになっていくのかなと思っています。
メンテナンスしやすいドキュメンテーションに少しでも近づけばと思い、このツールを作ってみました。
明日はpco2699 さんの記事です!
About
技術ネタ中心にその他雑多なことを。