Skip to content

feat(css_formatter): improve SCSS @each rule formatting#10202

Merged
denbezrukov merged 3 commits into
mainfrom
db/scss-formatter-10
May 2, 2026
Merged

feat(css_formatter): improve SCSS @each rule formatting#10202
denbezrukov merged 3 commits into
mainfrom
db/scss-formatter-10

Conversation

@denbezrukov

Copy link
Copy Markdown
Contributor

This PR was implemented with assistance from Codex AI.

Summary

Aligned @each formatting with Prettier for source-tight in spacing and direct map/list iterables.

@each $item in$list {
	color: $item;
}

@each $animal, $color, $cursor in (puma, black, default),
	(sea-slug, blue, pointer), (egret, white, move)
{
}

@each $element, $size in(h1: 20px, h2: 16px, h3: 14px) {
}

@each $element, $size in (h1: 20px, h2: 16px, h3: 14px) {
}

Test Plan

  • cargo test -p biome_css_formatter --test

@changeset-bot

changeset-bot Bot commented May 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: df6d204

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 May 2, 2026
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 53188 53188 0
Passed 51970 51970 0
Failed 1176 1176 0
Panics 42 42 0
Coverage 97.71% 97.71% 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 658 658 0
Passed 574 574 0
Failed 84 84 0
Panics 0 0 0
Coverage 87.23% 87.23% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18876 18876 0
Passed 13010 13010 0
Failed 5865 5865 0
Panics 1 1 0
Coverage 68.92% 68.92% 0.00%

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces a dedicated ScssEachHeader AST node and a FormatScssEachHeader formatter to centralise @each header formatting (bindings, in, iterable). Comment-placement for @each ... in /* comment */ (a, b) now attaches comments to the iterable expression node (list or map) and the formatter moves iterable-list-specific logic out of general expression formatting into the new auxiliary module. Parser, codegen mappings, map-layout heuristics and spacing-after-in utilities were updated to match the new node shape.

Possibly related PRs

Suggested labels

L-SCSS

Suggested reviewers

  • dyc3
  • ematipico
  • Netail
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: improving SCSS @each rule formatting.
Description check ✅ Passed The description explains the alignment with Prettier for source-tight spacing and direct map/list iterables, with concrete examples and test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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-formatter-10

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/biome_css_formatter/src/scss/auxiliary/each_header.rs (2)

26-26: 💤 Low value

Prefer !bindings.is_empty() over len() > 0.

Minor nit: idiomatic Rust favours !bindings.is_empty() for clarity.

