終わりが見えないシリーズになってきた。
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
になるパターンの詳細は次回へ持ち越し。
続くったら続く。