Skip to content

Commit 94d6e72

Browse files
Implement fallback plugins when arbitrary values result in css from multiple plugins (#9376)
* use test with non-any type plugin * choose backgroundSize over backgroundPosition Ensure that `backgroundColor` can take any value * add tests to verify fallback plugins * implement fallback plugins Whenever an arbitrary value results in css from multiple plugins we first try to resolve a falback plugin. The fallback mechanism works like this: - If A has type `any` and B has type `color`, then B should win. > This is because `A` will match *anything*, but the more precise type should win instead. E.g.: `backgroundColor` has the type `any` so `bg-[100px_200px]` would match both the `backgroundColor` and `backgroundSize` but `backgroundSize` matched because of a specific type and not because of the `any` type. - If A has type `length` and B has type `[length, { disambiguate: true }]`, then B should win. > This is because `B` marked the `length` as the plugin that should win in case a clash happens. * Add any type to a handful of plugins Needs tests tho * Add any type to `border-{x,y,t,r,b,l}` plugins * Add test for any type * Split on multiple lines * fixup * add tests for implicit `any` types * rename `disambiguate` to `preferOnConflict` * update tests to reflect `any` types a bit better * update changelog * annotate any-type test with a bit more information Just for future debugging reasons! Co-authored-by: Jordan Pittman <[email protected]>
1 parent 52ab315 commit 94d6e72

10 files changed

+997
-88
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
- Don't emit generated utilities with invalid uses of theme functions ([#9319](https://github.com/tailwindlabs/tailwindcss/pull/9319))
4141
- Revert change that only listened for stdin close on TTYs ([#9331](https://github.com/tailwindlabs/tailwindcss/pull/9331))
4242
- Ignore unset values (like `null` or `undefined`) when resolving the classList for intellisense ([#9385](https://github.com/tailwindlabs/tailwindcss/pull/9385))
43+
- Implement fallback plugins when arbitrary values result in css from multiple plugins ([#9376](https://github.com/tailwindlabs/tailwindcss/pull/9376))
4344

4445
## [3.1.8] - 2022-08-05
4546

src/corePlugins.js

+17-17
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,7 @@ export let corePlugins = {
10621062
}
10631063
},
10641064
},
1065-
{ values: theme('divideWidth'), type: ['line-width', 'length'] }
1065+
{ values: theme('divideWidth'), type: ['line-width', 'length', 'any'] }
10661066
)
10671067

10681068
addUtilities({
@@ -1110,7 +1110,7 @@ export let corePlugins = {
11101110
},
11111111
{
11121112
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('divideColor'))),
1113-
type: 'color',
1113+
type: ['color', 'any'],
11141114
}
11151115
)
11161116
},
@@ -1290,7 +1290,7 @@ export let corePlugins = {
12901290
},
12911291
{
12921292
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
1293-
type: ['color'],
1293+
type: ['color', 'any'],
12941294
}
12951295
)
12961296

@@ -1327,7 +1327,7 @@ export let corePlugins = {
13271327
},
13281328
{
13291329
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
1330-
type: 'color',
1330+
type: ['color', 'any'],
13311331
}
13321332
)
13331333

@@ -1388,7 +1388,7 @@ export let corePlugins = {
13881388
},
13891389
{
13901390
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
1391-
type: 'color',
1391+
type: ['color', 'any'],
13921392
}
13931393
)
13941394
},
@@ -1414,7 +1414,7 @@ export let corePlugins = {
14141414
})
14151415
},
14161416
},
1417-
{ values: flattenColorPalette(theme('backgroundColor')), type: 'color' }
1417+
{ values: flattenColorPalette(theme('backgroundColor')), type: ['color', 'any'] }
14181418
)
14191419
},
14201420

@@ -1482,7 +1482,7 @@ export let corePlugins = {
14821482
},
14831483

14841484
backgroundSize: createUtilityPlugin('backgroundSize', [['bg', ['background-size']]], {
1485-
type: ['lookup', 'length', 'percentage'],
1485+
type: ['lookup', ['length', { preferOnConflict: true }], 'percentage'],
14861486
}),
14871487

14881488
backgroundAttachment: ({ addUtilities }) => {
@@ -1543,7 +1543,7 @@ export let corePlugins = {
15431543
return { stroke: toColorValue(value) }
15441544
},
15451545
},
1546-
{ values: flattenColorPalette(theme('stroke')), type: ['color', 'url'] }
1546+
{ values: flattenColorPalette(theme('stroke')), type: ['color', 'url', 'any'] }
15471547
)
15481548
},
15491549

@@ -1654,7 +1654,7 @@ export let corePlugins = {
16541654
},
16551655

16561656
fontWeight: createUtilityPlugin('fontWeight', [['font', ['fontWeight']]], {
1657-
type: ['lookup', 'number'],
1657+
type: ['lookup', 'number', 'any'],
16581658
}),
16591659

16601660
textTransform: ({ addUtilities }) => {
@@ -1750,7 +1750,7 @@ export let corePlugins = {
17501750
})
17511751
},
17521752
},
1753-
{ values: flattenColorPalette(theme('textColor')), type: 'color' }
1753+
{ values: flattenColorPalette(theme('textColor')), type: ['color', 'any'] }
17541754
)
17551755
},
17561756

@@ -1772,7 +1772,7 @@ export let corePlugins = {
17721772
return { 'text-decoration-color': toColorValue(value) }
17731773
},
17741774
},
1775-
{ values: flattenColorPalette(theme('textDecorationColor')), type: ['color'] }
1775+
{ values: flattenColorPalette(theme('textDecorationColor')), type: ['color', 'any'] }
17761776
)
17771777
},
17781778

@@ -1795,7 +1795,7 @@ export let corePlugins = {
17951795
textUnderlineOffset: createUtilityPlugin(
17961796
'textUnderlineOffset',
17971797
[['underline-offset', ['text-underline-offset']]],
1798-
{ type: ['length', 'percentage'] }
1798+
{ type: ['length', 'percentage', 'any'] }
17991799
),
18001800

18011801
fontSmoothing: ({ addUtilities }) => {
@@ -1968,7 +1968,7 @@ export let corePlugins = {
19681968
}
19691969
},
19701970
},
1971-
{ values: flattenColorPalette(theme('boxShadowColor')), type: ['color'] }
1971+
{ values: flattenColorPalette(theme('boxShadowColor')), type: ['color', 'any'] }
19721972
)
19731973
},
19741974

@@ -1990,7 +1990,7 @@ export let corePlugins = {
19901990
}),
19911991

19921992
outlineOffset: createUtilityPlugin('outlineOffset', [['outline-offset', ['outline-offset']]], {
1993-
type: ['length', 'number', 'percentage'],
1993+
type: ['length', 'number', 'percentage', 'any'],
19941994
supportsNegativeValues: true,
19951995
}),
19961996

@@ -2001,7 +2001,7 @@ export let corePlugins = {
20012001
return { 'outline-color': toColorValue(value) }
20022002
},
20032003
},
2004-
{ values: flattenColorPalette(theme('outlineColor')), type: ['color'] }
2004+
{ values: flattenColorPalette(theme('outlineColor')), type: ['color', 'any'] }
20052005
)
20062006
},
20072007

@@ -2081,7 +2081,7 @@ export let corePlugins = {
20812081
([modifier]) => modifier !== 'DEFAULT'
20822082
)
20832083
),
2084-
type: 'color',
2084+
type: ['color', 'any'],
20852085
}
20862086
)
20872087
},
@@ -2108,7 +2108,7 @@ export let corePlugins = {
21082108
}
21092109
},
21102110
},
2111-
{ values: flattenColorPalette(theme('ringOffsetColor')), type: 'color' }
2111+
{ values: flattenColorPalette(theme('ringOffsetColor')), type: ['color', 'any'] }
21122112
)
21132113
},
21142114

src/lib/generateRules.js

+114-47
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser'
33
import parseObjectStyles from '../util/parseObjectStyles'
44
import isPlainObject from '../util/isPlainObject'
55
import prefixSelector from '../util/prefixSelector'
6-
import { updateAllClasses } from '../util/pluginUtils'
6+
import { updateAllClasses, typeMap } from '../util/pluginUtils'
77
import log from '../util/log'
88
import * as sharedState from './sharedState'
99
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
@@ -539,68 +539,135 @@ function* resolveMatches(candidate, context, original = candidate) {
539539
}
540540

541541
if (matchesPerPlugin.length > 0) {
542-
typesByMatches.set(matchesPerPlugin, sort.options?.type)
542+
let matchingTypes = (sort.options?.types ?? [])
543+
.map(({ type }) => type)
544+
// Only track the types for this plugin that resulted in some result
545+
.filter((type) => {
546+
return Boolean(
547+
typeMap[type](modifier, sort.options, {
548+
tailwindConfig: context.tailwindConfig,
549+
})
550+
)
551+
})
552+
553+
if (matchingTypes.length > 0) {
554+
typesByMatches.set(matchesPerPlugin, matchingTypes)
555+
}
556+
543557
matches.push(matchesPerPlugin)
544558
}
545559
}
546560

