Discard a bare semicolon inside a block's contents - #193
Merged
TylerBrinks merged 1 commit intoJul 23, 2026
Merged
Conversation
"Consume a block's contents" lists <semicolon-token> alongside <whitespace-token> with the action "Discard a token from input" (CSS Syntax 3 5.5.5). FillRules passed it on to CreateStyle instead, which hands it to SelectorConstructor, which has no recovery for an unexpected semicolon: it folds the token into the next rule's selector and invalidates it. Worse, the run-on rule keeps consuming, so the block's own closing brace is swallowed and the following sibling rule is lost too: @media screen { .a { color: red; } ; } @media print { .b { color: blue; } } /* disappeared */ Deliberately not applied to CreateRules. "Consume a stylesheet's contents" (5.5.1) has no <semicolon-token> case, so a stray semicolon at the top level falls to "anything else" and is consumed into the next qualified rule's prelude, invalidating it - only a block passes <semicolon-token> as the stop token to "consume a qualified rule" (5.5.3). A test pins that asymmetry so it is not "fixed" later by mistake. CssParseSheetWithAtAndCommentDoesNotTakeForever3 covers exactly the swallowing case above and asserted the count it produced at the time, 1. Its source has two @media rules, so the spec-correct count is 2; updated, with the timeout guard the test exists for left intact.
jhaygood86
force-pushed
the
bugfix/top-level-empty-statement
branch
from
July 22, 2026 21:46
6cac6b8 to
47ee682
Compare
jhaygood86
marked this pull request as ready for review
July 22, 2026 21:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A bare
;inside a grouping rule's block invalidates the next rule and swallows the block's closing brace, taking the following sibling rule with it:FillRuleshands the semicolon toCreateRule→CreateStyle→SelectorConstructor, which has no recovery for an unexpected semicolon. It folds the token into the next rule's selector and invalidates it; the run-on rule then keeps consuming past the block's}.What the spec says
§5.5.5 "Consume a block's contents" lists
<semicolon-token>together with<whitespace-token>:So inside a block a stray semicolon is simply thrown away, exactly like whitespace.
Fix
Discard the token in
FillRules, which is ExCSS's implementation of that algorithm (it backs@media,@supportsand@container).Why the top level is excluded
The asymmetry is intentional and is the part I originally got wrong.
§5.5.1 "Consume a stylesheet's contents" has cases for
<whitespace-token>,<EOF-token>,<CDO-token>/<CDC-token>and<at-keyword-token>, then "anything else → Consume a qualified rule". There is no<semicolon-token>case, so a top-level;falls into "anything else".§5.5.3 "Consume a qualified rule" only treats a semicolon specially when it is the stop token, and a stop token is passed in exactly one place — from §5.5.5,
"consume a qualified rule from input, with nested set to true, and <semicolon-token> as the stop token". At the top level none is passed, so the;is consumed as a component value into the prelude, leaving an invalid selector and an invalid rule.That is what ExCSS already does, so the top level is left untouched. A test now pins it so it doesn't get "fixed" by mistake later.
Acid2 agrees, and I had it backwards the first time round. Its parser-torture block is a run of deliberately-dropped rules, and the trailing
;is the mechanism for one of them:One existing assertion changed
CssParseSheetWithAtAndCommentDoesNotTakeForever3is exactly the swallowing case above. Its source contains two@mediarules and it assertedAssert.Equal(1, sheet.Rules.Length)— the count the old behaviour produced. The spec-correct count is 2, so the assertion is updated and the second rule's type is now asserted too. The[Fact(Timeout = 1000)]guard the test exists for is untouched.This is the only pre-existing assertion this PR (or any of my others) modifies. Flagging it explicitly since it is a behaviour change, not just an addition.
Tests
5 new tests in
Sheet.cs: a semicolon between rules in a block, the swallowed-sibling case, repeated semicolons,@supportsas well as@media, and the top-level case asserting the spec-correct invalidating behaviour.Against
master, the 4 block tests fail plus the updated one; the top-level test passes onmasterunchanged, confirming it documents existing behaviour. The other 1262 existing tests stay green, and all seven target frameworks build with no new warnings.