Skip to content

Commit 68d896b

Browse files
Don't output unparsable arbitrary values (#7789)
* Refactor * Don’t output unparsable arbitrary values * Update changelog
1 parent c6097d5 commit 68d896b

File tree

3 files changed

+77
-43
lines changed

3 files changed

+77
-43
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Only add `!` to selector class matching template candidate when using important modifier with mutli-class selectors ([#7664](https://github.com/tailwindlabs/tailwindcss/pull/7664))
1919
- Correctly parse and prefix animation names with dots ([#7163](https://github.com/tailwindlabs/tailwindcss/pull/7163))
2020
- Fix extraction from template literal/function with array ([#7481](https://github.com/tailwindlabs/tailwindcss/pull/7481))
21+
- Don't output unparsable arbitrary values ([#7789](https://github.com/tailwindlabs/tailwindcss/pull/7789))
2122

2223
### Changed
2324

src/lib/generateRules.js

+62-43
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,19 @@ function looksLikeUri(declaration) {
303303
}
304304
}
305305

306+
function isParsableNode(node) {
307+
let isParsable = true
308+
309+
node.walkDecls((decl) => {
310+
if (!isParsableCssValue(decl.name, decl.value)) {
311+
isParsable = false
312+
return false
313+
}
314+
})
315+
316+
return isParsable
317+
}
318+
306319
function isParsableCssValue(property, value) {
307320
// We don't want to to treat [https://example.com] as a custom property
308321
// Even though, according to the CSS grammar, it's a totally valid CSS declaration
@@ -456,60 +469,66 @@ function* resolveMatches(candidate, context) {
456469
}
457470
}
458471

459-
// Only keep the result of the very first plugin if we are dealing with
460-
// arbitrary values, to protect against ambiguity.
461-
if (isArbitraryValue(modifier) && matches.length > 1) {
462-
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
472+
if (isArbitraryValue(modifier)) {
473+
// When generated arbitrary values are ambiguous, we can't know
474+
// which to pick so don't generate any utilities for them
475+
if (matches.length > 1) {
476+
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
463477

464-
// Remove duplicates, so that we can detect proper unique types for each plugin.
465-
for (let pluginTypes of typesPerPlugin) {
466-
for (let type of pluginTypes) {
467-
let removeFromOwnGroup = false
478+
// Remove duplicates, so that we can detect proper unique types for each plugin.
479+
for (let pluginTypes of typesPerPlugin) {
480+
for (let type of pluginTypes) {
481+
let removeFromOwnGroup = false
468482

469-
for (let otherGroup of typesPerPlugin) {
470-
if (pluginTypes === otherGroup) continue
483+
for (let otherGroup of typesPerPlugin) {
484+
if (pluginTypes === otherGroup) continue
471485

472-
if (otherGroup.has(type)) {
473-
otherGroup.delete(type)
474-
removeFromOwnGroup = true
486+
if (otherGroup.has(type)) {
487+
otherGroup.delete(type)
488+
removeFromOwnGroup = true
489+
}
475490
}
476-
}
477491

478-
if (removeFromOwnGroup) pluginTypes.delete(type)
492+
if (removeFromOwnGroup) pluginTypes.delete(type)
493+
}
479494
}
480-
}
481495

482-
let messages = []
483-
484-
for (let [idx, group] of typesPerPlugin.entries()) {
485-
for (let type of group) {
486-
let rules = matches[idx]
487-
.map(([, rule]) => rule)
488-
.flat()
489-
.map((rule) =>
490-
rule
491-
.toString()
492-
.split('\n')
493-
.slice(1, -1) // Remove selector and closing '}'
494-
.map((line) => line.trim())
495-
.map((x) => ` ${x}`) // Re-indent
496-
.join('\n')
496+
let messages = []
497+
498+
for (let [idx, group] of typesPerPlugin.entries()) {
499+
for (let type of group) {
500+
let rules = matches[idx]
501+
.map(([, rule]) => rule)
502+
.flat()
503+
.map((rule) =>
504+
rule
505+
.toString()
506+
.split('\n')
507+
.slice(1, -1) // Remove selector and closing '}'
508+
.map((line) => line.trim())
509+
.map((x) => ` ${x}`) // Re-indent
510+
.join('\n')
511+
)
512+
.join('\n\n')
513+
514+
messages.push(
515+
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
497516
)
498-
.join('\n\n')
499-
500-
messages.push(` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``)
501-
break
517+
break
518+
}
502519
}
520+
521+
log.warn([
522+
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
523+
...messages,
524+
`If this is content and not a class, replace it with \`${candidate
525+
.replace('[', '[')
526+
.replace(']', ']')}\` to silence this warning.`,
527+
])
528+
continue
503529
}
504530

505-
log.warn([
506-
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
507-
...messages,
508-
`If this is content and not a class, replace it with \`${candidate
509-
.replace('[', '[')
510-
.replace(']', ']')}\` to silence this warning.`,
511-
])
512-
continue
531+
matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
513532
}
514533

515534
matches = matches.flat()

tests/arbitrary-values.test.js

+14
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,17 @@ it('should be possible to read theme values in arbitrary values (with quotes) wh
370370
`)
371371
})
372372
})
373+
374+
it('should not output unparsable arbitrary CSS values', () => {
375+
let config = {
376+
content: [
377+
{
378+
raw: 'let classes = `w-[${sizes.width}]`',
379+
},
380+
],
381+
}
382+
383+
return run('@tailwind utilities', config).then((result) => {
384+
return expect(result.css).toMatchFormattedCss(``)
385+
})
386+
})

0 commit comments

Comments
 (0)