`calc()` 内の `0<unit>` を誤って `0` に正規化するバグを修正

tailwindlabs/tailwindcss

px-[calc(1rem+0px)] のような任意値クラスを正規化する際、calc() 内の 0px0 に書き換えることで有効なCSSが無効になる不具合を修正しました。正規化ロジックが適用される文脈(トップレベルか関数の引数か)を考慮するようになっています。

背景

Tailwind CSS v4の canonicalizeCandidates は、m-[0rem]m-0 のように書き換える「正規化(canonicalization)」を行い、等価なクラスを検出して suggestCanonicalClasses によるリント提案を実現しています。この正規化の一環として、0<unit> 形式(例: 0px0rem)を単純な 0 に畳み込む処理が実装されていました。

この畳み込みはトップレベルの <length> 型プロパティには安全です。しかし tailwindcss-intellisense#1579 で報告されたように、calc() の内部では同じ変換が無効なCSSを生成する原因になります。calc(1rem + 0px) は有効なCSSですが、0px0 に置き換えた calc(1rem + 0) はブラウザが拒否する無効な式です(CSS.supports("padding-inline", "calc(1rem + 0)")false を返すことで確認できます)。

問題は演算子によっても挙動が異なります。加算(+)では有効から無効への劣化が起きる一方、乗算(*)では calc(1rem * 0px) はそもそも px² という無効な単位を生じる無効な式であり、calc(1rem * 0) は有効になります。単純な「0<unit>0」の一律変換では、このコンテキスト依存の正確さを表現できませんでした。

技術的な変更

constant-fold-declaration.tsconstantFoldDeclarationAst 関数に、正規化後の値が 0 になるケースでの文脈チェックが追加されました。

変更前は、canonicalizeDimension0 を返した時点で無条件に書き換えていました。

// 変更前(概略)
if (canonical === null) return
if (canonical === valueNode.value) return

folded = true
return WalkAction.ReplaceSkip(ValueParser.word(canonical))

変更後は、canonical === '0' かつ親ノードが functioncalc() など)の場合、単位なしの 0 への置き換えを行わず、代わりに canonicalizeDimensionfalse(単位保持モード)で呼び出して 0px のような単位付きの値を保持します。

if (canonical === '0') {
  if (ctx.parent?.kind === 'function') {
    let withUnit = canonicalizeDimension(valueNode.value, rem, false)
    if (withUnit === null) return

    folded = true
    return WalkAction.ReplaceSkip(ValueParser.word(withUnit))
  }
}

folded = true
return WalkAction.ReplaceSkip(ValueParser.word(canonical))

このために exit コールバックのシグネチャも exit(valueNode) から exit(valueNode, ctx) に変更され、ウォーカーのコンテキスト(親ノードへの参照)を受け取れるようになっています。

乗算の畳み込みにも同様の文脈対応が追加されました。0<unit> * unitless のパターンは関数内では 0<unit> に、トップレベルでは 0 に畳み込まれます。

// 0<unit> * something-without-unit を折りたたむ
if (operator === '*' && lhs?.[0] === 0 && lhs?.[1] !== null && rhs?.[1] === null) {
  folded = true
  if (ctx.parent?.kind === 'function') {
    return WalkAction.ReplaceSkip(ValueParser.word(`0${lhs[1]}`))
  } else {
    return WalkAction.ReplaceSkip(ValueParser.word('0'))
  }
}

テストケースでは、この変更による具体的な挙動が文書化されています。

  • calc(-0px * -1)0(トップレベルでは単位を除去)
  • calc(calc(0px * -1) + 1rem)calc(0px + 1rem)(ネストされた calc では単位を保持)
  • px-[calc(1rem+0px)]px-41rem = 16px16px + 0px = 16px = 1rem = spacing-4 として折りたたまれる)

またCSSカスタムプロパティ(--foo:0px)は型が不明なため、0<unit>0 の変換が行われなくなっています。[--foo:0px] の正規化候補は [--foo:0px] のまま維持されます。

設計判断

「文脈に応じた畳み込み」 の方針が採用され、変換の安全性を親ノードの種別で判定するアプローチが取られました。

PR本文では calc(0s + 1rem) のような型不一致の無効式を 1rem に畳み込む最適化の可能性にも言及していますが、意図的に実装を見送っています。無効なCSSを有効なCSSへと変換することで、本来ユーザーが意図しない正規化候補が提案されるリスクがあるためです。「無効から有効への昇格は行わない」という保守的な判断が一貫して適用されています。

CSSカスタムプロパティへの対処も同様の原則に基づいています。型システムが関与しない変数値については、正規化の安全性が保証できないため変換対象から除外されています。

まとめ

0<unit> の畳み込みに「親ノードが関数か否か」という文脈チェックを加えることで、suggestCanonicalClasses が無効なCSSを提案するケースを排除しました。「無効から有効への変換は行わない」という一貫した原則のもと、型の安全性が保証できないコンテキストでは積極的な最適化を意図的に避ける設計判断が随所に見られます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
96c984a6

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)」という3部構成が明確に守られており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

Tailwind CSSの内部実装に関する詳細な内容であり、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションが総論・各論・結論で構成され、各パラグラフはトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されており、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

Diffからコードの変更点やテストケースを正確に引用し、技術的な変更点を的確に説明しています。一部概略化されたコードも、主旨を伝える上で適切です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「canonicalization」「CSS math function」「ウォーカー」といった技術用語を文脈に応じて正確に使用しています。

説明の技術的正確性 ✓ PASS

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

「calc()内での0pxと0の挙動の違い」や「文脈に応じた正規化ロジック」など、技術的な説明がPR情報とDiffの内容に裏付けられており、正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張が、PRのDescriptionやDiff内のコード・テストケースによって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#20127)やIssue番号(#1579)などの固有名詞は正確に引用されています。

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

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

記事のタイトルはPRの主題を的確に要約しており、内容との整合性が取れています。

外部知識の正確性 ✓ PASS

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

「Tailwind CSS v4」という言及はPRに明記されていませんが、リポジトリの文脈から妥当な範囲であり、捏造には該当しません。

時間表現の正確性 ✓ PASS

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

時間表現に関する歪曲や誤りは見られませんでした。