`calc()` 正規化処理で演算子周辺の空白が失われるバグを修正

tailwindlabs/tailwindcss

任意値における負値正規化(canonicalization)処理で、calc() 式内の数学演算子周辺の空白が失われ、無効なCSSが生成されるバグが修正されました。

背景

Tailwind CSS には、冗長なクラス記述を簡潔な等価表現へ正規化する仕組みがあります。たとえば left-[calc(-1*(var(--my-var1)+var(--my-var2)))] は、-left-[(var(--my-var1)+var(--my-var2))] という短い形式に正規化されます。この正規化処理では calc(-1 * <value>) を取り除き、代わりにクラス名の先頭に - を付けることで同等の意味を表現します。

しかし #20010 で報告されたように、この正規化後の形式を直接記述すると、生成されるCSSに問題が生じていました。任意値のパース時には calc() コンテキストを検出して演算子周辺に空白を補完する処理が走りますが、calc(<value> * -1) のラッピングはパース後に追加されるため、補完処理が適用されないまま出力されていたのです。

正規化前の left-[calc(-1*(var(--my-var1)+var(--my-var2)))] は意図通りのCSSを生成していたのに対し、等価なはずの正規化後の記述 -left-[(var(--my-var1)+var(--my-var2))] は無効なCSSを出力していました。正規化後の形式を推奨するツールの指示に従うと壊れたCSSが生成される状況でもありました。

技術的な変更

packages/tailwindcss/src/utilities.ts の負値処理箇所に、addWhitespaceAroundMathOperators の呼び出しが追加されました。

変更前:

return desc.handle(negative ? `calc(${value} * -1)` : value, dataType)

変更後:

return desc.handle(
  negative ? addWhitespaceAroundMathOperators(`calc(${value} * -1)`) : value,
  dataType,
)

この変更により、-left-[(var(--my-var1)+var(--my-var2))] に対して生成されるCSSが以下のように修正されます。

修正前(無効なCSS):

.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
  left: calc((var(--my-var1)+var(--my-var2)) * -1);
}

修正後(有効なCSS):

.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
  left: calc((var(--my-var1) + var(--my-var2)) * -1);
}

addWhitespaceAroundMathOperators./utils/math-operators から新たにインポートされており、+- などの演算子の前後に適切な空白を挿入します。負値でない通常のケース(negativefalse)は変更の影響を受けず、既存の挙動はそのまま維持されます。

リグレッションテストとして、packages/tailwindcss/src/utilities.test.tsleft テストケースに -left-[(var(--my-var1)+var(--my-var2))] が追加されました。修正前はこのテストが失敗し、修正後に正しい出力を示すことで変更の妥当性を確認しています。

設計判断

空白補完の責務を「calc() ラッピング時」に移すアプローチが採用されました。

PRの説明では、正規化後の値に対して空白補完が行われていない根本原因として、パースのタイミングと calc() ラッピングのタイミングのずれが挙げられています。修正はパース処理には手を加えず、ラッピング処理の出口で空白を補完する方式を採用しており、変更箇所を最小限に抑えています。

PRでは今後のフォローアップとして2点が言及されています。一点目は、正規化時の余分な括弧の除去(-left-[(var(--a)+var(--b))]-left-[var(--a)+var(--b)])です。現時点では値が単独で式になりえるため calc((<value>) * -1) の形式を維持する必要があります。二点目は、正規化処理が同等性の判定にシグネチャを用いる仕組みの問題で、空白を除去したシグネチャの比較では本来異なるケースが同一視されうると指摘されています。いずれもより広範なテストを要するとして、今回のPRの対象からは意図的に除外されています。

まとめ

本PRは、パース後に適用される calc() ラッピング処理に空白補完を組み込むことで、正規化形式の任意値クラスから有効なCSSが生成されない問題を解消しました。根本的な正規化ロジックの問題は残されており、フォローアップPRへの道筋も示されていますが、まず実際のユーザー影響を修正する実用的な判断が取られています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
eb1a1e86

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
3回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という構成が明確で、非常に理解しやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト、Issue番号とPR番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Tailwind CSSの内部実装に関する内容であり、専門知識を持つエンジニアという対象読者に適切です。過度な説明がなく簡潔です。

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

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

各セクションが総論→各論で構成され、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、可読性が高いです。

Diff内容との照合 ⚠ WARNING

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

コードブロック内のテンプレートリテラルが、元のDiffのバッククォート(`)ではなくシングルクォート(')で記述されていますが、技術的な理解を妨げるものではありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「正規化(canonicalization)」「任意値」「パース」といった技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

問題の根本原因(パースとcalc()ラッピングのタイミングのずれ)と解決策(ラッピング時に空白を補完する)の説明が、PR情報と一致しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiff内容で裏付けられており、ハルシネーション(創作)は見られません。「設計判断」セクションでの今後の課題に関する言及もPRに基づいています。

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

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

PR番号(#20011)、Issue番号(#20010)、ファイルパスなどがすべて正確に記載されています。

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

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

記事のタイトルはPRの主題「Ensure math operators are surrounded by whitespace in arbitrary values」を的確に要約しており、内容と一致しています。

外部知識の正確性 ✓ PASS

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

記事はPR情報に限定して記述されており、バージョン情報やリリース予定など、PR外の知識を持ち込んでいません。

時間表現の正確性 ✓ PASS

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

「今後のフォローアップとして」といった時間表現は、PR内の「this will be a follow up PR」という記述と一致しており、正確です。