Skip to content

At-rules mentioned inside CSS comments corrupt pre-processing, producing false diagnostics at wrong line numbers #1615

Description

@Cierra-Runis

What version of VS Code are you using?

1.136.0 (x64, commit 520fb30b2d3d324b4cb2342f6e88e2cd93751de1)

What version of Tailwind CSS IntelliSense are you using?

0.16.0

What version of Tailwind CSS are you using?

v4.3.0

What package manager are you using?

pnpm

What operating system are you using?

Windows 11 (10.0.26200)

Tailwind CSS Stylesheet (v4) or config file (v3)

Minimal reproduction — this file is 100% valid CSS:

/**
 * This comment mentions the @variant at-rule in prose.
 */
@theme {
  --color-brand: #000;
}
.a {
  /* A label */
  color: red;
  background: blue;
}

Describe your issue

Opening the file above produces a bogus diagnostic:

{ expected   (css-lcurlyexpected, source: tailwindcss)   at line 7, column 8

Line 7 is .a { — perfectly valid CSS. Deleting just the @ on line 2 (@variantvariant) makes the error disappear; putting it back brings it right back.

Root cause

nf() in dist/tailwindModeServer.js rewrites Tailwind at-rules into vanilla CSS before handing the document to vscode-css-languageservice. Those rewrites are plain regexes over the whole document text, with no awareness of comments:

t = t.replace(/@screen(\s+[^{]+){/g,          Id(-2))
t = t.replace(/@variants(\s+[^{]+){/g,        Id())
t = t.replace(/@responsive(\s*){/g,           Id())
t = t.replace(/@(utility)(\s+[^{]+){/g,       ua())
t = t.replace(/@(theme)(\s+[^{]*){/g,         ua())
t = t.replace(/@(custom-variant)(\s+[^;{]+);/g, ...)
t = t.replace(/@(custom-variant)(\s+[^{]+){/g, ua())
t = t.replace(/@(variant)(\s+[^{]+){/g,       ua())

Two separate defects compound here.

1. The regexes match at-rule names written inside comments, and [^{]+ spans newlines.

In the repro, @variant on line 2 matches, and [^{]+ runs greedily forward until the next { — which is the one on line 4 (@theme {, already rewritten to ._____ { by the preceding pass). So lines 2–4 are consumed as a single match, including the */ on line 3 that closes the block comment.

After pre-processing the document becomes:

/**
 * This comment mentions the ._______                              {
  --color-brand: #000;
}
.a {
  /* A label */
  color: red;
  background: blue;
}

The /** on line 1 is now unterminated, so the CSS parser treats everything as a comment until the next */ it finds — the one in /* A label */. It resumes there, hits color: red; at the top level, parses color as a selector, and reports { expected at the :.

2. ua() does not preserve the newlines it consumes, so every subsequent diagnostic is reported at the wrong line.

function ua(t=0){return(e,n,i)=>{let r="_".repeat(n.length),s=" ".repeat(i.length+t);return`.${r}${s}{`}}

The replacement is length-preserving in characters but pads with spaces only. When the match spanned N newlines, those N lines collapse into one and all positions after it shift up by N. Id() already handles this correctly:

function Id(t=0){return(e,n)=>{let i=n.split("\n");
  return i.length>1
    ? `@media(℘)${"\n".repeat(i.length-1)}${" ".repeat(i[i.length-1].length)}{`
    : `@media(℘)${" ".repeat(n.length+t)}{`}}

…but ua() (used by @utility, @theme, @custom-variant and @variant) does not, and neither does the @custom-variant …; replacement. So @screen / @variants / @responsive corrupt comments without shifting line numbers, while the ua() group corrupts comments and shifts them.

How this looks on a real file

I hit this on a 618-line stylesheet whose header comment happened to contain the phrase `@variant dark`. That single mention swallowed 15 lines (including the comment terminator), the file was handed to the parser as 604 lines, and the error surfaced at line 110, column 13 — 14 lines away from anything related, on an ordinary --ctp-base: #eff1f5; declaration. There is no realistic way to trace that back to a word in a comment 100 lines earlier.

Suggested fix

  • Skip comment ranges when applying these rewrites (tokenize, or at minimum blank out /* … */ spans before matching and restore afterwards).
  • Make every replacement newline-count-preserving, the way Id() already is, so diagnostics land on the right line even when a rewrite does span lines. Applying this to ua() and to the @custom-variant …; branch would fix the position skew independently of the comment issue.
  • Optionally, stop [^{]+ / [^;{]+ from crossing blank lines or comment boundaries, to limit the blast radius of any future mismatch.

Related

#1565 reports the user-visible symptom (errors in @theme blocks that contain comments) and a commenter there noticed that removing the @ from comments fixes it, but that issue also bundles a separate request about color swatches and doesn't identify the pre-processing step as the cause. Filing this separately with the minimal reproduction and root cause; happy to have it closed as a duplicate if you'd rather track it there.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions