🧊

gajus/eslint-plugin-jsdocのコードを読む Part 2

gajus/eslint-plugin-jsdocのコードを読む Part 1 | Memory ice cubes https://leaysgur.github.io/posts/2024/02/22/133316/

これの続き。

概観ではなく実装の詳細を追っていく回。

iterateJsdoc(iter, config)

https://github.com/gajus/eslint-plugin-jsdoc/blob/37df54dc8535eaed65b4dadaca2dc072e4c7bc4e/src/iterateJsdoc.js#L2383

現在する52/53ルールのエントリーとなる親玉で、引数として、各ルールとしての実装とその設定を受け取る。

設定によって動作が変わるようで、

  • config.checkFile: trueの場合
  • config.iterateAllJsdocs: trueの場合
  • それ以外

大別すると、この3パターンの用途に分かれてた。

しかしこの設定、他にもフラグ変数がいろいろある。

  • checkFile: 1ルール(no-bad-blocks
  • iterateAllJsdocs: 30ルール
  • checkPrivate: 2ルール(empty-tags, check-access
  • checkInternal: 1ルール(empty-tags
  • nonGlobalSettings: 1ルール(no-restricted-syntax
  • noTracking: 1ルール(require-param
  • contextSelected: 2ルール(no-restricted-syntax, no-missing-syntax
  • matchContext: 2ルール(match-name, no-missing-syntax

これは・・・、フラグ地獄だ!

checkFile()

https://github.com/gajus/eslint-plugin-jsdoc/blob/37df54dc8535eaed65b4dadaca2dc072e4c7bc4e/src/iterateJsdoc.js#L2340

  • VisitorはProgram:exitのみ
    • つまり1度きり
  • 対象はソース中のすべてのコメント
    • eslintsourceCode.getAllComments()
    • JSDocでもそうでなくても、複数行でもそうでなくても
  • すべてのコメントを、ルール側にまとめて渡してる
    • iter({ allComments })

これを使ったルールはnon-recommendedのno-bad-blocksだけ。これは、複数行コメントがあったら、JSDocコメント(/**はじまり)にしろって言うルール。

iterateAllJsdocs()

https://github.com/gajus/eslint-plugin-jsdoc/blob/37df54dc8535eaed65b4dadaca2dc072e4c7bc4e/src/iterateJsdoc.js#L2132

30/53ルールで使われてたメインのユースケース。

  • Visitorは、*:not(Program)Program:exitの2つ
  • *:not(Program)
    • つまりすべてのASTノードに対する待ち受け
    • jsdoccommentgetJSDocComment()で、そのノードに紐づくJSDocコメントを取得
      • 型としては単一のestree.Comment | eslint.AST.Token
      • 見つからなかった場合はreturn
    • trackedとしてマーク
    • 見つけたCommentは、ノードと共にローカル関数のcallIterator()へ渡す
  • Program:exit
    • eslintsourceCode.getAllComments()からすべてのコメントを取得
    • さっきtrackedとしてマークしていないものを取得
    • callIterator()へ渡す

つまりは、特定のノードに紐づいていようといまいと、ソース中のJSDocコメントならすべて対象にして、ルールのハンドラを呼んでる。

callIterator()がやってるのは、

  • 受け取ったComment配列のそれぞれを、そのノードと共に処理していく
    • *:not(Program)から呼ばれるときは、[node]として1つだけ入ってる
      • つまり、1ノード1コメントの関係でのチェックになる
    • Program:exitから呼ばれるときは、untrackedなJSDocがまとめて配列になってる
      • そのため、^\/\*\*\sに合致するかチェックをまたやってる
      • この場合、ノードは存在しないのでnull
  • jsdoccommentparseComment()で、インデントや中身をパース
  • 設定のcontexts[].commentをチェックし、処理を続行するか判定
  • まとめてiterate()に渡す
    • ノード(あれば)
    • 紐づいてたJSDocのComment、それをパースして得た独自のJSDoc AST
    • ルール側で定義したiteratorなど

iterate()がやってるのは、

  • ルール側で最後に呼ぶreporterの作成
  • ノードに絡むutilsの生成
  • 設定に応じて、ルールのハンドラを呼ぶかどうかの決定
    • interanalprivateのタグを見つつ、処理を続行するか判定
    • 冒頭のフラグcheckInternalcheckPrivateはここで出番
  • ルール側で定義したiteratorの呼び出し

全コメントを対象に取りつつも、checkFile()と違うのは、

  • ルール側のハンドラは、各コメントごとに呼ばれる
  • JSDocを持つノードとセットで呼ばれる(あれば)
    • JSDocではないコメントは対象にならない
  • コメントの中身などが独自ASTにパース済

というあたりか。うーん、わかるようで、わからない。

ノードに紐づいていることが重要なら、untrackedのnullは困るのでは? ちゃんと調べてないけど、ノードがなかったら何もしない、みたいなルールが見つかりませんように・・・。

それ以外

残るは、checkFileでもiterateAllJsdocsでもなかった場合。

  • 実行対象コンテキストのチェック
    • 変数contextsが、config.contextDefaultsなど各種のルール側の設定によって決まる?
    • デフォルトはどうやらこの4つが入ったstring[]
      • ArrowFunctionExpression
      • FunctionDeclaration
      • FunctionExpression
      • TSDeclareFunction
    • 条件によっては、iterateAllJsdocs()にフォールバック
      • contextsanyという文字列が含まれる場合
      • このときは、さっきは未指定だったcontexts関連の引数が増えて呼ばれる
      • (このチェックは先にできなかったのだろうか)
  • contextsで指定されたAST種別を使って、最終的にESLintにわたすハンドラを再生成
    • その中身は、checkJsdoc()というローカル関数

これcontextDefaultsの指定が漏れてたとしても、その他contextsの指定がなければ、ベタ書きされてる同じデフォルト値が使われるので、結果は同じってこと・・・?

https://github.com/gajus/eslint-plugin-jsdoc/blob/ab893bae6aa5f05228390cb3ce4487485360cba8/src/iterateJsdoc.js#L2525-L2531

checkJsdoc()がやってるのは、

  • jsdoccommentgetJSDocComment()で、そのノードに紐づく単一のJSDocコメントを取得
  • jsdoccommentparseComment()で、インデントや中身をパース
  • handler(jsdoc)falseを返したらreturn
    • これはcallIterator()でも同じくiterate()を呼ぶ直前にあちこちで呼ばれてる
    • 指定したコンテキストに合致しない場合は処理をスキップするためのチェックらしい
  • 先述のiterate()に渡す

難解すぎる。

けど役割から察すると、特定のノードだけを、JSDocと一緒にチェックしたい感じか。

iterateJsdoc()のまとめ

わからないなりに、精いっぱい察っしてみるに、

  • checkFile: trueの場合
    • ルールのハンドラを1度だけ呼ぶ
    • ファイル内のすべてのコメントを渡して委ねる
  • iterateAllJsdocs: trueの場合
    • ファイル内のすべてのJSDocコメントをハンドラごとに呼ぶ
    • ノードに属する場合もあれば、属さない場合もある
  • それ以外の場合
    • Function関連の4種類のノードと、それに紐づく単一のJSDocコメントしか基本的に見ない
    • ただしcontexts[].context関連の設定により、その対象は拡大できるようになってる
      • Function以外にもチェックしたい場合は、自分で指定する
    • anyを指定することで、iterateAllJsdocs相当にできる(なぜ?)
  • どの場合も、contexts[].commentの指定によっては、処理をスキップできる

ということだろうか。

後日、iterateAllJsdocs()node | nullを前提としてる不可解な謎を調べた。

結果、以下の特定のルールでは、nodeがある場合に追加処理をしてることがわかった。

  • check-tag-names
  • informative-docs
  • no-undefined-types

それ以外のルールでは、nodenullにしたとしても、すべてのテストがPASSした。

Context? contexts?

このコードベースには、コンテキストという名前のものが複数登場する。

この後者がコード中のいろんなところに登場してた難解なやつ。それらしいDocsがあったので読んでみると、

eslint-plugin-jsdoc/docs/advanced.md at main · gajus/eslint-plugin-jsdoc https://github.com/gajus/eslint-plugin-jsdoc/blob/ab893bae6aa5f05228390cb3ce4487485360cba8/docs/advanced.md#ast-and-selectors

Advancedと銘打つだけのことはあり、とても難しいことが書いてある・・・。

  • ESLintにはSelectorsという機能がある
    • ルールの実行対象を、ソース内でも一部のASTノードのみに限定したりできるらしい
  • それを、contexts[].contextに配列で指定できる
  • ASTの種別を文字列で指定するか、特定のルール向けにオブジェクトで指定する
  • 指定されたものはesqueryでマッチングされ、ルールの対象とするかどうかが決める
    • CSSのセレクタみたく*:has()>みたいな柔軟なクエリで書ける
  • ASTノードの部分はパーサー実装に依存する
    • ので、パーサーに応じて好きなように絞り込みできる

そして、

  • contexts[].commentには、Jsdoc*JsdocType*なASTノードも指定できる
    • これはJSDocコメントをパースしてるjsdoccommentが扱ってる独自のASTで、ESTreeには定義されてない

とのこと。

なるほど〜〜・・・これ使いこなしてる人おるんか?

その他のutils

まず、この1700行のファイルからexportされてる40種類の関数がある。

https://github.com/gajus/eslint-plugin-jsdoc/blob/ab893bae6aa5f05228390cb3ce4487485360cba8/src/jsdocUtils.js

で、これを使ってiterateJsdoc()の各ノードや呼び出し時点のスコープでbindされる、1000行の59種類の関数もある。

https://github.com/gajus/eslint-plugin-jsdoc/blob/ab893bae6aa5f05228390cb3ce4487485360cba8/src/iterateJsdoc.js#L2008-L2020

気が遠くなるな。

まだ続く

@es-joy/jsdoccommentのコードもあわせて読もうと思ってたが、あまりのボリュームに気力がなくなった。

ここまでの感想を述べておくと、

  • すべてがごっちゃになっててとっても読みにくい
    • まあ10年近く生きてるコードやし・・・?
    • スパゲティになるのも仕方ないか・・・?
  • あらゆるコードがutilsに集約されているせいで、その中での条件分岐がカオス
    • コードの重複を避けたい気持ちはわかるけども
    • どれもこれも引数が10ヶくらいある
    • かといってルールの実装も薄くはない
  • 特定のルールのためだけに、あらゆるホットパスに手が入ってる状態
    • しかもそういうのに限ってrecommendedではなかったり
  • JSDoc TSで書かれてるけど、型を握りつぶしてたり汎用的過ぎたり
    • つまりガバガバであまり有用ではない
    • なんのために型つけてるのかわからない
  • コードもさることながら、設計というか信念というかが見えにくい

次で終わりにしたい。