ドキュメントはコードから自動生成させていきたい
ドキュメントは腐りやすい
ドキュメントはどうやっても更新されなくなってしまう。Wikiに書こうか、Confluenceのほうがいいのか、色々置き場所を考えてはみるものの、大きく改善することはあまりない。
その理由のひとつが、コードとドキュメントのライフサイクルが違うこと。元来ドキュメントは「コーディングの前に書くもの」であったし、その意識はなくてもPull Requestに「あのドキュメントも更新しておきます」と書いている時点で、それはライフサイクルが合っていないものを、信頼性の低いプロトコルで同期を取ろうとしているようなもの。
それを解決する手段のひとつが、コードとドキュメントを同じリポジトリに置くということだけれど、READMEが大量にできるというのもそれはそれで見通しもメンテナンス性もあまり良くない。
必要とされるドキュメントの形はさまざま
それなら、とコードからドキュメントを自動生成させたいと思う。動いているコードは常に現状を正しく表しているので、そこから生成されるドキュメントも常に正しい(バグがないとは言っていない)。
JSDoc(やその源流のJavaDocなど)がそれに近いアプローチをとっているが、あまりうまく活用できたことがない。
コードは変えたけどコメントを直し忘れた、というケースがあるように、やはり「コードがコメントの通りに動いていること」は理想だけどそうならないケースがある。もちろんちゃんと運用したら良いツールなんだけど、そこが少しもったいない。
もうひとつは自由度の低さというのか、スコープの違いというのか。普段アプリケーションを作っている身としては、関数一覧が並んでいたらそんなに嬉しい?という気持ちになるし、生成されたドキュメントを見るよりコードを直接見たほうが早い。チーム外の人のためにわざわざドキュメントを生成してもほとんど見られないだろうし、なによりも別に知りたいことは関数一覧じゃなかったりする。
だからコードから生成しつつ、自由度が高い方法が欲しかった。
babylonでコードからドキュメントを生成する
とは言ってもJSDocのアプローチは捨てがたく、より面白いことができないかとbabylonに手を付けてみたら思いの外簡単だった。
babylonはあのbabelが使っているAST(Abstract Syntax Tree)を作るparser、平たく言うと「ソースコードを読み込んでその構造をオブジェクトにしますよ」というやつ。コードをbabylonにかけると、「クラス名はこれですよ」「メソッドはこんなのがありますよ」というのが簡単に取れるというもの。babelはこれを使って、コードを構造化したあと、いい感じに変換していい感じに出力している。
どれくらい簡単かというと、こういうコードがあれば
class Hoge { getMsg1() { return "hello" } getMsg2() { return "world" } } module.exports = Hoge
こういうコードを書くだけでメソッド一覧が取得できる。
const { parse } = require('babylon') const traverse = require('babel-traverse').default const fs = require('fs') const code = fs.readFileSync('./index.js', 'utf-8') const ast = parse(code) let className let methods = [] traverse(ast, { Class: path => { className = path.node.id.name }, ClassMethod: path => { methods.push(path.node.key.name) } }) console.log(`class: ${className}, methods: ${methods}`) // => class: Hoge, methods: getMsg1,getMsg2
もちろんbabelのpluginでflowやJSXも読めるし、コード上のほぼすべての情報が取れるので、例えば「Componentを継承しているクラスの一覧がほしい」とかもすぐにできる。もちろんクラスやメソッド一覧だけじゃなくて、定数一覧でも何でも。
// pluginを使う const ast = parse(code,{ plugins: ["flow"]})
これを使って例えば「外部サービスに接続している場所一覧」みたいなのをMarkdown形式でファイルに出力しておけば、自由度が高く必要な情報だけに絞ったドキュメントが自動生成される環境が作れる。
コメントも価値あるものとなる
そしてそれにdoctrineというJSDocパーサーを合わせるのも結構面白かった。結局JSDocに戻ってるじゃん、という気持ちにもなるが、例えば別ドキュメントへのリンクとか、あまり更新されないコメントであれば保守にも耐えられるだろう。たぶん。
先ほどの例でいくと、
/** * @see http://example.com */ class Hoge { getMsg1() { return "hello" } getMsg2() { return "world" } } module.exports = Hoge
というものをdoctrineで読み込むとこうなる。
const { parse } = require('babylon') const traverse = require('babel-traverse').default const fs = require('fs') const doctrine = require("doctrine") const code = fs.readFileSync('./index.js', 'utf-8') const ast = parse(code) let className let link let methods = [] traverse(ast, { Class: path => { className = path.node.id.name const comments = path.node.leadingComments.map(comment => comment.value) if (comments.length > 0) { const tags = doctrine.parse(comments[0], { unwrap: true }).tags link = tags.find(tag => tag.title === "see").description } }, ClassMethod: path => { methods.push(path.node.key.name) } }) console.log(`class: [${className}](${link})`) // => class: [Hoge](http://example.com/juuyouna/reference)
どういうドキュメントがあると嬉しいのか、それはどういうアーキテクチャなら簡単に生成できるのか、アイデアがあれば何でも出来そうな気持ちに久しぶりに楽しくなった。