続いてます。
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)
のやつね・・・
- その他にもいくつかのノードに特別な前処理
LogicalExpression
VariableDeclaration
- 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
というものも追加される。
ownLine
endOfLine
remaining
このいずれかになる模様。
- 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しか見れない。
ふう。とりあえず、次へ進むか・・・。