TSDocとCompiler API

TS Compiler API座談会

#tsc_api_study @akito0107

Introduction

自己紹介

  • 伊藤 瑛(いとう あきと)
  • @Akito0107 Twitter / Github / blog
  • TypeScript / Go をよく書いています
  • Test技術に興味があります

書いたもの

  • favalid
    • frontend向け軽量validator
  • catacli
    • TypeScript特化のcommand line parser

今日のテーマ

  • コメント

  • (Compiler API的には Trivia と呼ばれる)
  • TypeScriptのDoc Commentの仕様であるTSDocの紹介
/**
 * これとか
 */
function aaa(a: number, b: string) {
  // これ
}

DocComments

DocCommentsとは

  • ソースコード書かれたコメントからDocumentationを出力する仕組み
/**
 * sum function
 *
 * @remarks
 * demo
 *
 * @param a
 * @param b
 */
export function sum(a: number, b: number) {
  return a + b;
}  

ex) TypeDoc

TypeScriptのDocComments

  • Third Partyのツールが乱立するなか、microsoft/tsdocが出てきた
  • TypeScript Compilerの開発チームもメンテナに含まれており、これが標準になっていく雰囲気
  • 現時点(2020/3)で仕様等のドキュメントはなし
  • TSDocに準拠したドキュメンテーションツールもなし
  • (*上記のrepositoryにはparser/eslint-pluginなどが含まれていて、ドキュメンテーション生成ツールは含まれていない)

TSDoc今わかっていること

  • JSDoc Basedのsyntaxになる
  • VSCodeが対応していて、TSDocのSyntax Hightlightingなどが効く
  • customTagによる拡張が可能

TSDocをさわってみる

  • 典型的なTSDocのフォーマット
  • 4つのsectionから構成される
/**
 * 1. Summary Section
 * コンポーネントのサマリーを完結に書く
 * Webなどで表示するとき、タイトルやindexになる部分
 *
 * @remarks
 * 2. Remark Block
 * コンポーネントの詳細を書く
 * 
 *
 * 3. Additional Block(s)
 * コンポーネントの具体的な仕様などを書く。@param, @example, @return など
 * @param a
 * @param b
 * 
 * (4. modifier tags)
 * blockには紐づかない。コンポーネントの状態(aspect)などを書く
 * @beta @public @private など
 */
export function sum(a: number, b: number) {
  return a + b;
}  

TSDocのtags

  • Block Tag
    • tagの後に文字(block)が入る
    • ex) @remarks, @param, @example など
  • Modifier Tag
    • blockが紐づかないtag
    • ex) @private, @public, @beta など
  • Inline Tag
    • どのsectionにでも書けるtag {} で囲う。
    • ex) {@link}など

その他TSDoc役立ちそうな?Link集

  • playground
  • tsdoc-config
    • customTagをparserのコードを書かずにjsonの定義ファイルだけで拡張出来るようにする仕組み
  • tsdocでsupportしているtag一覧
    • ここのコードに書いてある
  • api-extractorのdoc
    • TSDocのメンテナと同じ人が作っているpackage、TSDoc自体のドキュメントではないが、Syntaxに関するDocumentとしては一番情報がある。

TSDocとDocumentation Tests

Documentation Tests

  • documentationに書いたsample codeを実行し、sample codeが実際に正しく実行されるかどうかをチェックする仕組み
  • Python, Elixir, Rustなどなどで実装されている
  • Documentationのメンテナンスし忘れを防止出来る

ex) Rust

/// ```
/// let result = add::sum(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn sum(a: i32, b: i32) -> i32 {
    a + b
}

TSDocとjestを使って作ってみた

  • tsdoc-testify
  • @example blockを整形して、jestで実行出来るようなtestのファイルにして書き出すツール
/**
 * sum function
 *
 * @remarks
 * demo
 *
 * @example
 *
 * ```
 * import * as assert from "assert";
 * import { sum } from "./sample";
 *
 * assert.equal(sum(2, 1), 3);
 * ```
 */
export function sum(a: number, b: number) {
  return a + b;
}

から

import * as assert from "assert";
import { sum } from "./sample";
test("/Users/akito/workspace/tsdoc-testify/examples/sample.ts_0", () => {
  assert.equal(sum(2, 1), 3);
});

が生成される。

demo

  • 基本的なコード生成
  • custom tags

