Skip to content

Commit d698c10

Browse files
authored
Ensure multiple --value(…) or --modifier(…) calls don't delete subsequent declarations (#17273)
This PR fixes a bug in the handling of `@utility`. Essentially if you had a declaration where you used a `--modifier(…)` _and_ a `--value(…)` and both caused the declaration to be removed, the declaration after the current one would be removed as well. This happened because 2 reasons: 1. Once we removed the declaration when we handled `--modifier(…)`, we didn't stop the walk and kept going. 2. The `replaceWith(…)` code allows you to call the function multiple times but immediately mutates the AST. This means that if you call it multiple times that you are potentially removing / updating nodes followed by the current one. E.g.: ```css @Utility mask-r-* { --mask-right: linear-gradient(to left, transparent, black --value(percentage)); --mask-right: linear-gradient( to left, transparent calc(var(--spacing) * --modifier(integer)), black calc(var(--spacing) * --value(integer)) ); mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); } ``` If this is used as `mask-r-10%`, then the first definition of `--mask-right` is kept, but the second definition of `--mask-right` is deleted because both `--modifier(integer)` and `--value(integer)` do not result in a valid value. However, the `mask-image` declaration was also removed because the `replaceWith(…)` function was called twice. Once for `--modifier(integer)` and once for `--value(integer)`. # Test plan 1. Added a test to cover this case. 2. Existing tests pass. 3. Added a hard error if we call `replaceWith(…)` multiple times.
1 parent cec7f05 commit d698c10

File tree

6 files changed

+37
-1
lines changed

6 files changed

+37
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- Pre-process `<template lang="…">` in Vue files ([#17252](https://github.com/tailwindlabs/tailwindcss/pull/17252))
2929
- Remove redundant `line-height: initial` from Preflight ([#15212](https://github.com/tailwindlabs/tailwindcss/pull/15212))
3030
- Prevent segfault when loaded in a worker thread on Linux ([#17276](https://github.com/tailwindlabs/tailwindcss/pull/17276))
31+
- Ensure multiple `--value(…)` or `--modifier(…)` calls don't delete subsequent declarations ([#17273](https://github.com/tailwindlabs/tailwindcss/pull/17273))
3132

3233
## [4.0.14] - 2025-03-13
3334

packages/tailwindcss/src/ast.ts

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export function walk(
149149
context,
150150
path,
151151
replaceWith(newNode) {
152+
if (replacedNode) return
152153
replacedNode = true
153154

154155
if (Array.isArray(newNode)) {

packages/tailwindcss/src/compat/selector-parser.ts

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export function walk(
9898
visit(node, {
9999
parent,
100100
replaceWith(newNode) {
101+
if (replacedNode) return
101102
replacedNode = true
102103

103104
if (Array.isArray(newNode)) {

packages/tailwindcss/src/utilities.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -17961,6 +17961,38 @@ describe('custom utilities', () => {
1796117961
}"
1796217962
`)
1796317963
})
17964+
17965+
// This originated from a bug. Essentially in the second `--mask-right` we
17966+
// call both `--modifier(…)` and `--value(…)`. The moment we processed
17967+
// `--modifier(…)` it deleted the `--mask-right` declaration (expected
17968+
// behavior). But we didn't properly stop so the `--value(…)` was still
17969+
// processed and also tried to remove the `--mask-right` declaration.
17970+
//
17971+
// This test now ensures that we only remove/replace a declaration once.
17972+
test('declaration nodes are only replaced/removed once', async () => {
17973+
let input = css`
17974+
@utility mask-r-* {
17975+
--mask-right: linear-gradient(to left, transparent, black --value(percentage));
17976+
--mask-right: linear-gradient(
17977+
to left,
17978+
transparent calc(var(--spacing) * --modifier(integer)),
17979+
black calc(var(--spacing) * --value(integer))
17980+
);
17981+
mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic);
17982+
}
17983+
17984+
@tailwind utilities;
17985+
`
17986+
17987+
expect(await compileCss(input, ['mask-r-20%'])).toMatchInlineSnapshot(`
17988+
".mask-r-20\\% {
17989+
--mask-right: linear-gradient(to left, transparent, black 20%);
17990+
-webkit-mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic);
17991+
-webkit-mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic);
17992+
mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic);
17993+
}"
17994+
`)
17995+
})
1796417996
})
1796517997

1796617998
test('resolve value based on `@theme`', async () => {

packages/tailwindcss/src/utilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4885,7 +4885,7 @@ export function createCssUtility(node: AtRule) {
48854885
// declaration can be removed.
48864886
if (modifier === null) {
48874887
replaceDeclarationWith([])
4888-
return ValueParser.ValueWalkAction.Skip
4888+
return ValueParser.ValueWalkAction.Stop
48894889
}
48904890

48914891
usedModifierFn = true

packages/tailwindcss/src/value-parser.ts

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function walk(
6969
visit(node, {
7070
parent,
7171
replaceWith(newNode) {
72+
if (replacedNode) return
7273
replacedNode = true
7374

7475
if (Array.isArray(newNode)) {

0 commit comments

Comments
 (0)