Skip to content

Commit 11bfa0a

Browse files
authored
Detect ambiguity in arbitrary values (tailwindlabs#5634)
* detect ambiguity in arbitrary values * update warning message
1 parent ef91875 commit 11bfa0a

File tree

2 files changed

+92
-3
lines changed

2 files changed

+92
-3
lines changed

src/lib/generateRules.js

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import parseObjectStyles from '../util/parseObjectStyles'
44
import isPlainObject from '../util/isPlainObject'
55
import prefixSelector from '../util/prefixSelector'
66
import { updateAllClasses } from '../util/pluginUtils'
7+
import log from '../util/log'
78

89
let classNameParser = selectorParser((selectors) => {
910
return selectors.first.filter(({ type }) => type === 'class').pop().value
@@ -225,15 +226,19 @@ function* resolveMatches(candidate, context) {
225226

226227
for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) {
227228
let matches = []
229+
let typesByMatches = new Map()
230+
228231
let [plugins, modifier] = matchedPlugins
229232
let isOnlyPlugin = plugins.length === 1
230233

231234
for (let [sort, plugin] of plugins) {
235+
let matchesPerPlugin = []
236+
232237
if (typeof plugin === 'function') {
233238
for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) {
234239
let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
235240
for (let rule of rules) {
236-
matches.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
241+
matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
237242
}
238243
}
239244
}
@@ -242,12 +247,80 @@ function* resolveMatches(candidate, context) {
242247
let ruleSet = plugin
243248
let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
244249
for (let rule of rules) {
245-
matches.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
250+
matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
246251
}
247252
}
253+
254+
if (matchesPerPlugin.length > 0) {
255+
typesByMatches.set(matchesPerPlugin, sort.options?.type)
256+
matches.push(matchesPerPlugin)
257+
}
248258
}
249259

250-
matches = applyPrefix(matches, context)
260+
// Only keep the result of the very first plugin if we are dealing with
261+
// arbitrary values, to protect against ambiguity.
262+
if (isArbitraryValue(modifier) && matches.length > 1) {
263+
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
264+
265+
// Remove duplicates, so that we can detect proper unique types for each plugin.
266+
for (let pluginTypes of typesPerPlugin) {
267+
for (let type of pluginTypes) {
268+
let removeFromOwnGroup = false
269+
270+
for (let otherGroup of typesPerPlugin) {
271+
if (pluginTypes === otherGroup) continue
272+
273+
if (otherGroup.has(type)) {
274+
otherGroup.delete(type)
275+
removeFromOwnGroup = true
276+
}
277+
}
278+
279+
if (removeFromOwnGroup) pluginTypes.delete(type)
280+
}
281+
}
282+
283+
let messages = []
284+
285+
for (let [idx, group] of typesPerPlugin.entries()) {
286+
for (let type of group) {
287+
let rules = matches[idx]
288+
.map(([, rule]) => rule)
289+
.flat()
290+
.map((rule) =>
291+
rule
292+
.toString()
293+
.split('\n')
294+
.slice(1, -1) // Remove selector and closing '}'
295+
.map((line) => line.trim())
296+
.map((x) => ` ${x}`) // Re-indent
297+
.join('\n')
298+
)
299+
.join('\n\n')
300+
301+
messages.push(
302+
` - Replace "${candidate}" with "${candidate.replace(
303+
'[',
304+
`[${type}:`
305+
)}" for:\n${rules}\n`
306+
)
307+
break
308+
}
309+
}
310+
311+
log.warn([
312+
// TODO: Update URL
313+
`The class "${candidate}" is ambiguous and matches multiple utilities. Use a type hint (https://tailwindcss.com/docs/just-in-time-mode#ambiguous-values) to fix this.`,
314+
'',
315+
...messages,
316+
`If this is just part of your content and not a class, replace it with "${candidate
317+
.replace('[', '[')
318+
.replace(']', ']')}" to silence this warning.`,
319+
])
320+
continue
321+
}
322+
323+
matches = applyPrefix(matches.flat(), context)
251324

252325
if (important) {
253326
matches = applyImportant(matches, context)
@@ -317,4 +390,8 @@ function generateRules(candidates, context) {
317390
})
318391
}
319392

393+
function isArbitraryValue(input) {
394+
return input.startsWith('[') && input.endsWith(']')
395+
}
396+
320397
export { resolveMatches, generateRules }

tests/arbitrary-values.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,15 @@ it('should not convert escaped underscores with spaces', () => {
197197
`)
198198
})
199199
})
200+
201+
it('should warn and not generate if arbitrary values are ambigu', () => {
202+
// If we don't protect against this, then `bg-[200px_100px]` would both
203+
// generate the background-size as well as the background-position utilities.
204+
let config = {
205+
content: [{ raw: html`<div class="bg-[200px_100px]"></div>` }],
206+
}
207+
208+
return run('@tailwind utilities', config).then((result) => {
209+
return expect(result.css).toMatchFormattedCss(css``)
210+
})
211+
})

0 commit comments

Comments
 (0)