`canonicalizeCandidates` のクラッシュ修正:空プロパティマップを持つユーティリティの安全な処理
collapse: true オプションで canonicalizeCandidates を呼び出した際、shadow-sm などのシャドウユーティリティが含まれるとクラッシュする不具合が修正されました。原因は collapseGroup 内で null が誤って返される実装上のバグで、1行の修正によって解消されています。
背景
eslint-plugin-better-tailwindcss の enforce-canonical-classes ルールが canonicalizeCandidates を collapse: true で呼び出した際にクラッシュが発見されました。このクラッシュはESLint全体を停止させる深刻な問題であり、カスタム設定なしの標準的なTailwind CSSの環境でも再現可能でした。
問題が発生するのは shadow-sm・shadow-md・shadow-lg・shadow-xl といった全シャドウユーティリティで、これらは CSS の出力として @property ルールや CSS カスタムプロパティのみを生成し、標準的な宣言プロパティを持ちません。以下のいずれかのエラーが状況に応じて発生していました:
TypeError: X is not iterableTypeError: Cannot read properties of null (reading 'has')
技術的な変更
collapseGroup 内の null 返却が根本原因でした。candidatePropertiesValues.map() のコールバックで、プロパティキーが存在しない場合に result が初期値の null のまま返されていました。
変更前:
let otherUtilities = candidatePropertiesValues.map((propertyValues) => {
let result: Set<string> | null = null
for (let property of propertyValues.keys()) {
// ... result を構築 ...
}
return result! // propertyValues にキーがなければ null を返す
})
変更後:
- return result!
+ return result ?? new Set<string>()
propertyValues.keys() が空の場合、ループは一度も実行されず result は null のままです。非nullアサーション演算子 ! はコンパイルエラーを抑制するだけで実行時の null は除去できないため、下流のコードで null に対してイテレーションや .has() 呼び出しが行われるとクラッシュしていました。修正では ?? new Set<string>() を使い、null の場合は空の Set を返すように変更しています。
テストとして canonicalize-candidates.test.ts に37行が追加され、shadow-sm + border、shadow-md + p-4、shadow-sm + shadow-md の各組み合わせでクラッシュしないこと、および候補がコラプスされずそのまま返されることが検証されています。
設計判断
空の Set を返すことがアルゴリズム上も意味的に正しい選択でした。コラプスアルゴリズムは「共通プロパティを持つユーティリティ同士をリンクして統合できるか」を判断します。標準プロパティを持たないユーティリティは他のどのユーティリティとも共通プロパティを持てないため、空の Set は「このユーティリティはどれとも連結できない」という状態を正確に表現しています。
PR の説明が明示するように、この修正は誤ったコラプスを引き起こさず、他のユーティリティの正当なコラプスも妨げません。null を特別処理するガード節を追加する代わりに、返り値の型を意味のある値に統一することで、下流コード全体の安全性が確保されています。
まとめ
result ?? new Set<string>() という1文字レベルの変更が、実行時クラッシュという重大な問題を根本から解消しています。非nullアサーション演算子の誤用に起因するこのバグは、返り値の型不変条件をコード上で保証することの重要性を示す典型例といえます。