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に持っていこうか・・・。