Skip to content

feat(css): add support for SCSS @for at-rule#9421

Merged
denbezrukov merged 3 commits intomainfrom
db/scss-at-rule-4
Mar 9, 2026
Merged

feat(css): add support for SCSS @for at-rule#9421
denbezrukov merged 3 commits intomainfrom
db/scss-at-rule-4

Conversation

@denbezrukov
Copy link
Contributor

Summary

Adds SCSS @for at-rule support to the CSS parser and formatter.

Test Plan

  • Added parser coverage for valid and invalid SCSS @for
  • Added formatter coverage for SCSS @for
  • Ran:
    • cargo test -p biome_css_parser
    • cargo test -p biome_css_formatter

@changeset-bot
Copy link

changeset-bot bot commented Mar 9, 2026

⚠️ No Changeset found

Latest commit: d62cf8e

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

This PR includes no changesets

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

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

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

@github-actions github-actions bot added A-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 9, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Parser conformance results on

js/262

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

jsx/babel

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

markdown/commonmark

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

symbols/microsoft

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

ts/babel

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

ts/microsoft

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

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 9, 2026

Merging this PR will not alter performance

✅ 29 untouched benchmarks
⏩ 187 skipped benchmarks1


Comparing db/scss-at-rule-4 (d62cf8e) with main (f2581b8)2

Open in CodSpeed

Footnotes

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

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

Walkthrough

Adds end-to-end support for the SCSS @for at-rule: AST node and codegen entry, lexer keywords (for, through), parser with diagnostics and helpers, parser re-exports, formatter rule and integration, generated mappings, and tests for both valid and error cases. New public formatting implementations and parser functions were introduced and the AnyCssAtRule union was extended to include ScssForAtRule.

Possibly related PRs

Suggested labels

L-SCSS

Suggested reviewers

  • ematipico
  • siketyan
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding SCSS @for at-rule support to the CSS parser and formatter.
Description check ✅ Passed The description clearly explains the changes, test coverage added, and test commands run—all directly related to the changeset.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch db/scss-at-rule-4

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs (1)

36-39: Recover to from after a missing binding.

If the binding parse fails, Line 37 still runs on the same token, so malformed input like @for 1 from 2 to 3 {} can stack a second from error and then start the lower-bound parse from the wrong place. A tiny recovery set around from / {, like the sibling @each rule uses, would keep the diagnostics much tidier.

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

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

In `@crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs` around lines
36 - 39, The binding parse (parse_scss_identifier(...).or_add_diagnostic(...))
can fail and leave the parser on the same token, causing p.expect(T![from]) and
parse_scss_expression_until(...) to run from the wrong place; capture the result
of parse_scss_identifier into a variable, and if it is ParsedSyntax::Absent
(i.e. the binding failed) perform a tiny recovery step that advances to the next
relevant sync tokens (e.g. a small set containing T![from] and T!['{'] similar
to the `@each` rule) before calling p.expect(T![from]) and
parse_scss_expression_until(..., SCSS_FOR_LOWER_BOUND_END_SET) so diagnostics
and position are correct (use expected_scss_for_binding /
expected_scss_expression for diagnostics as currently done).
🤖 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/for_at_rule.rs`:
- Around line 36-39: The binding parse
(parse_scss_identifier(...).or_add_diagnostic(...)) can fail and leave the
parser on the same token, causing p.expect(T![from]) and
parse_scss_expression_until(...) to run from the wrong place; capture the result
of parse_scss_identifier into a variable, and if it is ParsedSyntax::Absent
(i.e. the binding failed) perform a tiny recovery step that advances to the next
relevant sync tokens (e.g. a small set containing T![from] and T!['{'] similar
to the `@each` rule) before calling p.expect(T![from]) and
parse_scss_expression_until(..., SCSS_FOR_LOWER_BOUND_END_SET) so diagnostics
and position are correct (use expected_scss_for_binding /
expected_scss_expression for diagnostics as currently done).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9bc46fca-8991-48bf-b834-fd6d166269e5

📥 Commits

Reviewing files that changed from the base of the PR and between 6742246 and 5a6f64c.

⛔ Files ignored due to path filters (8)
  • 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_parser/tests/css_test_suite/error/scss/at-rule/for.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.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 (16)
  • crates/biome_css_formatter/src/css/any/at_rule.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/statements/for_at_rule.rs
  • crates/biome_css_formatter/src/scss/statements/mod.rs
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss
  • crates/biome_css_parser/src/lexer/mod.rs
  • crates/biome_css_parser/src/lexer/tests.rs
  • crates/biome_css_parser/src/syntax/at_rule/mod.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs
  • crates/biome_css_parser/src/syntax/scss/mod.rs
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss
  • 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs (1)

53-60: Consider returning ParsedSyntax for consistency.

The parse_scss_for_range_operator function doesn't return a ParsedSyntax, unlike most other parse_* functions. Whilst it works correctly, returning ParsedSyntax would allow the caller to use .or_add_diagnostic() uniformly and match the codebase convention.

That said, for a simple keyword bump this is a minor stylistic point.

♻️ Optional refactor
 #[inline]
-fn parse_scss_for_range_operator(p: &mut CssParser) {
+fn parse_scss_for_range_operator(p: &mut CssParser) -> ParsedSyntax {
     if p.at(T![to]) || p.at(T![through]) {
+        let m = p.start();
         p.bump_any();
+        Present(m.complete(p, SCSS_FOR_RANGE_OPERATOR))
     } else {
-        p.error(expected_scss_for_range_operator(p, p.cur_range()));
+        Absent
     }
 }

Then in the caller:

-    parse_scss_for_range_operator(p);
+    parse_scss_for_range_operator(p).or_add_diagnostic(p, expected_scss_for_range_operator);

Based on learnings: "Parse rules must take a mutable reference to the parser as their only parameter and return a ParsedSyntax".

🤖 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/for_at_rule.rs` around lines
53 - 60, parse_scss_for_range_operator currently has no return value; change its
signature to return ParsedSyntax and return the bump/error result so callers can
chain .or_add_diagnostic(). Specifically, update fn
parse_scss_for_range_operator(p: &mut CssParser) to return ParsedSyntax, return
the result of p.bump_any() when T![to] or T![through] is present, and return the
result of p.error(expected_scss_for_range_operator(p, p.cur_range())) in the
else branch; keep the same helper expected_scss_for_range_operator and
p.cur_range() usage so callers (e.g., sites that will call .or_add_diagnostic())
can use the normalized ParsedSyntax return value.
🤖 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_formatter/src/scss/statements/for_at_rule.rs`:
- Around line 10-35: The code is formatting SyntaxResult<_> fields directly and
can panic on recovered/malformed `@for` nodes; after calling node.as_fields()
(ScssForAtRuleFields), `?`-resolve each SyntaxResult field (e.g., lower_bound,
upper_bound, operator, block, variable, from_token, for_token) into concrete
values before calling write! so any parse errors return early instead of causing
a panic; update the code to bind resolved values (let lower_bound =
lower_bound?; etc.) and then pass those resolved values into the write! call
that formats the for_at_rule.

---

Nitpick comments:
In `@crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs`:
- Around line 53-60: parse_scss_for_range_operator currently has no return
value; change its signature to return ParsedSyntax and return the bump/error
result so callers can chain .or_add_diagnostic(). Specifically, update fn
parse_scss_for_range_operator(p: &mut CssParser) to return ParsedSyntax, return
the result of p.bump_any() when T![to] or T![through] is present, and return the
result of p.error(expected_scss_for_range_operator(p, p.cur_range())) in the
else branch; keep the same helper expected_scss_for_range_operator and
p.cur_range() usage so callers (e.g., sites that will call .or_add_diagnostic())
can use the normalized ParsedSyntax return value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8bce789c-a6be-4a39-b261-b4d5e4418f5c

📥 Commits

Reviewing files that changed from the base of the PR and between d361224 and d62cf8e.

⛔ Files ignored due to path filters (9)
  • 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/for.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.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 (16)
  • crates/biome_css_formatter/src/css/any/at_rule.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/statements/for_at_rule.rs
  • crates/biome_css_formatter/src/scss/statements/mod.rs
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss
  • crates/biome_css_parser/src/lexer/mod.rs
  • crates/biome_css_parser/src/lexer/tests.rs
  • crates/biome_css_parser/src/syntax/at_rule/mod.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs
  • crates/biome_css_parser/src/syntax/scss/mod.rs
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss
  • 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 skipped from review as they are similar to previous changes (12)
  • crates/biome_css_formatter/src/scss/statements/mod.rs
  • crates/biome_css_parser/src/lexer/tests.rs
  • xtask/codegen/src/css_kinds_src.rs
  • crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss
  • crates/biome_css_parser/src/lexer/mod.rs
  • crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss
  • crates/biome_css_parser/src/syntax/scss/mod.rs
  • xtask/codegen/css.ungram
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/css/any/at_rule.rs

Comment on lines +10 to +35
let ScssForAtRuleFields {
for_token,
variable,
from_token,
lower_bound,
operator,
upper_bound,
block,
} = node.as_fields();

write!(
f,
[
for_token.format(),
space(),
variable.format(),
space(),
from_token.format(),
space(),
lower_bound.format(),
space(),
operator.format(),
space(),
upper_bound.format(),
space(),
block.format()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid a panic on recovered @for nodes.

Line 23 through Line 35 format SyntaxResult<_> fields directly. If the parser recovers a malformed or incomplete @for, this path can panic instead of returning a normal formatting error. Please resolve the fields with ? before write!.

🩹 Suggested fix
         let ScssForAtRuleFields {
             for_token,
             variable,
             from_token,
             lower_bound,
             operator,
             upper_bound,
             block,
         } = node.as_fields();
+        let for_token = for_token?;
+        let variable = variable?;
+        let from_token = from_token?;
+        let lower_bound = lower_bound?;
+        let operator = operator?;
+        let upper_bound = upper_bound?;
+        let block = block?;
 
         write!(
             f,
             [
                 for_token.format(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ScssForAtRuleFields {
for_token,
variable,
from_token,
lower_bound,
operator,
upper_bound,
block,
} = node.as_fields();
write!(
f,
[
for_token.format(),
space(),
variable.format(),
space(),
from_token.format(),
space(),
lower_bound.format(),
space(),
operator.format(),
space(),
upper_bound.format(),
space(),
block.format()
let ScssForAtRuleFields {
for_token,
variable,
from_token,
lower_bound,
operator,
upper_bound,
block,
} = node.as_fields();
let for_token = for_token?;
let variable = variable?;
let from_token = from_token?;
let lower_bound = lower_bound?;
let operator = operator?;
let upper_bound = upper_bound?;
let block = block?;
write!(
f,
[
for_token.format(),
space(),
variable.format(),
space(),
from_token.format(),
space(),
lower_bound.format(),
space(),
operator.format(),
space(),
upper_bound.format(),
space(),
block.format()
🤖 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/for_at_rule.rs` around lines
10 - 35, The code is formatting SyntaxResult<_> fields directly and can panic on
recovered/malformed `@for` nodes; after calling node.as_fields()
(ScssForAtRuleFields), `?`-resolve each SyntaxResult field (e.g., lower_bound,
upper_bound, operator, block, variable, from_token, for_token) into concrete
values before calling write! so any parse errors return early instead of causing
a panic; update the code to bind resolved values (let lower_bound =
lower_bound?; etc.) and then pass those resolved values into the write! call
that formats the for_at_rule.

@denbezrukov denbezrukov merged commit ecd00e8 into main Mar 9, 2026
18 checks passed
@denbezrukov denbezrukov deleted the db/scss-at-rule-4 branch March 9, 2026 18:52
@Netail Netail added the L-SCSS label Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Formatter Area: formatter A-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.

2 participants