Skip to content

feat(css): support scss nesting declarations in declaration lists#9135

Merged
denbezrukov merged 2 commits intonextfrom
db/scss-nesting-declarations-mr
Feb 18, 2026
Merged

feat(css): support scss nesting declarations in declaration lists#9135
denbezrukov merged 2 commits intonextfrom
db/scss-nesting-declarations-mr

Conversation

@denbezrukov
Copy link
Contributor

Summary

  • Adds support for SCSS nesting declarations in declaration lists and declaration-or-rule lists.
  • Implements Sass-compatible disambiguation between nested declarations and nested qualified
    rules.
  • Uses whitespace after : as an ambiguity signal because Sass does:
    • no whitespace after colon (font:bold, font::before) can be selector-like and is
      speculatively parsed as a nested rule (strict selector parse, recovery disabled),
    • whitespace/newline after colon (font: bold, font:\n bold) is treated as a nested
      declaration/value.

Test Plan

  • cargo test -p biome_css_parser
  • cargo test -p biome_css_formatter

This PR was created with AI assistance (Codex).

@changeset-bot
Copy link

changeset-bot bot commented Feb 18, 2026

⚠️ No Changeset found

Latest commit: 644c079

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions bot added A-Linter Area: linter A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-JavaScript Language: JavaScript and super languages L-CSS Language: CSS L-HTML Language: HTML and super languages L-Grit Language: GritQL L-Tailwind Language: Tailwind CSS labels Feb 18, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 18, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 95 skipped benchmarks1


Comparing db/scss-nesting-declarations-mr (644c079) with next (a1976dd)

Open in CodSpeed

Footnotes

  1. 95 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Walkthrough

This pull request introduces support for SCSS nesting declarations throughout the Biome CSS toolchain. It adds a new ScssNestingDeclaration AST node defined in the grammar, implements parsing logic with detection and recovery mechanisms for SCSS nested property syntax, adds formatting support for the new node type across multiple formatter matchers, updates the CSS lexer with whitespace-tracking state, and propagates after_whitespace field additions across other lexers.

Possibly related PRs

Suggested labels

A-Parser, A-Formatter, A-Tooling, L-CSS, L-SCSS

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding support for SCSS nesting declarations in declaration lists.
Description check ✅ Passed The description is directly related to the changeset, explaining the feature, implementation approach (whitespace-based disambiguation), and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch db/scss-nesting-declarations-mr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (8)
crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs (1)

246-246: LGTM — UnknownKind is a safe choice; NestedRuleOrAtRule is worth considering.

Consistent with the ScssDeclaration arm immediately above. Mapping to UnknownKind is conservative and risk-free: ties are broken by source position, so relative order of SCSS nesting declarations is preserved.

One optional thought: a SCSS nesting declaration (font: { size: 10px; }) is semantically a nested block, so NestedRuleOrAtRule could be argued as the more precise bucket — sorting it alongside .child {} rather than after it. Up to you whether that matters for the intended UX.

♻️ Alternative: treat as a nested rule rather than unknown
-            AnyCssDeclarationOrRule::ScssNestingDeclaration(_) => NodeKindOrder::UnknownKind,
+            AnyCssDeclarationOrRule::ScssNestingDeclaration(_) => NodeKindOrder::NestedRuleOrAtRule,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs` at line
246, The match arm mapping AnyCssDeclarationOrRule::ScssNestingDeclaration
currently returns NodeKindOrder::UnknownKind; if you prefer nested SCSS blocks
to sort with rules rather than after them, change that arm to return
NodeKindOrder::NestedRuleOrAtRule instead (update the match in
use_sorted_properties.rs where ScssNestingDeclaration is handled). Ensure the
symbol names AnyCssDeclarationOrRule::ScssNestingDeclaration and
NodeKindOrder::NestedRuleOrAtRule are used so the declaration is categorized as
a nested rule.
crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss (1)

1-6: Missing test case for the double-colon (::) pseudo-element path.

The PR description calls out font::before as a canonical example of the "no-whitespace" disambiguation — it should be parsed as a nested selector, not a nesting declaration. But no test here exercises that code path.

Consider adding:

+font::before {
+  content: "";
+}

inside .test to confirm that a double-colon pseudo-element is correctly recognised as a nested rule rather than silently misclassified as a nesting declaration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss`
around lines 1 - 6, Add a test line inside the .test block that exercises the
double-colon pseudo-element disambiguation (e.g., use
"font::before{color:green;}" or similar) so the parser treats "font::before" as
a nested selector rather than a nesting declaration; update the test file's
.test block alongside existing cases (selectors like "label:hover", "font:bold",
"font:12px", "font: bold") to include the "font::before" nested rule to verify
the no-whitespace double-colon path is handled.
crates/biome_css_parser/tests/quick_test.rs (1)

