Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reject invalid custom and arbitrary variants #8345

Merged
merged 4 commits into from May 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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