続いてます。
Prettier のコードを読む Part 2 | Memory ice cubes https://leaysgur.github.io/posts/2024/09/02/144240/
今回からは、メインの整形処理で、具体的にどういうことが起こってるのかを追ってく。
おさらい
https://github.com/prettier/prettier/blob/3.3.3/src/main/core.js#L25
整形ロジックの中心であるcoreFormat(text, options)の詳細を調べたかった。
そしてここでは、
- 整形前コード > AST
- AST > Doc
- Doc > 整形後コード
という3段階の工程があることがわかってて、それぞれ具体的にどういうことが行われてるのか。
// 1
let a
= 42; // 2
というファイルに対して、prettier t.js --no-semiを実行すると、
// 1
let a = 42 // 2
というコードが出力される。
- コメントの行頭が揃って
- 変数宣言が1列になり
- セミコロンが削除され
- 最後に空行が追加される
これらの処理がどう行われてるか?ということを調べたい。
まずは、整形前コードからASTの工程。
入口はこちら。
resolveParser(options)
まずはどのパーサーを使うかを割り出す。といっても、ここに到達した時点で何を使うかは既に決まってるので、単にJSとして参照を解決するだけ。
CLIで、パーサー指定なしで、JSファイルに対して実行した場合は、
- parser: babel
- printer: estree
というデフォルト構成になる。
parser.parse(text, options)
https://github.com/prettier/prettier/blob/3.3.3/src/language-js/parse/babel.js
初期化してパース実行まで。
Babelのパーサーは、他のパーサーより比較的いろんなことをやってるように見える。(デフォルトだから?)
パーサーのプラグインやらを整えたら、parse(text, options)でASTにする。
- v8のIntrinsicなんかにも対応してるのね
- パイプラインオペレーター(
|>)のためのひと手間なんかもある
最後にpostprocess(ast, options)してASTを返すのがパーサーのお作法らしい。
postprocess(ast, options)
どうやら自称ESTree互換な各ASTの差分を吸収する後処理らしい。
BabelのAST特有な処理もあれば、そうでないものもある。
ParenthesizedExpressionにコメントが紐づかず、透過的に扱えるよう位置を偽装/** @type {Foo} */ (foo)のやつね・・・
- その他にもいくつかのノードに特別な前処理
LogicalExpressionVariableDeclaration- etc…
- 連続するブロックコメントのマージ
- すべての行が
*ではじまるブロックコメントが隣接してたら
- すべての行が
ふむ。
単にパーサーからASTを取得してるだけかと思ってたけど、ASTに対して変更を加えてたことがわかった。
printAstToDoc(ast, options) > prepareToPrint(ast, options)
https://github.com/prettier/prettier/blob/3.3.3/src/main/ast-to-doc.js#L31
このDoc化の直前にも、またしてもASTに変更を加えてることがわかってる。
https://github.com/prettier/prettier/blob/3.3.3/src/main/ast-to-doc.js#L128
やってることは2つ。
- ASTの各ノードに対して、
Commentを紐づける printer.preprocess(ast, options)があれば実行- ESTreeのプリンターにはない
コメントはやはり鬼門である。
attachComments(ast, options)
まず、ESTree.Commentに対して下処理をしてて、それがdecorateComment(rootNode, comment, options)というやつ。
decorateComment(node, comment, options)
4番目の引数もあって、自身で再帰するときに使ってた。
ここでは、コメントごとに以下の3つを探してる。
followingNode: 自分の後にあるノードprecedingNode: 自分の前にあるノードenclosingNode: 自分を内包するノード
今回のコード例だと、コメントが2つあって、
- 1つ目:
followingNodeがVariableDeclaration、ほかはundefined - 2つ目:
precedingNodeがVariableDeclaration、ほかはundefined
対象のノードは、自分の位置と対象ノードの位置を使って2分探査で探しつつ、同時にコメントを紐づけるにふさわしいかの判定もやってる。
https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/main/comments/attach.js#L18 https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/comments/printer-methods.js#L14
なるほどな!これを理解するのは無理!
次に、拡張したESTree.Commentの配列を回して、ASTのノード側に反映していく。
同時に、comment.placementというものも追加される。
ownLineendOfLineremaining
このいずれかになる模様。
- 1つ目: コメント単独の行なので、
endOfLineなコメント- どういう種類の
endOfLineコメントかを判定 - どれにも合致せず、
followingNodeがあるのでaddLeadingComment(node, comment)で処理
- どういう種類の
- 2つ目: こちらもはコメント単独行ではないけど、
endOfLineなコメント- 同じく17パターンには合致しない
precedingNodeがあるのでaddTrailingComment(node, comment)で処理
このaddXxxComment()が呼ばれると、そのノードにcommentsプロパティが生えることはわかった。
ループが終わると、最後にbreakTies()という処理を呼んでる。
comment.placementがremainingになっていて、その中で既知の12パターンに合致しなかったとき、はぐれコメントとしてマークされるらしい。
それを最終的にどこかのノードに紐づける最後の砦みたいなことをしてる・・・と思う。
ここまでやって、やっと、Doc化するためのASTのできあがり。
@babel/parserのattachCommentオプション
というか、このあたりをいじってて初めて知ったけど、BabelのパーサーにはattachCommentというオプションがある。
これを有効にすると、特定のノードにleadingCommentsとtrailingCommentsとinnerCommentsというものが生えるようになる・・・。
https://github.com/babel/babel/blob/04485b5796956e38fb5cfdd8bcbc11c2ff2bae7f/packages/babel-parser/src/parser/comments.ts https://github.com/babel/babel/blob/04485b5796956e38fb5cfdd8bcbc11c2ff2bae7f/packages/babel-parser/ast/comment-attachment.md
ただPrettierの場合は、Babel以外のパーサーにも対応するために、自前でやらざるを得ないということか。
あと少し調べてみたところ、どうやらPrettierとBabelとでは、やはり解釈というか挙動が違うようだった。
やっぱコメントの用途をパーサーレベルで解釈するのは無理ってことかね・・・。
続く
この時点でのASTはこんな感じ。
{
"ast": {
"type": "File",
"start": 0,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 4,
"column": 0,
"index": 25
}
},
"range": [0, 25],
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 4,
"column": 0,
"index": 25
}
},
"range": [0, 25],
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 7,
"end": 19,
"loc": {
"start": {
"line": 2,
"column": 0,
"index": 7
},
"end": {
"line": 3,
"column": 6,
"index": 19
}
},
"range": [7, 19],
"declarations": [
{
"type": "VariableDeclarator",
"start": 11,
"end": 18,
"loc": {
"start": {
"line": 2,
"column": 4,
"index": 11
},
"end": {
"line": 3,
"column": 5,
"index": 18
}
},
"range": [11, 18],
"id": {
"type": "Identifier",
"start": 11,
"end": 12,
"loc": {
"start": {
"line": 2,
"column": 4,
"index": 11
},
"end": {
"line": 2,
"column": 5,
"index": 12
},
"identifierName": "a"
},
"range": [11, 12],
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 16,
"end": 18,
"loc": {
"start": {
"line": 3,
"column": 3,
"index": 16
},
"end": {
"line": 3,
"column": 5,
"index": 18
}
},
"range": [16, 18],
"extra": {
"rawValue": 42,
"raw": "42"
},
"value": 42
}
}
],
"kind": "let",
"trailingComments": [
{
"type": "CommentLine",
"value": " 2",
"start": 20,
"end": 24,
"loc": {
"start": {
"line": 3,
"column": 7,
"index": 20
},
"end": {
"line": 3,
"column": 11,
"index": 24
}
},
"placement": "endOfLine",
"leading": false,
"trailing": true,
"printed": false,
"nodeDescription": "VariableDeclaration"
}
],
"leadingComments": [
{
"type": "CommentLine",
"value": " 1",
"start": 2,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 2,
"index": 2
},
"end": {
"line": 1,
"column": 6,
"index": 6
}
},
"placement": "endOfLine",
"leading": true,
"trailing": false,
"printed": false,
"nodeDescription": "VariableDeclaration"
}
],
"comments": [
{
"type": "CommentLine",
"value": " 1",
"start": 2,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 2,
"index": 2
},
"end": {
"line": 1,
"column": 6,
"index": 6
}
},
"placement": "endOfLine",
"leading": true,
"trailing": false,
"printed": false,
"nodeDescription": "VariableDeclaration"
},
{
"type": "CommentLine",
"value": " 2",
"start": 20,
"end": 24,
"loc": {
"start": {
"line": 3,
"column": 7,
"index": 20
},
"end": {
"line": 3,
"column": 11,
"index": 24
}
},
"placement": "endOfLine",
"leading": false,
"trailing": true,
"printed": false,
"nodeDescription": "VariableDeclaration"
}
]
}
],
"directives": []
},
"tokens": [/* omit */]
},
"comments": [/* omit */]
}
これはPlaygroundで表示したpreprocessed ASTというやつ。(tokensとcommentsは省略した。)
VariableDeclarationのノードにcommentsが生えてるのがわかる。((leading|trailing)CommentsはBabel特有のやつ)
元のESTree.Commentから増えたのは、
placement: string: さっきの3パターンだが、Doc化には使われてなさそうleading: boolean: 前にあるのかどうかtrailing: boolean: 後にあるのかどうかprinted: boolean: Doc化済フラグ(後でdeleteされる・・・)nodeDescription: string: デバッグ用のメッセージ
というプロパティたち。重要なのはやはりleadingとtrailingか。
ちなみに、CLIの--debug-print-astだと、commentsが生える前のASTしか見れない。
ふう。とりあえず、次へ進むか・・・。