このASTまわりを越えてやっと、全工程の1/3に達することができる・・・。
Prettier のコードを読む Part 5 | Memory ice cubes https://leaysgur.github.io/posts/2024/09/17/162302/
年内に読み切れるかしら?
おさらい
decorateComment()
で近接ノードの情報を拡張したComment
を、ループで処理していたところ。
まずはコメントの位置をownLine
かendOfLine
かそれ以外に分類し、その上でコメントをノードに紐づけてた。
今回は、ownLine
でもendOfLine
でもない、その他のremaining
に分類されたコメントたちの中でも、最後の例外処理を扱うところを読んでいく。
if (precedingNode && followingNode)
この部分。
// Otherwise, text exists both before and after the comment on
// the same line. If there is both a preceding and following
// node, use a tie-breaking algorithm to determine if it should
// be attached to the next or previous node. In the last case,
// simply attach the right node;
const tieCount = tiesToBreak.length;
if (tieCount > 0) {
const lastTie = tiesToBreak[tieCount - 1];
if (lastTie.followingNode !== followingNode) {
breakTies(tiesToBreak, options);
}
}
tiesToBreak.push(context);
どうやら、
tiesToBreak
の配列にコメントが1つでも存在するとき- そのコメントの
followingNode
と、今見てるコメントのfollowingNode
が違ったら breakTies()
を実行して- 新たにコメントを
tiesToBreak
に追加する
改行ではなく何かしらのトークンに挟まれたコメントで、明示的に対処できなかった残り物をどうするか?という処理らしい。 トークンの隙間に存在できてる時点で、これは複数行コメントであるはず。
具体的には、こういうものと想定。
let a, /* THIS */ /* and THAT */ b = 1;
複数の複数行コメントが並ぶなんて滅多にないと思うので、本当に例外処理なのだなあ。
breakTies(tiesToBreak, options)
呼ばれ方が2パターンある。
- さっきの
if
文を通り、既にキューがあった場合followingNode
の同一性をチェックしてから
- さっきの
if
文を通らず、最後の仕上げとして- こっちでは、
followingNode
のチェックはしない
- こっちでは、
これが呼ばれるとき、tiesToBreak
は常に1つ以上のコメントを含むことになり、breakTies()
は処理の最後にtiesToBreak.length = 0
としてる。
なので要素が2つ以上になるケースは、followingNode
のチェック次第ってこと。
isGap(gapText, options)
という処理で、コメント間が空白のみかどうかのチェックをして、前につくか後につくかを決める。
ESTreeのprinter.isGap
は、flow
向けに少し正規表現を調整してるだけで、デフォルトでは/^[\s(]*$/u
というパターンが使われる。(
も見てるのが気になるが・・・。
これで割り出した位置によって前後を決めて、それぞれaddTrailingComment()
かaddLeadingComment()
する。
その後、precedingNode
とfollowingNode
のcomments
を、またlocStart
の順に並べ直してる。
わかるようでわからない処理だが、必要なんやろうこれも。
addXxxComments()
addLeadingComment(node, comment)
addTrailingComment(node, comment)
addDanglingComment(node, comment)
何気なく使ってたこれらが何をやってるかも見ておく。
これらがやってるのは、Comment
にいくつかのプロパティを生やすこと。
printed: boolean = false
- Doc化するときに使う、処理済かどうかのフラグ
nodeDescription: string
- どのノードに紐づいたかのデバッグ情報
leading: boolean
addLeadingComment()
の場合にtrue
、それ以外はfalse
trailing: boolean
addTrailingComment()
の場合にtrue
、それ以外はfalse
そのうえで、各ノードにcomments
配列を用意して、この拡張されたComment
をpushする。
ちなみにこの様子は、Debug > Show commentsのチェックボックスを有効にしたPlaygroundでも確認できる。
ただコードを読む限り、他にもプロパティ生やしてなかった?ということで、あれこれgrepして調べた結果がこれ。
type ExtendedComment = ESTree.Comment & {
placement: "ownLine" | "endOfLine" | "remaining";
printed: boolean;
nodeDescription: string;
leading: boolean;
trailing: boolean;
marker?: "implements" | "extends" | "mixins";
unignore?: true;
};
謎のプロパティが他にも生えてる。
marker
addDanglingComment(node, comment [, marker])
でこっそり渡されるパターンが存在した。
handleClassComments()
の中で、followingNode
が存在するケースでのみ発生する可能性がある模様。
ただ、このプロパティを生やさないようにしても、利用箇所をコメントアウトしても、なんとテストは通る上、どういうコードを書けばこの処理に到達できるのか、わからなかった。
このフラグが利用されてるのは、後でコメントをDoc化するときで、printDanglingComments()
という処理の中。
class
をDoc化する部分でも、個別にprintDanglingComments(path, options, { marker })
してるところもあったので、ここでだけ使われてて、コメントにプロパティを生やす意味は実はない・・・?
unignore
handleUnionTypeComments()
とhandleIgnoreComments()
で、そのコメントがprettier-ignore
だった場合にtrue
が入ってる。
https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/comments/handle-comments.js#L727 https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/comments/handle-comments.js#L896
このフラグが利用されるのは、isPrettierIgnoreComment()
という判定の中。
ただ、コードとしてisPrettierIgnoreComment()
がtrue
だったらunignore = true
するようになってるが、そのisPrettierIgnoreComment()
では、!comment.unignore
が条件になってる。
つまり1度だけ呼びたいとかそういうこと?わからん・・・が、消すとテストがこけるので、必要なんであろう。
export type a =
| foo1&bar1
// prettier-ignore
| qux1&qux2;
このコードで、unignore: true
が観測できる。
そのあと
最後、各ノード側のcomments
にComment
を紐づけた後で、Comment
側のprecedingNode
やらは不要になるらしく、消してた。
(なので、Playgroundでは近接ノードの情報が見えない)
(ってか消すなら最初から別の場所に置いてほしいし、その他にも消えてない独自プロパティはまだあると思うし、なんのため?感はある)
ともあれこれでattachComments(ast, options)
はおわり。
その後は、printer.preprocess()
が定義されてれば呼んでるけど、ESTreeのプリンターにはないので無視。
というわけで、prepareToPrint()
によるASTの準備がついに完了となり、Doc化の工程へ進める。
CommentCheckFlags
なるものもある
/** @enum {number} */
const CommentCheckFlags = {
/** Check comment is a leading comment */
Leading: 1 << 1,
/** Check comment is a trailing comment */
Trailing: 1 << 2,
/** Check comment is a dangling comment */
Dangling: 1 << 3,
/** Check comment is a block comment */
Block: 1 << 4,
/** Check comment is a line comment */
Line: 1 << 5,
/** Check comment is a `prettier-ignore` comment */
PrettierIgnore: 1 << 6,
/** Check comment is the first attached comment */
First: 1 << 7,
/** Check comment is the last attached comment */
Last: 1 << 8,
};
コメントまわりの処理を追ってて見つけた。
どうやら・・・、
- コメントにまつわる情報をビットを使ったフラグとして管理していて
- これを使って任意のチェック関数を作れるようになっていて
hasComment(node, flags)
とgetComments(node, flags)
というユーティリティを公開しつつ- 他にもいくつか既製のユーティリティを公開してる
という感じらしい。 (どうせなら、このコメントまわりを処理するフェーズで、このビットフラグだけを生やせばよかったのでは?)
コメントまわりユーティリティ、使ったり使わなかったりあちこちで自作してたり、混迷を極めておられる感じだ・・・。
ここまでのまとめ
Comment
に、leading
とtrailing
のフラグを生やしつつ、各ノードに紐づけることが主目的- しかしその他の情報もコンテキストによっては必要で、少なくとも
CommentCheckFlags
に含まれる情報は必要- コードとしては、必ずこのビットフラグを使ってるわけではない
- 他にも
marker
やunignore
といった追加のフラグを局所的に見ることもある - 結局またユーティリティで
ownLine
かどうか見てるものもありそう・・・?
こと、ASTにコメントを紐づける段階で必要なものは、placementの分類と近接ノードの情報だけで、Babelなど一部のASTによる独自に紐付けられた結果なんかは使われてない。
逆にいうと、これまで見てきた幾重にもなってる処理を経なければ、Prettierが欲するleading
とtrailing
の適切なノードへの紐付けはできないということ。
それにしても、改行があるかとか空白のみが続くかとか、ASTの限界って感じの処理のオンパレードであるなあ・・・。
元コード文字列からASTを手にいれるまで
総まとめ。
- まず任意のパーサーで
parse(text, options)
- パーサーごとにオプションはいろいろあるが、コメントを出力する設定は必要
line
やcolumn
は、主にエラーメッセージのために使われてるだけだが、1箇所だけメインの処理でも使われてる- https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/comments/handle-comments.js#L204-L209
- ただこれは、2点間に改行がないかチェックすればいいだけなので、実質は不要のはず
- ASTを微調整する
postprocess(ast, options)
- ここでの
visitNode(ast, fn)
は、単なるVisitorではなく、返り値によってそのノードを上書きする
- ここでの
attachComments(ast, options)
でノードの各所にコメントの紐付けdecorateComment()
: まずコメントの近接ノードを取得- それを使って、
ownLine
かendOfLine
かremaining
か、どこに位置しているかを判定 - その位置ごとに、特殊なケースに対処しつつ、紐付けていく
breakTies()
で最後の例外処理
というのが全体の流れになってて、JSでもJSXでもTSでも関係なく、すべてこういう流れ。
いよいよ次からは、ASTをDocにする部分へ。