From 7fa2a200b2349b24e9f603e92300a032db3d2103 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 14 May 2022 13:58:02 -0400 Subject: [PATCH] Reject invalid custom and arbitrary variants (#8345) * 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 --- src/lib/generateRules.js | 6 ++++- src/lib/setupContextUtils.js | 20 ++++++++++++++++- tests/arbitrary-variants.test.js | 28 +++++++++++++++++++++++ tests/variants.test.js | 38 ++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index a14f7163a076..ccba747d9020 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -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' @@ -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 diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 573341a6c488..488b8f975307 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -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, '') @@ -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) }) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 45dbf70c5a34..ffa4a70732d4 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -77,6 +77,34 @@ test('arbitrary variants with modifiers', () => { }) }) +test('variants without & or an at-rule are ignored', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+ `, + }, + ], + 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`
` }], diff --git a/tests/variants.test.js b/tests/variants.test.js index 2689c06cde01..c3031fd11d67 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -206,6 +206,44 @@ describe('custom advanced variants', () => { `) }) }) + + test('variant format string must include at-rule or & (1)', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + 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`
`, + }, + ], + 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 () => {