🧊

Prettier のコードを読む Part.5

終わりが見えないシリーズになってきた。

Prettier のコードを読む Part.4 | Memory ice cubes https://leaysgur.github.io/posts/2024/09/13/094947/

息切れしない程度にぼちぼち読み進めていきたい。

おさらい

整形処理を行うためには、コード文字列をASTに変換したあと、コメントの情報を埋め込む必要があった。

そのコメントの紐づけ処理は、attachComments(ast, options)という関数で行われていて、その内情を探ってたところ。

まずは準備段階として、ASTから得られるCommentの配列それぞれに、近接ノードの情報を拡張するdecorateComment(node, comment, options)を呼ぶところまでは前回で読んだ。

今回は、それら拡張されたCommentをループして、いざASTの各ノードに情報を反映させていくところを読んでいく。

全体の流れ

具体的にはこのループの中身。

for (const [index, context] of decoratedComments.entries()) {
  // ...
}

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

重厚なif-elseの果てに、次のいずれかでノードにコメントが紐づけられる。

  • addLeadingComment(node, comment)
  • addTrailingComment(node, comment)
  • addDanglingComment(node, comment)

なんらかのユーティリティ関数を呼んでいたとしても、その先ではこれらに行き着く模様。(詳細は追々で)

で、ここでの処理は、大きく分けて3つの分岐がある。

  • placement: ownLine
  • placement: endOfLine
  • placement: remaining

このplacementプロパティは、どさくさに紛れてCommentに追加で生やされてるものの、なんとここ以外では使われてない。

まずは、どうやってこれらの分類が決まるかを調べていく。

placement: ownLine

isOwnLineComment(text, options, decoratedComments, commentIndex)の場合。

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

まずそのコメントの開始位置を割り出す。

もしprecedingNodeがある場合、

  • 自身よりも前方にあるコメントで
  • 同じprecedingNodeを持っていて
  • 自身との間にはスペースのみ(改行は含まない)

なら、その同じ行の先頭コメントの開始位置を使う。

そしてこの開始位置でもって、hasNewLine(text, commentStart, { backwards: true })した結果を返す。

hasNewLine(text, startIndex, options)

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/utils/has-newline.js#L12

skipSpaces()skipNewLine()の2つのユーティリティを使ってる。

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

skipSpaces()は、実際にはskip(" \t")から作られた関数で、与えられた位置からこれらの文字に合致するものをスキップしていって、違うものを見つけたらその位置を返す。

つまり、そのコメントの前方にある、スペースとタブ以外の文字の位置を返す。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/utils/skip-newline.js#L12

次に、skipSpaces()で得た位置を使って、skipNewLine()してる。

skipNewLine()では、渡された位置にある文字が、改行コードなどいわゆるLine terminatorかどうかをチェックしてる。

もしそうだった場合は、その改行(つまりは空行)をスキップした位置を返す。

そして、これら2つの位置を比較して返す。

つまり、

  • コメントの開始位置より前方に
  • スペースだけが続いたあと
  • 新たに改行があったら

それはownLineなコメントということになる。

placement: endOfLine

isOwnLineComment()と似てるけど、チェックの向きが違うのがisEndOfLineComment()ってこと。

今回も同じく、followingNodeがある場合は、

  • 自身よりも後方にあるコメントで
  • 同じfollowingNodeを持っていて
  • 自身との間にはスペースのみ(改行は含まない)

なら、同じ行の最後尾のコメントの位置を使う。

そしてまたもその位置を使って、hasNewLine()する。

つまり、

  • ownLineではなく
  • コメントの開始位置より後方に
  • スペースだけが続いたあと
  • 新たに改行があったら

それはendOfLineなコメントということになる。

placement: remaining

その他、ownLineでもendOfLineでもない場合、remainingなコメントということになる。

placementのまとめ

// endOfLine: 前方に改行がないから、ownLineになれない
// ownLine: 前方に改行があり、スペースをスキップした位置と同じだから
// ownLine: 前方に改行があり、スペースをスキップした位置と同じだから
const a = [
  1, // endOfLine: 前方に改行がないが、後方には改行があるので
  /* ownLine: 同上 */ /* ownLine: 同上、位置判定に使うのはこの行の先頭のコメント */
  2,
  /* ownLine: 同上 */ 3,
]; // endOfLine: 次の改行がもしなかったら、remainingになってしまう

それ以外の場所、つまり改行(とスペース)に接してないものはすべて、remainingという扱いになる。

では、分類方法がわかったところで処理の内容へ。

ownLineの処理

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

コードはこういう感じ。

// If a comment exists on its own line, prefer a leading comment.
// We also need to check if it's the first line of the file.
if (handleOwnLineComment(...args)) {
  // We're good
} else if (followingNode) {
  // Always a leading comment.
  addLeadingComment(followingNode, comment);
} else if (precedingNode) {
  addTrailingComment(precedingNode, comment);
} else if (enclosingNode) {
  addDanglingComment(enclosingNode, comment);
} else {
  // There are no nodes, let's attach it to the root of the ast
  addDanglingComment(ast, comment);
}

ownLineコメントの特殊なケースを先にチェックし、それ以外は順番にノードの有無に応じて処理していく。

handleOwnLineComment(context)

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

圧巻のコードがこれ。

function handleOwnLineComment(context) {
  return [
    handleIgnoreComments,
    handleConditionalExpressionComments,
    handleLastFunctionArgComments,
    handleLastComponentArgComments,
    handleMemberExpressionComments,
    handleIfStatementComments,
    handleWhileComments,
    handleTryStatementComments,
    handleClassComments,
    handleForComments,
    handleUnionTypeComments,
    handleOnlyComments,
    handleModuleSpecifiersComments,
    handleAssignmentPatternComments,
    handleMethodNameComments,
    handleLabeledStatementComments,
    handleBreakAndContinueStatementComments,
    handleNestedConditionalExpressionComments,
    handleCommentsInDestructuringPattern,
  ].some((fn) => fn(context));
}

必要だから書いてあるんやと思うけど、こんなにあるんか特殊なケース・・・。

もはや個別に書いていくことはしないが、

ということはわかった。

endOfLineの処理

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

これも、endOfLineコメントの特殊なケースを先にチェックして、後は流れに任せてる。

handleEndOfLineComment(context)

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

function handleEndOfLineComment(context) {
  return [
    handleClosureTypeCastComments,
    handleLastFunctionArgComments,
    handleConditionalExpressionComments,
    handleModuleSpecifiersComments,
    handleIfStatementComments,
    handleWhileComments,
    handleTryStatementComments,
    handleClassComments,
    handleLabeledStatementComments,
    handleCallExpressionComments,
    handlePropertyComments,
    handleOnlyComments,
    handleVariableDeclaratorComments,
    handleBreakAndContinueStatementComments,
    handleSwitchDefaultCaseComments,
    handleLastUnionElementInExpression,
  ].some((fn) => fn(context));
}

さっきと同じ流れだが、ここでもendOfLine個別のものもあれば、共通で使われてるものもある。

remainingの処理

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

if (handleRemainingComment(...args)) {
  // We're good
} else 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);
} else if (precedingNode) {
  addTrailingComment(precedingNode, comment);
} else if (followingNode) {
  addLeadingComment(followingNode, comment);
} else if (enclosingNode) {
  addDanglingComment(enclosingNode, comment);
} else {
  // There are no nodes, let's attach it to the root of the ast
  addDanglingComment(ast, comment);
}

さすがに残り物の処理だけあって、不確実な状態らしい。

気になるのはもちろんprecedingNodefollowingNodeも存在する場合の分岐だが、まずはhandleRemainingComment()を。

それ以外は、ownLineendOfLineと似たような流れで見ての通り。

handleRemainingComment(context)

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

function handleRemainingComment(context) {
  return [
    handleIgnoreComments,
    handleIfStatementComments,
    handleWhileComments,
    handleObjectPropertyAssignment,
    handleCommentInEmptyParens,
    handleMethodNameComments,
    handleOnlyComments,
    handleCommentAfterArrowParams,
    handleFunctionNameComments,
    handleTSMappedTypeComments,
    handleBreakAndContinueStatementComments,
    handleTSFunctionTrailingComments,
  ].some((fn) => fn(context));
}

他の2つに比べると、専用のものが多い。やはり例外処理ってことかな。

ここまでのまとめ

長くなってきたので、placementremainingになるパターンの詳細は次回へ持ち越し。

続くったら続く。