From 41d9cae8e53378d16087fcf359eb785c2fd42ce4 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 8 Sep 2026 21:46:59 +0800 Subject: [PATCH] Reject candidates with multiple modifiers (#20466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `segment()` preserves empty top-level segments, but the candidate and variant parsers currently use the truthiness of the third segment to detect additional modifiers. As a result, inputs such as `bg-red-500/50/`, `bg-red-500/50//foo`, `group-hover/foo/:flex`, and `group-hover/foo//bar:flex` can be parsed as valid candidates even though they contain multiple slash modifier segments. This change checks the number of segments instead of the value of the third segment. Single modifiers such as `bg-red-500/50` and `group-hover/foo:flex` continue to parse normally, while all additional top-level `/` segments are rejected. ## Test plan - `pnpm exec vitest run packages/tailwindcss/src/candidate.test.ts --hideSkippedTests` - `pnpm exec vitest run packages/tailwindcss/src --hideSkippedTests` - `pnpm exec prettier --check packages/tailwindcss/src/candidate.ts packages/tailwindcss/src/candidate.test.ts` `pnpm --filter=tailwindcss lint` was also attempted, but currently fails on existing cross-package dependency and Bun type errors unrelated to this change. --------- Co-authored-by: Robin Malfait --- CHANGELOG.md | 1 + packages/tailwindcss/src/candidate.test.ts | 15 +++++++++++ packages/tailwindcss/src/candidate.ts | 30 ++++++++++++---------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2ac8d83..338a78eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419)) - Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420)) - Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434)) +- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index d66bfcd66..b8f914e1b 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -516,6 +516,8 @@ it('should not parse functional utilities with multiple modifiers', () => { utilities.functional('bg', () => []) expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-red-500/50/', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-red-500/50//foo', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should parse a utility with an arbitrary value', () => { @@ -1416,6 +1418,19 @@ it('should parse a functional variant with a modifier', () => { `) }) +it('should not parse variants with multiple modifiers', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.static('hover', () => {}) + variants.compound('group', Compounds.StyleRules, () => {}) + + expect(run('group-hover/foo:flex', { utilities, variants })).toHaveLength(1) + expect(run('group-hover/foo/:flex', { utilities, variants })).toEqual([]) + expect(run('group-hover/foo//bar:flex', { utilities, variants })).toEqual([]) +}) + it('should parse a functional variant starting with @', () => { let utilities = new Utilities() utilities.static('flex', () => []) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index b63588ce7..b4a5988e0 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -372,6 +372,14 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter } } + // If there's more than one modifier, the utility is invalid. + // + // E.g.: + // + // - `bg-red-500/50/50` + let parts = segment(base, '/') + if (parts.length > 2) return + // Figure out the new base and the modifier segment if present. // // E.g.: @@ -381,14 +389,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // ^^^^^^^^^^ -> Base without modifier // ^^ -> Modifier segment // ``` - let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/') - - // If there's more than one modifier, the utility is invalid. - // - // E.g.: - // - // - `bg-red-500/50/50` - if (additionalModifier) return + let [baseWithoutModifier, modifierSegment = null] = parts let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment) @@ -707,17 +708,18 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia // Static, functional and compound variants { - // group-hover/group-name - // ^^^^^^^^^^^ -> Variant without modifier - // ^^^^^^^^^^ -> Modifier - let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/') - // If there's more than one modifier, the variant is invalid. // // E.g.: // // - `group-hover/foo/bar` - if (additionalModifier) return null + let parts = segment(variant, '/') + if (parts.length > 2) return null + + // group-hover/group-name + // ^^^^^^^^^^^ -> Variant without modifier + // ^^^^^^^^^^ -> Modifier + let [variantWithoutModifier, modifier = null] = parts let roots = findRoots(variantWithoutModifier, (root) => { return designSystem.variants.has(root)