Skip to content

Do not terminate a hack-prefixed property before a comment - #2126

Merged
ai merged 2 commits into
postcss:mainfrom
Jaybhade:fix/hack-prefixed-custom-property-semicolon
Aug 6, 2026
Merged

Do not terminate a hack-prefixed property before a comment#2126
ai merged 2 commits into
postcss:mainfrom
Jaybhade:fix/hack-prefixed-custom-property-semicolon

Conversation

@Jaybhade

@Jaybhade Jaybhade commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PostCSS aims for byte-to-byte equal output, but since 8.5.22 the stringifier invents a semicolon in a declaration list:

postcss.parse('a{*--x:red/*c*/}').toString()
// => 'a{*--x:red;/*c*/}'   (8.5.21 and earlier: unchanged)

_--x behaves the same. The output stays valid CSS and re-parses to the same AST, so nothing breaks downstream — but a tool that only means to touch what it edits now emits an unrelated ;.

Why

The parser decides whether a declaration is a custom property from the first token of the declaration:

let customProperty = start[1].startsWith('--')   // lib/parser.js, other()

#2117 re-derived that decision in the stringifier from prop:

child.type === 'decl' && child.prop.startsWith('--')

Those two agree almost always, but decl() strips the */_ hack prefix out of prop and into raws.before after the parser has made its choice:

if (node.prop[0] === '_' || node.prop[0] === '*') {
  node.raws.before += node.prop[0]
  node.prop = node.prop.slice(1)
}

So *--x:red is parsed as a normal declaration — its value stops at the first comment, and the comment is already a separate node — while the stringifier sees prop === '--x' and terminates it. The semicolon #2117 adds is there to stop a custom property's value from swallowing a following comment; here there is nothing to protect, because *--x:red will not re-parse as a custom property either. The same mismatch happens whenever anything but spaces precedes the property, since decl() collects those tokens into before too (a{'s'--x:1/*c*/}).

Fix

Check before alongside prop, so the stringifier asks the same question the parser did — will this re-parse as a custom property? — and terminates only the declarations that need it.

Tests

  • keeps hack-prefixed property before a comment unchanged*--x / _--x round-trip byte-for-byte, and still re-parse to decl,comment.
  • terminates indented custom property followed by a comment — a whitespace-only before is still a custom property and still gets its semicolon, so the narrowing can't be over-read as before === ''.

The two tests from #2115/#2117 pass unchanged. pnpm unit is 683/683, types and size-limit pass, and lint is clean apart from the pre-existing RootExit sort warning on main.

I also re-ran a round-trip check over ~320k generated stylesheets (parse(css).toString() === css, plus AST-shape stability across a re-parse) at both root level and nested in a rule/at-rule: every failure it reported was this one cause, and after the change it reports none.

A declaration is read as a custom property when the *first token* of the
declaration starts with `--`, but the stringifier decided it from `prop`.
The `*`/`_` hack prefix is moved out of `prop` into `raws.before` after that
decision, so for `*--x:red` the parser builds a normal declaration -- whose
value stops at the first comment -- while the stringifier saw a custom
property and terminated it:

  postcss.parse('a{*--x:red/*c*/}').toString()
  // => 'a{*--x:red;/*c*/}', semicolon invented

The same applies to any declaration with something other than spaces in
`before`. Check `before` alongside `prop` so the two stay in sync: only a
declaration that will re-parse as a custom property can swallow a following
comment, and only that one needs the semicolon.
Comment thread lib/stringifier.js Outdated
// is moved from `prop` into `before` after that decision, so `*--x` is a normal
// declaration that stops at the first comment. Re-checking `before` here keeps
// this in sync: with anything but spaces in front of the property, the output
// will not re-parse as a custom property and needs no terminating semicolon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice hack. Just ask LLM to be very minimalistic in comment.

Nowadays be smart is mean use less words.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cut to two lines in dcec52b.

@ai
ai merged commit 8f37847 into postcss:main Aug 6, 2026
10 checks passed
@ai

ai commented Aug 6, 2026

Copy link
Copy Markdown
Member

Thanks

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants