`canonicalizeCandidates` のクラッシュ修正:空プロパティマップを持つユーティリティの安全な処理

tailwindlabs/tailwindcss

collapse: true オプションで canonicalizeCandidates を呼び出した際、shadow-sm などのシャドウユーティリティが含まれるとクラッシュする不具合が修正されました。原因は collapseGroup 内で null が誤って返される実装上のバグで、1行の修正によって解消されています。

背景

eslint-plugin-better-tailwindcssenforce-canonical-classes ルールが canonicalizeCandidatescollapse: true で呼び出した際にクラッシュが発見されました。このクラッシュはESLint全体を停止させる深刻な問題であり、カスタム設定なしの標準的なTailwind CSSの環境でも再現可能でした。

問題が発生するのは shadow-smshadow-mdshadow-lgshadow-xl といった全シャドウユーティリティで、これらは CSS の出力として @property ルールや CSS カスタムプロパティのみを生成し、標準的な宣言プロパティを持ちません。以下のいずれかのエラーが状況に応じて発生していました:

  • TypeError: X is not iterable
  • TypeError: 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() が空の場合、ループは一度も実行されず resultnull のままです。非nullアサーション演算子 ! はコンパイルエラーを抑制するだけで実行時の null は除去できないため、下流のコードで null に対してイテレーションや .has() 呼び出しが行われるとクラッシュしていました。修正では ?? new Set<string>() を使い、null の場合は空の Set を返すように変更しています。

テストとして canonicalize-candidates.test.ts に37行が追加され、shadow-sm + bordershadow-md + p-4shadow-sm + shadow-md の各組み合わせでクラッシュしないこと、および候補がコラプスされずそのまま返されることが検証されています。

設計判断

空の Set を返すことがアルゴリズム上も意味的に正しい選択でした。コラプスアルゴリズムは「共通プロパティを持つユーティリティ同士をリンクして統合できるか」を判断します。標準プロパティを持たないユーティリティは他のどのユーティリティとも共通プロパティを持てないため、空の Set は「このユーティリティはどれとも連結できない」という状態を正確に表現しています。

PR の説明が明示するように、この修正は誤ったコラプスを引き起こさず、他のユーティリティの正当なコラプスも妨げません。null を特別処理するガード節を追加する代わりに、返り値の型を意味のある値に統一することで、下流コード全体の安全性が確保されています。

まとめ

result ?? new Set<string>() という1文字レベルの変更が、実行時クラッシュという重大な問題を根本から解消しています。非nullアサーション演算子の誤用に起因するこのバグは、返り値の型不変条件をコード上で保証することの重要性を示す典型例といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6af1f822

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が明確です。リード文で要旨を述べ、背景、技術的変更、設計判断の各セクションで詳細を掘り下げ、最後にまとめで締めくくるという理想的な構成になっています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト(`typescript:packages/tailwindcss/src/canonicalize-candidates.ts`)およびPR番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門用語(`canonicalizeCandidates`, `@property`ルール, 非nullアサーション演算子など)が適切に使用されており、対象読者であるエンジニアに適した技術レベルで書かれています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論から各論へと展開され、各段落はトピックセンテンスで始まっています。1段落1トピックの原則が守られ、段落長も適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコード変更の説明(`?? new Set<string>()`の追加)は、提供されたDiffとPR Descriptionの内容を正確に反映しています。テストケースの追加に関する説明もDiff情報と一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

PR情報で使われている`canonicalizeCandidates`や`collapseGroup`などの技術用語が正確に使用されており、誤用は見られません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

クラッシュの根本原因(`null`返却)、修正による効果、設計判断の妥当性(空のSetが意味的に正しい理由)など、すべての技術的説明がPR情報と整合しており、論理的で正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(クラッシュ発見の経緯、影響範囲、エラーメッセージ、テスト内容など)は、提供されたPR Descriptionによって裏付けられており、ハルシネーション(捏造)は検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#19727)やテストコードで追加された行数(37行)などの数値・固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの主題「`canonicalize`における空プロパティマップを持つユーティリティの処理」を的確に要約しており、読者に内容を正しく伝えています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に記載のないバージョン情報やリリース予定などの外部知識は含まれておらず、事実に基づいた記述が徹底されています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内で時間表現に関する記述はなく、PR情報との齟齬はありません。