Skip to content

feat(css): parse scss interpolation for selectors and declaration names#9529

Merged
denbezrukov merged 12 commits intomainfrom
db/interpolation-2
Mar 18, 2026
Merged

feat(css): parse scss interpolation for selectors and declaration names#9529
denbezrukov merged 12 commits intomainfrom
db/interpolation-2

Conversation

@denbezrukov
Copy link
Contributor

This PR was created with AI assistance (Codex).

Summary

Adds support for SCSS interpolation in selector names and declaration names in the CSS parser and formatter.

New syntax supported by this PR includes valid SCSS like:

  .foo-#{$name} {
    color: red;
  }

  #{$type} {
    color: red;
  }

  ns|#{$type} {
    color: red;
  }

  %#{$placeholder} {
    color: red;
  }

  .button {
    #{$name}: 1px;
    margin-#{$side}: 1px;
    --theme-#{$slot}: red;
  }

  @supports (#{$name}: 1px) {
    .button {
      color: red;
    }
  }

  @page {
    --theme-#{$slot}: red;
  }

Test Plan

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

@changeset-bot
Copy link

changeset-bot bot commented Mar 17, 2026

⚠️ No Changeset found

Latest commit: 59e93a1

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-CSS Language: CSS and super languages labels Mar 17, 2026
@denbezrukov denbezrukov changed the title feat(css): SCSS interpolation handling for selectors and declaration names feat(css): parse scss interpolation for selectors and declaration names Mar 17, 2026
@github-actions
Copy link
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 53139 53139 0
Passed 51919 51919 0
Failed 1178 1178 0
Panics 42 42 0
Coverage 97.70% 97.70% 0.00%

jsx/babel

Test result main count This PR count Difference
Total 38 38 0
Passed 37 37 0
Failed 1 1 0
Panics 0 0 0
Coverage 97.37% 97.37% 0.00%

markdown/commonmark

Test result main count This PR count Difference
Total 652 652 0
Passed 652 652 0
Failed 0 0 0
Panics 0 0 0
Coverage 100.00% 100.00% 0.00%

symbols/microsoft

Test result main count This PR count Difference
Total 5466 5466 0
Passed 1915 1915 0
Failed 3551 3551 0
Panics 0 0 0
Coverage 35.03% 35.03% 0.00%

ts/babel

Test result main count This PR count Difference
Total 636 636 0
Passed 568 568 0
Failed 68 68 0
Panics 0 0 0
Coverage 89.31% 89.31% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18875 18875 0
Passed 13014 13014 0
Failed 5860 5860 0
Panics 1 1 0
Coverage 68.95% 68.95% 0.00%

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 17, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 156 skipped benchmarks1


Comparing db/interpolation-2 (59e93a1) with main (1302740)2

Open in CodSpeed

Footnotes

  1. 156 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.

  2. No successful run was found on main (bc709f6) during the generation of this report, so 1302740 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds SCSS interpolated-identifier support across parser, lexer, formatter, analyzer, lints and semantic handling. Parser: new detection/parsing for interpolated property names and nesting (parse_scss_interpolated_property_name, parse_scss_interpolated_property_declaration, try_parse_scss_nesting_declaration, parse_declaration_with_semicolon and helpers), plus value-end-set driven property parsing. Lexer/token-source: exposes has_preceding_whitespace. Formatter: handles ScssInterpolatedIdentifier. Analyzer/lints/semantic: adjust property ordering, skip unknown-property diagnostics and semantic events for ScssInterpolatedIdentifier. Many SCSS test fixtures added.

Possibly related PRs

Suggested labels

L-SCSS

Suggested reviewers

  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: adding SCSS interpolation support for selectors and declaration names in the CSS parser.
Description check ✅ Passed The description is clearly related to the changeset, providing concrete examples of supported SCSS syntax and a test plan.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch db/interpolation-2
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_css_parser/src/syntax/declaration.rs (1)

143-153: ⚠️ Potential issue | 🟡 Minor

