canonicalize処理でリストが空になるバグを修正
w-5 h-5 size-5 のように、canonicalize後の置換先クラスが元のリストに既に存在する場合に、すべてのクラスが削除されて空リストになるバグが修正されました。
背景
canonicalize処理は、冗長なユーティリティクラスをより簡潔な表現に統合する機能です。例えば w-5 h-5 は size-5 に置換されます。しかし、置換先クラスが元のリストに既に含まれている場合に、誤ってそのクラスまで削除対象にマークしてしまうという問題がありました。
具体的には w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5 というクラス列を処理する際、次のような2段階の処理が行われます。まず w-[calc(1rem+0.25rem)] と h-[calc(1rem+0.25rem)] が w-5 と h-5 にそれぞれ単純化されます。次に w-5 h-5 が size-5 にcollapseされます。このとき、元のリストに既に存在する size-5 も「combo内の要素」として drop(削除対象) にマークされ、結果として何も残らない空リストになっていました。
flex のような無関係なクラスが追加されていた場合(w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5 flex)でも、期待される size-5 flex ではなく flex だけが返されるという症状でした。
技術的な変更
packages/tailwindcss/src/canonicalize-candidates.ts の collapseCandidates 関数内で、drop への追加前に「combo内の要素が置換先(replacement)と同じかどうか」を確認するガード条件が追加されました。
変更前:
// We can replace all items in the combo with the replacement
for (let item of combo) {
drop.add(candidates[item])
}
// Use the replacement
result.add(replacement)
break
変更後:
// Use the replacement
result.add(replacement)
// We can replace all items in the combo with the replacement. If the
// replacement is already part of the combo, keep that one around.
for (let item of combo) {
if (candidates[item] !== replacement) {
drop.add(candidates[item])
}
}
break
変更点は2つです。第一に、result.add(replacement) を drop への追加ループより前に移動しました。第二に、drop.add(candidates[item]) の実行条件として candidates[item] !== replacement を追加しました。これにより、comboの中に置換先と同じクラスが含まれていても、そのクラスは削除対象に含まれなくなります。
テストファイル canonicalize-candidates.test.ts には3つのケースが追加されました。w-8 w-8 → w-8(重複のcollapse)、w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5 → size-5(バグの直接的なケース)、そして w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5 flex → size-5 flex(無関係なクラスが存在する場合)の3パターンです。
設計判断
result.add(replacement) の移動 と ガード条件の追加 という2つの変更が組み合わされた点に、修正の設計意図が現れています。
result.add(replacement) を先に実行するよう順序を変更することで、コードの読み手に「まず置換先を確定させ、その後で何を削除するかを決める」という処理の意図が明確に伝わります。ガード条件 candidates[item] !== replacement は、「comboに含まれる要素が置換先そのものである場合、それは削除ではなく保持(result.add済み)の対象である」というロジックを正確に表現しています。副作用の最小化という観点でも、既存の drop と result の2つの集合を使う設計を変えず、判定条件の追加だけで問題を解消している点は理にかなっています。
まとめ
この修正は8行の変更ながら、canonicalize処理の多段階適用において「置換先が元のリストに既に存在するケース」という見落としやすいエッジケースを正確に捕捉しています。drop と result の集合操作において、同一要素が両方に含まれ得るという状態を未然に防ぐことで、より堅牢なcanonicalization処理が実現されています。