Skip to content

Commit df96ea5

Browse files
authored
Fix infinite loop when using @variant inside @custom-variant that points to another @custom-variant (tailwindlabs#19633)
This PR fixes an infinite loop when you use a `@variant` inside of a `@custom-variant`, where the `@variant` used is another `@custom-variant`. The issue stems from the fact that a `@custom-variant` can use a `@slot` that we have to replace with the proper AST nodes. However in this setup, the AST nodes will include a `@slot` node as well, which causes us to replace the `@slot` again, and so on, causing an infinite loop. ```css @custom-variant a { @slot; } @custom-variant b { @variant a { @slot; } } ``` The solution here is to replace the `@slot` nodes and then skip walking the nodes that were just inserted. This does mean that we end up with a `@slot` node in the final AST but that's not a real issue because that will get replaced later when handling the next `@custom-variant`. ## Test plan 1. Existing tests still pass 2. Added a regression test to ensure that the infinite loop does not happen anymore 3. Added additional tests to ensure that the behavior is correct Thanks @wongjn for your initial debugging help and providing a test case as well! Fixes: tailwindlabs#19618
1 parent d52c94f commit df96ea5

3 files changed

Lines changed: 93 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Detect utilities when containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
2525
- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525))
2626
- Align `@utility` name validation with Oxide scanner rules ([#19524](https://github.com/tailwindlabs/tailwindcss/pull/19524))
27+
- Fix infinite loop when using `@variant` inside `@custom-variant` ([#19633](https://github.com/tailwindlabs/tailwindcss/pull/19633))
2728

2829
### Deprecated
2930

packages/tailwindcss/src/index.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4558,6 +4558,97 @@ describe('@custom-variant', () => {
45584558
`)
45594559
})
45604560

4561+
// https://github.com/tailwindlabs/tailwindcss/issues/19618
4562+
test('@custom-variant can use a @variant that eventually uses another @custom-variant', async () => {
4563+
expect(
4564+
await compileCss(
4565+
css`
4566+
@custom-variant a {
4567+
@slot;
4568+
}
4569+
4570+
@custom-variant b {
4571+
@variant a {
4572+
@slot;
4573+
}
4574+
}
4575+
4576+
@tailwind utilities;
4577+
`,
4578+
['a:flex', 'b:flex', 'a:b:flex', 'b:a:flex'],
4579+
),
4580+
).toMatchInlineSnapshot(`
4581+
".a\\:flex, .b\\:flex, .a\\:b\\:flex, .b\\:a\\:flex {
4582+
display: flex;
4583+
}"
4584+
`)
4585+
})
4586+
4587+
test('@custom-variant can use a @variant that eventually uses another @custom-variant (2)', async () => {
4588+
expect(
4589+
await compileCss(
4590+
css`
4591+
@custom-variant a {
4592+
.a {
4593+
@slot;
4594+
}
4595+
}
4596+
4597+
@custom-variant b {
4598+
.b {
4599+
@variant a {
4600+
.a-inside-b {
4601+
@slot;
4602+
}
4603+
}
4604+
}
4605+
}
4606+
4607+
@tailwind utilities;
4608+
`,
4609+
['a:flex', 'b:flex', 'a:b:flex', 'b:a:flex'],
4610+
),
4611+
).toMatchInlineSnapshot(`
4612+
".a\\:flex .a, .b\\:flex .b .a .a-inside-b, .a\\:b\\:flex .a .b .a .a-inside-b, .b\\:a\\:flex .b .a .a-inside-b .a {
4613+
display: flex;
4614+
}"
4615+
`)
4616+
})
4617+
4618+
// https://github.com/tailwindlabs/tailwindcss/issues/19618#issuecomment-3830775912
4619+
test('@custom-variant can use existing @slot @variants', async () => {
4620+
expect(
4621+
await compileCss(
4622+
css`
4623+
@custom-variant hocus {
4624+
@variant hover {
4625+
@variant focus {
4626+
@slot;
4627+
}
4628+
}
4629+
}
4630+
4631+
@custom-variant hover {
4632+
&:hover {
4633+
@slot;
4634+
}
4635+
4636+
&[data-hover] {
4637+
@slot;
4638+
}
4639+
}
4640+
4641+
@tailwind utilities;
4642+
`,
4643+
['hocus:flex'],
4644+
),
4645+
).toMatchInlineSnapshot(`
4646+
".hocus\\:flex:hover:focus, .hocus\\:flex[data-hover]:focus {
4647+
display: flex;
4648+
}"
4649+
`)
4650+
})
4651+
45614652
test('@custom-variant setup that results in a circular dependency error can be solved', async () => {
45624653
expect(
45634654
await compileCss(

packages/tailwindcss/src/variants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {
11961196
walk(ast, (node) => {
11971197
// Replace `@slot` with rule nodes
11981198
if (node.kind === 'at-rule' && node.name === '@slot') {
1199-
return WalkAction.Replace(nodes)
1199+
return WalkAction.ReplaceSkip(nodes)
12001200
}
12011201

12021202
// Wrap `@keyframes` and `@property` in `AtRoot` nodes

0 commit comments

Comments
 (0)