属性セレクタと型セレクタの分割パースを修正
属性セレクタの直後に型セレクタが続く場合、Tailwind CSS の selector parser が誤って単一セレクタとして扱っていたバグを解消し、正しく二つのノードに分割できるようになりました。この修正により、AST の構造が期待通りになり、以降の処理が正確に行われます。
背景
このバグは、[data-foo]div のような CSS をパースした際に、{ kind: 'compound', nodes: [{ kind: 'selector', value: '[data-foo]div' }] } という一つのノードにまとめられていたことが原因です。属性セレクタと型セレクタは本来別個の要素として扱われるべきですが、パーサが閉じ括弧 ] 後の切れ目を認識できずに結合してしまっていました。その結果、複合セレクタを利用するプラグインやユーティリティで不整合が発生するリスクがありました。修正によって、[data-foo]div が { kind: 'compound', nodes: [{ kind: 'selector', value: '[data-foo]' }, { kind: 'selector', value: 'div' }] } と正しく分割され、期待通りの AST が生成されます。
技術的な変更
selector parser の内部ロジックが属性セレクタの終端検出を改善しました。OPEN_BRACKET ケースで属性セレクタ全体を取得し、閉じ括弧 ] を見つけた時点で append(selector(input.slice(start, i + 1))) を呼び出してバッファをフラッシュするよう変更されています。
// 修正前(抜粋)
case OPEN_BRACKET:
// 旧ロジックは属性セレクタの終了を検出せず、バッファが残ったまま次の文字へ進んでいた
break;
// 修正後(抜粋)
case OPEN_BRACKET:
// 属性セレクタの開始位置を記録し、閉じ括弧が見つかったら即座にセレクタとして追加
const start = i;
// ... 文字走査 ...
if (char === CLOSE_BRACKET) {
append(selector(input.slice(start, i + 1)));
buffer = '';
}
break;
テストコードも同様に拡充され、属性セレクタと型セレクタの組み合わせ、属性セレクタ同士の連続、ユニバーサルセレクタとの組み合わせすべてを検証するケースが追加されました。これにより回帰テストが充実し、将来的な変更で同様の誤りが再発しにくくなります。
it('parses a type selector after an attribute selector', () => {
expect(parse('[data-foo]div')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]' },
{ kind: 'selector', value: 'div' },
],
},
])
})
設計判断
パーサ内部の状態機構を最小限の変更に留め、属性セレクタの終了時にバッファを確実にフラッシュするというシンプルな方針が採られました。この方針は既存の API や設定に影響を与えず、バグ修正のみを目的としたため後方互換性を保持できます。また、特別な新キーやオプションを導入せず、コードベースの一貫性を保つ設計判断といえます。
まとめ
属性セレクタの後に続く型セレクタを正しく分割できるようになったことで、Tailwind CSS のセレクタ解析が仕様通りに動作し、プラグインやユーティリティが安定して利用できるようになりました。追加されたテストにより、この修正が将来のリグレッションから守られることが保証されています。