Don't treat \ as an escape inside CSS comments (#20508)

<!--

👋 Hey, thanks for your interest in contributing to Tailwind!

**Please ask first before starting work on any significant new
features.**

It's never a fun experience to have your pull request declined after
investing a lot of time and effort into a new feature. To avoid this
from happening, we request that contributors create a discussion to
first discuss any significant new features.

For more info, check out the contributing guide:


https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md

-->

## Summary

<!--

Provide a summary of the issue and the changes you're making. How does
your change solve the problem?

-->

The CSS parser treated `\` inside comments as an escape and skipped the
character after it. Because of that, a comment ending in `\*/` was never
closed, and the CSS that followed was swallowed or turned into a broken
rule:

```css
/* C:\temp\*/
.a { color: red }
```

Before this change the `.a` rule disappeared entirely, and `/* \*/ .a {
color: red }` produced the selector `* \*/ .a`. The same thing happened
to comments inside declaration values, where the comment ran on until
the next `*/` it could find, pulling following declarations into the
value.

Per [CSS Syntax Level 3
§4.3.2](https://www.w3.org/TR/css-syntax-3/#consume-comment), a comment
ends at the first `*/` and escapes aren't processed inside comments.
This removes the backslash handling from both comment-scanning loops in
`css-parser.ts` (top-level and inside declaration values).

The existing test `/*Hello, \*\/ world!*/` keeps passing, since that
input contains no `*/` before the final one.

## Test plan

<!--

Explain how you tested your changes. Include the exact commands that you
used to verify the change works and include screenshots/screen
recordings of the update behavior in the browser if applicable.

-->

Added two tests to `packages/tailwindcss/src/css-parser.test.ts` (both
run with Unix and Windows line endings):

- a top-level comment ending in `\*/` followed by a rule
- a comment ending in `\*/` inside a custom property value, followed by
another declaration

Both fail without the change to `css-parser.ts` and pass with it.

```sh
pnpm vitest run packages/tailwindcss/src/css-parser.test.ts
pnpm vitest run --project tailwindcss
pnpm run lint
```

---------

Co-authored-by: Robin Malfait <robin.malfait@shopify.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
한국 2026-09-25 20:16:58 +09:00 • committed by GitHub
parent 86f3daf912
commit 722acdfebf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 54 additions and 13 deletions

View file

@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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))
- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508))
## [4.3.3] - 2026-07-16

View file

@ -31,6 +31,23 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
).toEqual([])
})
it('should end a comment at `*/` even when it is preceded by a `\\`', () => {
expect(
parse(css`
/* C:\temp\*/
.foo {
color: red;
}
`),
).toEqual([
{
kind: 'rule',
selector: '.foo',
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
},
])
})
it('should parse a comment inside of a selector and ignore it', () => {
expect(
parse(css`
@ -448,6 +465,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
])
})
it('should end a comment in a custom property at `*/` even when it is preceded by a `\\`', () => {
expect(
parse(css`
--foo: /* C:\temp\*/ bar;
--bar: /* baz */ qux;
`),
).toEqual([
{
kind: 'declaration',
property: '--foo',
value: '/* C:\\temp\\*/ bar',
important: false,
},
{
kind: 'declaration',
property: '--bar',
value: '/* baz */ qux',
important: false,
},
])
})
it('should parse empty custom properties', () => {
expect(
parse(css`

View file

@ -128,19 +128,24 @@ export function parse(input: string, opts?: ParseOptions) {
// ^^^^^^^^^^^^^
// }
// ```
//
// The escape character `\` in comments are ignored, this means that a
// the end of a comment preceded by `\` does _not_ mean that the end of the
// comment is escaped and therefore we have to keep parsing.
//
// This is a valid comment:
// ```
// /*C:\*/
// ```
// See: https://www.w3.org/TR/css-syntax-3/#consume-comment
else if (currentChar === SLASH && input.charCodeAt(i + 1) === ASTERISK) {
let start = i
for (let j = i + 2; j < input.length; j++) {
peekChar = input.charCodeAt(j)
// Current character is a `\` therefore the next character is escaped.
if (peekChar === BACKSLASH) {
j += 1
}
// End of the comment
else if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) {
// End of the comment.
if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) {
i = j + 1
break
}
@ -224,13 +229,9 @@ export function parse(input: string, opts?: ParseOptions) {
else if (peekChar === SLASH && input.charCodeAt(j + 1) === ASTERISK) {
for (let k = j + 2; k < input.length; k++) {
peekChar = input.charCodeAt(k)
// Current character is a `\` therefore the next character is escaped.
if (peekChar === BACKSLASH) {
k += 1
}
// End of the comment
else if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) {
// End of the comment.
if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) {
j = k + 1
break
}