exportしているモジュールのJSDocやTSDocを必須にする

TypeScriptでの開発時、exportしているモジュールに対してはTSDocを書くようコーディング規約を定めたが、ヒアドキュメントを書く習慣のないメンバーが多く、書き漏れが頻発した。

都度コードレビューで指摘するのも面倒なので、ESLintで何とかならないかと思い、調べたのでメモ。

環境

ESLint v8.21.0、eslint-plugin-jsdoc v39.3.4。

ESLintへのプラグイン追加

昔はESLintにJSDocサポートが組み込まれていたが、廃止済み。

eslint.org

Microsofteslint-plugin-tsdocを公開しているが、これはTSDoc仕様に準拠したドキュメントかのチェックのため、やりたいこととは異なる。

上記ブログでも移行先として指定されていた、 eslint-plugin-jsdoc がTSDocにも対応している。

github.com

以下の記事で設定の実例もあった。

this.aereal.org

yarn add -D eslint-plugin-jsdoc で追加。

.eslintrcの設定

READMEを元に設定。

pluginsjsdocextendsplugin:jsdoc/recommended を追加する。

settings.jsdoc.mode で、 typescript を指定できるが、 @typescript-eslint を使用している場合は省略可能。

これでひとまず動くが、以下を変更したい。

  1. TSDocが書かれていない場合はパスするため、exportしているモジュールなどファイル外に公開されている部分には必須化
  2. 型情報があるため、 @returns および @param の型定義を省略可能とする
  3. @param がない場合や、 @param に説明がない場合、デフォルトではWarningのため、Errorにする
  4. export された typeinterface で定義したフィールドなどのTSDocも必須にする

それぞれ設定していく。

公開モジュールでのJSDoc/TSDoc必須化

jsdoc/require-jsdoc が対象。

publicOnlytrue にすると、exportされたモジュールのみ対象となる。

また、 require で対象を細かく指定できる。

  • ArrowFunctionExpression: アロー関数式
  • ClassDeclaration: クラス宣言( class MyClass { ... } )
  • ClassExpression: クラス式( const MyClass = class { ... } )
  • FunctionDeclaration: 関数宣言( function myFunction { ... } )
  • FunctionExpression: 関数式( const myFunction = function { ... } )
  • MethodDefinition: メソッド定義( const obj = { myMethod() { ... } } )

デフォルトでは FunctionDeclaration のみ true なので、すべて有効にする。

{
  "rules": {
    "jsdoc/require-jsdoc": [
      "error",
      {
        "publicOnly": true,
        "require": {
          "ArrowFunctionExpression": true,
          "ClassDeclaration": true,
          "ClassExpression": true,
          "FunctionDeclaration": true,
          "FunctionExpression": true,
          "MethodDefinition": true
        }
      }
    ]
  }
}
module.exportsのJSDocを省略可能にする

"publicOnly": true, の場合、デフォルトではES Modules形式の export ... とCommonJS形式の module.exports = ... が対象となる。

TypeScriptでコーディングする場合、 module.exports は使わないが、ライブラリ等の設定ファイルである *.config.js では module.exports が使われており、それらも警告される。

設定ファイルにJSDocを書く意味はなく、またプロダクトコード内でCommonJS形式を用いることもないため、ES Modulesのみチェックするよう、 "publicOnly": { "esm": true, "cjs": false }, に変更。

公開されたclassのプロパティや、type・interfaceでのJSDoc/TSDoc必須化

これでexportされた関数やクラスにTSDocがないとエラーが出るようになったが、クラスのプロパティや、type・interfaceおよびそのプロパティやメソッドに対してはチェックが効かない。

調べると、以下のStack Overflowの質問やissueにて、contextsに文字列を設定している。

contextsに指定できる値は、ESLintパーサーによって異なるが、AST定義名の模様。

READMEからリンクされているAST Explorerにて、コードを張り付け、パーサーを @typescript-eslint/parser にすることで、TypeScriptのAST定義名を確認できた。

AST Explorerでのパーサーの設定

構文 AST定義名
classのプロパティ PropertyDefinition
interface TSInterfaceDeclaration
type TSTypeAliasDeclaration
interfaceやtypeのプロパティ TSPropertySignature
interfaceやtypeのメソッド TSMethodSignature
enum TSEnumDeclaration
enumのメンバー TSEnumMember

