Skip to content

Commit 3cf5c2d

Browse files
authored
Disallow empty arbitrary values (#15055)
This PR makes the candidate parser more strict by not allowing empty arbitrary values. Examples that are not allowed anymore: - `bg-[]` — arbitrary value - `bg-()` — arbitrary value, var shorthand - `bg-[length:]` — arbitrary value, with typehint - `bg-(length:)` — arbitrary value, with typehint, var shorthand - `bg-red-500/[]` — arbitrary modifier - `bg-red-500/()` — arbitrary modifier, var shorthand - `data-[]:flex` — arbitrary value for variant - `data-():flex` — arbitrary value for variant, var shorthand - `group-visible/[]:flex` — arbitrary modifier for variant - `group-visible/():flex` — arbitrary modifier for variant, var shorthand If you are trying to trick the parser by injecting some spaces like this: - `bg-[_]` Then that is also not allowed.
1 parent 4f63a5a commit 3cf5c2d

File tree

11 files changed

+311
-32
lines changed

11 files changed

+311
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
### Changed
2121

2222
- Use single drop shadow values instead of multiple ([#15056](https://github.com/tailwindlabs/tailwindcss/pull/15056))
23+
- Do not parse invalid candidates with empty arbitrary values ([#15055](https://github.com/tailwindlabs/tailwindcss/pull/15055))
2324

2425
## [4.0.0-alpha.35] - 2024-11-20
2526

crates/oxide/src/parser.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -538,14 +538,17 @@ impl<'a> Extractor<'a> {
538538
return ParseAction::Consume;
539539
}
540540

541-
if let Arbitrary::Brackets { start_idx } = self.arbitrary {
541+
if let Arbitrary::Brackets { start_idx: _ } = self.arbitrary {
542542
trace!("Arbitrary::End\t");
543543
self.arbitrary = Arbitrary::None;
544544

545-
if self.cursor.pos - start_idx == 1 {
546-
// We have an empty arbitrary value, which is not allowed
547-
return ParseAction::Skip;
548-
}
545+
// TODO: This is temporarily disabled such that the upgrade tool can work
546+
// with legacy arbitrary values. This will be re-enabled in the future (or
547+
// with a flag)
548+
// if self.cursor.pos - start_idx == 1 {
549+
// // We have an empty arbitrary value, which is not allowed
550+
// return ParseAction::Skip;
551+
// }
549552
}
550553
}
551554

@@ -1517,4 +1520,23 @@ mod test {
15171520

15181521
assert_eq!(candidates, vec![("div", 1), ("class", 5), ("flex", 12),]);
15191522
}
1523+
1524+
#[test]
1525+
fn empty_arbitrary_values_are_allowed_for_codemods() {
1526+
let candidates = run(
1527+
r#"<div class="group-[]:flex group-[]/name:flex peer-[]:flex peer-[]/name:flex"></div>"#,
1528+
false,
1529+
);
1530+
assert_eq!(
1531+
candidates,
1532+
vec![
1533+
"div",
1534+
"class",
1535+
"group-[]:flex",
1536+
"group-[]/name:flex",
1537+
"peer-[]:flex",
1538+
"peer-[]/name:flex"
1539+
]
1540+
);
1541+
}
15201542
}

integrations/upgrade/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ test(
6868
'src/index.html': html`
6969
<h1>🤠👋</h1>
7070
<div
71-
class="!flex sm:!block bg-gradient-to-t bg-[--my-red] max-w-screen-md ml-[theme(screens.md)]"
71+
class="!flex sm:!block bg-gradient-to-t bg-[--my-red] max-w-screen-md ml-[theme(screens.md)] group-[]:flex"
7272
></div>
7373
<!-- Migrate to sm -->
7474
<div class="blur shadow rounded inset-shadow drop-shadow"></div>
@@ -100,7 +100,7 @@ test(
100100
--- ./src/index.html ---
101101
<h1>🤠👋</h1>
102102
<div
103-
class="flex! sm:block! bg-linear-to-t bg-(--my-red) max-w-(--breakpoint-md) ml-(--breakpoint-md)"
103+
class="flex! sm:block! bg-linear-to-t bg-(--my-red) max-w-(--breakpoint-md) ml-(--breakpoint-md) in-[.group]:flex"
104104
></div>
105105
<!-- Migrate to sm -->
106106
<div class="blur-sm shadow-sm rounded-sm inset-shadow-sm drop-shadow-sm"></div>
@@ -194,7 +194,7 @@ test(
194194
`,
195195
'src/index.html': html`
196196
<div
197-
class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex [color:red]"
197+
class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex [color:red] group-[]:tw__flex"
198198
></div>
199199
`,
200200
'src/input.css': css`
@@ -215,7 +215,7 @@ test(
215215
"
216216
--- ./src/index.html ---
217217
<div
218-
class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red]"
218+
class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red] tw:in-[.tw\\:group]:flex"
219219
></div>
220220
221221
--- ./src/input.css ---
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import { expect, test } from 'vitest'
3+
import { handleEmptyArbitraryValues } from './handle-empty-arbitrary-values'
4+
import { prefix } from './prefix'
5+
6+
test.each([
7+
['group-[]:flex', 'group-[&]:flex'],
8+
['group-[]/name:flex', 'group-[&]/name:flex'],
9+
10+
['peer-[]:flex', 'peer-[&]:flex'],
11+
['peer-[]/name:flex', 'peer-[&]/name:flex'],
12+
])('%s => %s (%#)', async (candidate, result) => {
13+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
14+
base: __dirname,
15+
})
16+
17+
expect(handleEmptyArbitraryValues(designSystem, {}, candidate)).toEqual(result)
18+
})
19+
20+
test.each([
21+
['group-[]:tw-flex', 'tw:group-[&]:flex'],
22+
['group-[]/name:tw-flex', 'tw:group-[&]/name:flex'],
23+
24+
['peer-[]:tw-flex', 'tw:peer-[&]:flex'],
25+
['peer-[]/name:tw-flex', 'tw:peer-[&]/name:flex'],
26+
])('%s => %s (%#)', async (candidate, result) => {
27+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss" prefix(tw);', {
28+
base: __dirname,
29+
})
30+
31+
expect(
32+
[handleEmptyArbitraryValues, prefix].reduce(
33+
(acc, step) => step(designSystem, { prefix: 'tw-' }, acc),
34+
candidate,
35+
),
36+
).toEqual(result)
37+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
2+
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
3+
4+
export function handleEmptyArbitraryValues(
5+
designSystem: DesignSystem,
6+
_userConfig: Config,
7+
rawCandidate: string,
8+
): string {
9+
// We can parse the candidate, nothing to do
10+
if (designSystem.parseCandidate(rawCandidate).length > 0) {
11+
return rawCandidate
12+
}
13+
14+
// No need to handle empty arbitrary values
15+
if (!rawCandidate.includes('[]')) {
16+
return rawCandidate
17+
}
18+
19+
// Add the `&` placeholder to the empty arbitrary values. Other codemods might
20+
// migrate these away, but if not, then it's at least valid to parse.
21+
//
22+
// E.g.: `group-[]:flex` => `group-[&]:flex`
23+
// E.g.: `group-[]/name:flex` => `group-[&]/name:flex`
24+
return rawCandidate
25+
.replaceAll('-[]:', '-[&]:') // End of variant
26+
.replaceAll('-[]/', '-[&]/') // With modifier
27+
}

packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
22
import { expect, test } from 'vitest'
3+
import { handleEmptyArbitraryValues } from './handle-empty-arbitrary-values'
34
import { modernizeArbitraryValues } from './modernize-arbitrary-values'
45
import { prefix } from './prefix'
56

@@ -31,6 +32,10 @@ test.each([
3132
['group-[]:flex', 'in-[.group]:flex'],
3233
['group-[]/name:flex', 'in-[.group\\/name]:flex'],
3334

35+
// Migrate `peer-[]` to a parsable `peer-[&]` instead:
36+
['peer-[]:flex', 'peer-[&]:flex'],
37+
['peer-[]/name:flex', 'peer-[&]/name:flex'],
38+
3439
// These shouldn't happen in the real world (because compound variants are
3540
// new). But this could happen once we allow codemods to run in v4+ projects.
3641
['has-group-[]:flex', 'has-in-[.group]:flex'],
@@ -85,12 +90,17 @@ test.each([
8590
['has-[[aria-visible]]:flex', 'has-aria-[visible]:flex'],
8691

8792
['has-[&:not(:nth-child(even))]:flex', 'has-odd:flex'],
88-
])('%s => %s', async (candidate, result) => {
93+
])('%s => %s (%#)', async (candidate, result) => {
8994
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
9095
base: __dirname,
9196
})
9297

93-
expect(modernizeArbitraryValues(designSystem, {}, candidate)).toEqual(result)
98+
expect(
99+
[handleEmptyArbitraryValues, modernizeArbitraryValues].reduce(
100+
(acc, step) => step(designSystem, {}, acc),
101+
candidate,
102+
),
103+
).toEqual(result)
94104
})
95105

96106
test.each([
@@ -101,6 +111,10 @@ test.each([
101111
['group-[]:tw-flex', 'tw:in-[.tw\\:group]:flex'],
102112
['group-[]/name:tw-flex', 'tw:in-[.tw\\:group\\/name]:flex'],
103113

114+
// Migrate `peer-[]` to a parsable `peer-[&]` instead:
115+
['peer-[]:tw-flex', 'tw:peer-[&]:flex'],
116+
['peer-[]/name:tw-flex', 'tw:peer-[&]/name:flex'],
117+
104118
// However, `.group` inside of an arbitrary variant should not be prefixed:
105119
['[.group_&]:tw-flex', 'tw:in-[.group]:flex'],
106120

@@ -110,13 +124,13 @@ test.each([
110124
['has-group-[]/name:tw-flex', 'tw:has-in-[.tw\\:group\\/name]:flex'],
111125
['not-group-[]:tw-flex', 'tw:not-in-[.tw\\:group]:flex'],
112126
['not-group-[]/name:tw-flex', 'tw:not-in-[.tw\\:group\\/name]:flex'],
113-
])('%s => %s', async (candidate, result) => {
127+
])('%s => %s (%#)', async (candidate, result) => {
114128
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss" prefix(tw);', {
115129
base: __dirname,
116130
})
117131

118132
expect(
119-
[prefix, modernizeArbitraryValues].reduce(
133+
[handleEmptyArbitraryValues, prefix, modernizeArbitraryValues].reduce(
120134
(acc, step) => step(designSystem, { prefix: 'tw-' }, acc),
121135
candidate,
122136
),

packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function modernizeArbitraryValues(
4343
variant.kind === 'compound' &&
4444
variant.root === 'group' &&
4545
variant.variant.kind === 'arbitrary' &&
46-
variant.variant.selector === '&:is()'
46+
variant.variant.selector === '&'
4747
) {
4848
// `group-[]`
4949
if (variant.modifier === null) {

packages/@tailwindcss-upgrade/src/template/migrate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { extractRawCandidates } from './candidates'
66
import { arbitraryValueToBareValue } from './codemods/arbitrary-value-to-bare-value'
77
import { automaticVarInjection } from './codemods/automatic-var-injection'
88
import { bgGradient } from './codemods/bg-gradient'
9+
import { handleEmptyArbitraryValues } from './codemods/handle-empty-arbitrary-values'
910
import { important } from './codemods/important'
1011
import { legacyArbitraryValues } from './codemods/legacy-arbitrary-values'
1112
import { legacyClasses } from './codemods/legacy-classes'
@@ -29,6 +30,7 @@ export type Migration = (
2930
) => string | Promise<string>
3031

3132
export const DEFAULT_MIGRATIONS: Migration[] = [
33+
handleEmptyArbitraryValues,
3234
prefix,
3335
important,
3436
bgGradient,

packages/tailwindcss/src/candidate.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,3 +1416,130 @@ it('should parse candidates with a prefix', () => {
14161416
]
14171417
`)
14181418
})
1419+
1420+
it.each([
1421+
// Empty arbitrary value
1422+
'bg-[]',
1423+
'bg-()',
1424+
// — Tricking the parser with a space is not allowed
1425+
'bg-[_]',
1426+
'bg-(_)',
1427+
1428+
// Empty arbitrary value, with typehint
1429+
'bg-[color:]',
1430+
'bg-(color:)',
1431+
// — Tricking the parser with a space is not allowed
1432+
'bg-[color:_]',
1433+
'bg-(color:_)',
1434+
1435+
// Empty arbitrary modifier
1436+
'bg-red-500/[]',
1437+
'bg-red-500/()',
1438+
// — Tricking the parser with a space is not allowed
1439+
'bg-red-500/[_]',
1440+
'bg-red-500/(_)',
1441+
1442+
// Empty arbitrary modifier for arbitrary properties
1443+
'[color:red]/[]',
1444+
'[color:red]/()',
1445+
// — Tricking the parser with a space is not allowed
1446+
'[color:red]/[_]',
1447+
'[color:red]/(_)',
1448+
1449+
// Empty arbitrary value and modifier
1450+
'bg-[]/[]',
1451+
'bg-()/[]',
1452+
'bg-[]/()',
1453+
'bg-()/()',
1454+
// — Tricking the parser with a space is not allowed
1455+
'bg-[_]/[]',
1456+
'bg-(_)/[]',
1457+
'bg-[_]/()',
1458+
'bg-(_)/()',
1459+
'bg-[]/[_]',
1460+
'bg-()/[_]',
1461+
'bg-[]/(_)',
1462+
'bg-()/(_)',
1463+
'bg-[_]/[_]',
1464+
'bg-(_)/[_]',
1465+
'bg-[_]/(_)',
1466+
'bg-(_)/(_)',
1467+
1468+
// Functional variants
1469+
// Empty arbitrary value in variant
1470+
'data-[]:flex',
1471+
'data-():flex',
1472+
// — Tricking the parser with a space is not allowed
1473+
'data-[_]:flex',
1474+
'data-(_):flex',
1475+
1476+
// Empty arbitrary modifier in variant
1477+
'data-foo/[]:flex',
1478+
'data-foo/():flex',
1479+
// — Tricking the parser with a space is not allowed
1480+
'data-foo/[_]:flex',
1481+
'data-foo/(_):flex',
1482+
1483+
// Empty arbitrary value and modifier in variant
1484+
'data-[]/[]:flex',
1485+
'data-()/[]:flex',
1486+
'data-[]/():flex',
1487+
'data-()/():flex',
1488+
// — Tricking the parser with a space is not allowed
1489+
'data-[_]/[]:flex',
1490+
'data-(_)/[]:flex',
1491+
'data-[_]/():flex',
1492+
'data-(_)/():flex',
1493+
'data-[]/[_]:flex',
1494+
'data-()/[_]:flex',
1495+
'data-[]/(_):flex',
1496+
'data-()/(_):flex',
1497+
'data-[_]/[_]:flex',
1498+
'data-(_)/[_]:flex',
1499+
'data-[_]/(_):flex',
1500+
'data-(_)/(_):flex',
1501+
1502+
// Compound variants
1503+
// Empty arbitrary value in variant
1504+
'group-data-[]:flex',
1505+
'group-data-():flex',
1506+
// — Tricking the parser with a space is not allowed
1507+
'group-data-[_]:flex',
1508+
'group-data-(_):flex',
1509+
1510+
// Empty arbitrary modifier in variant
1511+
'group-data-foo/[]:flex',
1512+
'group-data-foo/():flex',
1513+
// — Tricking the parser with a space is not allowed
1514+
'group-data-foo/[_]:flex',
1515+
'group-data-foo/(_):flex',
1516+
1517+
// Empty arbitrary value and modifier in variant
1518+
'group-data-[]/[]:flex',
1519+
'group-data-()/[]:flex',
1520+
'group-data-[]/():flex',
1521+
'group-data-()/():flex',
1522+
// — Tricking the parser with a space is not allowed
1523+
'group-data-[_]/[]:flex',
1524+
'group-data-(_)/[]:flex',
1525+
'group-data-[_]/():flex',
1526+
'group-data-(_)/():flex',
1527+
'group-data-[]/[_]:flex',
1528+
'group-data-()/[_]:flex',
1529+
'group-data-[]/(_):flex',
1530+
'group-data-()/(_):flex',
1531+
'group-data-[_]/[_]:flex',
1532+
'group-data-(_)/[_]:flex',
1533+
'group-data-[_]/(_):flex',
1534+
'group-data-(_)/(_):flex',
1535+
])('should not parse invalid empty arbitrary values: %s', (rawCandidate) => {
1536+
let utilities = new Utilities()
1537+
utilities.static('flex', () => [])
1538+
utilities.functional('bg', () => [])
1539+
1540+
let variants = new Variants()
1541+
variants.functional('data', () => {})
1542+
variants.compound('group', Compounds.StyleRules, () => {})
1543+
1544+
expect(run(rawCandidate, { utilities, variants })).toEqual([])
1545+
})

0 commit comments

Comments
 (0)