ESDoc - The Good Documentation For JavaScript

こんにちは。会員事業部の丸山@h13i32maruです*1

ソフトウェアのドキュメント(マニュアル)を書くには色々なツールや方法があります。 JavaScriptの場合はJSDocというドキュメンテーションツールがデファクトスタンダードです。 ですが、JavaScriptの最新仕様であるECMAScript2015(ES2015)がリリースされたことにより、 JSDocの競合として新しいツールが登場したり、JSDoc自体もバージョンアップしたりしています。 本記事ではそうした新しいツールの一つであるESDocを紹介します。

ESDocとは?

ESDocとは私が2015年4月から開発を行っている、JavaScript(ES2015)向けの良いドキュメントを作るためのドキュメンテーションツールです。

最近ではReactiveX/RxJSLinkedIn/hopscotchLonlyplanet/rizzo-nextなどのプロダクトに採用され始めています。もちろんESDoc自身もESDocでドキュメントが書かれてています。

f:id:h13i32maru:20151218113731p:plain ESDocで作成されたESDocのドキュメント

良いドキュメントとは?

本来なら、良いドキュメントとはどういうものなのかを解説したいのですが、そのためには「ドキュメントとはそもそもどういうものなのか?」「ドキュメントはなぜ書くのか?」「ドキュメントはどういう構造をしているのか?」「ドキュメントの評価はどうすればよいのか?」などについて解説する必要があります。ですが、このブログはそれを書くには狭すぎるため*2、今回は割愛します。

このようなドキュメントに対する根本的な考えをThe Web Explorerにて紹介しています。The Web Explorerは多数の著者による共著になっており、ESDoc以外にもWebの最先端の話が収録された書籍になりますので、興味のある方は是非御覧ください。

f:id:h13i32maru:20151218113818p:plain
The Web Explorerの表紙

どうやって良いドキュメントを作るのか?

ESDocの基本的な動作はJSDocやJavadocと同じくソースコード中に特定の記法で書かれたコメント(ドキュメントコメント)を元にドキュメントを作成するツールです。

/**
 * sums two values.
 * @param {number} a - a first value.
 * @param {number} b - a second value.
 * @return {number} result value.
 */
function sum(a, b) {
  return a + b;
}

しかし、JSDocやJavadocはソフトウェアのAPIリファレンスを作成することを目的としていますが、ESDocの目的はそれだけではありません。 ESDocの目的はソフトウェアを使用するために必要な情報全般を含んだドキュメントを作成することです。 ここでいうドキュメントとは一般的にマニュアルと呼ばれ、インストール方法、チュートリアル、サンプル、変更履歴などの情報をまとめたものを指します。

そして、ESDocは良いドキュメントを作成するために次のような特徴的な機能を提供しています。

  • マニュアルの統合
  • カバレッジ
  • Lint
  • 静的解析
  • テストコードの統合
  • 組み込み検索
  • ホスティング

以降ではこれらの機能を簡単に紹介していきます。

マニュアルの統合

マニュアルの統合機能はMarkdownで書かれた複数のファイルをドキュメントに組み込むという機能です。 これによって、インストール方法、チュートリアル、サンプル、変更履歴など、ドキュメント(マニュアル)に必要な内容を包括的に作成できます。

f:id:h13i32maru:20151218113844p:plain
マニュアルとして組み込まれたチュートリアル

カバレッジ

カバレッジ機能とはテストカバレッジと同じように、ドキュメントが書かれるべきところ(クラスやメソッド)にどれだけ書かれているかを確認する機能です。 ドキュメントのカバレッジを可視化することで継続的にドキュメントを保守するモチベーションに繋がると考えています。 このカバレッジ機能についてひとつ注意してほしいことは、かならずしもカバレッジ100%を目指す必要はないということです。 あくまでもドキュメントの継続性を改善するためとして、どの程度のカバレッジが必要かは各開発者の判断にお任せします。

f:id:h13i32maru:20151218120903p:plain
ドキュメントを書くべき箇所(350)と実際に書かれている箇所(323)

f:id:h13i32maru:20151218113901p:plain
全体のカバレッジと各ファイルごとのカバレッジ

Lint

Lint機能とはドキュメントがコードに対して間違っていないかを検証するための機能です。 あくまでもドキュメントとコードの不一致を検証するのであって、ドキュメント自体が正しいかどうかを検証できるものではありません。 今のところ、このLintが対象にするのはメソッドのシグネチャとドキュメントが一致しているかどうかだけです。

f:id:h13i32maru:20151218113918p:plain
仮引数はpだが、ドキュメントにはxと記載されているため警告を表示

静的解析

静的解析機能とはドキュメントコメントが書かれていないコードからもそのコードから分かる情報をつかってなるべくドキュメントを生成するというものです。例えば次のようにドキュメントが一切書いていないソースコードの場合でも「Class構文によるクラスと継承」「デフォルト引数による仮引数の型」「return文による戻り値の存在」「テンプレートリテラルによる文字列型」を解析することで、ある程度ドキュメントを作成することができます。

export default class Foo extends Bar {
  baz(p = 'Alice') {
    return `hello ${p}`;
  }
}

テストコードの統合

テストコードの統合機能とはテストコード自体も有益なドキュメントと考え、テストコードとテスト対象を関連付けてドキュメントを生成するというものです。テストコードとテスト対象を次のように@testを使って関連付けると、生成されたFooクラスのドキュメントにはテストコードへのリンクが、テストコードの一覧にはテスト対象へのリンクが自動的に追加されます。

/** @test {Foo} */
describe('Foo is useful class', ()=>{
  /** @test {Foo#bar} */
  it('is useful method', ()=>{
    const foo = new Foo();
    assert(typeof foo.bar, 'function');
  });
});

f:id:h13i32maru:20151218113931p:plain
クラスをテストしているテストコードへのリンクを表示

f:id:h13i32maru:20151218113933p:plain
テストコードが対象としているクラスやメソッドへのリンクを表示

組み込み検索

組み込み検索機能とはその名前のとおり、ドキュメント内を検索できる機能です。 この組み込み検索機能はJavaScriptだけで実装されているため、サーバ側の実装は一切不要です。 静的ファイルとしてWebサーバで配信する、もしくはローカルのファイルをブラウザで直接開いて検索できます。

f:id:h13i32maru:20151218113950p:plain
ドキュメント内を任意のキーワードで検索

ホスティング

ドキュメントを作成したあと、どこかで公開して誰でも閲覧できるようにする必要があります。 そこで手軽にドキュメントを公開できる仕組みとして、ESDoc専用のホスティングサービスを提供しています。 このホスティングサービスはESDocを使用しているGitHubのリポジトリURLを登録するだけで利用できます。

f:id:h13i32maru:20151218114003p:plain
ホスティングサービスに登録されているドキュメント

f:id:h13i32maru:20151218114006p:plain
ホスティングサービスへの登録方法

おわりに

ソフトウェアはコードとテスト、そしてドキュメントから成り立つものです。コードとテストはたくさんの解説書や議論がありますが、ドキュメントはそうではありません。そのためドキュメンテーションツールもあまり進化していません。そのような状況に何かしら一石を投じたいと思い、ESDocを開発しています。もしこれを期にドキュメントについて興味を持たれた方がいらっしゃれば是非、ドキュメントについても考えてみてください。より良いドキュメントはより良いソフトウェアを作ります。

*1:CodeLunch.fmもよろしくお願いします!

*2:フェルマーの言葉より

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/