walkコンテキストに`index`と`siblings`を追加し、冗長なノード探索を解消
Tailwind CSSのAST走査関数 walk のコンテキストオブジェクトに index と siblings フィールドが追加され、ウォーク中のノード位置と兄弟ノードリストへの直接アクセスが可能になりました。これにより、従来必要だった ctx.parent?.nodes.indexOf(node) のような迂回的なコードを排除できます。
背景
walk 関数はTailwind CSSのASTを再帰的に走査するコア関数で、各ノードの訪問時に VisitContext<T> オブジェクトが渡されます。これまでのコンテキストには parent・depth・path() のみが含まれており、現在のノードが兄弟ノード列の何番目に位置するかを知るには ctx.parent?.nodes.indexOf(node) という追加のO(n)探索が必要でした。
さらに、ルート直下のノードを走査している場合(ctx.parent === null)は ctx.parent?.nodes が undefined になるため、呼び出し元は ctx.parent?.nodes ?? ast のようにAST変数を別途保持しなければなりませんでした。インラインで walk([nodeA, nodeB], ...) と呼び出した場合、ルートの配列参照を取り出す手段が存在しないという構造的な問題もありました。
walk 実装は走査時に現在位置のインデックスと兄弟配列を既に把握しているため、それをコンテキスト経由で公開するだけでこれらの問題を解消できます。
技術的な変更
VisitContext<T> インターフェースと walkImplementation 関数に index と siblings が追加されました。
walk.ts インターフェース定義の変更:
export interface VisitContext<T> {
parent: Parent<T> | null
depth: number
index: number // 追加
siblings: T[] // 追加
path: () => T[]
}
walkImplementation 内の初期化と更新:
let ctx: VisitContext<T> = {
parent: null,
depth: 0,
index: 0,
siblings: ast, // ルートレベルではast自体が兄弟配列
path() { ... },
}
// 子ノードに降りる際に更新
ctx.parent = parent
ctx.siblings = nodes
// Enter フェーズ
if (offset >= 0) {
ctx.index = offset
...
}
// Exit フェーズ
let index = ~offset // 2の補数でオリジナルのオフセットを復元
ctx.index = index
この実装上の重要な点は、ctx.siblings はルートレベルでも null にならないことです。ctx.parent === null の場合でも ctx.siblings にはルートのAST配列が設定されるため、呼び出し元での ?? [] フォールバックが不要になります。
呼び出し側のコードの変化は顕著で、candidate.ts と canonicalize-candidates.ts の複数箇所で冗長なパターンが一掃されています。
変更前(candidate.ts):
let parentArray = ctx.parent === null ? ast : (ctx.parent.nodes ?? [])
let idx = parentArray.indexOf(node) ?? -1
let previous = parentArray[idx - 1]
let next = parentArray[idx + 1]
変更後(candidate.ts):
let idx = ctx.index
let previous = ctx.siblings[idx - 1]
let next = ctx.siblings[idx + 1]
canonicalize-candidates.ts では (ctx.parent?.nodes ?? []).some(...) が ctx.siblings.some(...) に、また idx = ctx.parent.nodes.indexOf(node) - 1 が idx = ctx.index - 1 に置き換えられています。if (ctx.parent) のガード節も不要になり、コードブロックがフラット化されています。
テストへの反映:
ウォークテストでは各訪問ノードに @ {index} を付加するよう更新され、ctx.index === ctx.siblings.indexOf(node) の等値アサーションも追加されました。これにより index の正確性がテストとして保証されています。
設計判断
既存の VisitContext<T> を拡張する形が採用され、walk 関数のシグネチャ自体は変わっていません。
siblings が ctx.parent.nodes への参照に留まらずルート配列もカバーする設計は、ルート直下のノードを特別扱いする必要をなくします。呼び出し元は ctx.parent の null チェックなしに ctx.siblings を安全に使用できます。また、index はウォーク実装が内部的に管理しているオフセット値をそのまま公開しているため、実装コストはコンテキストオブジェクトへの代入2行のみと最小限です。
cssContext ヘルパー(ast.ts)にも index と siblings のパススルーが追加されており、CSS特有のコンテキストオブジェクトを使用する呼び出し元でも同じ恩恵を受けられます。
まとめ
この変更は walk 関数が既知の情報を呼び出し元に公開するだけという小さな変更ながら、indexOf によるO(n)探索の排除と、ルートノードのヌルガード処理の解消という実質的な改善をもたらします。コンテキストオブジェクトの拡張パターンは後方互換性を保ちつつAPIを漸進的に改善する典型例といえます。