Skip to content

Commit e8546df

Browse files
Don’t allow at-rule-only variants to be compounded (#14015)
* Don’t allow at-rule-only variants to be compounded * Update changelog --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent cf846a5 commit e8546df

File tree

3 files changed

+126
-10
lines changed

3 files changed

+126
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Ensure opacity modifier with variables work with `color-mix()` ([#13972](https://github.com/tailwindlabs/tailwindcss/pull/13972))
1515
- Discard invalid `variants` and `utilities` with modifiers ([#13977](https://github.com/tailwindlabs/tailwindcss/pull/13977))
1616
- Add missing utilities that exist in v3, such as `resize`, `fill-none`, `accent-none`, `drop-shadow-none`, and negative `hue-rotate` and `backdrop-hue-rotate` utilities ([#13971](https://github.com/tailwindlabs/tailwindcss/pull/13971))
17+
- Don’t allow at-rule-only variants to be compounded ([#14015](https://github.com/tailwindlabs/tailwindcss/pull/14015))
1718

1819
### Added
1920

packages/tailwindcss/src/variants.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,15 @@ test('group-[...]', () => {
725725
display: flex;
726726
}"
727727
`)
728+
729+
expect(
730+
compileCss(
731+
css`
732+
@tailwind utilities;
733+
`,
734+
['group-[@media_foo]:flex'],
735+
),
736+
).toEqual('')
728737
})
729738

730739
test('group-*', () => {
@@ -752,6 +761,16 @@ test('group-*', () => {
752761
display: flex;
753762
}"
754763
`)
764+
765+
expect(
766+
compileCss(
767+
css`
768+
@variant custom-at-rule (@media foo);
769+
@tailwind utilities;
770+
`,
771+
['group-custom-at-rule:flex'],
772+
),
773+
).toEqual('')
755774
})
756775

757776
test('peer-[...]', () => {
@@ -784,6 +803,15 @@ test('peer-[...]', () => {
784803
display: flex;
785804
}"
786805
`)
806+
807+
expect(
808+
compileCss(
809+
css`
810+
@tailwind utilities;
811+
`,
812+
['peer-[@media_foo]:flex'],
813+
),
814+
).toEqual('')
787815
})
788816

789817
test('peer-*', () => {
@@ -811,6 +839,16 @@ test('peer-*', () => {
811839
display: flex;
812840
}"
813841
`)
842+
843+
expect(
844+
compileCss(
845+
css`
846+
@variant custom-at-rule (@media foo);
847+
@tailwind utilities;
848+
`,
849+
['peer-custom-at-rule:flex'],
850+
),
851+
).toEqual('')
814852
})
815853

816854
test('ltr', () => {
@@ -1505,7 +1543,15 @@ test('not', () => {
15051543
}"
15061544
`)
15071545

1508-
expect(run(['not-[:checked]/foo:flex'])).toEqual('')
1546+
expect(
1547+
compileCss(
1548+
css`
1549+
@variant custom-at-rule (@media foo);
1550+
@tailwind utilities;
1551+
`,
1552+
['not-[:checked]/foo:flex', 'not-[@media_print]:flex', 'not-custom-at-rule:flex'],
1553+
),
1554+
).toEqual('')
15091555
})
15101556

15111557
test('has', () => {
@@ -1518,7 +1564,7 @@ test('has', () => {
15181564
'group-has-checked:flex',
15191565

15201566
'peer-has-[:checked]:flex',
1521-
'peer-has-[:checked]/parent-name:flex',
1567+
'peer-has-[:checked]/sibling-name:flex',
15221568
'peer-has-checked:flex',
15231569
]),
15241570
).toMatchInlineSnapshot(`
@@ -1542,15 +1588,24 @@ test('has', () => {
15421588
display: flex;
15431589
}
15441590
1545-
.peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) {
1591+
.peer-has-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *) {
15461592
display: flex;
15471593
}
15481594
15491595
.has-\\[\\:checked\\]\\:flex:has(:checked) {
15501596
display: flex;
15511597
}"
15521598
`)
1553-
expect(run(['has-[:checked]/foo:flex'])).toEqual('')
1599+
1600+
expect(
1601+
compileCss(
1602+
css`
1603+
@variant custom-at-rule (@media foo);
1604+
@tailwind utilities;
1605+
`,
1606+
['has-[:checked]/foo:flex', 'has-[@media_print]:flex', 'has-custom-at-rule:flex'],
1607+
),
1608+
).toEqual('')
15541609
})
15551610

15561611
test('aria', () => {

packages/tailwindcss/src/variants.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,28 @@ export function createVariants(theme: Theme): Variants {
205205

206206
variants.compound('not', (ruleNode, variant) => {
207207
if (variant.modifier) return null
208-
ruleNode.selector = `&:not(${ruleNode.selector.replace('&', '*')})`
208+
209+
let didApply = false
210+
211+
walk([ruleNode], (node) => {
212+
if (node.kind !== 'rule') return WalkAction.Continue
213+
214+
// Skip past at-rules, and continue traversing the children of the at-rule
215+
if (node.selector[0] === '@') return WalkAction.Continue
216+
217+
// Replace `&` in target variant with `*`, so variants like `&:hover`
218+
// become `&:not(*:hover)`. The `*` will often be optimized away.
219+
node.selector = `&:not(${node.selector.replace('&', '*')})`
220+
221+
// Track that the variant was actually applied
222+
didApply = true
223+
})
224+
225+
// If the node wasn't modified, this variant is not compatible with
226+
// `not-*` so discard the candidate.
227+
if (!didApply) {
228+
return null
229+
}
209230
})
210231

211232
variants.compound('group', (ruleNode, variant) => {
@@ -215,6 +236,8 @@ export function createVariants(theme: Theme): Variants {
215236
? `:where(.group\\/${variant.modifier.value})`
216237
: ':where(.group)'
217238

239+
let didApply = false
240+
218241
walk([ruleNode], (node) => {
219242
if (node.kind !== 'rule') return WalkAction.Continue
220243

@@ -234,10 +257,17 @@ export function createVariants(theme: Theme): Variants {
234257
node.selector = `:is(${node.selector})`
235258
}
236259

237-
// Use `:where` to make sure the specificity of group variants isn't higher
238-
// than the specificity of other variants.
239260
node.selector = `&:is(${node.selector} *)`
261+
262+
// Track that the variant was actually applied
263+
didApply = true
240264
})
265+
266+
// If the node wasn't modified, this variant is not compatible with
267+
// `group-*` so discard the candidate.
268+
if (!didApply) {
269+
return null
270+
}
241271
})
242272

243273
variants.suggest('group', () => {
@@ -253,6 +283,8 @@ export function createVariants(theme: Theme): Variants {
253283
? `:where(.peer\\/${variant.modifier.value})`
254284
: ':where(.peer)'
255285

286+
let didApply = false
287+
256288
walk([ruleNode], (node) => {
257289
if (node.kind !== 'rule') return WalkAction.Continue
258290

@@ -272,10 +304,17 @@ export function createVariants(theme: Theme): Variants {
272304
node.selector = `:is(${node.selector})`
273305
}
274306

275-
// Use `:where` to make sure the specificity of group variants isn't higher
276-
// than the specificity of other variants.
277307
node.selector = `&:is(${node.selector} ~ *)`
308+
309+
// Track that the variant was actually applied
310+
didApply = true
278311
})
312+
313+
// If the node wasn't modified, this variant is not compatible with
314+
// `peer-*` so discard the candidate.
315+
if (!didApply) {
316+
return null
317+
}
279318
})
280319

281320
variants.suggest('peer', () => {
@@ -392,7 +431,28 @@ export function createVariants(theme: Theme): Variants {
392431

393432
variants.compound('has', (ruleNode, variant) => {
394433
if (variant.modifier) return null
395-
ruleNode.selector = `&:has(${ruleNode.selector.replace('&', '*')})`
434+
435+
let didApply = false
436+
437+
walk([ruleNode], (node) => {
438+
if (node.kind !== 'rule') return WalkAction.Continue
439+
440+
// Skip past at-rules, and continue traversing the children of the at-rule
441+
if (node.selector[0] === '@') return WalkAction.Continue
442+
443+
// Replace `&` in target variant with `*`, so variants like `&:hover`
444+
// become `&:has(*:hover)`. The `*` will often be optimized away.
445+
node.selector = `&:has(${node.selector.replace('&', '*')})`
446+
447+
// Track that the variant was actually applied
448+
didApply = true
449+
})
450+
451+
// If the node wasn't modified, this variant is not compatible with
452+
// `has-*` so discard the candidate.
453+
if (!didApply) {
454+
return null
455+
}
396456
})
397457

398458
variants.suggest('has', () => {

0 commit comments

Comments
 (0)