🧊

Prettier のコードを読む Part.6

このASTまわりを越えてやっと、全工程の1/3に達することができる・・・。

Prettier のコードを読む Part.5 | Memory ice cubes https://leaysgur.github.io/posts/2024/09/17/162302/

年内に読み切れるかしら?

おさらい

decorateComment()で近接ノードの情報を拡張したCommentを、ループで処理していたところ。

まずはコメントの位置をownLineendOfLineかそれ以外に分類し、その上でコメントをノードに紐づけてた。

今回は、ownLineでもendOfLineでもない、その他のremainingに分類されたコメントたちの中でも、最後の例外処理を扱うところを読んでいく。

if (precedingNode && followingNode)

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/main/comments/attach.js#L252

この部分。

// 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)

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/main/comments/attach.js#L345

呼ばれ方が2パターンある。

  • さっきのif文を通り、既にキューがあった場合
    • followingNodeの同一性をチェックしてから
  • さっきのif文を通らず、最後の仕上げとして
    • こっちでは、followingNodeのチェックはしない

これが呼ばれるとき、tiesToBreakは常に1つ以上のコメントを含むことになり、breakTies()は処理の最後にtiesToBreak.length = 0としてる。 なので要素が2つ以上になるケースは、followingNodeのチェック次第ってこと。

isGap(gapText, options)という処理で、コメント間が空白のみかどうかのチェックをして、前につくか後につくかを決める。

ESTreeのprinter.isGapは、flow向けに少し正規表現を調整してるだけで、デフォルトでは/^[\s(]*$/uというパターンが使われる。(も見てるのが気になるが・・・。

これで割り出した位置によって前後を決めて、それぞれaddTrailingComment()addLeadingComment()する。

その後、precedingNodefollowingNodecommentsを、またlocStartの順に並べ直してる。

わかるようでわからない処理だが、必要なんやろうこれも。

addXxxComments()

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/main/comments/utils.js

  • 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])でこっそり渡されるパターンが存在した。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/comments/handle-comments.js#L477

handleClassComments()の中で、followingNodeが存在するケースでのみ発生する可能性がある模様。

ただ、このプロパティを生やさないようにしても、利用箇所をコメントアウトしても、なんとテストは通る上、どういうコードを書けばこの処理に到達できるのか、わからなかった。

このフラグが利用されてるのは、後でコメントをDoc化するときで、printDanglingComments()という処理の中。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/main/comments/print.js#L119

classをDoc化する部分でも、個別にprintDanglingComments(path, options, { marker })してるところもあったので、ここでだけ使われてて、コメントにプロパティを生やす意味は実はない・・・?

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/class.js#L154

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()という判定の中。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/utils/index.js#L994

ただ、コードとしてisPrettierIgnoreComment()trueだったらunignore = trueするようになってるが、そのisPrettierIgnoreComment()では、!comment.unignoreが条件になってる。

つまり1度だけ呼びたいとかそういうこと?わからん・・・が、消すとテストがこけるので、必要なんであろう。

export type a =
  | foo1&bar1
  // prettier-ignore
  | qux1&qux2;

このコードで、unignore: trueが観測できる。

そのあと

最後、各ノード側のcommentsCommentを紐づけた後で、Comment側のprecedingNodeやらは不要になるらしく、消してた。 (なので、Playgroundでは近接ノードの情報が見えない) (ってか消すなら最初から別の場所に置いてほしいし、その他にも消えてない独自プロパティはまだあると思うし、なんのため?感はある)

ともあれこれでattachComments(ast, options)はおわり。

その後は、printer.preprocess()が定義されてれば呼んでるけど、ESTreeのプリンターにはないので無視。

というわけで、prepareToPrint()によるASTの準備がついに完了となり、Doc化の工程へ進める。

CommentCheckFlagsなるものもある

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/utils/index.js#L1004-L1022

/** @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,
};

コメントまわりの処理を追ってて見つけた。

どうやら・・・、

という感じらしい。 (どうせなら、このコメントまわりを処理するフェーズで、このビットフラグだけを生やせばよかったのでは?)

コメントまわりユーティリティ、使ったり使わなかったりあちこちで自作してたり、混迷を極めておられる感じだ・・・。

ここまでのまとめ

  • Commentに、leadingtrailingのフラグを生やしつつ、各ノードに紐づけることが主目的
  • しかしその他の情報もコンテキストによっては必要で、少なくともCommentCheckFlagsに含まれる情報は必要
    • コードとしては、必ずこのビットフラグを使ってるわけではない
  • 他にもmarkerunignoreといった追加のフラグを局所的に見ることもある
  • 結局またユーティリティでownLineかどうか見てるものもありそう・・・?

こと、ASTにコメントを紐づける段階で必要なものは、placementの分類と近接ノードの情報だけで、Babelなど一部のASTによる独自に紐付けられた結果なんかは使われてない。

逆にいうと、これまで見てきた幾重にもなってる処理を経なければ、Prettierが欲するleadingtrailingの適切なノードへの紐付けはできないということ。

それにしても、改行があるかとか空白のみが続くかとか、ASTの限界って感じの処理のオンパレードであるなあ・・・。

元コード文字列からASTを手にいれるまで

総まとめ。

  • まず任意のパーサーでparse(text, options)
  • ASTを微調整するpostprocess(ast, options)
    • ここでのvisitNode(ast, fn)は、単なるVisitorではなく、返り値によってそのノードを上書きする
  • attachComments(ast, options)でノードの各所にコメントの紐付け
    • decorateComment(): まずコメントの近接ノードを取得
    • それを使って、ownLineendOfLineremainingか、どこに位置しているかを判定
    • その位置ごとに、特殊なケースに対処しつつ、紐付けていく
    • breakTies()で最後の例外処理

というのが全体の流れになってて、JSでもJSXでもTSでも関係なく、すべてこういう流れ。

いよいよ次からは、ASTをDocにする部分へ。