🔧 Suggested change
-        if bindings.len() > 0 && is_direct_list_or_map_iterable(&iterable) {
+        if !bindings.is_empty() && is_direct_list_or_map_iterable(&iterable) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_formatter/src/scss/auxiliary/each_header.rs` at line 26, The
condition uses bindings.len() > 0 which is non-idiomatic; change the check to
use !bindings.is_empty() in the if expression that also calls
is_direct_list_or_map_iterable(&iterable) so the line reads roughly: if
!bindings.is_empty() && is_direct_list_or_map_iterable(&iterable) — update the
reference to bindings, iterable and keep the rest of the logic intact.

69-88: 💤 Low value

Silent no-op for unexpected expression items warrants a defensive check.

The _ => Ok(()) branch silently succeeds without formatting anything. Given that is_direct_list_or_map_iterable at line 26 already guards entry, this path should be unreachable under normal conditions. Consider adding a debug_assert! to catch unexpected states during development.

🛡️ Defensive assertion suggestion
             Some(AnyScssExpressionItem::ScssMapExpression(map)) => write!(
                 f,
                 [FormatOwnedWithRule::new(
                     map,
                     FormatScssEachIterableMap::new(self.bindings, self.in_token, node.syntax())
                 )]
             ),
-            _ => Ok(()),
+            _ => {
+                debug_assert!(false, "FormatScssEachIterable reached with non-list/map item");
+                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/auxiliary/each_header.rs` around lines 69
- 88, The match in FormatScssEachIterable::fmt_fields currently falls through to
`_ => Ok(())`, silently doing nothing for unexpected expression kinds; add a
defensive debug assertion to catch unreachable states during development by
asserting the node shape returned by single_expression_item is one of the
handled variants. Modify fmt_fields (impl FormatNodeRule<ScssExpression> for
FormatScssEachIterable) to keep the existing arms for
AnyScssExpressionItem::ScssListExpression and ::ScssMapExpression but replace
the `_ => Ok(())` branch with a
debug_assert!(matches!(single_expression_item(node),
Some(AnyScssExpressionItem::ScssListExpression(_) |
AnyScssExpressionItem::ScssMapExpression(_)))) (or debug_assert!(false, "..."))
and then return Err or Ok(()) as before; reference function names: fmt_fields,
single_expression_item, AnyScssExpressionItem, FormatScssEachIterableList, and
FormatScssEachIterableMap so you update the exact match arm safely.
🤖 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_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs`:
- Around line 572-573: This change renames the named-field slots exposed to
GritQL for ScssEachAtRule (previously exposing bindings, iterable, block; now
exposing header and block) and is a user-facing breaking change—add a BREAKING
CHANGES entry in the release changelog and/or migration guide that documents the
rename and gives the migration pattern: update any GritQL patterns matching
ScssEachAtRule { bindings: $b, iterable: $i, ... } to ScssEachAtRule { header:
ScssEachHeader { bindings: $b, iterable: $i }, block: $blk } and mention that
patterns using the old top-level bindings/iterable will stop matching silently.

---

Nitpick comments:
In `@crates/biome_css_formatter/src/scss/auxiliary/each_header.rs`:
- Line 26: The condition uses bindings.len() > 0 which is non-idiomatic; change
the check to use !bindings.is_empty() in the if expression that also calls
is_direct_list_or_map_iterable(&iterable) so the line reads roughly: if
!bindings.is_empty() && is_direct_list_or_map_iterable(&iterable) — update the
reference to bindings, iterable and keep the rest of the logic intact.
- Around line 69-88: The match in FormatScssEachIterable::fmt_fields currently
falls through to `_ => Ok(())`, silently doing nothing for unexpected expression
kinds; add a defensive debug assertion to catch unreachable states during
development by asserting the node shape returned by single_expression_item is
one of the handled variants. Modify fmt_fields (impl
FormatNodeRule<ScssExpression> for FormatScssEachIterable) to keep the existing
arms for AnyScssExpressionItem::ScssListExpression and ::ScssMapExpression but
replace the `_ => Ok(())` branch with a
debug_assert!(matches!(single_expression_item(node),
Some(AnyScssExpressionItem::ScssListExpression(_) |
AnyScssExpressionItem::ScssMapExpression(_)))) (or debug_assert!(false, "..."))
and then return Err or Ok(()) as before; reference function names: fmt_fields,
single_expression_item, AnyScssExpressionItem, FormatScssEachIterableList, and
FormatScssEachIterableMap so you update the exact match arm safely.
🪄 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: c0c9c506-1aeb-402b-b316-f273b31f6938

📥 Commits

Reviewing files that changed from the base of the PR and between 183c8fa and 15e9922.

⛔ Files ignored due to path filters (14)
  • 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/prettier/scss/atrule/each.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/function/arbitrary-arguments.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/interpolation/4294.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/map/function-argument/function-argument-2.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/map/function-argument/functional-argument.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/scss/at-rule/each.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/each.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/each.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 (15)
  • crates/biome_css_formatter/src/comments.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/auxiliary/each_header.rs
  • crates/biome_css_formatter/src/scss/auxiliary/expression.rs
  • crates/biome_css_formatter/src/scss/auxiliary/expression/each_iterable_list.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_formatter/src/scss/statements/each_at_rule.rs
  • crates/biome_css_formatter/src/utils/mod.rs
  • crates/biome_css_formatter/src/utils/scss_each.rs
  • crates/biome_css_formatter/src/utils/scss_map_layout.rs
  • crates/biome_css_formatter/tests/specs/scss/at-rule/each.scss
  • crates/biome_css_parser/src/syntax/scss/at_rule/each_at_rule.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_formatter/src/scss/auxiliary/expression/each_iterable_list.rs

@codspeed-hq

codspeed-hq Bot commented May 2, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 38 untouched benchmarks
⏩ 218 skipped benchmarks1


Comparing db/scss-formatter-10 (df6d204) with main (183c8fa)

Open in CodSpeed

Footnotes

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

@denbezrukov denbezrukov force-pushed the db/scss-formatter-10 branch from 15e9922 to 5807b19 Compare May 2, 2026 11:44

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 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/auxiliary/each_header.rs`:
- Around line 183-194: The current line-comment branch inserts hard_line_break()
between binding.node()?.format() and in_token.format(), which can produce
"$x\nin // comment ..."; instead move the break so the binding and in_token stay
together: in the fill.entry call that builds group(indent(format_args![...]))
concatenate binding.node()?.format() and in_token.format() (with a space) before
invoking hard_line_break(), then place hard_line_break() and finally
format_leading_comments(comment_owner) so the forced break happens after "in"
(i.e., before the iterable items) rather than between the binding and "in".

In `@crates/biome_css_formatter/tests/specs/scss/at-rule/each.scss`:
- Around line 46-66: Add a snapshot test for the same-line "in (" spacing
variant by inserting a new `@each` case that uses "in (" on the same line (e.g.
`@each` $element, $size in (h1:20px,h2:16px,h3:14px) { }) alongside the existing
`@each` examples; update the specs snapshot for the at-rule/each.scss tests so the
formatter is exercised for the `in (` branch and the new case is committed with
its snapshot.
🪄 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: dd44765d-15d1-417a-870e-2e808519d9ae

📥 Commits

Reviewing files that changed from the base of the PR and between 15e9922 and 5807b19.

⛔ Files ignored due to path filters (14)
  • 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/prettier/scss/atrule/each.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/function/arbitrary-arguments.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/interpolation/4294.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/map/function-argument/function-argument-2.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/prettier/scss/map/function-argument/functional-argument.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_formatter/tests/specs/scss/at-rule/each.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/each.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/each.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 (15)
  • crates/biome_css_formatter/src/comments.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/auxiliary/each_header.rs
  • crates/biome_css_formatter/src/scss/auxiliary/expression.rs
  • crates/biome_css_formatter/src/scss/auxiliary/expression/each_iterable_list.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_formatter/src/scss/statements/each_at_rule.rs
  • crates/biome_css_formatter/src/utils/mod.rs
  • crates/biome_css_formatter/src/utils/scss_each.rs
  • crates/biome_css_formatter/src/utils/scss_map_layout.rs
  • crates/biome_css_formatter/tests/specs/scss/at-rule/each.scss
  • crates/biome_css_parser/src/syntax/scss/at_rule/each_at_rule.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_formatter/src/scss/auxiliary/expression/each_iterable_list.rs
✅ Files skipped from review due to trivial changes (9)
  • crates/biome_css_formatter/src/utils/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_parser/src/syntax/scss/at_rule/each_at_rule.rs
  • xtask/codegen/src/css_kinds_src.rs
  • crates/biome_css_formatter/src/scss/statements/each_at_rule.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/comments.rs
  • xtask/codegen/css.ungram
  • crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/biome_css_formatter/src/scss/auxiliary/expression.rs
  • crates/biome_css_formatter/src/utils/scss_each.rs

Comment thread crates/biome_css_formatter/src/scss/auxiliary/each_header.rs
Comment thread crates/biome_css_formatter/tests/specs/scss/at-rule/each.scss
@denbezrukov denbezrukov merged commit dc0c877 into main May 2, 2026
29 checks passed
@denbezrukov denbezrukov deleted the db/scss-formatter-10 branch May 2, 2026 18:41
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.

1 participant