この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: booleanaddLeadingComment()の場合にtrue、それ以外はfalse
trailing: booleanaddTrailingComment()の場合に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にする部分へ。