feat(css): add support for SCSS string interpolation#9756
Conversation
|
Merging this PR will not alter performance
Comparing Footnotes
|
Parser conformance results onjs/262
jsx/babel
markdown/commonmark
symbols/microsoft
ts/babel
ts/microsoft
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorMove
ScssInterpolatedStringfromAnyScssImportIteminto 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 throughScssPlainImport.urlbecause it still usesAnyCssImportUrl. IntroduceAnyScssImportUrlto include interpolated strings, then use it inScssPlainImport.url.Don't forget to run
just gen-grammar cssbefore 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_SETis also defined inexpression/interpolation.rs. Consider consolidating into a shared constant to prevent drift.♻️ Possible consolidation
Move the constant to a shared location (e.g.,
expression/mod.rsor 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
⛔ Files ignored due to path filters (32)
Cargo.lockis excluded by!**/*.lockand included by**crates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_factory/src/generated/node_factory.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_factory/src/generated/syntax_factory.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nesting.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_error.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_error.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/forward.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/use.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_syntax/src/generated/kind.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_syntax/src/generated/macros.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_syntax/src/generated/nodes.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_syntax/src/generated/nodes_mut.rsis excluded by!**/generated/**,!**/generated/**and included by**
📒 Files selected for processing (70)
crates/biome_css_analyze/Cargo.tomlcrates/biome_css_analyze/src/lint/correctness/no_unknown_media_feature_name.rscrates/biome_css_analyze/src/lint/correctness/no_unknown_unit.rscrates/biome_css_analyze/src/lint/nursery/use_baseline.rscrates/biome_css_analyze/tests/spec_tests.rscrates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scsscrates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scsscrates/biome_css_formatter/src/css/any/attribute_matcher_value.rscrates/biome_css_formatter/src/css/any/mod.rscrates/biome_css_formatter/src/css/any/pseudo_value.rscrates/biome_css_formatter/src/css/any/query_feature_name.rscrates/biome_css_formatter/src/css/any/query_feature_value.rscrates/biome_css_formatter/src/css/any/url_value.rscrates/biome_css_formatter/src/css/any/value.rscrates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rscrates/biome_css_formatter/src/generated.rscrates/biome_css_formatter/src/scss/any/import_item.rscrates/biome_css_formatter/src/scss/any/interpolated_string_part.rscrates/biome_css_formatter/src/scss/any/mod.rscrates/biome_css_formatter/src/scss/auxiliary/interpolated_string.rscrates/biome_css_formatter/src/scss/auxiliary/mod.rscrates/biome_css_formatter/src/scss/auxiliary/string_text.rscrates/biome_css_formatter/src/scss/lists/interpolated_string_part_list.rscrates/biome_css_formatter/src/scss/lists/mod.rscrates/biome_css_formatter/src/utils/string_utils.rscrates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scsscrates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scsscrates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scsscrates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/options.jsoncrates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scsscrates/biome_css_parser/Cargo.tomlcrates/biome_css_parser/src/lexer/mod.rscrates/biome_css_parser/src/lexer/tests.rscrates/biome_css_parser/src/parser.rscrates/biome_css_parser/src/syntax/at_rule/feature.rscrates/biome_css_parser/src/syntax/at_rule/media.rscrates/biome_css_parser/src/syntax/mod.rscrates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rscrates/biome_css_parser/src/syntax/scss/expression/interpolation.rscrates/biome_css_parser/src/syntax/scss/expression/list.rscrates/biome_css_parser/src/syntax/scss/expression/mod.rscrates/biome_css_parser/src/syntax/scss/expression/primary.rscrates/biome_css_parser/src/syntax/scss/identifiers/interpolated_regular.rscrates/biome_css_parser/src/syntax/scss/identifiers/interpolated_selector.rscrates/biome_css_parser/src/syntax/scss/mod.rscrates/biome_css_parser/src/syntax/scss/value/interpolated_string.rscrates/biome_css_parser/src/syntax/scss/value/mod.rscrates/biome_css_parser/src/syntax/scss/value/parent_selector.rscrates/biome_css_parser/src/syntax/selector/attribute.rscrates/biome_css_parser/src/syntax/selector/pseudo_class/function_value_list.rscrates/biome_css_parser/src/syntax/value/url.rscrates/biome_css_parser/src/token_source.rscrates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.csscrates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.csscrates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scsscrates/biome_css_syntax/src/import_ext.rscrates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rsxtask/codegen/css.ungramxtask/codegen/src/css_kinds_src.rs
💤 Files with no reviewable changes (1)
- crates/biome_css_parser/src/syntax/scss/value/parent_selector.rs
xtask/codegen/src/css_kinds_src.rs
Outdated
| @@ -322,6 +323,8 @@ pub const CSS_KINDS_SRC: KindsSrc = KindsSrc { | |||
| "COMMENT", | |||
| "MULTILINE_COMMENT", | |||
| "GRIT_METAVARIABLE", | |||
| "SCSS_RECOVERED_OUTER_STRING_QUOTE", | |||
There was a problem hiding this comment.
This new token feels a bit unusual. Why does it exist?
There was a problem hiding this comment.
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.
| if CssSyntaxFeatures::Scss.is_supported(p) { | ||
| is_at_scss_interpolated_identifier(p) | ||
| } else { | ||
| is_at_identifier(p) | ||
| } |
There was a problem hiding this comment.
We usually use parse_exclusive_syntax for this kind of thing
| if CssSyntaxFeatures::Scss.is_supported(p) { | ||
| parse_scss_interpolated_identifier(p) | ||
| } else { | ||
| parse_regular_identifier(p) | ||
| } |
| 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; | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Good point. Maybe add some TODO/NOTE in the part of the codes that need changing once SCSS is ready
There was a problem hiding this comment.
ah, yeah you're right. all's good then
…ved classification for SCSS string interpolation
fd1cfc1 to
41a3cfd
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
crates/biome_css_parser/src/lexer/mod.rs (1)
838-902:⚠️ Potential issue | 🟠 MajorCSS escape handling is still short-changing CRLF and form feed
scan_string_bodystill treats only\n/\ras string-breaking newlines,scan_string_escapestill advances only one byte for\\\r\n, andscan_hex_escapestill 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
⛔ Files ignored due to path filters (32)
Cargo.lockis excluded by!**/*.lockand included by**crates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_factory/src/generated/node_factory.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_factory/src/generated/syntax_factory.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nesting.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_error.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_error.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/forward.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/use.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_syntax/src/generated/kind.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_syntax/src/generated/macros.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_syntax/src/generated/nodes.rsis excluded by!**/generated/**,!**/generated/**and included by**crates/biome_css_syntax/src/generated/nodes_mut.rsis excluded by!**/generated/**,!**/generated/**and included by**
📒 Files selected for processing (71)
crates/biome_css_analyze/Cargo.tomlcrates/biome_css_analyze/src/lint/correctness/no_unknown_media_feature_name.rscrates/biome_css_analyze/src/lint/correctness/no_unknown_unit.rscrates/biome_css_analyze/src/lint/nursery/use_baseline.rscrates/biome_css_analyze/tests/spec_tests.rscrates/biome_css_analyze/tests/specs/correctness/noUnknownMediaFeatureName/invalid.scsscrates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.scsscrates/biome_css_formatter/src/css/any/attribute_matcher_value.rscrates/biome_css_formatter/src/css/any/mod.rscrates/biome_css_formatter/src/css/any/pseudo_value.rscrates/biome_css_formatter/src/css/any/query_feature_name.rscrates/biome_css_formatter/src/css/any/query_feature_value.rscrates/biome_css_formatter/src/css/any/url_value.rscrates/biome_css_formatter/src/css/any/value.rscrates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rscrates/biome_css_formatter/src/generated.rscrates/biome_css_formatter/src/scss/any/import_item.rscrates/biome_css_formatter/src/scss/any/interpolated_string_part.rscrates/biome_css_formatter/src/scss/any/mod.rscrates/biome_css_formatter/src/scss/auxiliary/interpolated_string.rscrates/biome_css_formatter/src/scss/auxiliary/mod.rscrates/biome_css_formatter/src/scss/auxiliary/string_text.rscrates/biome_css_formatter/src/scss/lists/interpolated_string_part_list.rscrates/biome_css_formatter/src/scss/lists/mod.rscrates/biome_css_formatter/src/utils/string_utils.rscrates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scsscrates/biome_css_formatter/tests/specs/css/scss/at-rule/media-interpolation.scsscrates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/interpolated-string.scsscrates/biome_css_formatter/tests/specs/css/scss/declaration/quote_style/options.jsoncrates/biome_css_formatter/tests/specs/css/scss/declaration/string-interpolation.scsscrates/biome_css_parser/Cargo.tomlcrates/biome_css_parser/src/lexer/mod.rscrates/biome_css_parser/src/lexer/tests.rscrates/biome_css_parser/src/parser.rscrates/biome_css_parser/src/syntax/at_rule/feature.rscrates/biome_css_parser/src/syntax/at_rule/media.rscrates/biome_css_parser/src/syntax/mod.rscrates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rscrates/biome_css_parser/src/syntax/scss/at_rule/module_clauses.rscrates/biome_css_parser/src/syntax/scss/declaration/mod.rscrates/biome_css_parser/src/syntax/scss/declaration/variable_modifier.rscrates/biome_css_parser/src/syntax/scss/expression/interpolation.rscrates/biome_css_parser/src/syntax/scss/expression/list.rscrates/biome_css_parser/src/syntax/scss/expression/mod.rscrates/biome_css_parser/src/syntax/scss/expression/primary.rscrates/biome_css_parser/src/syntax/scss/identifiers/interpolated_regular.rscrates/biome_css_parser/src/syntax/scss/identifiers/interpolated_selector.rscrates/biome_css_parser/src/syntax/scss/mod.rscrates/biome_css_parser/src/syntax/scss/value/interpolated_string.rscrates/biome_css_parser/src/syntax/scss/value/mod.rscrates/biome_css_parser/src/syntax/scss/value/parent_selector.rscrates/biome_css_parser/src/syntax/selector/attribute.rscrates/biome_css_parser/src/syntax/selector/pseudo_class/function_value_list.rscrates/biome_css_parser/src/syntax/value/url.rscrates/biome_css_parser/src/token_source.rscrates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_container/at_rule_container_missing_query_value.csscrates/biome_css_parser/tests/css_test_suite/error/at_rule/at_rule_media_condition_required.csscrates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/media-interpolation.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/interpolation-non-string-quote.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-escape.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-invalid-string-text.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation-recovery-after-inner-string.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/value/string-interpolation.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/media-interpolation.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/selector/interpolated-string-values.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/value/plain-vs-interpolated-string.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation-nested-strings.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/value/string-interpolation.scsscrates/biome_css_syntax/src/import_ext.rscrates/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
There was a problem hiding this comment.
♻️ Duplicate comments (1)
crates/biome_css_parser/src/lexer/mod.rs (1)
970-975:⚠️ Potential issue | 🟠 MajorCRLF handling after hex escapes remains incomplete.
The past review suggested handling
\r\nas a single unit here, but the current code advances only 1 byte for any whitespace. This leaves\nbehind when a hex escape is followed by\r\n. The fix inscan_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: Useb'\x0C'for consistency with other byte literals.The pattern mixes
b'\t',b' ', etc. with raw0x0C. Usingb'\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: Thelenfield inNewlinevariant is never used.You compute
lenat line 877–881 to distinguish\r\nfrom\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
📒 Files selected for processing (1)
crates/biome_css_parser/src/lexer/mod.rs
…ng optional stop tokens
Summary
Adds first-class support for SCSS interpolated strings.
Quoted SCSS strings
Nested string-origin interpolation
Selector string values
import
Media / container queries
Test Plan
green CI, new tests