実装の流れ

  1. 対象のfileをTS Compiler APIでparse
  2. DeclartionKindのNodeを抽出し、exportされている関数・クラスを取得
  3. syntaxKindにより ts.getTrailingCommentRangests.getLeadingCommentRanges を使い分け ts.CommentRangeを取得する
  4. 取得したts.CommentRangeとそれに紐づくts.Nodetsdoc.TSDocParserでさらにparseし、@exampleブロックを集める
  5. @example blockのtsのコードをTS Compier APIでさらにParse、importのblockとコードのblockを分ける
  6. 各exampleのimportblockをmergeし、コードのblockをtestCallExpressionのbodyにいれたASTを作り、ASTをprintする

実装の流れ

  1. 対象のfileをTS Compiler APIでparse
  2. DeclartionKindのNodeを抽出し、exportされている関数・クラスを取得
  3. `syntaxKind`により `ts.getTrailingCommentRanges` と `ts.getLeadingCommentRanges` を使い分け `ts.CommentRange`を取得する

  4. 取得した`ts.CommentRange`とそれに紐づく`ts.Node`を`tsdoc.TSDocParser`でさらにparseし、`@example`ブロックを集める

  5. @example blockのtsのコードをTS Compier APIでさらにParse、importのblockとコードのblockを分ける
  6. 各exampleのimportblockをmergeし、コードのblockをtestCallExpressionのbodyにいれたASTを作り、ASTをprintする

Triviaの取得

  • Compiler API的にはCommentはTriviaとして扱われる(*Trivia = 取るに足らないもの)
  • Commentそのものを直接取得するAPIはないが、コメントが書かれている範囲(CommentRange)を取得することで、TSDoc Parserでparseに必要な情報を集めることができる
  • 対象のNodeの前に定義されているrangeを取得するには ts.getLeadingCommentRanges を使う
  • 対象のNodeの後ろ(改行まで)に定義されてるrangeを取得するにはts.getTrailingCommentRangesを使う
/**
 * comment A (ts.getLeadingCommentRangeを使う)
 * 
 * @remarks
 * remarks
 */
export const a = {...} // commentB (ts.getTrailingCommentRangesを使う)

実際のコード (抜粋)

export function extractComments(source: ts.SourceFile) {

  function walk(node: ts.Node) {
    if (!isDeclarationKind(node.kind)) {
      node.forEachChild(walk);
      return false;
    }

    const buffer = source.getFullText();
    // ファイルのfull textが必要
    const range = ts.getLeadingCommentRanges(buffer, node.pos)

    ...
    
    node.forEachChild(walk);
    return false;
  }
  walk(source)

TSDoc Parserの使い方

  • コメントをparseするためには parseRangeparseStringのメソッドがある
  • TS Compiler APIと組み合わせるには parseRange の方が便利
  • parseRangeを呼び出すためには、ts.CommentRage ではなく tsdoc.TextRangeが必要だが、tsdocに変換するメソッドが提供されている
  • DocCommentのASTが取得できたら、あとはAST Viewerを見ながらなんとかする

const textRange: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(
          buffer, // ts.SourceFileのgetFullText()から
          range.pos,
          range.end
);

const tsdocParser: tsdoc.TSDocParser = new tsdoc.TSDocParser(config);

const parserContext = tsdocParser.parseRange(comment.textRange); 
return parserContext.docComment; // docのASTが入っている

(Tips) TSのソースコード生成

  • ts.createSourceFile & ts.createPrinter を使ってASTからソースコードを生成する
  • 複数のstatementsをfileに書き込む時は、ts.updateSourceFileNodeを使うと便利
  const ast = ts.updateSourceFileNode(
    ts.createSourceFile( // 空のfileを作る
      "filename",
      "",
      ts.ScriptTarget.Latest,
      false,
      ts.ScriptKind.TS 
    ),
    [...statements] // ここに書き込む対象のstatementsを渡す
  );

終わりに

もうちょいしたかった話

  • typescript-json-schema
    • TypeScriptの型定義からjson schemaを生成するツール
    • コメントにより、TypeScriptには存在しない型を定義できる( =integer など)
  • コメントをうまく使えば、ソースコード自動生成の幅を広げられそう(な気がする)
export interface Shape {
    /**
     * The size of the shape.
     * 
     * @minimum 0
     * @TJS-type integer
     */
    size: number;
}

まとめ

  • DocCommentsとTSDocにまつわる話
    • TSDocの仕様と現状について
  • Documentation Tests
    • Compiler APIとTSDoc Parserで擬似的に再現してみた
  • Compiler APIのtipsとTSDoc Parserの使い方

ありがとうございました