セレクターコンビネーターの型付けと空白処理の統一

tailwindlabs/tailwindcss

セレクターパーサーの combinator ノードに明示的な型を導入し、空白処理をパーサー内部に集約することで、利用側コードでの .trim() 呼び出しを不要にしました。

背景

#20088 によって listcomplexcompound ノードが追加されたことで、セレクターのAST構造が整理されましたが、コンビネーターの値には依然として空白が混入する可能性がありました。具体的には、.foo + .bar のような入力を解析すると combinator ノードの value' + '(前後にスペース付き)として格納されるケースがあり、利用側のコードは ast[1].value.trim() === '>' のように .trim() を呼び出してから比較する必要がありました。

この問題は canonicalize-candidates.ts の複数箇所に散らばっており、コメントで「// space, but trimmed because there could be multiple spaces」と注釈を付けなければ意図が伝わらない状態でした。本PRはこの根本原因をパーサー側で解消するフォローアップです。

技術的な変更

Combinator 型を導入し、コンビネーター値を正規化したことが本PRの核心です。selector-parser.ts にユニオン型 Combinator を新設し、SelectorCombinatorNode.value の型を string から Combinator に変更しました。

type Combinator =
  | ' ' // Descendant combinator
  | '>' // Child combinator
  | '+' // Next-sibling combinator
  | '~' // Subsequent-sibling combinator

パーサー内部では、.foo \n\t .bar のように複数の空白や改行が混在した子孫コンビネーターも、単一のスペース ' ' に正規化して格納します。新たに追加されたテストケースがこの挙動を明文化しています。

expect(parse('.foo  \n\t .bar')).toEqual([
  {
    kind: 'complex',
    nodes: [
      { kind: 'selector', value: '.foo' },
      { kind: 'combinator', value: ' ' },
      { kind: 'selector', value: '.bar' },
    ],
  },
])

正規化によって空白情報が失われるため、AST を CSS 文字列に戻す際の制御が必要になります。toCss 関数には minify フラグ(デフォルト false)が追加されました。

変更前:

export function toCss(ast: SelectorAstNode[]) {
  // ...
  case 'combinator': {
    if (node.value === ' ') {
      css += node.value
    } else {
      css += ` ${node.value} `
    }
  }
}

変更後:

export function toCss(ast: SelectorAstNode[], minify = false) {
  // ...
  case 'combinator': {
    if (minify || node.value === ' ') {
      css += node.value
    } else {
      css += ` ${node.value} `
    }
  }
}

minify = false(デフォルト)では >+ などのコンビネーターを >+ のように前後にスペースを付けて出力します。minify = true を指定するとすべてのコンビネーター値をそのまま出力し、空白を一切挿入しません。正規化(canonicalization)用途では toCss(ast, true) が使用されます。

この変更を受けて canonicalize-candidates.ts では、.trim() を伴う比較がすべて直接比較に置き換えられました。

変更前:

ast[1].kind === 'combinator' &&
ast[1].value.trim() === '>' &&
// ...
ast[1].value.trim() === '' && // space, but trimmed because there could be multiple spaces

変更後:

ast[1].kind === 'combinator' &&
ast[1].value === '>' &&
// ...
ast[1].value === ' ' &&

設計判断

パーサーが正規化の責務を持ち、利用側は正規化された値のみを扱うという設計方針が採用されました。

toCssminify フラグを追加する代替案として、別途 minify 専用の関数を用意する方法も考えられますが、既存のシグネチャを最小限の変更で拡張するアプローチが選ばれています。デフォルトを false(空白あり)にすることで、既存の呼び出し元への影響をゼロに抑えつつ、正規化が必要な箇所だけ true を渡せるようになっています。

TypeScriptの型システムを活用して Combinator をリテラル型のユニオンとして定義することで、コンパイル時に不正な値の混入を防ぐ効果もあります。コードのドキュメントとしての役割も兼ねており、CSS仕様における4種類のコンビネーターが型定義に直接反映されています。

まとめ

コンビネーターの正規化責務をパーサー内部に集約したことで、利用側コードから .trim() と説明コメントが一掃され、AST操作の意図がコードから直接読み取れるようになりました。Combinator 型の導入はこの設計をTypeScriptの型システムで保証するものであり、今後のセレクター処理の拡張においても不正な値の混入をコンパイル時に検出できる基盤となっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
3eabc2df

この記事は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リンク記法の正確性

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

対象読者への適合性 ✓ PASS

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

AST、パーサー、正規化といった専門用語を前提としており、専門知識を持つエンジニアという対象読者に適合した内容になっています。

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

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

各セクションが総論・各論・結論の構造を持ち、各パラグラフもトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内のすべてのコードブロック(型定義、テストケース、変更前後の比較)が、提供されたDiff情報と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「コンビネーター」「AST」「正規化(canonicalization)」「リテラル型ユニオン」など、技術用語が正確かつ文脈に即して使用されています。

説明の技術的正確性 ✓ PASS

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

「.trim()が不要になる」「空白が単一スペースに正規化される」「minifyフラグで出力が制御される」といった説明は、すべてDiffの内容とPRの記述によって裏付けられており、技術的に正確です。

事実の突合 ✓ PASS

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

記事の主張はすべてPRのDescriptionやDiffから裏付け可能です。「設計判断」セクションの記述も、コードの意図を解説するものであり、ハルシネーション(捏造)は認められません。

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

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

PR番号(#20088, #20089)やファイルパスなど、記事に含まれる数値や固有名詞はすべて正確です。

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

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

記事のタイトル「セレクターコンビネーターの型付けと空白処理の統一」は、PRの主題である「Simplify selector combinators」を具体的に表現しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

バージョン情報やリリース予定など、PR情報にない外部知識の持ち込みはなく、提供された情報源の範囲内で記事が構成されています。

時間表現の正確性 ✓ PASS

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

「依然として〜可能性がありました」といった過去形と、「〜ようになりました」といった変更後の状態を示す表現が正しく使われており、時間的な前後関係に誤解を生む表現はありません。