Tighten optional-semicolon detection before }.

At Line 149, the optional path keys only on nth_at(1, T!['}']). If the current token is not ;, this can bypass a required ; diagnostic for invalid input just before }.

Suggested fix
-    if !p.at(T!['}']) {
-        if p.nth_at(1, T!['}']) {
-            p.eat(T![;]);
-        } else {
-            p.expect(T![;]);
-        }
-    }
+    if !p.at(T!['}']) {
+        if p.at(T![;]) && p.nth_at(1, T!['}']) {
+            p.eat(T![;]);
+        } else {
+            p.expect(T![;]);
+        }
+    }

Based on learnings: Use p.eat(token) for optional tokens and p.expect(token) for required 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/declaration.rs` around lines 143 - 153, In
parse_optional_declaration_semicolon, tighten the semicolon check so you first
handle an actual semicolon token and only treat the semicolon as optional when
there's a real semicolon or the next token is a closing brace; change the
branching to: if p.at(T![;]) then p.eat(T![;]) else if p.nth_at(1, T!['}']) then
do nothing (semicolon optional) else p.expect(T![;]) to ensure invalid tokens
before `}` produce a required-semicolon diagnostic; reference
parse_optional_declaration_semicolon, p.at, p.eat, p.nth_at, and p.expect.
🧹 Nitpick comments (1)
crates/biome_css_analyze/src/lint/correctness/no_unknown_property.rs (1)

107-119: Variable naming is confusing.

The variable is_at_rule_supporting_descriptors returns true when the ancestor is an at-rule that does not support descriptors (i.e., it's not in AnyDescriptorSupportingAtRules). The name suggests the opposite of what it checks.

Consider renaming to is_inside_non_descriptor_at_rule or similar for clarity.

♻️ Suggested rename
-        let is_at_rule_supporting_descriptors = node.syntax().ancestors().skip(1).any(|ancestor| {
+        let is_inside_non_descriptor_at_rule = node.syntax().ancestors().skip(1).any(|ancestor| {
             if AnyCssAtRule::can_cast(ancestor.kind())
                 && !AnyDescriptorSupportingAtRules::can_cast(ancestor.kind())
             {
                 return true;
             }

             false
         });

-        if is_at_rule_supporting_descriptors {
+        if is_inside_non_descriptor_at_rule {
             return None;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_analyze/src/lint/correctness/no_unknown_property.rs` around
lines 107 - 119, The variable is_at_rule_supporting_descriptors is misnamed
because its truthy condition detects an at-rule that does NOT support
descriptors; rename it (e.g., is_inside_non_descriptor_at_rule) and update its
declaration and all usages (the node.syntax().ancestors().skip(1).any(...)
closure, the AnyCssAtRule / AnyDescriptorSupportingAtRules checks, and the
subsequent if is_at_rule_supporting_descriptors { return None; }) so the name
accurately reflects the logic and the conditional becomes if
is_inside_non_descriptor_at_rule { return None; }.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Around line 170-171: try_parse_scss_nesting_declaration currently passes a
completed declaration into complete_declaration_with_semicolon which relies on
parse_optional_declaration_semicolon that hard-codes '}' as the boundary; change
parse_optional_declaration_semicolon to accept the end_kind parameter (the same
enum/value used by try_parse_scss_nesting_declaration), use that boundary
instead of the hard-coded '}', and update complete_declaration_with_semicolon
(or add complete_declaration_with_semicolon_with_end_kind) to forward the
end_kind from try_parse_scss_nesting_declaration; update all callers of
parse_optional_declaration_semicolon / complete_declaration_with_semicolon
accordingly so semicolon diagnostics honor the provided end_kind.

---

