終わりが見えないシリーズになってきた。
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()) {
// ...
}
重厚な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)の場合。
まずそのコメントの開始位置を割り出す。
もしprecedingNodeがある場合、
- 自身よりも前方にあるコメントで
- 同じ
precedingNodeを持っていて - 自身との間にはスペースのみ(改行は含まない)
なら、その同じ行の先頭コメントの開始位置を使う。
そしてこの開始位置でもって、hasNewLine(text, commentStart, { backwards: true })した結果を返す。
hasNewLine(text, startIndex, options)
skipSpaces()とskipNewLine()の2つのユーティリティを使ってる。
https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/utils/skip.js
skipSpaces()は、実際にはskip(" \t")から作られた関数で、与えられた位置からこれらの文字に合致するものをスキップしていって、違うものを見つけたらその位置を返す。
つまり、そのコメントの前方にある、スペースとタブ以外の文字の位置を返す。
次に、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の処理
コードはこういう感じ。
// 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)
圧巻のコードがこれ。
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));
}
必要だから書いてあるんやと思うけど、こんなにあるんか特殊なケース・・・。
もはや個別に書いていくことはしないが、
- 半分くらいは
ownLine専用だが、他は共通で使われてるものもある - CSTではないがゆえに、元コード文字列から文字を探すこともあるとのこと
- 引数
contextはdecorateComment()の返り値そのままastもまるごと入ってたりする- が、それを使うのは
handleOnlyComments()で、parser: flowのときだけとか
ということはわかった。
endOfLineの処理
これも、endOfLineコメントの特殊なケースを先にチェックして、後は流れに任せてる。
handleEndOfLineComment(context)
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の処理
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);
}
さすがに残り物の処理だけあって、不確実な状態らしい。
気になるのはもちろんprecedingNodeもfollowingNodeも存在する場合の分岐だが、まずはhandleRemainingComment()を。
それ以外は、ownLineとendOfLineと似たような流れで見ての通り。
handleRemainingComment(context)
function handleRemainingComment(context) {
return [
handleIgnoreComments,
handleIfStatementComments,
handleWhileComments,
handleObjectPropertyAssignment,
handleCommentInEmptyParens,
handleMethodNameComments,
handleOnlyComments,
handleCommentAfterArrowParams,
handleFunctionNameComments,
handleTSMappedTypeComments,
handleBreakAndContinueStatementComments,
handleTSFunctionTrailingComments,
].some((fn) => fn(context));
}
他の2つに比べると、専用のものが多い。やはり例外処理ってことかな。
ここまでのまとめ
長くなってきたので、placementがremainingになるパターンの詳細は次回へ持ち越し。
続くったら続く。