Skip to content

Commit 35a5e8c

Browse files
Discard invalid declarations when parsing CSS (#16093)
I discovered this when triaging an error someone had on Tailwind Play. 1. When we see a `;` we often assume a valid declaration precedes it but that may not be the case 2. When we see the name of a custom property we assume everything that follows will be a valid declaration but that is not necessarily the case 3. A bare identifier inside of a rule is treated as a declaration which is not the case This PR fixes all three of these by ignoring these invalid cases. Though some should probably be turned into errors. --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent 9572202 commit 35a5e8c

File tree

3 files changed

+73
-2
lines changed

3 files changed

+73
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Vite: Transform `<style>` blocks in HTML files ([#16069](https://github.com/tailwindlabs/tailwindcss/pull/16069))
2020
- Prevent camelCasing CSS custom properties added by JavaScript plugins ([#16103](https://github.com/tailwindlabs/tailwindcss/pull/16103))
2121
- Do not emit `@keyframes` in `@theme reference` ([#16120](https://github.com/tailwindlabs/tailwindcss/pull/16120))
22+
- Discard invalid declarations when parsing CSS ([#16093](https://github.com/tailwindlabs/tailwindcss/pull/16093))
2223

2324
## [4.0.1] - 2025-01-29
2425

packages/tailwindcss/src/css-parser.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
329329
])
330330
})
331331

332+
it('should parse a custom property with an empty value', () => {
333+
expect(parse('--foo:;')).toEqual([
334+
{
335+
kind: 'declaration',
336+
property: '--foo',
337+
value: '',
338+
important: false,
339+
},
340+
])
341+
})
342+
343+
it('should parse a custom property with a space value', () => {
344+
expect(parse('--foo: ;')).toEqual([
345+
{
346+
kind: 'declaration',
347+
property: '--foo',
348+
value: '',
349+
important: false,
350+
},
351+
])
352+
})
353+
332354
it('should parse a custom property with a block including nested "css"', () => {
333355
expect(
334356
parse(css`
@@ -1097,5 +1119,39 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
10971119
`),
10981120
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`)
10991121
})
1122+
1123+
it('should error when incomplete custom properties are used', () => {
1124+
expect(() => parse('--foo')).toThrowErrorMatchingInlineSnapshot(
1125+
`[Error: Invalid custom property, expected a value]`,
1126+
)
1127+
})
1128+
1129+
it('should error when incomplete custom properties are used inside rules', () => {
1130+
expect(() => parse('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot(
1131+
`[Error: Invalid custom property, expected a value]`,
1132+
)
1133+
})
1134+
1135+
it('should error when a declaration is incomplete', () => {
1136+
expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot(
1137+
`[Error: Invalid declaration: \`bar\`]`,
1138+
)
1139+
})
1140+
1141+
it('should error when a semicolon exists after an at-rule with a body', () => {
1142+
expect(() => parse('@plugin "foo" {} ;')).toThrowErrorMatchingInlineSnapshot(
1143+
`[Error: Unexpected semicolon]`,
1144+
)
1145+
})
1146+
1147+
it('should error when consecutive semicolons exist', () => {
1148+
expect(() => parse(';;;')).toThrowErrorMatchingInlineSnapshot(`[Error: Unexpected semicolon]`)
1149+
})
1150+
1151+
it('should error when consecutive semicolons exist after a declaration', () => {
1152+
expect(() => parse('.foo { color: red;;; }')).toThrowErrorMatchingInlineSnapshot(
1153+
`[Error: Unexpected semicolon]`,
1154+
)
1155+
})
11001156
})
11011157
})

packages/tailwindcss/src/css-parser.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ export function parse(input: string) {
286286
}
287287

288288
let declaration = parseDeclaration(buffer, colonIdx)
289+
if (!declaration) throw new Error(`Invalid custom property, expected a value`)
290+
289291
if (parent) {
290292
parent.nodes.push(declaration)
291293
} else {
@@ -337,6 +339,11 @@ export function parse(input: string) {
337339
closingBracketStack[closingBracketStack.length - 1] !== ')'
338340
) {
339341
let declaration = parseDeclaration(buffer)
342+
if (!declaration) {
343+
if (buffer.length === 0) throw new Error('Unexpected semicolon')
344+
throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
345+
}
346+
340347
if (parent) {
341348
parent.nodes.push(declaration)
342349
} else {
@@ -435,7 +442,10 @@ export function parse(input: string) {
435442

436443
// Attach the declaration to the parent.
437444
if (parent) {
438-
parent.nodes.push(parseDeclaration(buffer, colonIdx))
445+
let node = parseDeclaration(buffer, colonIdx)
446+
if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
447+
448+
parent.nodes.push(node)
439449
}
440450
}
441451
}
@@ -543,7 +553,11 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
543553
return atRule(buffer.trim(), '', nodes)
544554
}
545555

546-
function parseDeclaration(buffer: string, colonIdx: number = buffer.indexOf(':')): Declaration {
556+
function parseDeclaration(
557+
buffer: string,
558+
colonIdx: number = buffer.indexOf(':'),
559+
): Declaration | null {
560+
if (colonIdx === -1) return null
547561
let importantIdx = buffer.indexOf('!important', colonIdx + 1)
548562
return decl(
549563
buffer.slice(0, colonIdx).trim(),

0 commit comments

Comments
 (0)