Skip to content

feat(css): add support for SCSS string interpolation#9756

Merged
denbezrukov merged 12 commits intomainfrom
db/scss-rework-interpolation
Apr 1, 2026
Merged

feat(css): add support for SCSS string interpolation#9756
denbezrukov merged 12 commits intomainfrom
db/scss-rework-interpolation

Conversation

@denbezrukov
Copy link
Copy Markdown
Contributor

This PR was created with AI assistance (Codex).

Summary

Adds first-class support for SCSS interpolated strings.

Quoted SCSS strings

  .box {
    content: "foo #{$bar} baz";
    nested: "#{"bar"}";
    asset: url("https://#{$host}/#{$path}");
  }

Nested string-origin interpolation

  .panel {
    nested_interpolated_inside_double: "#{'a #{$b}'}";
    quadruple_nested_chain: "#{"root #{'branch #{"leaf #{'twig #{$bud}'}"}'}"}";
  }

Selector string values

  [data-theme="#{$theme}"] { color: red; }
  :lang("#{$locale}") { color: blue; }

import

  @import "#{$file}.scss";
  @import url("foo.css"), "#{$bar}";

Media / container queries

  @media (#{$feature}: #{$value}) {}
  @media (min-width: #{$value}px) {}
  @container (min-width: #{$value}px) {}

Test Plan

green CI, new tests

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 31, 2026

⚠️ No Changeset found

Latest commit: 550d282

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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 31, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 196 skipped benchmarks1


Comparing db/scss-rework-interpolation (550d282) with main (716e4e1)

Open in CodSpeed

Footnotes

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

@github-actions
Copy link
Copy Markdown
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 5467 5467 0
Passed 1915 1915 0
Failed 3552 3552 0
Panics 0 0 0
Coverage 35.03% 35.03% 0.00%

ts/babel

Test result main count This PR count Difference
Total 640 640 0
Passed 569 569 0
Failed 71 71 0
Panics 0 0 0
Coverage 88.91% 88.91% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18876 18876 0
Passed 13014 13014 0
Failed 5861 5861 0
Panics 1 1 0
Coverage 68.94% 68.94% 0.00%

@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 L-Grit Language: GritQL labels Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 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-string support across lexer, parser, formatter, analyzer and tests: new AST nodes/tokens (ScssInterpolatedString, ScssStringText, SCSS_STRING_QUOTE, SCSS_STRING_CONTENT_LITERAL, part-list, etc.), lexer SCSS string contexts and interpolation recovery, token-source checkpoint/state for SCSS string interpolation, parser recognition and new parse paths for interpolated strings/identifiers in values, URLs, imports, media/container query features, selectors and attribute matchers, formatter rules and generated wiring for the new nodes, lint changes to treat literal vs dynamic media feature names differently, Cargo dependency updates, codegen/kind mappings, and many new/updated test fixtures.

Possibly related PRs

Suggested labels

L-SCSS

Suggested reviewers

  • dyc3
  • ematipico
  • Netail
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: adding SCSS string interpolation support.
Description check ✅ Passed The description clearly relates to the changeset, providing concrete examples of SCSS interpolation in strings, selectors, imports, and media queries.

✏️ 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/scss-rework-interpolation

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
Copy Markdown
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: 2

Caution

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

⚠️ Outside diff range comments (1)
xtask/codegen/css.ungram (1)

1685-1685: ⚠️ Potential issue | 🟠 Major

Move ScssInterpolatedString from AnyScssImportItem into the plain-import URL field.

Sass interpolation only works for plain CSS imports (including those with media queries). The current grammar allows @import "#{$var}"; as a standalone Sass import item, which is invalid per the Sass spec. Meanwhile, valid forms like @import "#{$file}.css" print; can't flow through ScssPlainImport.url because it still uses AnyCssImportUrl. Introduce AnyScssImportUrl to include interpolated strings, then use it in ScssPlainImport.url.

Don't forget to run just gen-grammar css before opening a PR.

💡 Suggested grammar shape
 AnyCssImportUrl = CssUrlFunction | CssString
 
+AnyScssImportUrl =
+	AnyCssImportUrl
+	| ScssInterpolatedString
+
 AnyScssImportItem =
 	CssString
-	| ScssInterpolatedString
 	| ScssPlainImport
 
 ScssPlainImport =
-	url: AnyCssImportUrl
+	url: AnyScssImportUrl
 	layer: AnyCssImportLayer?
 	supports: CssImportSupports?
 	media: CssMediaQueryList
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtask/codegen/css.ungram` at line 1685, The grammar currently allows
ScssInterpolatedString as an AnyScssImportItem which permits invalid standalone
Sass imports; change the grammar so that AnyScssImportUrl (a new union type)
includes ScssInterpolatedString and any existing CssUrlFunction/CssString forms,
then update ScssPlainImport.url to use AnyScssImportUrl instead of
AnyCssImportUrl and remove ScssInterpolatedString from AnyScssImportItem; after
making these edits run `just gen-grammar css` to regenerate artifacts.
🧹 Nitpick comments (2)
crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs (1)

27-29: Consider adding one snapshot for unquoted attribute interpolation.

This branch looks right; a fixture like [data-theme=#{$theme}] would pin quote-normalisation behaviour for this exact path.

As per coding guidelines "'formatters require snapshot tests with valid/invalid cases'."

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

In `@crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs`
around lines 27 - 29, Add a snapshot test covering unquoted attribute
interpolation to ensure quote-normalisation for the
AnyCssAttributeMatcherValue::ScssInterpolatedString branch; create a fixture
such as `[data-theme=#{$theme}]`, run the formatter (targeting the css/auxiliary
attribute matcher path that uses ScssInterpolatedString), capture the output and
add/update the snapshot in the test suite so this exact case is validated going
forward.
crates/biome_css_parser/src/syntax/scss/value/interpolated_string.rs (1)

24-25: Consider extracting duplicated token set.

This SCSS_INTERPOLATION_END_TOKEN_SET is also defined in expression/interpolation.rs. Consider consolidating into a shared constant to prevent drift.

♻️ Possible consolidation

Move the constant to a shared location (e.g., expression/mod.rs or a dedicated constants module) and re-export it where needed.

🤖 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/value/interpolated_string.rs` around
lines 24 - 25, The SCSS_INTERPOLATION_END_TOKEN_SET constant is duplicated
(present in interpolated_string.rs and expression/interpolation.rs); extract
this token set into a single shared constant (e.g., declare
SCSS_INTERPOLATION_END_TOKEN_SET in expression/mod.rs or a new constants module)
and re-export or pub use it so both interpolated_string.rs and
expression/interpolation.rs import the same symbol; update references to the
original SCSS_INTERPOLATION_END_TOKEN_SET in both files to use the shared
constant and remove the duplicate definition.
🤖 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/lexer/mod.rs`:
- Around line 904-953: The string/hex escape handling misses CRLF and other
whitespace per the CSS spec: update scan_string_escape to treat a CR followed by
LF as a single line-continuation (if byte==b'\r' and
self.byte_at(next+1)==Some(b'\n') advance by 2), and update scan_hex_escape to
consume any single whitespace character after the hex digits (accept b'\t', b'
', b'\n', b'\r', and form feed 0x0C) instead of only tab/space; keep return
values/semantics of scan_string_escape and scan_hex_escape the same (use the
existing symbols scan_string_escape and scan_hex_escape and the cursor/next
logic to implement these checks).

In `@crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs`:
- Around line 191-198: The interpolated-string branch in parse_scss_import_item
currently returns a bare ScssInterpolatedString and therefore skips import
modifiers; change parse_scss_import_item so that when
is_at_scss_interpolated_string(p) you call parse_scss_interpolated_string(p) and
then, if is_at_import_modifier(p) is true, consume the modifiers using
parse_scss_plain_import_modifiers() (same pattern used by
parse_scss_string_import_item), returning a combined node that includes the
interpolated string plus parsed modifiers.

---

Outside diff comments:
In `@xtask/codegen/css.ungram`:
- Line 1685: The grammar currently allows ScssInterpolatedString as an
AnyScssImportItem which permits invalid standalone Sass imports; change the
grammar so that AnyScssImportUrl (a new union type) includes
ScssInterpolatedString and any existing CssUrlFunction/CssString forms, then
update ScssPlainImport.url to use AnyScssImportUrl instead of AnyCssImportUrl
and remove ScssInterpolatedString from AnyScssImportItem; after making these
edits run `just gen-grammar css` to regenerate artifacts.

---

Nitpick comments:
In `@crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs`:
- Around line 27-29: Add a snapshot test covering unquoted attribute
interpolation to ensure quote-normalisation for the
AnyCssAttributeMatcherValue::ScssInterpolatedString branch; create a fixture
such as `[data-theme=#{$theme}]`, run the formatter (targeting the css/auxiliary
attribute matcher path that uses ScssInterpolatedString), capture the output and
add/update the snapshot in the test suite so this exact case is validated going
forward.

In `@crates/biome_css_parser/src/syntax/scss/value/interpolated_string.rs`:
- Around line 24-25: The SCSS_INTERPOLATION_END_TOKEN_SET constant is duplicated
(present in interpolated_string.rs and expression/interpolation.rs); extract
this token set into a single shared constant (e.g., declare
SCSS_INTERPOLATION_END_TOKEN_SET in expression/mod.rs or a new constants module)
and re-export or pub use it so both interpolated_string.rs and
expression/interpolation.rs import the same symbol; update references to the
original SCSS_INTERPOLATION_END_TOKEN_SET in both files to use the shared
constant and remove the duplicate definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e2f0539c-070d-4b6c-bf06-af2b82bf2cfb

📥 Commits

Reviewing files that changed from the base of the PR and between 716e4e1 and 9209833.

⛔ Files ignored due to path filters (32)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_factory/src/generated/node_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_factory/src/generated/syntax_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nesting.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_error.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_error.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/forward.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/use.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_syntax/src/generated/kind.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_syntax/src/generated/macros.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_syntax/src/generated/nodes_mut.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (70)
  • crates/biome_css_analyze/Cargo.toml
  • crates/biome_css_analyze/src/lint/correctness/no_unknown_media_feature_name.rs
  • crates/biome_css_analyze/src/lint/correctness/no_unknown_unit.rs
  • crates/biome_css_analyze/src/lint/nursery/use_baseline.rs
  • crates/biome_css_analyze/tests/spec_tests.rs
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scss
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scss
  • crates/biome_css_formatter/src/css/any/attribute_matcher_value.rs
  • crates/biome_css_formatter/src/css/any/mod.rs
  • crates/biome_css_formatter/src/css/any/pseudo_value.rs
  • crates/biome_css_formatter/src/css/any/query_feature_name.rs
  • crates/biome_css_formatter/src/css/any/query_feature_value.rs
  • crates/biome_css_formatter/src/css/any/url_value.rs
  • crates/biome_css_formatter/src/css/any/value.rs
  • crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/any/import_item.rs
  • crates/biome_css_formatter/src/scss/any/interpolated_string_part.rs
  • crates/biome_css_formatter/src/scss/any/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/interpolated_string.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/string_text.rs
  • crates/biome_css_formatter/src/scss/lists/interpolated_string_part_list.rs
  • crates/biome_css_formatter/src/scss/lists/mod.rs
  • crates/biome_css_formatter/src/utils/string_utils.rs
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/options.json
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scss
  • crates/biome_css_parser/Cargo.toml
  • crates/biome_css_parser/src/lexer/mod.rs
  • crates/biome_css_parser/src/lexer/tests.rs
  • crates/biome_css_parser/src/parser.rs
  • crates/biome_css_parser/src/syntax/at_rule/feature.rs
  • crates/biome_css_parser/src/syntax/at_rule/media.rs
  • crates/biome_css_parser/src/syntax/mod.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs
  • crates/biome_css_parser/src/syntax/scss/expression/interpolation.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/expression/primary.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/mod.rs
  • crates/biome_css_parser/src/syntax/scss/value/interpolated_string.rs
  • crates/biome_css_parser/src/syntax/scss/value/mod.rs
  • crates/biome_css_parser/src/syntax/scss/value/parent_selector.rs
  • crates/biome_css_parser/src/syntax/selector/attribute.rs
  • crates/biome_css_parser/src/syntax/selector/pseudo_class/function_value_list.rs
  • crates/biome_css_parser/src/syntax/value/url.rs
  • crates/biome_css_parser/src/token_source.rs
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.css
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scss
  • crates/biome_css_syntax/src/import_ext.rs
  • crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs
  • xtask/codegen/css.ungram
  • xtask/codegen/src/css_kinds_src.rs
💤 Files with no reviewable changes (1)
  • crates/biome_css_parser/src/syntax/scss/value/parent_selector.rs

@@ -322,6 +323,8 @@ pub const CSS_KINDS_SRC: KindsSrc = KindsSrc {
"COMMENT",
"MULTILINE_COMMENT",
"GRIT_METAVARIABLE",
"SCSS_RECOVERED_OUTER_STRING_QUOTE",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new token feels a bit unusual. Why does it exist?

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.

SCSS_RECOVERED_OUTER_STRING_QUOTE was a recovery-only token for one lexer ambiguity: inside string-origin #{...}, a same-quote byte could mean either “start a valid nested string” or “the outer string closed early before }”.

I think, you're right here and it's better to remove it.

Now we don’t need a special token. The lexer does a non-mutating scan instead:

  • valid nested string -> normal string token path
  • early outer close -> normal SCSS_STRING_QUOTE

The parser uses the existing cached pending_scss_string_start state to tell those two cases apart.

Comment on lines +34 to +38
if CssSyntaxFeatures::Scss.is_supported(p) {
is_at_scss_interpolated_identifier(p)
} else {
is_at_identifier(p)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We usually use parse_exclusive_syntax for this kind of thing

Comment on lines +76 to +80
if CssSyntaxFeatures::Scss.is_supported(p) {
parse_scss_interpolated_identifier(p)
} else {
parse_regular_identifier(p)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here

fn is_at_scss_interpolated_query_feature_value(p: &mut CssParser) -> bool {
if !CssSyntaxFeatures::Scss.is_supported(p) || !is_at_scss_interpolation(p) {
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is_supported is kind of a code smell here. We want to parse_exclusive_syntax so that we can emit a clear diagnostic to the user about it.

Unless there's something I'm missing?

Copy link
Copy Markdown
Contributor Author

@denbezrukov denbezrukov Apr 1, 2026

Choose a reason for hiding this comment

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

Hi!

I totally agree with you here, parse_exclusive_syntax is preferable. I'm planning to revisit all such places once the parser is ready.

Right now, these changes are already in master, and if we start reporting them (I think we already do this for some parts that were previously in the next branch), users may start seeing SCSS-related suggestions in CSS files, while simply changing the file extension won't fix the issue.

As for string interpolation, I think it's better to use is_supported, because in CSS it is technically valid syntax, even though it`s a rare case. For example:

.class {
  someproperty: "here is interpolation #{wow} but it's a valid string";
}

What do you think?

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.

Good point. Maybe add some TODO/NOTE in the part of the codes that need changing once SCSS is ready

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ah, yeah you're right. all's good then

@denbezrukov denbezrukov force-pushed the db/scss-rework-interpolation branch from fd1cfc1 to 41a3cfd Compare April 1, 2026 09:51
Copy link
Copy Markdown
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/lexer/mod.rs (1)

838-902: ⚠️ Potential issue | 🟠 Major

CSS escape handling is still short-changing CRLF and form feed

scan_string_body still treats only \n / \r as string-breaking newlines, scan_string_escape still advances only one byte for \\\r\n, and scan_hex_escape still swallows only space/tab after hex digits. CSS consumes CRLF as a single line continuation and allows one optional whitespace code point there, including LF, CR, and FF, so valid escapes will still be mis-lexed here.

🩹 Possible fix
-                WHS if matches!(byte, b'\n' | b'\r') => {
+                WHS if matches!(byte, b'\n' | b'\r' | b'\x0C') => {
                     let len = if byte == b'\r' && self.byte_at(offset + 1) == Some(b'\n') {
                         2
                     } else {
                         1
                     };
@@
-            Some(b'\n' | b'\r') => (next + 1, false),
+            Some(b'\n' | b'\x0C') => (next + 1, false),
+            Some(b'\r') => {
+                let next_offset = if self.byte_at(next + 1) == Some(b'\n') {
+                    next + 2
+                } else {
+                    next + 1
+                };
+                (next_offset, false)
+            }
@@
-        if self
-            .byte_at(cursor)
-            .is_some_and(|byte| matches!(byte, b'\t' | b' '))
-        {
-            cursor += 1;
-        }
+        match self.byte_at(cursor) {
+            Some(b'\t' | b' ' | b'\n' | b'\x0C') => cursor += 1,
+            Some(b'\r') => {
+                cursor += 1;
+                if self.byte_at(cursor) == Some(b'\n') {
+                    cursor += 1;
+                }
+            }
+            _ => {}
+        }
In CSS Syntax Module Level 3, when consuming an escaped code point, does the optional whitespace after 1-6 hex digits include LF, CR, and form feed, and should a backslash followed by CRLF inside a CSS string be consumed as a single line continuation?

Also applies to: 920-969

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

In `@crates/biome_css_parser/src/lexer/mod.rs` around lines 838 - 902,
scan_string_body, scan_string_escape, and scan_hex_escape currently mishandle
CRLF and form feed: update them so a backslash followed by CRLF is treated as a
single line continuation (advance over both CR and LF), CRLF is considered one
newline in scan_string_body (len = 2) and scan_string_escape should advance two
bytes for CRLF sequences, and scan_hex_escape should treat the optional
whitespace after 1–6 hex digits as any single CSS whitespace code point (space,
tab, LF, CR, or FF) rather than only space/tab; adjust the logic in
scan_string_body (the CR/LF branch), scan_string_escape (the BSL handling that
advances over newline continuations), and scan_hex_escape (the optional
whitespace consumption) to consume the correct number of bytes and include form
feed as whitespace.
🤖 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/media.rs`:
- Around line 248-253: The chained recovery is swallowing the closing paren
because AnyInParensChainParseRecovery does not treat ')' as a stop token; update
the recovery so ')' halts recovery to avoid consuming the closing paren and
producing CSS_BOGUS. Concretely, when calling
parse_any_media_in_parens(...).or_recover(...), modify the recovery class or its
constructor so it also recognizes T![)] as a recovery boundary (e.g. extend
AnyInParensChainParseRecovery to include T![)] in its stop set or add an
alternative recovery that stops on T![)]), and apply the same change to the
other parse_any_media_in_parens/or_recover usage mentioned.

---

Duplicate comments:
In `@crates/biome_css_parser/src/lexer/mod.rs`:
- Around line 838-902: scan_string_body, scan_string_escape, and scan_hex_escape
currently mishandle CRLF and form feed: update them so a backslash followed by
CRLF is treated as a single line continuation (advance over both CR and LF),
CRLF is considered one newline in scan_string_body (len = 2) and
scan_string_escape should advance two bytes for CRLF sequences, and
scan_hex_escape should treat the optional whitespace after 1–6 hex digits as any
single CSS whitespace code point (space, tab, LF, CR, or FF) rather than only
space/tab; adjust the logic in scan_string_body (the CR/LF branch),
scan_string_escape (the BSL handling that advances over newline continuations),
and scan_hex_escape (the optional whitespace consumption) to consume the correct
number of bytes and include form feed as whitespace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 153c8d5a-3aa8-4542-b28a-af5b886cd8e3

📥 Commits

Reviewing files that changed from the base of the PR and between 9209833 and 485bcd8.

⛔ Files ignored due to path filters (32)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_factory/src/generated/node_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_factory/src/generated/syntax_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nesting.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_error.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_error.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/forward.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/use.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_syntax/src/generated/kind.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_syntax/src/generated/macros.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_syntax/src/generated/nodes_mut.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (71)
  • crates/biome_css_analyze/Cargo.toml
  • crates/biome_css_analyze/src/lint/correctness/no_unknown_media_feature_name.rs
  • crates/biome_css_analyze/src/lint/correctness/no_unknown_unit.rs
  • crates/biome_css_analyze/src/lint/nursery/use_baseline.rs
  • crates/biome_css_analyze/tests/spec_tests.rs
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scss
  • crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scss
  • crates/biome_css_formatter/src/css/any/attribute_matcher_value.rs
  • crates/biome_css_formatter/src/css/any/mod.rs
  • crates/biome_css_formatter/src/css/any/pseudo_value.rs
  • crates/biome_css_formatter/src/css/any/query_feature_name.rs
  • crates/biome_css_formatter/src/css/any/query_feature_value.rs
  • crates/biome_css_formatter/src/css/any/url_value.rs
  • crates/biome_css_formatter/src/css/any/value.rs
  • crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/any/import_item.rs
  • crates/biome_css_formatter/src/scss/any/interpolated_string_part.rs
  • crates/biome_css_formatter/src/scss/any/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/interpolated_string.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/string_text.rs
  • crates/biome_css_formatter/src/scss/lists/interpolated_string_part_list.rs
  • crates/biome_css_formatter/src/scss/lists/mod.rs
  • crates/biome_css_formatter/src/utils/string_utils.rs
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scss
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/options.json
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scss
  • crates/biome_css_parser/Cargo.toml
  • crates/biome_css_parser/src/lexer/mod.rs
  • crates/biome_css_parser/src/lexer/tests.rs
  • crates/biome_css_parser/src/parser.rs
  • crates/biome_css_parser/src/syntax/at_rule/feature.rs
  • crates/biome_css_parser/src/syntax/at_rule/media.rs
  • crates/biome_css_parser/src/syntax/mod.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/module_clauses.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/mod.rs
  • crates/biome_css_parser/src/syntax/scss/declaration/variable_modifier.rs
  • crates/biome_css_parser/src/syntax/scss/expression/interpolation.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/expression/primary.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/mod.rs
  • crates/biome_css_parser/src/syntax/scss/value/interpolated_string.rs
  • crates/biome_css_parser/src/syntax/scss/value/mod.rs
  • crates/biome_css_parser/src/syntax/scss/value/parent_selector.rs
  • crates/biome_css_parser/src/syntax/selector/attribute.rs
  • crates/biome_css_parser/src/syntax/selector/pseudo_class/function_value_list.rs
  • crates/biome_css_parser/src/syntax/value/url.rs
  • crates/biome_css_parser/src/token_source.rs
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.css
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scss
  • crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scss
  • crates/biome_css_syntax/src/import_ext.rs
  • crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs
💤 Files with no reviewable changes (1)
  • crates/biome_css_parser/src/syntax/scss/value/parent_selector.rs
✅ Files skipped from review due to trivial changes (27)
  • crates/biome_css_formatter/src/scss/any/import_item.rs
  • crates/biome_css_analyze/Cargo.toml
  • crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/options.json
  • crates/biome_css_parser/src/syntax/scss/at_rule/module_clauses.rs
  • crates/biome_css_formatter/src/css/any/pseudo_value.rs
  • crates/biome_css_parser/Cargo.toml
  • crates/biome_css_analyze/tests/spec_tests.rs
  • crates/biome_css_formatter/src/css/any/query_feature_value.rs
  • crates/biome_css_formatter/src/css/any/mod.rs
  • crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css
  • crates/biome_css_formatter/src/scss/any/mod.rs
  • crates/biome_css_formatter/src/scss/lists/mod.rs
  • crates/biome_css_parser/src/lexer/tests.rs
  • crates/biome_css_formatter/src/css/any/url_value.rs
  • crates/biome_css_formatter/src/css/any/value.rs
  • crates/biome_css_parser/src/syntax/value/url.rs
  • crates/biome_css_formatter/src/css/any/attribute_matcher_value.rs
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss
  • crates/biome_css_formatter/src/css/any/query_feature_name.rs
  • crates/biome_css_analyze/src/lint/nursery/use_baseline.rs
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss
  • crates/biome_css_formatter/src/scss/any/interpolated_string_part.rs
  • crates/biome_css_formatter/src/scss/lists/interpolated_string_part_list.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_parser/src/syntax/scss/value/mod.rs
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss
🚧 Files skipped from review as they are similar to previous changes (18)
  • crates/biome_css_parser/src/syntax/selector/pseudo_class/function_value_list.rs
  • crates/biome_css_parser/src/syntax/selector/attribute.rs
  • crates/biome_css_parser/src/syntax/scss/expression/primary.rs
  • crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs
  • crates/biome_css_syntax/src/import_ext.rs
  • crates/biome_css_parser/src/syntax/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/string_text.rs
  • crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs
  • crates/biome_css_formatter/src/utils/string_utils.rs
  • crates/biome_css_parser/src/syntax/scss/expression/list.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/interpolated_regular.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs
  • crates/biome_css_parser/src/syntax/scss/identifiers/interpolated_selector.rs
  • crates/biome_css_formatter/src/scss/auxiliary/interpolated_string.rs
  • crates/biome_css_parser/src/syntax/scss/expression/interpolation.rs
  • crates/biome_css_parser/src/syntax/scss/expression/mod.rs
  • crates/biome_css_parser/src/parser.rs
  • crates/biome_css_formatter/src/generated.rs

Copy link
Copy Markdown
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.

♻️ Duplicate comments (1)
crates/biome_css_parser/src/lexer/mod.rs (1)

970-975: ⚠️ Potential issue | 🟠 Major

CRLF handling after hex escapes remains incomplete.

The past review suggested handling \r\n as a single unit here, but the current code advances only 1 byte for any whitespace. This leaves \n behind when a hex escape is followed by \r\n. The fix in scan_string_escape (lines 925–931) shows the correct pattern.

🔧 Proposed fix
-        if self
-            .byte_at(cursor)
-            .is_some_and(|byte| matches!(byte, b'\t' | b' ' | b'\n' | b'\r' | 0x0C))
-        {
-            cursor += 1;
-        }
+        match self.byte_at(cursor) {
+            Some(b'\t' | b' ' | b'\n' | b'\x0C') => {
+                cursor += 1;
+            }
+            Some(b'\r') => {
+                cursor += 1;
+                if self.byte_at(cursor) == Some(b'\n') {
+                    cursor += 1;
+                }
+            }
+            _ => {}
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/lexer/mod.rs` around lines 970 - 975, After
hex-escape handling the code only advances cursor by 1 for any whitespace which
leaves a trailing '\n' when the sequence is '\r\n'; update the
whitespace-consumption logic in the lexer (the block that calls
self.byte_at(cursor) and currently does cursor += 1) to treat CRLF as a single
unit: if the current byte is b'\r' and self.byte_at(cursor + 1) is Some(b'\n')
then advance cursor by 2, otherwise advance by 1 — follow the same pattern used
in scan_string_escape for correct CRLF handling.
🧹 Nitpick comments (2)
crates/biome_css_parser/src/lexer/mod.rs (2)

972-972: Use b'\x0C' for consistency with other byte literals.

The pattern mixes b'\t', b' ', etc. with raw 0x0C. Using b'\x0C' (form feed) would be more consistent.

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

In `@crates/biome_css_parser/src/lexer/mod.rs` at line 972, The closure used in
the is_some_and check mixes byte literal styles; update the raw 0x0C byte to the
consistent byte-literal form b'\x0C' inside the closure (the is_some_and(|byte|
matches!(byte, b'\t' | b' ' | b'\n' | b'\r' | b'\x0C'))), keeping the same match
set and behavior.

1961-1962: The len field in Newline variant is never used.

You compute len at line 877–881 to distinguish \r\n from \n, but all callers (lines 749, 792) destructure with .. and ignore it. Either use it or remove it to reduce noise.

🧹 Option: remove unused field
-    Newline { position: usize, len: usize },
+    Newline { position: usize },

And simplify the scan:

-                let len = if byte == b'\r' && self.byte_at(offset + 1) == Some(b'\n') {
-                    2
-                } else {
-                    1
-                };
-
                 return StringBodyScan {
-                    stop: StringBodyScanStop::Newline { position, len },
+                    stop: StringBodyScanStop::Newline { position },
                     issues,
                 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_parser/src/lexer/mod.rs` around lines 1961 - 1962, The
Newline enum variant has an unused field len; remove len from Newline (change
Newline { position: usize, len: usize } to Newline { position: usize }) and then
update all places that construct or pattern-match it: stop computing or passing
len where Newline is created, remove the extra len-related computation in the
scanner code that currently distinguishes "\r\n" vs "\n", and simplify
destructuring sites that used ".." to just match Newline { position } (or use
explicit fields). Ensure any helper that computed len is deleted or its logic
incorporated if still needed elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/biome_css_parser/src/lexer/mod.rs`:
- Around line 970-975: After hex-escape handling the code only advances cursor
by 1 for any whitespace which leaves a trailing '\n' when the sequence is
'\r\n'; update the whitespace-consumption logic in the lexer (the block that
calls self.byte_at(cursor) and currently does cursor += 1) to treat CRLF as a
single unit: if the current byte is b'\r' and self.byte_at(cursor + 1) is
Some(b'\n') then advance cursor by 2, otherwise advance by 1 — follow the same
pattern used in scan_string_escape for correct CRLF handling.

---

Nitpick comments:
In `@crates/biome_css_parser/src/lexer/mod.rs`:
- Line 972: The closure used in the is_some_and check mixes byte literal styles;
update the raw 0x0C byte to the consistent byte-literal form b'\x0C' inside the
closure (the is_some_and(|byte| matches!(byte, b'\t' | b' ' | b'\n' | b'\r' |
b'\x0C'))), keeping the same match set and behavior.
- Around line 1961-1962: The Newline enum variant has an unused field len;
remove len from Newline (change Newline { position: usize, len: usize } to
Newline { position: usize }) and then update all places that construct or
pattern-match it: stop computing or passing len where Newline is created, remove
the extra len-related computation in the scanner code that currently
distinguishes "\r\n" vs "\n", and simplify destructuring sites that used ".." to
just match Newline { position } (or use explicit fields). Ensure any helper that
computed len is deleted or its logic incorporated if still needed elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2efdb832-f8a9-43d7-a40b-f8d64b05d30e

📥 Commits

Reviewing files that changed from the base of the PR and between 60671b7 and 71df235.

📒 Files selected for processing (1)
  • crates/biome_css_parser/src/lexer/mod.rs

@denbezrukov denbezrukov merged commit 923da30 into main Apr 1, 2026
35 checks passed
@denbezrukov denbezrukov deleted the db/scss-rework-interpolation branch April 1, 2026 20:29
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 L-Grit Language: GritQL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants