feat(css): add support for SCSS @import and @extend at-rules#9458
feat(css): add support for SCSS @import and @extend at-rules#9458denbezrukov merged 4 commits intomainfrom
@import and @extend at-rules#9458Conversation
|
Parser conformance results onjs/262
jsx/babel
markdown/commonmark
symbols/microsoft
ts/babel
ts/microsoft
|
Merging this PR will not alter performance
Comparing Footnotes
|
WalkthroughAdds SCSS 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)
📝 Coding Plan for PR comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/biome_css_parser/src/syntax/selector/mod.rs (1)
309-328:⚠️ Potential issue | 🟡 MinorKeep
%placeholderon the conditional-syntax path.By folding the SCSS feature check into simple-selector prediction, plain CSS now sees
%fooas “not a selector” rather than “unsupported SCSS syntax”. That skips the usual feature-gated bogus-node path and weakens recovery/diagnostics for CSS files. Please route placeholder parsing through the existingConditionalParsedSyntax/or_invalid_to_bogus()pattern instead. Based on learnings: UseConditionalParsedSyntaxfor syntax that is only valid in specific contexts (e.g., strict mode, file types, language versions) and callor_invalid_to_bogus()to convert to a bogus node if not supported.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_css_parser/src/syntax/selector/mod.rs` around lines 309 - 328, The parser currently short-circuits SCSS placeholder parsing in parse_simple_selector by returning parse_scss_placeholder_selector when is_nth_at_scss_placeholder_selector is true, which bypasses feature-gating; instead, remove the direct return and route placeholder parsing through the conditional-syntax pattern: when is_nth_at_scss_placeholder_selector(p, 0) is true, wrap the parse_scss_placeholder_selector(p) call in a ConditionalParsedSyntax (or use ConditionalParsedSyntax::new/appropriate constructor) and then call .or_invalid_to_bogus() so that if the SCSS feature is disabled it converts to a bogus node/invalid syntax; keep references to parse_simple_selector, is_nth_at_scss_placeholder_selector, parse_scss_placeholder_selector, ConditionalParsedSyntax, and or_invalid_to_bogus when making the change.
🧹 Nitpick comments (3)
crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss (1)
1-4: Add a recovery sentinel after the broken@extends.At the moment this only proves diagnostics at EOF. A tiny valid rule afterwards would also prove the parser recovers cleanly after malformed
@extendstatements.Possible fixture tweak
`@extend`; `@extend` !optional; `@extend` .message !; `@extend` .message + +.after { + color: red; +}Based on learnings, "All code changes MUST include tests - lint rules need snapshot tests in
tests/specs/{group}/{rule}/, formatter changes need snapshot tests with valid/invalid cases, parser changes need test files covering valid and error cases"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss` around lines 1 - 4, Add a small valid CSS rule after the malformed `@extend` statements in extend.scss so the parser has a recovery sentinel and we can assert it continues parsing beyond EOF; locate the broken tokens (the lines containing the `@extend` usages: `@extend;`, `@extend !optional;`, `@extend .message !;`, and the unterminated `@extend .message`) and append a simple valid rule (e.g., a selector with a property declaration) to prove recovery and update snapshots accordingly.crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss (1)
1-22: Please cover the trickier import-classification branches too.This fixture hits the easy cases, but the new formatter logic also has to tell CSS imports from plain SCSS imports for inputs like
.CSS?cache=1#section, protocol-relative URLs, andhttps://…. Those branches are only pinned in the parser suite right now, which leaves a sneaky formatter regression path open.Possible additions
`@import` "print.css" print , screen; `@import` url("theme.css"); + +@import "theme.CSS?cache=1#section"; + +@import "//example.com/theme"; + +@import "https://example.com/theme.css";Based on learnings, "All code changes MUST include tests - lint rules need snapshot tests in
tests/specs/{group}/{rule}/, formatter changes need 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/tests/specs/css/scss/at-rule/import.scss` around lines 1 - 22, Add snapshot cases to the import.scss fixture to cover the tricky import-classification branches the formatter must handle: include imports with query/hash like `@import` "theme.CSS?cache=1#section"; protocol-relative and absolute URLs like `@import` "//cdn.example.com/library.css"; `@import` "https://example.com/style.css"; and mixed forms with media lists (e.g., `@import` "print.css" print, screen;) and url(...) variants (e.g., `@import` url("//cdn.example.com/x.css?ver=1#a") screen;). Update tests/specs/css/scss/at-rule/import.scss by adding these lines so the formatter snapshot verifies CSS vs SCSS import detection for all edge cases.crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss (1)
1-16: Please add non-rule-body contexts to this formatter spec.All the
@extendsamples here live inside qualified rules. The new formatter is also wired into root statement lists and nested directive blocks, so I'd add one top-level@extendand one inside@includeto pin those paths as well.Possible additions
.alert{ `@extend` .message,.info; `@extend` a:hover; } + +@extend .message; + +@include themed-button { + `@extend` .message; +}Based on learnings, "All code changes MUST include tests - lint rules need snapshot tests in
tests/specs/{group}/{rule}/, formatter changes need 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/tests/specs/css/scss/at-rule/extend.scss` around lines 1 - 16, Add non-rule-body contexts for `@extend` by inserting one top-level/root statement `@extend` example and one `@extend` nested inside a directive block (use an `@include` block) alongside the existing qualified-rule examples; e.g., add a root-level use that extends %toolbelt and an `@include` { `@extend` %toolbelt; } case to ensure the formatter is exercised in root statement lists and nested directive blocks, and update the formatter snapshot tests accordingly.
🤖 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/tests/css_test_suite/error/scss/at-rule/extend_interpolation.scss`:
- Line 1: The test case currently treating "@extend .icon-#{$name};" as an error
is incorrect; update the test organization by moving the case with the
interpolated selector ".icon-#{$name}" out of the error suite and into the
valid/accepted Sass test suite so the parser recognizes interpolation in `@extend`
as valid per Sass spec; ensure the test harness references the same literal
"@extend .icon-#{$name};" content when relocating the file.
---
Outside diff comments:
In `@crates/biome_css_parser/src/syntax/selector/mod.rs`:
- Around line 309-328: The parser currently short-circuits SCSS placeholder
parsing in parse_simple_selector by returning parse_scss_placeholder_selector
when is_nth_at_scss_placeholder_selector is true, which bypasses feature-gating;
instead, remove the direct return and route placeholder parsing through the
conditional-syntax pattern: when is_nth_at_scss_placeholder_selector(p, 0) is
true, wrap the parse_scss_placeholder_selector(p) call in a
ConditionalParsedSyntax (or use ConditionalParsedSyntax::new/appropriate
constructor) and then call .or_invalid_to_bogus() so that if the SCSS feature is
disabled it converts to a bogus node/invalid syntax; keep references to
parse_simple_selector, is_nth_at_scss_placeholder_selector,
parse_scss_placeholder_selector, ConditionalParsedSyntax, and
or_invalid_to_bogus when making the change.
---
Nitpick comments:
In `@crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss`:
- Around line 1-16: Add non-rule-body contexts for `@extend` by inserting one
top-level/root statement `@extend` example and one `@extend` nested inside a
directive block (use an `@include` block) alongside the existing qualified-rule
examples; e.g., add a root-level use that extends %toolbelt and an `@include` {
`@extend` %toolbelt; } case to ensure the formatter is exercised in root statement
lists and nested directive blocks, and update the formatter snapshot tests
accordingly.
In `@crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss`:
- Around line 1-22: Add snapshot cases to the import.scss fixture to cover the
tricky import-classification branches the formatter must handle: include imports
with query/hash like `@import` "theme.CSS?cache=1#section"; protocol-relative and
absolute URLs like `@import` "//cdn.example.com/library.css"; `@import`
"https://example.com/style.css"; and mixed forms with media lists (e.g., `@import`
"print.css" print, screen;) and url(...) variants (e.g., `@import`
url("//cdn.example.com/x.css?ver=1#a") screen;). Update
tests/specs/css/scss/at-rule/import.scss by adding these lines so the formatter
snapshot verifies CSS vs SCSS import detection for all edge cases.
In `@crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss`:
- Around line 1-4: Add a small valid CSS rule after the malformed `@extend`
statements in extend.scss so the parser has a recovery sentinel and we can
assert it continues parsing beyond EOF; locate the broken tokens (the lines
containing the `@extend` usages: `@extend;`, `@extend !optional;`, `@extend
.message !;`, and the unterminated `@extend .message`) and append a simple valid
rule (e.g., a selector with a property declaration) to prove recovery and update
snapshots accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6580e767-0da1-4dff-855d-885e8e14167f
⛔ Files ignored due to path filters (21)
crates/biome_css_analyze/tests/specs/suspicious/noUnknownAtRules/valid.css.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/extend.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend_interpolation.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector_list.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_relative_selector_list.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/disabled/pseudo_class_function_selector_disabled.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/enabled/pseudo_class_function_selector_enabled.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector_list.css.snapis excluded by!**/*.snapand included by**crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.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/supports-expression.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 (42)
crates/biome_css_analyze/src/lint/nursery/use_baseline.rscrates/biome_css_formatter/src/css/any/at_rule.rscrates/biome_css_formatter/src/css/any/simple_selector.rscrates/biome_css_formatter/src/css/statements/import_at_rule.rscrates/biome_css_formatter/src/generated.rscrates/biome_css_formatter/src/scss/any/import_item.rscrates/biome_css_formatter/src/scss/any/mod.rscrates/biome_css_formatter/src/scss/auxiliary/extend_optional_modifier.rscrates/biome_css_formatter/src/scss/auxiliary/mod.rscrates/biome_css_formatter/src/scss/auxiliary/plain_import.rscrates/biome_css_formatter/src/scss/lists/import_item_list.rscrates/biome_css_formatter/src/scss/lists/mod.rscrates/biome_css_formatter/src/scss/mod.rscrates/biome_css_formatter/src/scss/selectors/mod.rscrates/biome_css_formatter/src/scss/selectors/placeholder_selector.rscrates/biome_css_formatter/src/scss/statements/extend_at_rule.rscrates/biome_css_formatter/src/scss/statements/import_at_rule.rscrates/biome_css_formatter/src/scss/statements/mod.rscrates/biome_css_formatter/src/utils/import.rscrates/biome_css_formatter/src/utils/mod.rscrates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scsscrates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scsscrates/biome_css_parser/src/lexer/mod.rscrates/biome_css_parser/src/syntax/at_rule/import.rscrates/biome_css_parser/src/syntax/at_rule/media.rscrates/biome_css_parser/src/syntax/at_rule/mod.rscrates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rscrates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rscrates/biome_css_parser/src/syntax/scss/at_rule/mod.rscrates/biome_css_parser/src/syntax/scss/mod.rscrates/biome_css_parser/src/syntax/scss/selector.rscrates/biome_css_parser/src/syntax/selector/mod.rscrates/biome_css_parser/src/syntax/value/url.rscrates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend_interpolation.scsscrates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scsscrates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scsscrates/biome_css_semantic/src/semantic_model/specificity.rscrates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rsxtask/codegen/css.ungramxtask/codegen/src/css_kinds_src.rs
| @@ -0,0 +1 @@ | |||
| @extend .icon-#{$name}; | |||
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
site:sass-lang.com "@extend" interpolation Sass official docs
💡 Result:
Sass’s official docs explicitly say you can use interpolation (#{...}) inside @extend rules—i.e., the selector you’re extending can be dynamically constructed. Interpolation “can be used almost anywhere,” including “@extends.” [1]
Related official references:
@extendlanguage reference (behavior/scope/limitations). [2]sass:selectormodule:selector.extend()documents@extend-equivalent behavior and shows@extend #{$extendee};in its definition example (interpolation used in an@extend). [5]
Move this to the valid suite—@extend with interpolation is standard Sass.
Sass docs explicitly permit interpolation in @extend selectors, and the sass:selector module even shows @extend #{$extendee}; as a valid example. Keeping .icon-#{$name} in the error suite flags a false negative.
🧰 Tools
🪛 Stylelint (17.4.0)
[error] 1-1: Expected a placeholder selector (e.g. %placeholder) to be used in @extend (scss/at-extend-no-missing-placeholder)
(scss/at-extend-no-missing-placeholder)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend_interpolation.scss`
at line 1, The test case currently treating "@extend .icon-#{$name};" as an
error is incorrect; update the test organization by moving the case with the
interpolated selector ".icon-#{$name}" out of the error suite and into the
valid/accepted Sass test suite so the parser recognizes interpolation in `@extend`
as valid per Sass spec; ensure the test harness references the same literal
"@extend .icon-#{$name};" content when relocating the file.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/biome_css_formatter/src/scss/statements/extend_at_rule.rs (1)
16-24: Remove the redundant innergroup().The selector list is unnecessarily wrapped in
group()at line 17, since the outergroup()at line 31 already keeps the selector list and optional modifier together as a single unit when breaking. Other similar formatters in the codebase (page_at_rule.rs, keyframes_item.rs) use only a single group for the same purpose.♻️ Suggested simplification
let target = format_once(|f| { - write!(f, [group(&css_selector_list.format())])?; + write!(f, [css_selector_list.format()])?; if let Some(optional_modifier) = optional_modifier { write!(f, [space(), optional_modifier.format()])?; } Ok(()) });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_css_formatter/src/scss/statements/extend_at_rule.rs` around lines 16 - 24, The selector list is wrapped twice in group(): remove the inner group() call inside the closure that builds target so the outer group that wraps format_once already controls breaking; specifically update the format_once closure used to build target (the code calling format_once with write!(f, [group(&css_selector_list.format())])?) to write the selector formatter directly (use css_selector_list.format() without group) while leaving the outer group(...) around the entire target/optional_modifier intact; ensure optional_modifier handling and spacing remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/biome_css_formatter/src/scss/statements/extend_at_rule.rs`:
- Around line 16-24: The selector list is wrapped twice in group(): remove the
inner group() call inside the closure that builds target so the outer group that
wraps format_once already controls breaking; specifically update the format_once
closure used to build target (the code calling format_once with write!(f,
[group(&css_selector_list.format())])?) to write the selector formatter directly
(use css_selector_list.format() without group) while leaving the outer
group(...) around the entire target/optional_modifier intact; ensure
optional_modifier handling and spacing remain unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5af7c2c8-b938-412b-aab4-a840de1a15da
📒 Files selected for processing (3)
crates/biome_css_formatter/src/scss/statements/extend_at_rule.rscrates/biome_css_parser/src/syntax/at_rule/mod.rscrates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs
crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs
Outdated
Show resolved
Hide resolved
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs (1)
71-74: Consider adding lookahead foroptionalkeyword.The predicate only checks for
!without verifying the following token isoptional. In the@extendcontext this works fine, but a more precise check (e.g.,p.at(T![!]) && p.nth_at(1, T![optional])) could yield clearer error messages if someone mistakenly writes!important.That said, the current approach is perfectly functional — just a thought for later polish.
🤖 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/at_rule/extend_at_rule.rs` around lines 71 - 74, is_at_scss_extend_optional_modifier currently only checks for a bang token (p.at(T![!])) which can false-positive for sequences like "!important"; update the predicate to ensure the bang is followed by the literal "optional" (e.g., use p.at(T![!]) && p.nth_at(1, T![optional])) so the function (is_at_scss_extend_optional_modifier) only returns true for the exact "@extend !optional" modifier and yields clearer diagnostics for cases like "!important".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs`:
- Around line 71-74: is_at_scss_extend_optional_modifier currently only checks
for a bang token (p.at(T![!])) which can false-positive for sequences like
"!important"; update the predicate to ensure the bang is followed by the literal
"optional" (e.g., use p.at(T![!]) && p.nth_at(1, T![optional])) so the function
(is_at_scss_extend_optional_modifier) only returns true for the exact "@extend
!optional" modifier and yields clearer diagnostics for cases like "!important".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8c472e92-646b-4fff-94b1-2f6633fd8285
📒 Files selected for processing (1)
crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs
Summary
Adds support for SCSS @import and @extend at-rules.
Test Plan
cargo test -p biome_css_parser/formatter/analyzer