いつまで続くのだろうか。
Prettier のコードを読む Part 3 | Memory ice cubes https://leaysgur.github.io/posts/2024/09/04/143544/
おさらい
整形処理のメイン工程では、ASTをPrettierのIRであるDocに変換するけど、そのためにまずASTにひと手間を加える必要があった。
任意のパーサーでコード文字列をASTにした直後、コメントの情報をASTに追加してた。
このattachComments(ast, options)
という関数が実行されると、各ノードにcomments
というプロパティが生える。
この処理の内容を、重点的に読んでいきたい回。
上からざっと行くと、
decorateComment()
で、コメントに対して近接のノード情報を保存したうえで- それらのコメントをループして、今度はノード側にコメントを紐づけていく
この2段階の処理の1つ目を見ていく。
decorateComment(node, comment, options [, enclosingNode])
コメント曰く、
As efficiently as possible, decorate the comment object with .precedingNode, .enclosingNode, and/or .followingNode properties, at least one of which is guaranteed to be defined.
なるほど。
まず初手でgetSortedChildNodes(node, options)
してることから、なんらかの順にソートしたものから探すらしい。
getSortedChildNodes(node, options)
キャッシュを使って動作を高速化してる。
第1引数のnode
は、基本的にはASTのルートとなるtype: Program|File
になると思うけど、attachComments()
の第4引数が使われるケースでは別のノードになる。
まず、printer.canAttachComment()
が実装されているかチェックしてるけど、これはestree
のプリンターには必ずあるので無視でいい。
その次、ここのメインである子ノードを取得するところ。
const childNodes = (
getCommentChildNodes(node, options) ?? [
...getChildren(node, {
getVisitorKeys: createGetVisitorKeysFunction(printerGetVisitorKeys),
}),
]
).flatMap((node) =>
canAttachComment(node) ? [node] : getSortedChildNodes(node, options),
);
まずは、getCommentChildNodes(node, options)
しようとする。
getCommentChildNodes(node, options)
// Prevent attaching comments to FunctionExpression in this case:
// class Foo {
// bar() // comment
// {
// baz();
// }
// }
というコメントが残されていて、
- 以下の特定のパーサーでのみ、
typescript
flow
acorn
espree
meriyah
__babel_estree
MethodDefinition
のノードで、value
がFunctionExpression
なら- etc…
というとき、特別な値が返るようになってた。
それ以外は、getChildren(node, options)
する。
ちなみに、BabelのパーサーにはMethodDefinition
というノードはなく、ClassMethod
というノードになる。
前回みたpostprocess(ast, options)
で各ASTの差異を吸収するくだりは終わったかと思ってたけど、まあやっぱこうなるわな〜。
getChildren(node, options)
getChildren()
はつまり、渡されたノード直下にぶらさがってるものを全部並べたいだけ。
オプションでfilter(node)
で除外できるようになってるけど、今回はそもそもfilter
を渡してないので、除外されるものはない。
filter
は、getCursorNode()
経由でgetDescendants()
がgetChildren()
を呼ぶコードパスでだけ指定されるみたい。
このときは、nodeContainCursor()
というカーソルが乗ってるノード以外を除外する指定になる。(こういうのはメソッド分けてくれ + これはもう少しやりようないんか?)
getVisitorKeys
では、
- Babelの
VISITOR_KEYS
をベースに、TSとFlowとAngularにも対応しつつ - 追加で対応したいキーを追加し
- 特定のノードでキーを除外する
ということをしてる。
既存のVisitor実装を使うのではなく、愚直にASTのオブジェクトのキーを個別に見て回ってるってこと。
残るは、canAttachComment(node) ? [node] : getSortedChildNodes(node, options)
の部分。
canAttachComment(node)
どうやら、コメントを紐づけたくないノードがあるらしい。
EmptyStatement
TemplateElement
Import
TSEmptyBodyFunctionExpression
ChainExpression
ノードのtype
がこれらに合致する場合はスキップして、代わりに再帰でgetSortedChildNodes()
を呼ぶことになる。
が、ざっと見る感じ、
ChainExpression
とTSEmptyBodyFunctionExpression
以外は、空になってそうgetCommentChildNodes()
はMethodDefinition
のためのものなので、完全に無駄getChildren()
だけでいいはずだが、ソートとキャッシュのために再帰してる?
というわけで、
- 基本的にはルートの直下ノードを列挙したい
- 一部のノードは透過的に無視して、その直下ノードを列挙する
そしてこれらを、ノードのstart
位置でソートして返す。ASTのノードの各キーを訪れる順序は、コードの登場位置とは関係ないから。
コメントの近接ノードを割り出す
改めて本題。ルートの直下ノードを列挙したところで、それらを使っていく。
let left = 0;
let right = childNodes.length;
while (left < right) {
// ...
}
コードでいうとここ。いわゆる二分探査をやっていく。
材料は、
- ASTのルートの直下ノード
- Babelの場合はルートが
type: File
で、直下はtype: Program
のみ
- Babelの場合はルートが
- 各コメント
で、大きく3パターンの分岐があり、二分探査で最も近くのものを探し続ける。
- コメントがそのノードの内部にある
- そのノードを親として、
decorateComment(nextRootNode, comment, options, thisEnclosingNode)
を再帰で呼びなおす
- そのノードを親として、
- コメントがそのノードの前方にある
- そのノードを
precedingNode
として確保
- そのノードを
- コメントがそのノードの後方にある
- そのノードを
followingNode
として確保
- そのノードを
なので、type: Program
なんかの場合は、必ずすべてのコメントが内部にあることになるので、即最初のパターンに入って再帰する。
なんなら、
export const fn = () => {
if (x) {
return {
y: [ 1, /* THIS */ 2 ],
}
}
}
こんな風にネストしてる場合は、
File
Program
ExportNamedDeclaration
- 中略
ObjectProperty
ArrayExpression
みたく、decorateComment()
を何度も再帰するようになってる。
(コメント位置から前後と親を見るほうが速そうに思うけど、できないもんだろうか?)
このコード例の場合は最終的に、
enclosingNode
:ArrayExpression
precedingNode
:NumericLiteral
(1)followingNode
:NumericLiteral
(2)
となる。
enclosingNode
がTemplateLiteral
だったら
テンプレートリテラル内のコメントについて、例外を処理してる。
コメントとリテラル内のExpressionの位置関係から、precedingNode
とfollowingNode
をnull
に戻してる。
どうやら、こういうコードがあった場合に、
`
${v1}
${/* THIS */
v2}
`
この処理がないとこうなっちゃう。
`
${v1 /* THIS */}
${v2}
`
こうなってほしいのに。
`
${v1}
${/* THIS */ v2}
`
という感じらしい。ニッチすぎる。
ここまでのまとめ
ASTにコメントを紐づけるattachComments()
の中でも、まだ準備段階みたいなコードだけで1記事になってしまった。
まとめるとするならば、
Comment
すべてに対してenclosingNode
,followingNode
,precedingNode
を見つける- ただしすべてのノードが対象になるわけではなく、いくつか例外もある
- 訪れるキーとして除外されているもの
canAttachComment()
できないもの
enclosingNode: TemplateLiteral
の場合は、例外処理がある
というわけで、続く・・・。