Commit 51aa9d7
fix(canonicalize): handle utilities with empty property maps in collapse (#19727)
## Problem
`canonicalizeCandidates` crashes when called with `collapse: true` and
the candidate list includes utilities whose CSS output contains no
standard declaration properties (only `@property` rules and CSS custom
properties).
This is reproducible with vanilla Tailwind CSS and no custom
configuration:
```js
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], { collapse: true })
// TypeError: X is not iterable
```
```js
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], { collapse: true })
// TypeError: Cannot read properties of null (reading 'has')
```
All shadow utilities (`shadow-sm`, `shadow-md`, `shadow-lg`,
`shadow-xl`) crash when combined with any other utility and `collapse:
true`.
This was discovered via `eslint-plugin-better-tailwindcss`, which calls
`canonicalizeCandidates` with `collapse: true` for its
`enforce-canonical-classes` rule. The crash brings down ESLint entirely.
## Root cause
In `collapseGroup`, the `otherUtilities` array is built by mapping over
each candidate's property values:
```ts
let otherUtilities = candidatePropertiesValues.map((propertyValues) => {
let result: Set<string> | null = null
for (let property of propertyValues.keys()) {
// ... builds result ...
}
return result! // returns null if propertyValues has no keys
})
```
When a utility like `shadow-sm` generates CSS with `@property` rules and
custom property declarations but no standard CSS properties,
`propertyValues.keys()` is empty, the loop never executes, and `result`
stays `null`. The non-null assertion `result!` returns `null` into the
array.
Downstream code then crashes when iterating or calling `.has()` on the
null entry:
```ts
for (let i = 0; i < otherUtilities.length; i++) {
let current = otherUtilities[i] // null
for (let property of current) { // "X is not iterable"
if (other.has(property)) { // "Cannot read properties of null"
```
## Fix
Return an empty `Set` instead of `null` when a utility has no property
keys:
```ts
return result ?? new Set<string>()
```
This is semantically correct: a utility with no standard properties
cannot be linked to or collapsed with any other utility, which is
exactly what an empty Set represents in the linking algorithm. It won't
cause false collapses or suppress valid collapses of other utilities.
## Test plan
- Added test: `collapse does not crash when utilities with no standard
properties are present`
- Verifies `shadow-sm + border`, `shadow-md + p-4`, and `shadow-sm +
shadow-md` don't throw
- Verifies the candidates are returned uncollapsed (correct behavior)
- All 1218 existing tests continue to pass
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>1 parent c586bd6 commit 51aa9d7
File tree
3 files changed
+39
-1
lines changed- packages/tailwindcss/src
3 files changed
+39
-1
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1220 | 1220 | | |
1221 | 1221 | | |
1222 | 1222 | | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
334 | 334 | | |
335 | 335 | | |
336 | 336 | | |
337 | | - | |
| 337 | + | |
338 | 338 | | |
339 | 339 | | |
340 | 340 | | |
| |||
0 commit comments