gajus/eslint-plugin-jsdocのコードを読む Part 1 | Memory ice cubes https://leaysgur.github.io/posts/2024/02/22/133316/
これの続き。
概観ではなく実装の詳細を追っていく回。
iterateJsdoc(iter, config)
現在する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()
- Visitorは
Program:exitのみ- つまり1度きり
- 対象はソース中のすべてのコメント
eslintのsourceCode.getAllComments()- JSDocでもそうでなくても、複数行でもそうでなくても
- すべてのコメントを、ルール側にまとめて渡してる
iter({ allComments })
これを使ったルールはnon-recommendedのno-bad-blocksだけ。これは、複数行コメントがあったら、JSDocコメント(/**はじまり)にしろって言うルール。
iterateAllJsdocs()
30/53ルールで使われてたメインのユースケース。
- Visitorは、
*:not(Program)とProgram:exitの2つ *:not(Program)- つまりすべてのASTノードに対する待ち受け
jsdoccommentのgetJSDocComment()で、そのノードに紐づくJSDocコメントを取得- 型としては単一の
estree.Comment | eslint.AST.Token - 見つからなかった場合はreturn
- 型としては単一の
- trackedとしてマーク
- 見つけた
Commentは、ノードと共にローカル関数のcallIterator()へ渡す
Program:exiteslintのsourceCode.getAllComments()からすべてのコメントを取得- さっきtrackedとしてマークしていないものを取得
callIterator()へ渡す
つまりは、特定のノードに紐づいていようといまいと、ソース中のJSDocコメントならすべて対象にして、ルールのハンドラを呼んでる。
callIterator()がやってるのは、
- 受け取った
Comment配列のそれぞれを、そのノードと共に処理していく*:not(Program)から呼ばれるときは、[node]として1つだけ入ってる- つまり、1ノード1コメントの関係でのチェックになる
Program:exitから呼ばれるときは、untrackedなJSDocがまとめて配列になってる- そのため、
^\/\*\*\sに合致するかチェックをまたやってる - この場合、ノードは存在しないので
null
- そのため、
jsdoccommentのparseComment()で、インデントや中身をパース- 設定の
contexts[].commentをチェックし、処理を続行するか判定 - まとめて
iterate()に渡す- ノード(あれば)
- 紐づいてたJSDocの
Comment、それをパースして得た独自のJSDoc AST - ルール側で定義した
iteratorなど
iterate()がやってるのは、
- ルール側で最後に呼ぶreporterの作成
- ノードに絡むutilsの生成
- 設定に応じて、ルールのハンドラを呼ぶかどうかの決定
interanalやprivateのタグを見つつ、処理を続行するか判定- 冒頭のフラグ
checkInternalとcheckPrivateはここで出番
- ルール側で定義した
iteratorの呼び出し
全コメントを対象に取りつつも、checkFile()と違うのは、
- ルール側のハンドラは、各コメントごとに呼ばれる
- JSDocを持つノードとセットで呼ばれる(あれば)
- JSDocではないコメントは対象にならない
- コメントの中身などが独自ASTにパース済
というあたりか。うーん、わかるようで、わからない。
ノードに紐づいていることが重要なら、untrackedのnullは困るのでは?
ちゃんと調べてないけど、ノードがなかったら何もしない、みたいなルールが見つかりませんように・・・。
それ以外
残るは、checkFileでもiterateAllJsdocsでもなかった場合。
- 実行対象コンテキストのチェック
- 変数
contextsが、config.contextDefaultsなど各種のルール側の設定によって決まる? - デフォルトはどうやらこの4つが入った
string[]ArrowFunctionExpressionFunctionDeclarationFunctionExpressionTSDeclareFunction
- 条件によっては、
iterateAllJsdocs()にフォールバックcontextsにanyという文字列が含まれる場合- このときは、さっきは未指定だった
contexts関連の引数が増えて呼ばれる - (このチェックは先にできなかったのだろうか)
- 変数
contextsで指定されたAST種別を使って、最終的にESLintにわたすハンドラを再生成- その中身は、
checkJsdoc()というローカル関数
- その中身は、
これcontextDefaultsの指定が漏れてたとしても、その他contextsの指定がなければ、ベタ書きされてる同じデフォルト値が使われるので、結果は同じってこと・・・?
checkJsdoc()がやってるのは、
jsdoccommentのgetJSDocComment()で、そのノードに紐づく単一のJSDocコメントを取得jsdoccommentのparseComment()で、インデントや中身をパース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-namesinformative-docsno-undefined-types
それ以外のルールでは、nodeをnullにしたとしても、すべてのテストがPASSした。
Context? contexts?
このコードベースには、コンテキストという名前のものが複数登場する。
- ESLintの
Rule.RuleContext型context.optionsやcontext.settingsとして設定を拾ったりcontext.getSourceCode().getAllComments()したり
- ESLintのSelectorとして指定できる
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()や>みたいな柔軟なクエリで書ける
- CSSのセレクタみたく
- ASTノードの部分はパーサー実装に依存する
- ので、パーサーに応じて好きなように絞り込みできる
そして、
contexts[].commentには、Jsdoc*やJsdocType*なASTノードも指定できる- これはJSDocコメントをパースしてる
jsdoccommentが扱ってる独自のASTで、ESTreeには定義されてない
- これはJSDocコメントをパースしてる
とのこと。
なるほど〜〜・・・これ使いこなしてる人おるんか?
その他のutils
まず、この1700行のファイルからexportされてる40種類の関数がある。
で、これを使ってiterateJsdoc()の各ノードや呼び出し時点のスコープでbindされる、1000行の59種類の関数もある。
気が遠くなるな。
まだ続く
@es-joy/jsdoccommentのコードもあわせて読もうと思ってたが、あまりのボリュームに気力がなくなった。
ここまでの感想を述べておくと、
- すべてがごっちゃになっててとっても読みにくい
- まあ10年近く生きてるコードやし・・・?
- スパゲティになるのも仕方ないか・・・?
- あらゆるコードがutilsに集約されているせいで、その中での条件分岐がカオス
- コードの重複を避けたい気持ちはわかるけども
- どれもこれも引数が10ヶくらいある
- かといってルールの実装も薄くはない
- 特定のルールのためだけに、あらゆるホットパスに手が入ってる状態
- しかもそういうのに限ってrecommendedではなかったり
- JSDoc TSで書かれてるけど、型を握りつぶしてたり汎用的過ぎたり
- つまりガバガバであまり有用ではない
- なんのために型つけてるのかわからない
- コードもさることながら、設計というか信念というかが見えにくい
次で終わりにしたい。