diff --git a/CHANGELOG.md b/CHANGELOG.md index ff0ec536b880..114412e42d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vite: Transform `` blocks in HTML files ([#16069](https://github.com/tailwindlabs/tailwindcss/pull/16069)) - Prevent camelCasing CSS custom properties added by JavaScript plugins ([#16103](https://github.com/tailwindlabs/tailwindcss/pull/16103)) - Do not emit `@keyframes` in `@theme reference` ([#16120](https://github.com/tailwindlabs/tailwindcss/pull/16120)) +- Discard invalid declarations when parsing CSS ([#16093](https://github.com/tailwindlabs/tailwindcss/pull/16093)) ## [4.0.1] - 2025-01-29 diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index a4b123b28fde..25d5b3a454ca 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -329,6 +329,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ]) }) + it('should parse a custom property with an empty value', () => { + expect(parse('--foo:;')).toEqual([ + { + kind: 'declaration', + property: '--foo', + value: '', + important: false, + }, + ]) + }) + + it('should parse a custom property with a space value', () => { + expect(parse('--foo: ;')).toEqual([ + { + kind: 'declaration', + property: '--foo', + value: '', + important: false, + }, + ]) + }) + it('should parse a custom property with a block including nested "css"', () => { expect( parse(css` @@ -1097,5 +1119,39 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { `), ).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`) }) + + it('should error when incomplete custom properties are used', () => { + expect(() => parse('--foo')).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid custom property, expected a value]`, + ) + }) + + it('should error when incomplete custom properties are used inside rules', () => { + expect(() => parse('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid custom property, expected a value]`, + ) + }) + + it('should error when a declaration is incomplete', () => { + expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid declaration: \`bar\`]`, + ) + }) + + it('should error when a semicolon exists after an at-rule with a body', () => { + expect(() => parse('@plugin "foo" {} ;')).toThrowErrorMatchingInlineSnapshot( + `[Error: Unexpected semicolon]`, + ) + }) + + it('should error when consecutive semicolons exist', () => { + expect(() => parse(';;;')).toThrowErrorMatchingInlineSnapshot(`[Error: Unexpected semicolon]`) + }) + + it('should error when consecutive semicolons exist after a declaration', () => { + expect(() => parse('.foo { color: red;;; }')).toThrowErrorMatchingInlineSnapshot( + `[Error: Unexpected semicolon]`, + ) + }) }) }) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index c12c63051cee..d80bb3e79807 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -286,6 +286,8 @@ export function parse(input: string) { } let declaration = parseDeclaration(buffer, colonIdx) + if (!declaration) throw new Error(`Invalid custom property, expected a value`) + if (parent) { parent.nodes.push(declaration) } else { @@ -337,6 +339,11 @@ export function parse(input: string) { closingBracketStack[closingBracketStack.length - 1] !== ')' ) { let declaration = parseDeclaration(buffer) + if (!declaration) { + if (buffer.length === 0) throw new Error('Unexpected semicolon') + throw new Error(`Invalid declaration: \`${buffer.trim()}\``) + } + if (parent) { parent.nodes.push(declaration) } else { @@ -435,7 +442,10 @@ export function parse(input: string) { // Attach the declaration to the parent. if (parent) { - parent.nodes.push(parseDeclaration(buffer, colonIdx)) + let node = parseDeclaration(buffer, colonIdx) + if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``) + + parent.nodes.push(node) } } } @@ -543,7 +553,11 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule { return atRule(buffer.trim(), '', nodes) } -function parseDeclaration(buffer: string, colonIdx: number = buffer.indexOf(':')): Declaration { +function parseDeclaration( + buffer: string, + colonIdx: number = buffer.indexOf(':'), +): Declaration | null { + if (colonIdx === -1) return null let importantIdx = buffer.indexOf('!important', colonIdx + 1) return decl( buffer.slice(0, colonIdx).trim(),