これらを jsdoc/require-jsdoc の contexts に設定すると、TSDocなしではエラーが出るが、中身が空でもJSDoc/TSDocのブロック( /** ~ */ )があればエラーにならない。

説明がない場合もエラーにするには、 jsdoc/require-description の contexts にもAST定義を設定すればいいが、 contexts は追記ではなく上書きのため、 jsdoc/require-jsdoc の contexts と同じものを設定すると、 require で指定している ArrowFunctionExpression などに対するチェックが行われなくなった。

jsdoc/require-jsdoc では、「require で true にしたもの + contexts」が対象となるのに対し、その他のルールでは contexts で指定したものを対象としている模様。

以下の様に、 jsdoc/require-description の contexts に require で true にしたものも含めることで、空ブロックもエラーとなった。

    "jsdoc/require-jsdoc": [
      "error",
      {
        "publicOnly": { "esm": true, "cjs": false },
        "require": {
          "ArrowFunctionExpression": true,
          "ClassDeclaration": true,
          "ClassExpression": true,
          "FunctionDeclaration": true,
          "FunctionExpression": true,
          "MethodDefinition": true
        },
        "contexts": [
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],
    "jsdoc/require-description": [
      "error",
      {
        "contexts": [
          "ArrowFunctionExpression",
          "ClassDeclaration",
          "ClassExpression",
          "FunctionDeclaration",
          "FunctionExpression",
          "MethodDefinition",
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],

ちなみに、未検証だが、 jsdoc/require-jsdoc の require を消して、 contexts を jsdoc/require-description のものと同じように指定することも可能な模様。

追記: JavaScriptで変数を用いた場合

.eslintrcJavaScriptにし、変数を用いることで、記述をまとめることができた。

const jsRequire = {
  ArrowFunctionExpression: true,
  ClassDeclaration: true,
  ClassExpression: true,
  FunctionDeclaration: true,
  FunctionExpression: true,
  MethodDefinition: true,
}

const tsContexts = [
  'PropertyDefinition',
  'TSInterfaceDeclaration',
  'TSTypeAliasDeclaration',
  'TSPropertySignature',
  'TSMethodSignature',
  'TSEnumDeclaration',
  'TSEnumMember',
]

/** @type {import('eslint').Linter.Config} */
module.exports = {
  rules: {
    'jsdoc/require-jsdoc': [
      'error',
      {
        publicOnly: { esm: true, cjs: false },
        require: jsRequire,
        contexts: tsContexts,
      },
    ],
    'jsdoc/require-description': [
      'error',
      {
        contexts: [Object.keys(jsRequire), tsContexts].flat(),
      },
    ],
  },
}

@returns および @param の型定義を省略可能にする

@returns の省略はjsdoc/require-returns@param の型定義はjsdoc/require-param-typeが対象のため、それぞれ off に変更。

ただ、require-returnsをoffにした場合、 @returns 自体を省略することは可能になるが、戻り値の詳細を書くために @returns を明示的に記述すると、やはり型定義を書くよう警告が出る。

@returns の型定義はjsdoc/require-returns-typeのため、これも off にする。

@param がない、または説明がない場合Errorにする

jsdoc/require-param@param がない場合のルール、 jsdoc/require-param-description が説明がない場合のルールのため、それぞれ error に変更。

引数を分割代入している場合の警告の抑制

デフォルトでは引数を分割代入していると、以下の様に、それぞれの値ごとに @param を書く必要がある。

/**
 * @param root0
 * @param root0.id
 * @param root0.name
 */
function example({ id, name }: exampleArguments) {}

型情報があれば値ごとの記述は不要のため、抑制したい。

jsdoc/require-param には、渡された引数自体の @param を制御する checkDestructuredRoots と、展開された値に対する @param を制御する checkDestructured がある。

今回は展開された値への抑制ができればいいため、 checkDestructuredfalse を指定する。

これで @param の記述は不要となったが、別途 Missing @param "root0.id" といった警告が jsdoc/check-param-names にて発生するようになった。

こちらにも同様の checkDestructured が存在するため、合わせて false に設定。

{
  "rules": {
    "jsdoc/check-param-names": ["error", { "checkDestructured": false }],
    "jsdoc/require-param": ["error", { "checkDestructured": false }]
  }
}

これで、以下の様な記述でも警告が出なくなる。

/**
 * @param arguments
 */
function example({ id, name }: exampleArguments) {}

なお、 checkTypesPattern で、型名に対する正規表現による抑制もできるが、対象となるのが @param の型定義であり、TypeScriptとはそぐわなかったため checkDestructured を使用した。

export していない関数の @param の省略

exportしていないモジュールにも、TSDocで概要だけ記述することがあるが、その場合 jsdoc/require-param から @param の記載がないと警告が出る。

jsdoc/require-jsdoc の publicOnlytrue にすることでTSDocを省略できるが、省略せずに書いた場合はタグの記述が必要となる。

公開していない関数であれば、 @param の省略をできないか確認したが、対応できそうなオプションは見つけられなかった。

回避策として、 @private@internal が付与されていればルールを無効化する設定があるため、そちらを有効化する。

TSDocのドキュメント を見ると、 @private は記載がないが、 @internal は記載されている。

ただ、エディタとしてVSCodeを利用しているが、JSDoc/TSDocのブロック( /** ~ */ )内で、 @private はコード補完が効くが、 @internal は効かなかった。

VSCodeコードスニペットの追加はできるが、TSDocのブロック内でのみ有効化する、といった設定方法がわからなかったので、コーディングの負荷軽減を優先し、 @private が付与されている場合のルールを無効化するよう設定した。

{
  "settings": {
    "jsdoc": { "ignorePrivate": true }
  }
}

@internal が付与されていればルールを無効化する場合、 settings.jsdoc.ignoreInternaltrue にすればいい。

@jsxImportSource の警告の抑制

@emotion/react を使っているが、 /** @jsxImportSource @emotion/react */ に対して、 jsdoc/check-tag-names から `Invalid JSDoc tag name "jsxImportSource". が発生する。

JSX関連のタグを有効化する jsxTags が用意されているため、 true に設定する。

{
  "rules": {
    "jsdoc/check-tag-names": ["error", { "jsxTags": true }],
  }
}

断念した設定

引数名による @param の省略

現在のプロジェクトでは、Reactコンポーネントのプロパティとして コンポーネント名Properties という型を定義し、 properties という名前でコンポーネントに渡している。

コンポーネントとの関係は明らかなため、できれば @param を省略したいと思ったが、引数名での制御はできない模様。

最終的な.eslintrcへの変更

今回の設定で変更した部分は、以下の様になった。

{
  "plugins": ["jsdoc"],
  "extends": ["plugin:jsdoc/recommended"],
  "rules": {
    "jsdoc/require-jsdoc": [
      "error",
      {
        "publicOnly": { "esm": true, "cjs": false },
        "require": {
          "ArrowFunctionExpression": true,
          "ClassDeclaration": true,
          "ClassExpression": true,
          "FunctionDeclaration": true,
          "FunctionExpression": true,
          "MethodDefinition": true
        },
        "contexts": [
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],
    "jsdoc/check-param-names": ["error", { "checkDestructured": false }],
    "jsdoc/check-tag-names": ["error", { "jsxTags": true }],
    "jsdoc/require-description": [
      "error",
      {
        "contexts": [
          "ArrowFunctionExpression",
          "ClassDeclaration",
          "ClassExpression",
          "FunctionDeclaration",
          "FunctionExpression",
          "MethodDefinition",
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],
    "jsdoc/require-param": ["error", { "checkDestructured": false }],
    "jsdoc/require-param-description": "error",
    "jsdoc/require-param-type": "off",
    "jsdoc/require-returns": "off",
    "jsdoc/require-returns-type": "off"
  },
  "settings": {
    "jsdoc": { "ignorePrivate": true }
  }
}

振り返り

これでTSDocの書き漏れや、書き方の不備についてチェックできるようになった。

export していないモジュールに対してTSDocで説明だけを書きたい場合、 @private を付ける必要があるが、特に問題なく運用できている。

jsdoc/require-jsdocjsdoc/require-description のcontextsなどに指定する文字列が重複しているのがやや気持ち悪い。 .eslintrcJSONではなくJavaScriptYAMLで書いておけば、それぞれ変数やアンカーでまとめられるので、そのうち変更しようかな。

-> 2021/10/21、JavaScriptの場合を追記。