Outside diff comments:
In `@crates/biome_css_parser/src/syntax/declaration.rs`:
- Around line 143-153: In parse_optional_declaration_semicolon, tighten the
semicolon check so you first handle an actual semicolon token and only treat the
semicolon as optional when there's a real semicolon or the next token is a
closing brace; change the branching to: if p.at(T![;]) then p.eat(T![;]) else if
p.nth_at(1, T!['}']) then do nothing (semicolon optional) else p.expect(T![;])
to ensure invalid tokens before `}` produce a required-semicolon diagnostic;
reference parse_optional_declaration_semicolon, p.at, p.eat, p.nth_at, and
p.expect.

---

Nitpick comments:
In `@crates/biome_css_analyze/src/lint/correctness/no_unknown_property.rs`:
- Around line 107-119: The variable is_at_rule_supporting_descriptors is
misnamed because its truthy condition detects an at-rule that does NOT support
descriptors; rename it (e.g., is_inside_non_descriptor_at_rule) and update its
declaration and all usages (the node.syntax().ancestors().skip(1).any(...)
closure, the AnyCssAtRule / AnyDescriptorSupportingAtRules checks, and the
subsequent if is_at_rule_supporting_descriptors { return None; }) so the name
accurately reflects the logic and the conditional becomes if
is_inside_non_descriptor_at_rule { return None; }.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 53b1ca54-7a84-4ba7-a480-ff2ad977d516

📥 Commits

Reviewing files that changed from the base of the PR and between bc709f6 and 58fb242.

⛔ Files ignored due to path filters (19)
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/declaration-block-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/supports-expression.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/page-at-rule.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/tailwind/supports.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/page-selector-like-declaration.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-interpolated-property-missing-colon.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-interpolated-property-missing-value.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-missing-value.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/declaration/interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/declaration-block-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/page.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/supports-expression.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/tailwind/supports.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (43)
  • crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs
  • crates/biome_css_analyze/src/lint/correctness/no_unknown_property.rs
  • crates/biome_css_formatter/src/css/any/declaration_name.rs
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/declaration-block-interpolation.scss
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/supports-expression.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/interpolation.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/page-at-rule.scss
  • crates/biome_css_formatter/tests/specs/css/tailwind/supports.css
  • crates/biome_css_parser/src/lexer/mod.rs
  • crates/biome_css_parser/src/syntax/at_rule/page.rs
  • crates/biome_css_parser/src/syntax/at_rule/supports/mod.rs
  • crates/biome_css_parser/src/syntax/block/declaration_or_at_rule_list_block.rs
  • crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs
  • crates/biome_css_parser/src/syntax/declaration.rs
  • crates/biome_css_parser/src/syntax/property/mod.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/mod.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/variable.rs
  • crates/biome_css_parser/src/syntax/scss/expression/list.rs
  • crates/biome_css_parser/src/syntax/scss/expression/mod.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/interpolated_identifier.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/interpolated_regular.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/interpolated_selector.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/mod.rs
  • crates/biome_css_parser/src/syntax/scss/mod.rs
  • crates/biome_css_parser/src/syntax/scss/property.rs
  • crates/biome_css_parser/src/token_source.rs
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/page-selector-like-declaration.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-interpolated-property-missing-colon.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-interpolated-property-missing-value.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/declaration/interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/declaration-block-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/page.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/supports-expression.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/tailwind/supports.css
  • crates/biome_css_semantic/src/events.rs
  • crates/biome_parser/src/lexer.rs
  • crates/biome_parser/src/lib.rs
  • crates/biome_parser/src/token_source.rs
  • xtask/codegen/css.ungram

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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs (1)

168-169: ⚠️ Potential issue | 🟠 Major

end_kind still isn’t propagated to semicolon completion (same issue as earlier).

try_parse_scss_nesting_declaration uses end_kind for acceptance (Lines 205-206), but regular declaration completion still goes through complete_declaration_with_semicolon, whose optional-semicolon logic is }-based. So non-} boundaries can still get wrong semicolon diagnostics/behaviour.

Also applies to: 189-206

🤖 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
168 - 169, The semicolon completion path in try_parse_scss_nesting_declaration
is not honoring the parser's end_kind (so optional-semicolon logic still uses a
'}' boundary); update the callsite(s) in try_parse_scss_nesting_declaration (and
the similar block around lines 189-206) to pass the end_kind through to the
completion routine instead of calling complete_declaration_with_semicolon
directly, and modify or overload complete_declaration_with_semicolon to accept
an end_kind parameter (or a predicate) so its optional-semicolon logic checks
the provided end_kind rather than hardcoding '}' as the boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Around line 39-45: The parser currently consumes an interpolated prefix in
parse_scss_nesting_declaration_candidate (and callers around lines handling the
`#{$...}`) and then returns Absent from parse_scss_nesting_declaration, leaving
the parser advanced — make this path transactional: wrap the work in a
checkpoint/rewind (or only consume tokens after confirming a following `:`) so
that parse_scss_nesting_declaration either fully succeeds or leaves the input
position unchanged; specifically update parse_scss_nesting_declaration_candidate
(and its use in parse_scss_nesting_declaration and
is_at_scss_nesting_declaration) to save the parser position before consuming the
interpolation and restore it on None, or delay consuming the interpolation until
the colon is confirmed.

---

Duplicate comments:
In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Around line 168-169: The semicolon completion path in
try_parse_scss_nesting_declaration is not honoring the parser's end_kind (so
optional-semicolon logic still uses a '}' boundary); update the callsite(s) in
try_parse_scss_nesting_declaration (and the similar block around lines 189-206)
to pass the end_kind through to the completion routine instead of calling
complete_declaration_with_semicolon directly, and modify or overload
complete_declaration_with_semicolon to accept an end_kind parameter (or a
predicate) so its optional-semicolon logic checks the provided end_kind rather
than hardcoding '}' as the boundary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e25c17f-1b01-4700-9980-69abe4269cf4

📥 Commits

Reviewing files that changed from the base of the PR and between 58fb242 and e33b8a0.

📒 Files selected for processing (2)
  • crates/biome_css_parser/src/syntax/at_rule/page.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs

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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
crates/biome_css_parser/src/syntax/declaration.rs (1)

136-153: ⚠️ Potential issue | 🟠 Major

Make the declaration boundary configurable.

This still treats } as the only place where the semicolon becomes optional. try_parse_scss_nesting_declaration(..., end_kind) now feeds declarations through here from other boundaries too, so those paths can pick up a bogus missing-semicolon diagnostic before try_parse happily commits the node.

💡 Possible direction
 pub(crate) fn complete_declaration_with_semicolon(
     p: &mut CssParser,
     declaration: CompletedMarker,
+    end_kind: CssSyntaxKind,
 ) -> CompletedMarker {
     let declaration_with_semicolon = declaration.precede(p);
 
-    parse_optional_declaration_semicolon(p);
+    parse_optional_declaration_semicolon(p, end_kind);
 
     declaration_with_semicolon.complete(p, CSS_DECLARATION_WITH_SEMICOLON)
 }
 
 #[inline]
-pub(crate) fn parse_optional_declaration_semicolon(p: &mut CssParser) {
-    if !p.at(T!['}']) {
-        if p.nth_at(1, T!['}']) {
+pub(crate) fn parse_optional_declaration_semicolon(
+    p: &mut CssParser,
+    end_kind: CssSyntaxKind,
+) {
+    if !p.at(end_kind) {
+        if p.nth_at(1, end_kind) {
             p.eat(T![;]);
         } else {
             p.expect(T![;]);
         }
     }
 }
🤖 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 136 - 153,
The helper parse_optional_declaration_semicolon currently only treats '}' as an
end boundary; make it configurable so callers (like
try_parse_scss_nesting_declaration) can pass the end-kind/predicate to avoid
emitting a missing-semicolon diagnostic for other boundaries. Change
parse_optional_declaration_semicolon(p: &mut CssParser) to accept an end check
(e.g., a token kind or closure parameter such as end_kind or is_end: impl
Fn(&CssParser) -> bool), replace the p.at(T!['}']) and p.nth_at(1, T!['}'])
checks with calls to that end predicate, and update all call sites (notably
try_parse_scss_nesting_declaration) to pass the appropriate boundary test so
semicolon handling is performed relative to the caller's end token(s).
crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs (1)

111-116: ⚠️ Potential issue | 🟠 Major

Don't surface Absent after consuming an interpolated prefix.

If parse_scss_interpolated_identifier(p) eats #{$...} and Line 113 then finds no :, this returns None after advancing the parser. parse_scss_nesting_declaration() turns that into Absent, which is the sort of parser gremlin that breaks list recovery. Please make this check transactional or defer consuming the prefix until : is known.

Based on learnings: "Parse rules must return ParsedSyntax::Absent if the rule can't predict by the next token(s) if they form the expected node, and must not progress the parser in this case."

🤖 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
111 - 116, The code consumes an interpolated identifier via
parse_scss_interpolated_identifier(p) then returns None (causing Absent) if no
':' is found, which advances the parser and breaks recovery; make this
transactional by snapshotting/reverting parser state or delaying consumption
until you know a ':' follows: e.g., create a checkpoint/save before calling
parse_scss_interpolated_identifier (or perform a lookahead for T![:] first), and
if no ':' is present rollback the parser state and return ParsedSyntax::Absent
from parse_scss_nesting_declaration rather than abandoning after partial
consumption; ensure references to declaration.abandon and property.abandon only
run after a successful, committed parse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_css_parser/src/syntax/at_rule/supports/mod.rs`:
- Around line 349-354: The parser for `@supports` values in
parse_supports_property_value currently calls parse_property_value_with_end_set
with END_OF_SUPPORTS_PROPERTY_VALUE_TOKEN_SET and
END_OF_PROPERTY_VALUE_TOKEN_SET, but the recovery set lacks the closing ')' so a
bad value can consume it; update the recovery configuration so ')' is treated as
a hard stop—either add the RightParenthesis token (or equivalent symbol used in
token sets) to END_OF_SUPPORTS_PROPERTY_VALUE_TOKEN_SET or change the call to
pass a token set that includes ')' as a recovery boundary before
END_OF_PROPERTY_VALUE_TOKEN_SET so parse_property_value_with_end_set will not
eat the closing parenthesis when recovering.

---

Duplicate comments:
In `@crates/biome_css_parser/src/syntax/declaration.rs`:
- Around line 136-153: The helper parse_optional_declaration_semicolon currently
only treats '}' as an end boundary; make it configurable so callers (like
try_parse_scss_nesting_declaration) can pass the end-kind/predicate to avoid
emitting a missing-semicolon diagnostic for other boundaries. Change
parse_optional_declaration_semicolon(p: &mut CssParser) to accept an end check
(e.g., a token kind or closure parameter such as end_kind or is_end: impl
Fn(&CssParser) -> bool), replace the p.at(T!['}']) and p.nth_at(1, T!['}'])
checks with calls to that end predicate, and update all call sites (notably
try_parse_scss_nesting_declaration) to pass the appropriate boundary test so
semicolon handling is performed relative to the caller's end token(s).

In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Around line 111-116: The code consumes an interpolated identifier via
parse_scss_interpolated_identifier(p) then returns None (causing Absent) if no
':' is found, which advances the parser and breaks recovery; make this
transactional by snapshotting/reverting parser state or delaying consumption
until you know a ':' follows: e.g., create a checkpoint/save before calling
parse_scss_interpolated_identifier (or perform a lookahead for T![:] first), and
if no ':' is present rollback the parser state and return ParsedSyntax::Absent
from parse_scss_nesting_declaration rather than abandoning after partial
consumption; ensure references to declaration.abandon and property.abandon only
run after a successful, committed parse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11cb227e-19b4-458a-ae6a-8998ab02096f

📥 Commits

Reviewing files that changed from the base of the PR and between e33b8a0 and fec7cad.

📒 Files selected for processing (10)
  • crates/biome_css_parser/src/syntax/at_rule/supports/mod.rs
  • crates/biome_css_parser/src/syntax/block/declaration_or_at_rule_list_block.rs
  • crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs
  • crates/biome_css_parser/src/syntax/declaration.rs
  • crates/biome_css_parser/src/syntax/property/mod.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/mod.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/interpolated_regular.rs
  • crates/biome_css_parser/src/syntax/scss/mod.rs
  • crates/biome_css_parser/src/syntax/scss/property.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/biome_css_parser/src/syntax/scss/declaration/mod.rs
  • crates/biome_css_parser/src/syntax/scss/property.rs

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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs (2)

35-40: ⚠️ Potential issue | 🟠 Major

Don't return Absent after consuming the interpolation.

is_at_scss_nesting_declaration() is true for #{$name} in the test below, but this entrypoint still maps a failed candidate to Absent without rewinding. That leaves the non-speculative path advanced while pretending nothing happened — classic parser gremlin territory. Please make this path transactional as well, or delay consuming the prefix until : is confirmed.

Based on learnings: Parse rules must return ParsedSyntax::Absent if the rule can't predict by the next token(s) if they form the expected node, and must not progress the parser in this case.

🤖 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
35 - 40, parse_scss_nesting_declaration currently returns ParsedSyntax::Absent
after calling parse_scss_nesting_declaration_candidate even though the candidate
may have consumed the interpolation (e.g., #{$name}), leaving the parser
advanced; make this entrypoint transactional by either performing the candidate
parse speculatively (use a checkpoint/rewind around
parse_scss_nesting_declaration_candidate) or defer consuming the interpolation
until you confirm the trailing ':' (i.e., only advance the main parser when the
candidate succeeds). Update parse_scss_nesting_declaration to restore the parser
position on candidate failure instead of map_or(Absent, …) so the parser state
is unchanged when returning ParsedSyntax::Absent.

143-160: ⚠️ Potential issue | 🟠 Major

Thread end_kind into semicolon completion too.

Right now end_kind only gets a cameo in the final acceptance check. The regular-declaration path still finalises through complete_declaration_with_semicolon(), so non-} boundaries can keep the wrong missing-semicolon behaviour here.

🤖 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
143 - 160, The regular-declaration path in
complete_scss_nesting_regular_declaration must pass the SCSS end_kind through to
the semicolon completion so the missing-semicolon logic respects non-`}`
boundaries: update complete_declaration_with_semicolon to accept an end_kind
parameter (or an equivalent flag), thread the appropriate end_kind from
ScssNestingMarkers (or the surrounding parser state) into the call instead of
using the default, and ensure you call markers.declaration.abandon(p) only after
you have completed/closed the declaration with the semicolon helper (or adjust
the order to precede/complete consistently) so the declaration marker is
available to complete_declaration_with_semicolon.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Line 121: The parser's could_be_selector check only treats bare identifiers or
a colon token as selector-like, so constructs like a:#{$pseudo} are
misclassified; update the condition that sets could_be_selector in nesting.rs to
also accept is_at_scss_interpolated_identifier(p) (alongside is_at_identifier(p)
and p.at(T![:])) so interpolated pseudo-selectors cause the parser to rewind and
parse a selector instead of committing SCSS_NESTING_DECLARATION; make this
change in the expression that assigns could_be_selector to ensure interpolated
identifiers are recognised.

---

Duplicate comments:
In `@crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs`:
- Around line 35-40: parse_scss_nesting_declaration currently returns
ParsedSyntax::Absent after calling parse_scss_nesting_declaration_candidate even
though the candidate may have consumed the interpolation (e.g., #{$name}),
leaving the parser advanced; make this entrypoint transactional by either
performing the candidate parse speculatively (use a checkpoint/rewind around
parse_scss_nesting_declaration_candidate) or defer consuming the interpolation
until you confirm the trailing ':' (i.e., only advance the main parser when the
candidate succeeds). Update parse_scss_nesting_declaration to restore the parser
position on candidate failure instead of map_or(Absent, …) so the parser state
is unchanged when returning ParsedSyntax::Absent.
- Around line 143-160: The regular-declaration path in
complete_scss_nesting_regular_declaration must pass the SCSS end_kind through to
the semicolon completion so the missing-semicolon logic respects non-`}`
boundaries: update complete_declaration_with_semicolon to accept an end_kind
parameter (or an equivalent flag), thread the appropriate end_kind from
ScssNestingMarkers (or the surrounding parser state) into the call instead of
using the default, and ensure you call markers.declaration.abandon(p) only after
you have completed/closed the declaration with the semicolon helper (or adjust
the order to precede/complete consistently) so the declaration marker is
available to complete_declaration_with_semicolon.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a5e9c6e-1363-4811-8a6a-1ca4527284fc

📥 Commits

Reviewing files that changed from the base of the PR and between fec7cad and 6d36c5e.

⛔ Files ignored due to path filters (3)
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-interpolated-property-missing-colon.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-interpolated-property-missing-value.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/supports-missing-value.scss.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (5)
  • crates/biome_css_parser/src/syntax/property/mod.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs
  • crates/biome_css_parser/src/syntax/scss/expression/list.rs
  • crates/biome_css_parser/src/syntax/scss/expression/mod.rs
  • crates/biome_css_parser/src/syntax/scss/mod.rs

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Admittedly, I've only glanced at it, but I don't see any major issues

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Man, and I thought SCSS was a simple super language 🫠

}
AnyCssDeclarationName::CssIdentifier(_) => NodeKindOrder::Declaration,
AnyCssDeclarationName::ScssInterpolatedIdentifier(name) => {
if name.to_trimmed_text().starts_with("--") {
Copy link
Member

Choose a reason for hiding this comment

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

This probably allocates a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

replaced with name.syntax().text_trimmed(), I suppose it doesn't allocate, we have the same here:

// ^^^^^^^^^^^^^^^^ CSS_GENERIC_COMPONENT_VALUE_LIST
CssSyntaxKind::CSS_GENERIC_COMPONENT_VALUE_LIST => return false,
CssSyntaxKind::CSS_FUNCTION => return parent.text_trimmed().starts_with("var"),
_ => {}
}

Copy link
Member

Choose a reason for hiding this comment

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

I believe it still allocates because we are stringifying a node, not a token, but we optimise later

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For syntax text_trimmed() returns SyntaxNodeText, and starts_with() walks token chunks directly without materializing a string, if I correctly understand the idea of SyntaxNodeText

Comment on lines +126 to +127
let property_name_text = property_name.to_trimmed_text();
let property_name_lower = property_name_text.to_ascii_lowercase_cow();
Copy link
Member

Choose a reason for hiding this comment

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

Is property_name a node? If so, then we're allocating a string. Any chance that we can have a method that returns the token only when there's no interpolation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make sense!
Done!

Comment on lines +19 to +21
pub(crate) fn parse_scss_interpolated_identifier(p: &mut CssParser) -> ParsedSyntax {
parse_scss_interpolated_identifier_with(p, parse_regular_interpolated_identifier_any_fragment)
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't see the point of function that only calls another function...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I agree that it looks like a wrapper, but:

  • parse_scss_interpolated_identifier(...) describes what syntax we are parsing.
  • parse_scss_interpolated_identifier_with(...) explains how it is implemented internally.
  • parse_regular_interpolated_identifier_any_fragment(...) treats whitespaces as a trivia.

Unfortunately, we need to keep two versions to distinguish whether whitespace is treated as trivia or not. I did try introducing a parameter for this, but it made the code less readable(

@denbezrukov denbezrukov merged commit 4df86ca into main Mar 18, 2026
30 checks passed
@denbezrukov denbezrukov deleted the db/interpolation-2 branch March 18, 2026 09:34
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 and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants