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:exit
eslint
の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[]
ArrowFunctionExpression
FunctionDeclaration
FunctionExpression
TSDeclareFunction
- 条件によっては、
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-names
informative-docs
no-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で書かれてるけど、型を握りつぶしてたり汎用的過ぎたり
- つまりガバガバであまり有用ではない
- なんのために型つけてるのかわからない
- コードもさることながら、設計というか信念というかが見えにくい
次で終わりにしたい。