From 9f47f29984533fc75842acf70c55c99c65e03a28 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 14 Jan 2025 15:53:54 -0500 Subject: [PATCH 1/5] Discard invalid variants such as `data-checked-[selected=1]:*` --- packages/tailwindcss/src/candidate.test.ts | 45 ++++++++++++++++++++++ packages/tailwindcss/src/candidate.ts | 36 +++++++++++++---- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 848590a46a88..8114e77a5628 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -836,6 +836,39 @@ it('should not parse invalid arbitrary values', () => { } }) +it('should not parse invalid arbitrary values in variants', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.functional('data', () => {}) + + for (let candidate of [ + 'data-foo-[#0088cc]:flex', + 'data-foo[#0088cc]:flex', + + 'data-foo-[color:var(--value)]:flex', + 'data-foo[color:var(--value)]:flex', + + 'data-foo-[#0088cc]/50:flex', + 'data-foo[#0088cc]/50:flex', + + 'data-foo-[#0088cc]/[50%]:flex', + 'data-foo[#0088cc]/[50%]:flex', + + 'data-foo-[#0088cc]:flex!', + 'data-foo[#0088cc]:flex!', + + 'data-foo-[var(--value)]:flex', + 'data-foo[var(--value)]:flex', + + 'data-foo-[var(--value)]:flex!', + 'data-foo[var(--value)]:flex!', + ]) { + expect(run(candidate, { utilities, variants })).toEqual([]) + } +}) + it('should parse a utility with an implicit variable as the modifier', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -966,6 +999,18 @@ it('should parse a utility with an explicit variable as the modifier that is imp `) }) +it('should not parse a partial variant', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.static('open', () => {}) + variants.functional('data', () => {}) + + expect(run('open-:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) + expect(run('data-:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) +}) + it('should parse a static 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 6c15b2ba6163..b4af1ca7a2ac 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -413,7 +413,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // Not an arbitrary value else { roots = findRoots(baseWithoutModifier, (root: string) => { - return designSystem.utilities.has(root, 'functional') + return designSystem.utilities.has(root, 'functional') ? 'functional' : null }) } @@ -587,7 +587,7 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia if (additionalModifier) return null let roots = findRoots(variantWithoutModifier, (root) => { - return designSystem.variants.has(root) + return designSystem.variants.has(root) ? designSystem.variants.kind(root) : null }) for (let [root, value] of roots) { @@ -702,9 +702,12 @@ type Root = [ value: string | null, ] -function* findRoots(input: string, exists: (input: string) => boolean): Iterable { +function* findRoots( + input: string, + kind: (input: string) => 'static' | 'functional' | 'compound' | 'arbitrary' | null, +): Iterable { // If there is an exact match, then that's the root. - if (exists(input)) { + if (kind(input)) { yield [input, null] } @@ -714,7 +717,7 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable if (idx === -1) { // Variants starting with `@` are special because they don't need a `-` // after the `@` (E.g.: `@-lg` should be written as `@lg`). - if (input[0] === '@' && exists('@')) { + if (input[0] === '@' && kind('@')) { yield ['@', input.slice(1)] } return @@ -729,9 +732,28 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable // `bg` -> Match do { let maybeRoot = input.slice(0, idx) + let rootKind = kind(maybeRoot) + + if (rootKind) { + let value = input.slice(idx + 1) + + // Compound variants e.g. not-in-[#foo] are ultimately split like so: + // - root: not + // - value: in-[#foo] + // - root: in + // - value: [#foo] + // + // However, other variants don't have this behavior, so we can skip + // over this possible variant if the root is not a compound variant + // and it contains an arbitrary value _after_ some other value + // e.g. `supports-display-[color:red]` is invalid + if (rootKind !== 'compound') { + if (value[value.length - 1] === ']' && value[0] !== '[') { + break + } + } - if (exists(maybeRoot)) { - let root: Root = [maybeRoot, input.slice(idx + 1)] + let root: Root = [maybeRoot, value] // If the leftover value is an empty string, it means that the value is an // invalid named value, e.g.: `bg-`. This makes the candidate invalid and we From 7415a497471bc1be3c344f46ba3145f24f8a6eec Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 14 Jan 2025 16:37:39 -0500 Subject: [PATCH 2/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d38c1b1b1a..46ba7a7c30f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add missing `main` and `browser` fields for `@tailwindcss/browser` ([#15594](https://github.com/tailwindlabs/tailwindcss/pull/15594)) - _Upgrade (experimental)_: Pretty print `--spacing(…)` to prevent ambiguity ([#15596](https://github.com/tailwindlabs/tailwindcss/pull/15596)) +- Discard invalid variants such as `data-checked-[selected=1]:*` ([#15629](https://github.com/tailwindlabs/tailwindcss/pull/15629)) ## [4.0.0-beta.9] - 2025-01-09 From 7f24a5ddc3a01306134b0a73b3e92561a8735bbf Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 15 Jan 2025 06:59:22 -0500 Subject: [PATCH 3/5] Move check out of `findRoots` --- packages/tailwindcss/src/candidate.ts | 54 +++++++++++++-------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index b4af1ca7a2ac..dd47c5ebad19 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -413,7 +413,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // Not an arbitrary value else { roots = findRoots(baseWithoutModifier, (root: string) => { - return designSystem.utilities.has(root, 'functional') ? 'functional' : null + return designSystem.utilities.has(root, 'functional') }) } @@ -587,10 +587,28 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia if (additionalModifier) return null let roots = findRoots(variantWithoutModifier, (root) => { - return designSystem.variants.has(root) ? designSystem.variants.kind(root) : null + return designSystem.variants.has(root) }) for (let [root, value] of roots) { + let kind = designSystem.variants.kind(root) + + // Compound variants e.g. not-in-[#foo] are ultimately split like so: + // - root: not + // - value: in-[#foo] + // - root: in + // - value: [#foo] + // + // However, other variants don't have this behavior, so we can skip + // over this possible variant if the root is not a compound variant + // and it contains an arbitrary value _after_ some other value + // e.g. `supports-display-[color:red]` is invalid + if (value && kind !== 'compound') { + if (value[value.length - 1] === ']' && value[0] !== '[') { + continue + } + } + switch (designSystem.variants.kind(root)) { case 'static': { // Static variants do not have a value @@ -702,12 +720,9 @@ type Root = [ value: string | null, ] -function* findRoots( - input: string, - kind: (input: string) => 'static' | 'functional' | 'compound' | 'arbitrary' | null, -): Iterable { +function* findRoots(input: string, exists: (input: string) => boolean): Iterable { // If there is an exact match, then that's the root. - if (kind(input)) { + if (exists(input)) { yield [input, null] } @@ -717,7 +732,7 @@ function* findRoots( if (idx === -1) { // Variants starting with `@` are special because they don't need a `-` // after the `@` (E.g.: `@-lg` should be written as `@lg`). - if (input[0] === '@' && kind('@')) { + if (input[0] === '@' && exists('@')) { yield ['@', input.slice(1)] } return @@ -732,28 +747,9 @@ function* findRoots( // `bg` -> Match do { let maybeRoot = input.slice(0, idx) - let rootKind = kind(maybeRoot) - - if (rootKind) { - let value = input.slice(idx + 1) - - // Compound variants e.g. not-in-[#foo] are ultimately split like so: - // - root: not - // - value: in-[#foo] - // - root: in - // - value: [#foo] - // - // However, other variants don't have this behavior, so we can skip - // over this possible variant if the root is not a compound variant - // and it contains an arbitrary value _after_ some other value - // e.g. `supports-display-[color:red]` is invalid - if (rootKind !== 'compound') { - if (value[value.length - 1] === ']' && value[0] !== '[') { - break - } - } - let root: Root = [maybeRoot, value] + if (exists(maybeRoot)) { + let root: Root = [maybeRoot, input.slice(idx + 1)] // If the leftover value is an empty string, it means that the value is an // invalid named value, e.g.: `bg-`. This makes the candidate invalid and we From 6a1ae3ad42156e28a6c9d52c0e27f9e8daa54ac0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 15 Jan 2025 07:06:36 -0500 Subject: [PATCH 4/5] =?UTF-8?q?Perform=20same=20check=20for=20`(=E2=80=A6)?= =?UTF-8?q?`=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tailwindcss/src/candidate.test.ts | 24 +++++++++++++++++++ packages/tailwindcss/src/candidate.ts | 28 ++++++++-------------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 8114e77a5628..723ae0fc8e08 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -864,6 +864,30 @@ it('should not parse invalid arbitrary values in variants', () => { 'data-foo-[var(--value)]:flex!', 'data-foo[var(--value)]:flex!', + + 'data-foo-(color:--value):flex', + 'data-foo(color:--value):flex', + + 'data-foo-(color:--value)/50:flex', + 'data-foo(color:--value)/50:flex', + + 'data-foo-(color:--value)/(--mod):flex', + 'data-foo(color:--value)/(--mod):flex', + + 'data-foo-(color:--value)/(number:--mod):flex', + 'data-foo(color:--value)/(number:--mod):flex', + + 'data-foo-(--value):flex', + 'data-foo(--value):flex', + + 'data-foo-(--value)/50:flex', + 'data-foo(--value)/50:flex', + + 'data-foo-(--value)/(--mod):flex', + 'data-foo(--value)/(--mod):flex', + + 'data-foo-(--value)/(number:--mod):flex', + 'data-foo(--value)/(number:--mod):flex', ]) { expect(run(candidate, { utilities, variants })).toEqual([]) } diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index dd47c5ebad19..532bbebf80a3 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -591,24 +591,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia }) for (let [root, value] of roots) { - let kind = designSystem.variants.kind(root) - - // Compound variants e.g. not-in-[#foo] are ultimately split like so: - // - root: not - // - value: in-[#foo] - // - root: in - // - value: [#foo] - // - // However, other variants don't have this behavior, so we can skip - // over this possible variant if the root is not a compound variant - // and it contains an arbitrary value _after_ some other value - // e.g. `supports-display-[color:red]` is invalid - if (value && kind !== 'compound') { - if (value[value.length - 1] === ']' && value[0] !== '[') { - continue - } - } - switch (designSystem.variants.kind(root)) { case 'static': { // Static variants do not have a value @@ -656,6 +638,11 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } + // Discard values like `foo-[#bar]` or `foo-(#bar)` + if (value[0] !== '[' && value[value.length - 1] === ']') { + continue + } + if (value[0] === '(' && value[value.length - 1] === ')') { let arbitraryValue = decodeArbitraryValue(value.slice(1, -1)) @@ -674,6 +661,11 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } + // Discard values like `foo-[#bar]` or `foo-(#bar)` + if (value[0] !== '(' && value[value.length - 1] === ')') { + continue + } + return { kind: 'functional', root, From a93d1ca53ceaab716a74e40bfe183886d1958b6d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 15 Jan 2025 08:01:47 -0500 Subject: [PATCH 5/5] Tweak code --- packages/tailwindcss/src/candidate.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 532bbebf80a3..abb230da005d 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -620,7 +620,10 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } - if (value[0] === '[' && value[value.length - 1] === ']') { + if (value[value.length - 1] === ']') { + // Discard values like `foo-[#bar]` + if (value[0] !== '[') continue + let arbitraryValue = decodeArbitraryValue(value.slice(1, -1)) // Empty arbitrary values are invalid. E.g.: `data-[]:` @@ -638,12 +641,10 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } - // Discard values like `foo-[#bar]` or `foo-(#bar)` - if (value[0] !== '[' && value[value.length - 1] === ']') { - continue - } + if (value[value.length - 1] === ')') { + // Discard values like `foo-(--bar)` + if (value[0] !== '(') continue - if (value[0] === '(' && value[value.length - 1] === ')') { let arbitraryValue = decodeArbitraryValue(value.slice(1, -1)) // Empty arbitrary values are invalid. E.g.: `data-():` @@ -661,11 +662,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } - // Discard values like `foo-[#bar]` or `foo-(#bar)` - if (value[0] !== '(' && value[value.length - 1] === ')') { - continue - } - return { kind: 'functional', root,