22-23: Unrelated options in quick test.

allow_metavariables() and allow_tailwind_directives() are not exercised by the current snippet. Trim them so the test stays focused on what's actually being debugged.

✂️ Suggested trim
     CssParserOptions::default()
         .allow_wrong_line_comments()
         .allow_css_modules()
-        .allow_metavariables()
-        .allow_tailwind_directives(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/tests/quick_test.rs` around lines 22 - 23, The test
config is enabling unrelated options allow_metavariables() and
allow_tailwind_directives() that aren’t used by the snippet; remove those method
calls from the parser/config builder in quick_test.rs (i.e., drop
allow_metavariables() and allow_tailwind_directives() from the chained
configuration) so the test only sets options relevant to the behavior being
exercised.
crates/biome_css_parser/src/syntax/declaration.rs (1)

25-31: The parse_exclusive_syntax error callback is unreachable.

is_at_scss_nesting_declaration already returns false when CssSyntaxFeatures::Scss is not supported, so this branch is only entered when SCSS is supported — meaning the scss_only_syntax_error callback can never fire.

This is the opposite design to is_at_scss_declaration (which deliberately omits the feature check, letting parse_exclusive_syntax handle the non-SCSS case). Since the feature check must stay inside is_at_scss_nesting_declaration (to prevent false positives on regular CSS identifier + : tokens), you could simplify parse_element to call parse_scss_nesting_declaration(p) directly:

♻️ Suggested simplification
 fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax {
     if is_at_scss_nesting_declaration(p) {
-        CssSyntaxFeatures::Scss.parse_exclusive_syntax(
-            p,
-            parse_scss_nesting_declaration,
-            |p, marker| scss_only_syntax_error(p, "SCSS nesting declarations", marker.range(p)),
-        )
+        parse_scss_nesting_declaration(p)
     } else {
         parse_any_declaration_with_semicolon(p)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/syntax/declaration.rs` around lines 25 - 31, The
parse_exclusive_syntax error callback is unreachable because
is_at_scss_nesting_declaration already guards for CssSyntaxFeatures::Scss;
update the branch so it calls parse_scss_nesting_declaration(p) directly
(instead of CssSyntaxFeatures::Scss.parse_exclusive_syntax(...,
scss_only_syntax_error, ...)), keeping the feature check inside
is_at_scss_nesting_declaration to avoid false positives and removing the
now-unnecessary scss_only_syntax_error callback in this path.
crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs (1)

111-143: Three-phase disambiguation is correct but complex — consider a clarifying comment.

The logic is:

  1. Speculative declaration parse: if the ambiguous name:ident form resolves to a semicolon-terminated CSS_DECLARATION_WITH_SEMICOLON, accept it.
  2. Speculative strict selector parse: if the form opens a { } block as a qualified rule, accept it.
  3. Fallback: parse as ScssNestingDeclaration non-speculatively.

This matches Sass's declaration-first strategy. One nit: inside the try_parse closure (lines 122–129), SCSS_NESTING_DECLARATION is explicitly rejected because the block form is ambiguous and should prefer the selector path. A one-line comment on line 122 explaining why the block form is rejected here would save the next reader a few minutes.

💡 Suggested comment
                 match declaration.kind(p) {
-                        Some(SCSS_NESTING_DECLARATION) => Err(()),
+                        // Block-form is ambiguous; reject so we can try selector parsing next.
+                        Some(SCSS_NESTING_DECLARATION) => Err(()),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs`
around lines 111 - 143, Add a one-line clarifying comment inside the try_parse
closure in declaration_or_rule_list_block.rs (around the match arm rejecting
SCSS_NESTING_DECLARATION) explaining that a block-form SCSS_NESTING_DECLARATION
is rejected in the speculative declaration phase because the three-phase
disambiguation prefers treating ambiguous block forms as selectors (qualified
rules) rather than declarations; reference the match on declaration.kind(p) and
the SCSS_NESTING_DECLARATION symbol so readers immediately understand why that
arm returns Err(()).
crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs (2)

83-99: complete_declaration_with_semicolon duplicates logic from parse_scss_declaration.

The semicolon-handling pattern (if !at('}'), if nth_at(1, '}'), eat-or-expect) is repeated from parse_scss_declaration in variable.rs (lines 57–64 in the relevant snippets). A shared helper could reduce this duplication — though I won't insist if you'd rather keep the two paths independent for now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs` around lines
83 - 99, The semicolon-handling logic in complete_declaration_with_semicolon is
duplicated from parse_scss_declaration (variable.rs); extract the shared pattern
into a small helper (e.g., consume_trailing_semicolon or
ensure_semicolon_if_not_closing) that accepts &mut CssParser and performs: if
!p.at('}') then if p.nth_at(1, '}') { p.eat(';') } else { p.expect(';') }; then
replace the duplicated blocks in complete_declaration_with_semicolon and
parse_scss_declaration to call this helper so both sites reuse the same logic
while leaving the surrounding marker handling (declaration.precede / m.complete)
unchanged.

101-111: Refactor to eliminate duplication: parse_declaration_important appears in both files.

The function in nesting.rs (lines 101–111) duplicates the logic in declaration.rs (lines 149–157). Although the declaration.rs version uses a helper function (is_at_declaration_important) while nesting.rs inlines the condition, the logic is identical. Extract parse_declaration_important from declaration.rs, make it pub(crate), and import it in nesting.rs to avoid duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs` around lines
101 - 111, Extract the existing parse_declaration_important implementation from
declaration.rs, mark it pub(crate) so it can be reused, and remove the duplicate
implementation in nesting.rs; update nesting.rs to import and call
declaration::parse_declaration_important instead of inlining the condition (also
reuse the existing helper is_at_declaration_important if present), ensuring
symbols parse_declaration_important and is_at_declaration_important are
referenced to locate the code to change.
crates/biome_css_parser/src/syntax/mod.rs (1)

288-297: Abandoned marker inside speculative parse — safe but worth noting.

When disable_selector_recovery is true and the selector doesn't reach {, the marker m is abandoned at line 295 before returning None. Since this path is always called inside try_parse (via try_parse_nested_qualified_rule_without_selector_recovery), the checkpoint rewind will restore the event list, so the abandoned marker is harmless. Just flagging this coupling for awareness — calling parse_nested_qualified_rule_with_selector_recovery(p, true) outside a try_parse would leak consumed tokens.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/syntax/mod.rs` around lines 288 - 297, The code
abandons marker m inside parse_nested_qualified_rule_with_selector_recovery when
disable_selector_recovery is true and the selector doesn't reach `{`, which is
safe only because callers
(try_parse_nested_qualified_rule_without_selector_recovery) currently call it
inside try_parse; change the implementation to avoid abandoning the marker
here—either remove the m.abandon(p) and simply return None so the caller
controls marker rollback, or guard the abandon with an explicit check/assert
that we are inside a speculative try_parse context; update
parse_nested_qualified_rule_with_selector_recovery (and document its contract)
so callers like try_parse_nested_qualified_rule_without_selector_recovery remain
valid and external callers are not able to leak consumed tokens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs`:
- Line 246: The match arm mapping
AnyCssDeclarationOrRule::ScssNestingDeclaration currently returns
NodeKindOrder::UnknownKind; if you prefer nested SCSS blocks to sort with rules
rather than after them, change that arm to return
NodeKindOrder::NestedRuleOrAtRule instead (update the match in
use_sorted_properties.rs where ScssNestingDeclaration is handled). Ensure the
symbol names AnyCssDeclarationOrRule::ScssNestingDeclaration and
NodeKindOrder::NestedRuleOrAtRule are used so the declaration is categorized as
a nested rule.

In
`@crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss`:
- Around line 1-6: Add a test line inside the .test block that exercises the
double-colon pseudo-element disambiguation (e.g., use
"font::before{color:green;}" or similar) so the parser treats "font::before" as
a nested selector rather than a nesting declaration; update the test file's
.test block alongside existing cases (selectors like "label:hover", "font:bold",
"font:12px", "font: bold") to include the "font::before" nested rule to verify
the no-whitespace double-colon path is handled.

In `@crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs`:
- Around line 111-143: Add a one-line clarifying comment inside the try_parse
closure in declaration_or_rule_list_block.rs (around the match arm rejecting
SCSS_NESTING_DECLARATION) explaining that a block-form SCSS_NESTING_DECLARATION
is rejected in the speculative declaration phase because the three-phase
disambiguation prefers treating ambiguous block forms as selectors (qualified
rules) rather than declarations; reference the match on declaration.kind(p) and
the SCSS_NESTING_DECLARATION symbol so readers immediately understand why that
arm returns Err(()).

In `@crates/biome_css_parser/src/syntax/declaration.rs`:
- Around line 25-31: The parse_exclusive_syntax error callback is unreachable
because is_at_scss_nesting_declaration already guards for
CssSyntaxFeatures::Scss; update the branch so it calls
parse_scss_nesting_declaration(p) directly (instead of
CssSyntaxFeatures::Scss.parse_exclusive_syntax(..., scss_only_syntax_error,
...)), keeping the feature check inside is_at_scss_nesting_declaration to avoid
false positives and removing the now-unnecessary scss_only_syntax_error callback
in this path.

In `@crates/biome_css_parser/src/syntax/mod.rs`:
- Around line 288-297: The code abandons marker m inside
parse_nested_qualified_rule_with_selector_recovery when
disable_selector_recovery is true and the selector doesn't reach `{`, which is
safe only because callers
(try_parse_nested_qualified_rule_without_selector_recovery) currently call it
inside try_parse; change the implementation to avoid abandoning the marker
here—either remove the m.abandon(p) and simply return None so the caller
controls marker rollback, or guard the abandon with an explicit check/assert
that we are inside a speculative try_parse context; update
parse_nested_qualified_rule_with_selector_recovery (and document its contract)
so callers like try_parse_nested_qualified_rule_without_selector_recovery remain
valid and external callers are not able to leak consumed tokens.

In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Around line 83-99: The semicolon-handling logic in
complete_declaration_with_semicolon is duplicated from parse_scss_declaration
(variable.rs); extract the shared pattern into a small helper (e.g.,
consume_trailing_semicolon or ensure_semicolon_if_not_closing) that accepts &mut
CssParser and performs: if !p.at('}') then if p.nth_at(1, '}') { p.eat(';') }
else { p.expect(';') }; then replace the duplicated blocks in
complete_declaration_with_semicolon and parse_scss_declaration to call this
helper so both sites reuse the same logic while leaving the surrounding marker
handling (declaration.precede / m.complete) unchanged.
- Around line 101-111: Extract the existing parse_declaration_important
implementation from declaration.rs, mark it pub(crate) so it can be reused, and
remove the duplicate implementation in nesting.rs; update nesting.rs to import
and call declaration::parse_declaration_important instead of inlining the
condition (also reuse the existing helper is_at_declaration_important if
present), ensuring symbols parse_declaration_important and
is_at_declaration_important are referenced to locate the code to change.

In `@crates/biome_css_parser/tests/quick_test.rs`:
- Around line 22-23: The test config is enabling unrelated options
allow_metavariables() and allow_tailwind_directives() that aren’t used by the
snippet; remove those method calls from the parser/config builder in
quick_test.rs (i.e., drop allow_metavariables() and allow_tailwind_directives()
from the chained configuration) so the test only sets options relevant to the
behavior being exercised.

@denbezrukov denbezrukov merged commit 7a36937 into next Feb 18, 2026
27 checks passed
@denbezrukov denbezrukov deleted the db/scss-nesting-declarations-mr branch February 18, 2026 17:13
denbezrukov added a commit that referenced this pull request Feb 19, 2026
@Netail Netail added the L-SCSS Language: SCSS label Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Formatter Area: formatter A-Linter Area: linter A-Parser Area: parser A-Tooling Area: internal tools L-CSS Language: CSS L-Grit Language: GritQL L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages L-SCSS Language: SCSS L-Tailwind Language: Tailwind CSS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants