Skip to content

Commit

Permalink
Reject invalid custom and arbitrary variants (#8345)
Browse files Browse the repository at this point in the history
* WIP

Still need to write error message

* Update error message

first pass at something better

* Detect invalid variant formats returned by functions

* Add proper error message

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
  • Loading branch information
adamwathan and thecrypticace committed May 14, 2022
1 parent e41bf3d commit 7fa2a20
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 2 deletions.
6 changes: 5 additions & 1 deletion src/lib/generateRules.js
Expand Up @@ -9,7 +9,7 @@ import * as sharedState from './sharedState'
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
import { asClass } from '../util/nameClass'
import { normalize } from '../util/dataTypes'
import { parseVariant } from './setupContextUtils'
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
import isValidArbitraryValue from '../util/isValidArbitraryValue'
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'

Expand Down Expand Up @@ -131,6 +131,10 @@ function applyVariant(variant, matches, context) {
if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
let selector = normalize(variant.slice(1, -1))

if (!isValidVariantFormatString(selector)) {
return []
}

let fn = parseVariant(selector)

let sort = Array.from(context.variantOrder.values()).pop() << 1n
Expand Down
20 changes: 19 additions & 1 deletion src/lib/setupContextUtils.js
Expand Up @@ -170,6 +170,10 @@ function withIdentifiers(styles) {
})
}

export function isValidVariantFormatString(format) {
return format.startsWith('@') || format.includes('&')
}

export function parseVariant(variant) {
variant = variant
.replace(/\n+/g, '')
Expand Down Expand Up @@ -221,10 +225,24 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
if (typeof variantFunction !== 'string') {
// Safelist public API functions
return ({ modifySelectors, container, separator }) => {
return variantFunction({ modifySelectors, container, separator })
let result = variantFunction({ modifySelectors, container, separator })

if (typeof result === 'string' && !isValidVariantFormatString(result)) {
throw new Error(
`Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`
)
}

return result
}
}

if (!isValidVariantFormatString(variantFunction)) {
throw new Error(
`Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`
)
}

return parseVariant(variantFunction)
})

Expand Down
28 changes: 28 additions & 0 deletions tests/arbitrary-variants.test.js
Expand Up @@ -77,6 +77,34 @@ test('arbitrary variants with modifiers', () => {
})
})

test('variants without & or an at-rule are ignored', () => {
let config = {
content: [
{
raw: html`
<div class="[div]:underline"></div>
<div class="[:hover]:underline"></div>
<div class="[wtf-bbq]:underline"></div>
<div class="[lol]:hover:underline"></div>
`,
},
],
corePlugins: { preflight: false },
}

let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`

return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})

test('arbitrary variants are sorted after other variants', () => {
let config = {
content: [{ raw: html`<div class="[&>*]:underline underline lg:underline"></div>` }],
Expand Down
38 changes: 38 additions & 0 deletions tests/variants.test.js
Expand Up @@ -206,6 +206,44 @@ describe('custom advanced variants', () => {
`)
})
})

test('variant format string must include at-rule or & (1)', async () => {
let config = {
content: [
{
raw: html` <div class="wtf-bbq:text-center"></div> `,
},
],
plugins: [
function ({ addVariant }) {
addVariant('wtf-bbq', 'lol')
},
],
}

await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
"Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
)
})

test('variant format string must include at-rule or & (2)', async () => {
let config = {
content: [
{
raw: html` <div class="wtf-bbq:text-center"></div> `,
},
],
plugins: [
function ({ addVariant }) {
addVariant('wtf-bbq', () => 'lol')
},
],
}

await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
"Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
)
})
})

test('stacked peer variants', async () => {
Expand Down

0 comments on commit 7fa2a20

Please sign in to comment.