Skip to content

Commit

Permalink
Discard invalid declarations when parsing CSS (#16093)
Browse files Browse the repository at this point in the history
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 <malfait.robin@gmail.com>
  • Loading branch information
thecrypticace and RobinMalfait authored Jan 31, 2025
1 parent 9572202 commit 35a5e8c
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Vite: Transform `<style>` 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

Expand Down
56 changes: 56 additions & 0 deletions packages/tailwindcss/src/css-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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]`,
)
})
})
})
18 changes: 16 additions & 2 deletions packages/tailwindcss/src/css-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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(),
Expand Down

0 comments on commit 35a5e8c

Please sign in to comment.