547561
if (isArbitraryValue(modifier)) {
548-
// When generated arbitrary values are ambiguous, we can't know
549-
// which to pick so don't generate any utilities for them
550562
if (matches.length > 1) {
551-
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
563+
// Partition plugins in 2 categories so that we can start searching in the plugins that
564+
// don't have `any` as a type first.
565+
let [withAny, withoutAny] = matches.reduce(
566+
(group, plugin) => {
567+
let hasAnyType = plugin.some(([{ options }]) =>
568+
options.types.some(({ type }) => type === 'any')
569+
)
552570

553-
// Remove duplicates, so that we can detect proper unique types for each plugin.
554-
for (let pluginTypes of typesPerPlugin) {
555-
for (let type of pluginTypes) {
556-
let removeFromOwnGroup = false
571+
if (hasAnyType) {
572+
group[0].push(plugin)
573+
} else {
574+
group[1].push(plugin)
575+
}
576+
return group
577+
},
578+
[[], []]
579+
)
557580

558-
for (let otherGroup of typesPerPlugin) {
559-
if (pluginTypes === otherGroup) continue
581+
function findFallback(matches) {
582+
// If only a single plugin matches, let's take that one
583+
if (matches.length === 1) {
584+
return matches[0]
585+
}
560586

561-
if (otherGroup.has(type)) {
562-
otherGroup.delete(type)
563-
removeFromOwnGroup = true
587+
// Otherwise, find the plugin that creates a valid rule given the arbitrary value, and
588+
// also has the correct type which preferOnConflicts the plugin in case of clashes.
589+
return matches.find((rules) => {
590+
let matchingTypes = typesByMatches.get(rules)
591+
return rules.some(([{ options }, rule]) => {
592+
if (!isParsableNode(rule)) {
593+
return false
564594
}
565-
}
566595

567-
if (removeFromOwnGroup) pluginTypes.delete(type)
568-
}
596+
return options.types.some(
597+
({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict
598+
)
599+
})
600+
})
569601
}
570602

571-
let messages = []
572-
573-
for (let [idx, group] of typesPerPlugin.entries()) {
574-
for (let type of group) {
575-
let rules = matches[idx]
576-
.map(([, rule]) => rule)
577-
.flat()
578-
.map((rule) =>
579-
rule
580-
.toString()
581-
.split('\n')
582-
.slice(1, -1) // Remove selector and closing '}'
583-
.map((line) => line.trim())
584-
.map((x) => ` ${x}`) // Re-indent
585-
.join('\n')
586-
)
587-
.join('\n\n')
603+
// Try to find a fallback plugin, because we already know that multiple plugins matched for
604+
// the given arbitrary value.
605+
let fallback = findFallback(withoutAny) ?? findFallback(withAny)
606+
if (fallback) {
607+
matches = [fallback]
608+
}
588609

589-
messages.push(
590-
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
591-
)
592-
break
610+
// We couldn't find a fallback plugin which means that there are now multiple plugins that
611+
// generated css for the current candidate. This means that the result is ambiguous and this
612+
// should not happen. We won't generate anything right now, so let's report this to the user
613+
// by logging some options about what they can do.
614+
else {
615+
let typesPerPlugin = matches.map(
616+
(match) => new Set([...(typesByMatches.get(match) ?? [])])
617+
)
618+
619+
// Remove duplicates, so that we can detect proper unique types for each plugin.
620+
for (let pluginTypes of typesPerPlugin) {
621+
for (let type of pluginTypes) {
622+
let removeFromOwnGroup = false
623+
624+
for (let otherGroup of typesPerPlugin) {
625+
if (pluginTypes === otherGroup) continue
626+
627+
if (otherGroup.has(type)) {
628+
otherGroup.delete(type)
629+
removeFromOwnGroup = true
630+
}
631+
}
632+
633+
if (removeFromOwnGroup) pluginTypes.delete(type)
634+
}
593635
}
594-
}
595636

596-
log.warn([
597-
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
598-
...messages,
599-
`If this is content and not a class, replace it with \`${candidate
600-
.replace('[', '&lsqb;')
601-
.replace(']', '&rsqb;')}\` to silence this warning.`,
602-
])
603-
continue
637+
let messages = []
638+
639+
for (let [idx, group] of typesPerPlugin.entries()) {
640+
for (let type of group) {
641+
let rules = matches[idx]
642+
.map(([, rule]) => rule)
643+
.flat()
644+
.map((rule) =>
645+
rule
646+
.toString()
647+
.split('\n')
648+
.slice(1, -1) // Remove selector and closing '}'
649+
.map((line) => line.trim())
650+
.map((x) => ` ${x}`) // Re-indent
651+
.join('\n')
652+
)
653+
.join('\n\n')
654+
655+
messages.push(
656+
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
657+
)
658+
break
659+
}
660+
}
661+
662+
log.warn([
663+
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
664+
...messages,
665+
`If this is content and not a class, replace it with \`${candidate
666+
.replace('[', '&lsqb;')
667+
.replace(']', '&rsqb;')}\` to silence this warning.`,
668+
])
669+
continue
670+
}
604671
}
605672

606673
matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))

0 commit comments

Comments
 (0)