ESLint を使って JSDoc / TSDoc の記述を必須化する
これはなに
コードベースに対し JSDoc の記述を必須化するための ESLint 設定手順をまとめたものです。
JSDoc を始めとする Doc コメントはコードに最も近いドキュメントであり、これがあるのと無いのとではコードベースの保守性に天と地ほどの差が生まれます。そんな JSDoc ですが、OSS ならともかく(内製・受託を問わず)商業ソフトウェア開発の現場では軽視されがちです。後からコーディング規約を定めたところで開発メンバーにドキュメントを書く習慣が備わっていなければ書き漏れが頻発するのが関の山です。
コードレビューで都度指摘するにはあまりにコストがかかるため、ESLint に委ねるのが望ましいです。
前提
- フレームワークは React(or Next.js)を使っている。
- TypeScript を主体としている。
- ビルドスクリプトや設定ファイルは JavaScript も併用している。
最終的な設定内容
.eslintrc.js
module.exports = {
extends: ["plugin:jsdoc/recommended-typescript-error"],
plugins: ["jsdoc"],
rules: {
// Enable
"jsdoc/check-param-names": [
"error",
{
checkDestructured: false,
},
],
"jsdoc/check-tag-names": [
"error",
{
definedTags: ["remarks", "typeParam"],
},
],
"jsdoc/require-description": [
"error",
{
contexts: [
"ArrowFunctionExpression",
"ClassDeclaration",
"ClassExpression",
"FunctionDeclaration",
"FunctionExpression",
"MethodDefinition",
"PropertyDefinition",
"VariableDeclaration",
"TSInterfaceDeclaration",
"TSTypeAliasDeclaration",
"TSPropertySignature",
"TSMethodSignature",
],
},
],
"jsdoc/require-hyphen-before-param-description": ["error", "always"],
"jsdoc/require-jsdoc": [
"error",
{
publicOnly: true,
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionDeclaration: true,
FunctionExpression: true,
MethodDefinition: true,
},
contexts: [
"PropertyDefinition",
"VariableDeclaration",
"TSInterfaceDeclaration",
"TSTypeAliasDeclaration",
"TSPropertySignature",
"TSMethodSignature",
],
checkConstructors: false,
},
],
"jsdoc/require-param": [
"error",
{
checkDestructuredRoots: false,
},
],
"jsdoc/tag-lines": [
"error",
"always",
{
startLines: 1,
applyToEndTag: false,
},
],
"jsdoc/sort-tags": [
"error",
{
reportIntraTagGroupSpacing: false,
},
],
// Disable
"jsdoc/require-returns": ["off"],
},
overrides: [
{
files: ["*.js", "*.mjs", "*.cjs"],
rules: {
"jsdoc/require-param-type": ["error"],
"jsdoc/require-returns": ["error"],
"jsdoc/require-returns-type": ["error"],
"jsdoc/no-types": ["off"],
},
},
],
};
ESLint プラグインを導入する
以前は ESLint 本体に JSDoc に関するルールが組み込まれていましたが、現在それらは廃止されているため上記のプラグインを導入します。
npm install -D eslint eslint-plugin-jsdoc
eslint-plugin-tsdoc は使用しない
このプラグインは JSDoc が TSDoc の仕様を満たしているかを検証するだけであり、これに頼らず eslint-plugin-jsdoc だけでも TSDoc の検証は十分可能なので見送ります。
ESLint を設定する
プラグインを読み込む
{
"plugins": ["jsdoc"],
}
eslint-plugin-jsdoc
という名称なので、 plugins
フィールドに "jsdoc"
を追加します。
ルールプリセット(extends)を読み込む
ベースとなるプリセット(extends)を読み込みます。eslint-plugin-jsdoc
には 52 のルールが存在し、うち最大 30 個が推奨ルールとされています。ひとつひとつ自分でルールを吟味して設定してもいいですが、さすがに数が多すぎるので素直にプリセットを利用します。数種類のプリセットが用意されていますが、TypeScript を主体としたコードベースであるのと厳格化したいことから recommended-typescript-error
を選択します。
{
"extends": ["plugin:jsdoc/recommended-typescript-error"]
}
ひとまずこれで動作するようになったものの、以下の課題が残っています。
As-IS | To-Be |
---|---|
エクスポートされていないモジュールも検閲される。 | エクスポートされたモジュールのみを検閲対象とし、関数コンポーネント内で定義したコールバック関数といったローカルなものは除外する。 |
分割代入しているパラメーターがあると、中身の各値ごとに @param の記述を強制される。 |
省略可能にする。大抵の場合は別途型定義しているため、そちらに JSDoc を記述すればよい。 |
検閲対象が関数宣言(function )のみとなっている。 |
関数式(Arrow func)、クラス宣言、変数および型定義も対象に含める。 |
Type Alias や Interface など TypeScript 固有のモジュールが対象外となっている。 | これらも対象に含める。 |
概要説明が省略可能になっている。 | 記述必須とする。 |
戻り値に関する記述が必須となっている。 | TypeScript であれば型推論によってある程度補完できるので省略可能にする。ただし JavaScript コードに関しては記述必須とする。 |
TSDoc 特有のタグが不正とみなされている。 |
remarks , typeParam など有用なものは使用可能にする。 |
一切の空行が許容されていない。 | 可読性を損なうのでタグブロックごとの空行を必須化する。 |
タグが自由な順序で記述可能となっている。 | 一貫した順序に統一する。 |
これらをすべて解消すべく、ここからルールをチューニングしていきます。
エクスポートされたモジュールのみを検閲対象とする
publicOnly
プロパティを true
にすることで JSDoc の強制対象をエクスポートしているものに限定します。
{
"rules": {
"jsdoc/require-jsdoc": [
"error",
{
"publicOnly": true,
},
],
}
}
一方で React コンポーネントの Props 型定義のようにエクスポートしていなくとも JSDoc を必須にしたくなるケースもあります。
type Props = {
/** 子要素 */
children: ReactNode;
/** 入力欄コントロールの名前 */
name: string;
};
export const FancyButton = ({ children, name = "" }: Props) => (
<button name={name}>{children}</button>
);
しかし上記の Props を対象に含めるには publicOnly
を false
にせざるを得ません。そうなると今度は React コンポーネント内のコールバック関数の宣言までも対象となってしまいます。
const App = () => {
const [count, setCount] = useState(0);
// この関数にも JSDoc の記述を強制されてしまう。
const handleClick = () => setCount((count) => count++);
return <button onClick={handleClick}>{count}</button>;
};
さすがにこれら全てに JSDoc を記述するのは過剰でしょう。どちらを取るか悩ましいですが、今回はこのような internal なものを対象外とし、 Props 型定義への記述は運用でカバーします。
@param
を省略可能にする
分割代入しているパラメーターの
React コンポーネントの Props は分割代入で受け取ることがセオリーですが、 require-param
を有効にしただけだと以下のような記述を要求されます。
type Props = {
/** 子要素 */
children: ReactNode;
/** 入力欄コントロールの名前 */
name: string;
};
/**
* おしゃれなボタンです。
*
* @param root0 FancyButton のプロパティ
* @param root0.children 子要素
* @param root0.name 入力欄コントロールの名前
*/
export const FancyButton = ({ children, name = "" }: Props) => (
<button name={name}>{children}</button>
);
IDE による補完の恩恵[1]を受けるには Props
型を定義している Type Alias 側に JSDoc を記述する必要がありますが、それですと記述内容が重複し冗長です。よって分割代入の場合のみ検閲対象外とします。
{
"rules": {
"jsdoc/require-param": [
"error",
{
"checkDestructuredRoots": false,
},
],
},
}
これで分割代入している箇所は対象外となります。
type Props = {
/** 子要素 */
children: ReactNode;
/** 入力欄コントロールの名前 */
name: string;
};
/**
* おしゃれなボタンです。
*/
export const FancyButton = ({ children, name = "" }: Props) => (
<button name={name}>{children}</button>
);
関数式(Arrow func)、クラス宣言、変数宣言および型定義も検閲対象に含める
デフォルトだと関数宣言(function
)しか検閲対象となっていないため、関数式(Arrow func)、クラス宣言および TypeScript の型定義も対象に含めます。
{
"rules": {
"jsdoc/require-jsdoc": [
"error",
{
"publicOnly": true,
+ "require": {
+ "ArrowFunctionExpression": true,
+ "ClassDeclaration": true,
+ "ClassExpression": true,
+ "FunctionDeclaration": true,
+ "FunctionExpression": true,
+ "MethodDefinition": true,
+ },
+ "contexts": [
+ "VariableDeclaration",
+ "TSInterfaceDeclaration",
+ "TSTypeAliasDeclaration",
+ "TSPropertySignature",
+ "TSMethodSignature",
+ ],
+ },
+ ],
}
}
関数式とクラス宣言は require
プロパティでそれぞれ細かく指定できます。
プロパティ名 | 構文 | 例 |
---|---|---|
ArrowFunctionExpression |
アロー関数式 | |
ClassDeclaration |
クラス宣言 | class Foo { ... } |
ClassExpression |
クラス式 | const Foo = class { ... } |
FunctionDeclaration |
関数宣言 | function foo { ... } |
FunctionExpression |
関数式 | const foo = function { ... } |
MethodDefinition |
メソッド定義 | const obj = { foo() { ... } } |
関数とクラス以外のモジュールに関しては、 contexts
プロパティにそれぞれの AST 定義名を追加することで検閲対象に含められます。
構文 | AST 定義名 |
---|---|
変数宣言 | VariableDeclaration |
Interface 宣言 | TSInterfaceDeclaration |
Type Alias 宣言 | TSTypeAliasDeclaration |
Interface および Type Alias のプロパティ | TSPropertySignature |
Interface および Type Alias のメソッド定義 | TSMethodSignature |
AST 定義名は AST Explorer のエディタにコードを貼り付けることで確認できます。
説明文の記述を必須化する
require-jsdoc
が強制するのは JSDoc のコメントブロック有無のみであり、その中身までは検閲しません。よって現状では以下の状態でも OK とみなされてしまいます。
/**
*
*/
export const FancyButton = () => <button>hello world</button>;
よって JSDoc ブロックに説明文の記述を必須化すべく require-description
を有効化します。
{
"rules": {
"jsdoc/require-description": [
"error",
{
"contexts": [
"ArrowFunctionExpression",
"ClassDeclaration",
"ClassExpression",
"FunctionDeclaration",
"FunctionExpression",
"MethodDefinition",
"PropertyDefinition",
"VariableDeclaration",
"TSInterfaceDeclaration",
"TSTypeAliasDeclaration",
"TSPropertySignature",
"TSMethodSignature",
],
},
],
// ...
}
}
require-jsdoc
同様、検閲対象とするモジュールを指定します。なお、 "ArrowFunctionExpression"
, "FunctionDeclaration"
, "FunctionExpression"
はデフォルトで対象となっていますが、ESLint のルール設定はマージではなく上書きとなります。よって設定をカスタマイズする際はこれらも忘れず記述する必要があります。
戻り値に関する記述を省略可能にする
記述するのが望ましいのは言うまでもないですが、パラメーターと比較すると重要度合いが一枚落ちるのと TypeScript であれば IDE による補助である程度の情報は得られることから省略可能にします。
{
"rules": {
"jsdoc/require-returns": ["off"],
},
}
ただし、require-returns-check
は有効のままにしておくと良いでしょう。こうしておくことで以下のような @returns
だけが記述されて放置されるという状態を予防げます。
/**
* Some description
*
* @returns
*/
JavaScript コードは必須とする
ビルドスクリプトやツールキットの設定といったファイルは JavaScript で書かれることが多いため、これらに対しては戻り値の記述を必須にします。
{
"overrides": [
{
"files": ['*.js', '*.mjs', "*.cjs"],
rules: {
"jsdoc/require-returns": ["error"],
"jsdoc/require-returns-type": ["error"],
"jsdoc/no-types": ["off"],
},
}
],
}
また、型推論がない JavaScript では JSDoc で型情報を補完するため、これの記述を必須化すべく require-returns-type
も併せて有効化しつつ[2]、 no-types
を無効化します。
/**
* Some description
*
* @returns An extended
*/
/**
* Some description
*
* @returns {boolean} An extended
*/
TSDoc 特有のタグを使用可能にする
不正な JSDoc タグが使用されていないかを検閲するルールですが、デフォルトだと TSDoc 特有のタグは全て不正扱いとなっています。しかし中には有用なタグも存在するので、それらを使用できるようにします。
{
"rules": {
"jsdoc/check-tag-names": [
"error",
{
"definedTags": ["typeParam", "remarks"],
},
],
// ...
}
}
definedTags
にタグ名を追加することで使用可能となります。今回は typeParam
と remarks
を追加しました。
/**
* Alias for array
*
* @typeParam T - Type of objects the list contains
*/
type List<T> = Array<T>;
/**
* The summary section should be brief.
*
* @remarks
* The main documentation for an API item is separated into a brief
* "summary" section optionally followed by an `@remarks` block containing
* additional details.
*/
- 参考文献: What is TSDoc? | TSDoc
タグブロックごとの空行を必須化する
ルールプリセットでは以下のように一切の空行が許可されていません。
/**
* Some description
* @param foo param1
* @param bar param2
* @return An extended
* @example
* ```
* some(foo, bar);
* ```
*/
好みによるところもありますが、人によって空行を部分的に入れたり入れなかったりと揺れる可能性が高くなります。それならばシンプルに各タグブロックごとに空行を入れることで統一する方が考慮事項が減ります。
{
"rules": {
"jsdoc/tag-lines": [
"error",
"always",
{
"startLines": 1,
"applyToEndTag": false,
},
],
// ...
}
}
/**
* Some description
*
* @param foo param1
*
* @param bar param2
*
* @return An extended
*
* @example
* ```
* some(foo, bar);
* ```
*/
タグの記述順序を一貫したものに統一する
具体的な順序は利用者が自由に設定可能ですが、統一さえされていれば十分なのでデフォルト設定のままにしておきます。ただし、ドキュメントにもあるように tag-lines
のオプションに always
を指定している場合は reportIntraTagGroupSpacing
を false
にする必要があります。
Whether to enable reporting and fixing of line breaks within tags of a given tag group. Defaults to
true
which will remove any line breaks at the end of such tags. Do not use withtrue
if you are usingtag-lines
andalways
.
reportIntraTagGroupSpacing
{
"rules": {
"jsdoc/sort-tags": [
"error",
{
"reportIntraTagGroupSpacing": false,
},
],
},
}
今回は tag-lines
を always
にしているので false
にします。これをしないと双方のルールが衝突して自動補正時に JSDoc が壊れてしまいます。
締め
エクスポートしていない型定義が一律対象外になってしまっているなど課題はありますが、コードレビューやコーディング規約で十分補完できることでしょう。
JSDoc をはじめドキュメンテーションは慣れていないと執筆が苦痛に感じるものですが、JSDoc が十分に整備されたコードベースは内部実装に潜らずともその関数の用法が理解できるため、特に新人が素早く理解するのに大いに役立ちます。
Discussion