gajus/eslint-plugin-jsdocのコードを読む Part 2 | Memory ice cubes https://leaysgur.github.io/posts/2024/02/22/140322/
これの続き。
最後に、eslint-plugin-jsdoc
がヘビーに依存しているjsdoccomment
と、そのjsdoccomment
がさらに依存してるjsdoc-type-pratt-parser
とcomment-parser
を読んでいく。
@es-joy/jsdoccomment
es-joy/jsdoccomment https://github.com/es-joy/jsdoccomment
READMEによると、昔はESLint本体に実装されてたSourceCode#getJSDocComment()
の代替として生まれたライブラリとのこと。
End-of-Life for Built-in JSDoc Support in ESLint - ESLint - Pluggable JavaScript Linter https://eslint.org/blog/2018/11/jsdoc-end-of-life/
ちなみに、eslint-plugin-jsdoc
で使われてたAPIは以下のとおり。
- parseComment
- findJSDocComment
- getJSDocComment
- getDecorator
- getReducedASTNode
- commentHandler
いきなりだが、parseComment()
以外はREADMEにすら書かれてない・・・。
というわけで嫌な予感がすごいので、全部ではなくiterateJsdoc()
で使われてるやつだけ読むことにする。
getJSDocComment(sourceCode, node, settings)
getReducedASTNode(node, sourceCode)
した上でfindJSDocComment(reducedNode, sourceCode, settings)
getReducedASTNode()
がやってるのは、よくわからないけど、元のノードから何かを削ったもの・・・?
例のごとくテストもない。
https://github.com/es-joy/jsdoccomment/blob/6aae5ea306015096e3d58cd22257e5222c54e3b4/src/jsdoccomment.js#L192 https://github.com/es-joy/jsdoccomment/blob/6aae5ea306015096e3d58cd22257e5222c54e3b4/test/jsdoccomment.js
findJSDocComment()
がやってるのは、
currentNode
に引数で受け取ったeslint.Rule.Node
なノードを設定- それがデコレータだったら先頭のデコレータを設定
sourceCode.getTokenBefore(currentNode, { includeComments: true })
で、自ノードより上方のトークンを1つだけ取得- それが
(
だったら、もう1つ手前を見る
- それが
- トークンがない、コメントではない場合は
null
を返して終わり - 行コメントだった場合は、もう1つ前のトークンも見て繰り返す
- 複数行コメントが引けたらその中身をチェックするフェーズへ進む
- その複数行コメントの中身が、
\*\s
にマッチするかチェック - つまり
/** JSDoc *
かどうか
- その複数行コメントの中身が、
- さらに
(min|max)Lines
に収まる範囲にあったかどうかチェック- (せっかくトークンで探したのに、行数を見てる?なんのために?)
- すべてOKなら、
estree.Comment
型が返る
というわけで、特定のASTノードに対して、最も近くにある単一のコメントトークンを返してる。
- 行コメント以外は遡らない(= ただの複数行コメントも即候補になる)
- 単一のコメントだけを返す
このあたりは、TSのJSDoc紐づけとは異なる挙動。TSはもっと貪欲に複数のコメントを紐づける。
ちなみに、@deprecated
になったESLint本家のコードはこちら。
行数チェックは1以下かどうかだけ、つまり直上の行か、同じ行の直前かどうかを見てる。これはたしかにトークンではわからないし納得。 そもそもJSDocコメントがあるかどうか探すべきノードも、クラスか関数しか見てなかったらしい。
parseComment()
- 第1引数は、さっき取得した
estree.Comment
を想定していつつも、実態は{ value: string }
さえあれば良いらしい- もしくはただの
string
でも(にしてもなんだこの冗長なコードは・・・)
- もしくはただの
- 第2引数はオプショナルなインデント文字列らしいが、
""
しか指定されてないし、デフォルト値と一緒 comment-parser
のparse()
を呼んで、comment-parser
のコメントBlock
型を得るcomment-parser
は、決まった書式(tag type name desc
の並び)しか想定していない仕様(バグでは?)らしい- なのでそれ以外の書式もカバーするために、独自のトークナイザーを一緒に渡してる
- そのままではインラインタグがパースされていないので、
description
とtags[].description
は別途パースして返してる- 正規表現のお化けだった
というわけで、getJSDocComment()
で取得したestree.Comment
のvalue
のコメント文字列をパースして、単一のJSDocブロックを表す構造体にしてる。
- ESTreeのASTとは違う(というかコメントはASTにない)し、
Comment
トークンとも違う、正規化されたただのオブジェクト - ただし
comment-parser
が定義してるJSDocブロックそのままではなく、インラインタグもパース済である
というあたりがポイントか。
commentHandler()
parseComment()
でパースしたブロックを受け取り、esquery
のmatches
でフィルタするための処理- 当然
esquery
はESTreeのASTを想定してるので、わざわざcomment-parser
のコメントブロックを、ESTree風のASTに変換してる
ここで定義してる独自ASTは、JsdocBlock
, JsdocTag
などなど。
eslint-plugin-jsdoc
では、contexts[].comment
で指定して最後の絞り込み処理を行うために使われてたやつ。
comment-parser
syavorsky/comment-parser: Generic JSDoc-like comment parser. https://github.com/syavorsky/comment-parser
jsdoccomment
を介してだけでなく、eslint-plugin-jsdoc
でも直接依存していて、いくつかのルールで使われてる。
このライブラリ自体は、
- 構文解析とかをやってるわけではなく、単純な文字列操作をしてるだけ
- 行ごとに区切って、空白で区切って、正規表現で、etc…
- だから先述の問題みたいなのもあるというわけね
- ESTree ASTのことは知らず、正規化された独自オブジェクトにパースするだけ
- JSDocコメント向けではなく、あくまで汎用的な構造化コメントのパーサー
- 複数のコメントをまとめてパースすることもできる
他パッケージへの依存もなく、内容も1000行ほどとシンプルだが、そのおかげでjsdoccomment
の仕事が増えてる感じ。
さっき見たparse()
が返すBlock
型は、このようなイメージ。
// https://github.com/syavorsky/comment-parser/blob/0d210d3ddc5863137850716b2c581f27cc9de617/src/primitives.ts#L16
export interface Block {
description: string;
tags: Spec[];
source: Line[];
problems: Problem[];
}
export interface Spec {
tag: string;
name: string;
default?: string;
type: string;
optional: boolean;
description: string;
problems: Problem[];
source: Line[];
}
ここに、inlineTags
というプロパティをjsdoccomment
によって追加したものを、eslint-plugin-jsdoc
の各ルールは受け取る(使うかどうかはルール次第)。
jsdoc-type-pratt-parser
jsdoc-type-pratt-parser/jsdoc-type-pratt-parser: A pratt parser for jsdoc types https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser
Prattパーサーという単語も初耳だったが、そういうアルゴリズムがあるらしい。
Operator-precedence parser - Wikipedia https://en.wikipedia.org/wiki/Operator-precedence_parser#Pratt_parsing
なんしか、JSDocコメント内で書かれる{}
内の型の指定の部分をパースしてASTにするのに使われてるらしい。
依存はないがトータルで4000行くらいのコードベース・・・!
JSDocだけでなく、ClosureやTypeScriptの型指定も扱えるようになってるとのこと。
eslint-plugin-jsdoc
でも、たしかにclosure
の型記法に対応してる風のコードがある・・・。
用途としては、jsdoccomment
がESTree風ASTを生成するときに、ついでに型指定部分をパースしてる。
jsdoccomment
の独自ASTであるJsdocTag
の中にある、parsedType
というキーがそれ。
すべては、eslint-plugin-jsdoc
のcontexts[].comment
の絞り込みでesquery
を使うために。
あと実は、jsdoccomment
でexport * from 'jsdoc-type-pratt-parser'
されてて、
- stringify
- parse
- tryParse
- traverse
これらが、eslint-plugin-jsdoc
の各ルールで使われてる。
コードだけでなく依存関係も本当に追いづらい・・・。
まとめ
Part 1-3の総まとめ。
eslint-plugin-jsdoc
が扱うのは、- ソース内のすべてのコメント(種別を問わない)
- ソース内のすべてのノードに紐づく直近単一のJSDocコメント、および、ノードに紐づかないすべてのJSDocコメント
- 特定のノードに紐づく、直近単一のJSDocコメント
- ルール側は、
iterateJsdoc()
によって各種コンテキストや設定がチェックされ、呼ばれるのを待つcontexts[].(context|comment)
や、各ルール側が想定するノードなどに応じて決まる- ここの実装が重そう
- 呼ばれたときは、以下のもの(抜粋)が渡されるので、用途に応じて使う
- jsdoc:
jsdoccomment
がパースしたBlock
型とパース済のインラインタグ - jsdocNode:
estree.Comment
トークン - node:
estree.AST.Node | null
- jsdoc:
- ESTreeにない独自JSDoc ASTは、
jsdoccomment
とjsdoc-type-pratt-parser
が定義してる- TypeScriptのASTとも微妙に違うし、とても簡素
というのが大まかな挙動かな。
そして大事なのが、まとめてはみたものの、60%くらいしか理解できてる自信はないってところ。
さて、これをどうやってOxcに持っていこうか・・・。