Skip to content

feat(css): scss basic expressions#9212

Merged
denbezrukov merged 6 commits intomainfrom
db/scss-expression-init
Feb 24, 2026
Merged

feat(css): scss basic expressions#9212
denbezrukov merged 6 commits intomainfrom
db/scss-expression-init

Conversation

@denbezrukov
Copy link
Contributor

  • ScssExpression
  • ScssParenthesizedExpression
  • ScssListExpression
  • ScssMapExpression
  .test {
    width: fn($alpha $beta);
    height: fn(1 2 3);

    width: fn((1 2));
    height: fn((1, 2, 3));
    colors: fn((primary: red, secondary: blue));
    empty-map: fn(());
  }

Implementation note:

  • Parsing uses manual loops instead of ParseNodeList for upcoming binary-expression work.

Test Plan

  • cargo test -p biome_css_parser
  • cargo test -p biome_css_formatter

@changeset-bot
Copy link

changeset-bot bot commented Feb 23, 2026

⚠️ No Changeset found

Latest commit: db5adc4

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 L-Grit Language: GritQL labels Feb 23, 2026
@github-actions
Copy link
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 52917 52917 0
Passed 51697 51697 0
Failed 1178 1178 0
Panics 42 42 0
Coverage 97.69% 97.69% 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 5464 5464 0
Passed 1915 1915 0
Failed 3549 3549 0
Panics 0 0 0
Coverage 35.05% 35.05% 0.00%

ts/babel

Test result main count This PR count Difference
Total 635 635 0
Passed 567 567 0
Failed 68 68 0
Panics 0 0 0
Coverage 89.29% 89.29% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18871 18871 0
Passed 13012 13012 0
Failed 5858 5858 0
Panics 1 1 0
Coverage 68.95% 68.95% 0.00%

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds full SCSS expression support: grammar and AST kinds for SCSS expressions, lists, maps and parenthesized expressions; a recoverable SCSS expression parser with diagnostics; generated formatter adapters (AsFormat/IntoFormat/FormatRule) and handwritten FormatNode/FormatRule implementations; AnyScssExpression / AnyScssExpressionItem formatter dispatch and wiring into AnyCssExpression; and parser/formatter tests exercising nested lists, maps and parentheses.

Possibly related PRs

Suggested labels

L-SCSS

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(css): scss basic expressions' accurately and concisely summarises the main change: adding support for basic SCSS expression types (parenthesised, list, and map expressions).
Description check ✅ Passed The description is directly related to the changeset, detailing the SCSS expression types added, providing concrete examples, noting implementation approach, and listing test coverage.

✏️ 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-expression-init

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.

Actionable comments posted: 2

🧹 Nitpick comments (9)
crates/biome_css_formatter/src/scss/auxiliary/map_expression_pair.rs (1)

3-5: Missing blank line between imports and #[derive] — run just f.

auxiliary/expression.rs (line 4) has the blank line; this file doesn't. As per coding guidelines, just f should be run before committing.

✏️ Trivial fix
 use biome_formatter::write;
+
 #[derive(Debug, Clone, Default)]
🤖 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/map_expression_pair.rs` around
lines 3 - 5, Add a blank line between the import(s) and the attribute/struct
declaration in this file: separate the `use biome_formatter::write;` line from
the `#[derive(Debug, Clone, Default)]` attribute for `pub(crate) struct
FormatScssMapExpressionPair`, then run the repository formatter (run `just f`)
to apply formatting across the crate.
crates/biome_css_formatter/src/scss/auxiliary/list_expression.rs (1)

3-5: Same missing blank line as map_expression_pair.rs — run just f.

✏️ Trivial fix
 use biome_formatter::{format_args, write};
+
 #[derive(Debug, Clone, Default)]
🤖 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/list_expression.rs` around
lines 3 - 5, The file list_expression.rs is missing a blank line between the use
statement and the item declaration (same issue as map_expression_pair.rs);
update the file so there is a single blank line separating the use
biome_formatter::{format_args, write}; line and the #[derive(Debug, Clone,
Default)] pub(crate) struct FormatScssListExpression declaration, then run the
formatter (just f) to ensure formatting passes.
crates/biome_css_formatter/src/scss/lists/map_expression_pair_list.rs (1)

11-11: Consider soft_line_break() over hard_line_break() as the pair separator.

hard_line_break() unconditionally forces a newline and causes enclosing groups to break regardless of expansion state, bypassing the normal group mechanics. Since the outer ScssMapExpression group already forces expansion via should_expand(true) for non-empty maps, soft_line_break() achieves the same result while staying consistent with the formatter's standard patterns.

♻️ Suggested change
-        let separator = hard_line_break();
+        let separator = soft_line_break();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_css_formatter/src/scss/lists/map_expression_pair_list.rs` at
line 11, The separator currently uses hard_line_break() which forces a newline
and breaks enclosing groups; change it to soft_line_break() in
map_expression_pair_list.rs by replacing the hard_line_break() call used to set
separator (the variable named separator) so it relies on the surrounding
ScssMapExpression's should_expand(true) behavior and follows the formatter's
normal group mechanics.
crates/biome_css_formatter/src/scss/auxiliary/map_expression.rs (1)

21-21: should_expand hard-wires multiline output for all non-empty maps.

.should_expand(!pairs.is_empty()) means even a single short pair like (a: 1) is always formatted across multiple lines, ignoring the configured print width. If that's the intended style (analogous to how Prettier/Biome always expands JS objects), great — just confirming it's deliberate and not an accidental copy of the always-expand pattern.

🤖 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/map_expression.rs` at line 21,
The current call to should_expand(!pairs.is_empty()) forces all non-empty maps
to be multiline; change it to compute expansion based on content and print width
instead of blindly expanding: replace the literal condition with a boolean that
checks pairs and whether they will exceed the configured line width (e.g.
!pairs.is_empty() && exceeds_print_width(&pairs, context.print_width) or call an
existing helper like needs_expansion(&pairs, &context)); update the code around
should_expand(...) so it only returns true when there are pairs and the
single-line representation would be too long or otherwise require expansion, and
add/ reuse a small helper (needs_expansion/exceeds_print_width) to simulate
single-line length using the same formatting rules.
crates/biome_css_parser/src/syntax/scss/expression/mod.rs (4)

16-17: SCSS_BINARY_OPERATOR_TOKEN_SET duplicates BINARY_OPERATION_TOKEN from function.rs.

Both are token_set![T![+], T![-], T![*], T![/]]. If the CSS set ever gains a new operator (e.g. **), this one silently diverges and SCSS stop-token behaviour breaks.

♻️ Suggested fix — reuse the existing constant
-const SCSS_BINARY_OPERATOR_TOKEN_SET: TokenSet<CssSyntaxKind> =
-    token_set![T![+], T![-], T![*], T![/]];
// at the top of the file, import from the parent module:
use crate::syntax::value::function::BINARY_OPERATION_TOKEN;

Then replace all usages of SCSS_BINARY_OPERATOR_TOKEN_SET with BINARY_OPERATION_TOKEN.

🤖 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/expression/mod.rs` around lines 16 -
17, Replace the duplicated SCSS_BINARY_OPERATOR_TOKEN_SET with the shared
BINARY_OPERATION_TOKEN from the value/function module: remove the local
SCSS_BINARY_OPERATOR_TOKEN_SET constant, add an import for
crate::syntax::value::function::BINARY_OPERATION_TOKEN at the top of the file,
and replace all references to SCSS_BINARY_OPERATOR_TOKEN_SET with
BINARY_OPERATION_TOKEN so the SCSS parser reuses the canonical operator set.

149-177: progress.assert_progressing(p) is placed after p.bump(T![,]), making it a guaranteed no-op.

The canonical biome pattern calls progress.assert_progressing(p) before consuming the separator, so it catches stagnation in the element-parsing body. Here the position already advanced (via the bump), so the assertion always passes and provides no infinite-loop protection for the pair-parsing work that follows.

The same pattern repeats in complete_scss_list_expression (lines 188–212).

In practice there's no actual infinite-loop risk because error recovery already breaks on failure — but the placement is misleading.

♻️ Suggested fix — move assertion before bump
     while p.at(T![,]) {
+        progress.assert_progressing(p);
         p.bump(T![,]);
 
         if p.at(T![')']) {
             break;
         }
 
-        progress.assert_progressing(p);
 
         let pair = match ...
🤖 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/expression/mod.rs` around lines 149 -
177, The progress.assert_progressing(p) call is currently after p.bump(T![,])
which makes it a no-op; move progress.assert_progressing(p) to immediately
before p.bump(T![,]) in this loop so you assert progress before consuming the
separator; apply the same change in the analogous loop inside
complete_scss_list_expression (move its progress.assert_progressing(p) to just
before the p.bump(T![,]) there) to restore the canonical infinite-loop
protection pattern around parse_scss_inner_expression_until and
parse_scss_map_expression_pair_with_key.

34-36: Redundant .union(SCSS_BINARY_OPERATOR_TOKEN_SET) — binary ops already present in END_OF_SCSS_EXPRESSION_TOKEN_SET.

END_OF_SCSS_EXPRESSION_TOKEN_SET is defined to already include SCSS_BINARY_OPERATOR_TOKEN_SET (line 26), so the union in parse_scss_expression_until is a no-op. Either remove it to reduce noise, or document why it's kept (e.g., "ensure future callers with narrower token sets still stop on binary ops").

🤖 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/expression/mod.rs` around lines 34 -
36, The call in parse_scss_expression_until currently unions
SCSS_BINARY_OPERATOR_TOKEN_SET into end_ts before passing to
parse_scss_expression_with_options, but END_OF_SCSS_EXPRESSION_TOKEN_SET already
contains SCSS_BINARY_OPERATOR_TOKEN_SET, so remove the redundant
.union(SCSS_BINARY_OPERATOR_TOKEN_SET) to simplify the call; update
parse_scss_expression_until to forward end_ts (or, if you want to keep the union
for an explicit contract, add a short comment explaining that it ensures binary
operators are always included for future narrower end sets) and reference
parse_scss_expression_with_options, END_OF_SCSS_EXPRESSION_TOKEN_SET, and
SCSS_BINARY_OPERATOR_TOKEN_SET when making the change.

46-78: allow_empty parameter is dead code — always false.

Neither call site passes true. If it's scaffolding for the upcoming binary-expression work, a comment explaining the intent would save the next reviewer some head-scratching.

🤖 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/expression/mod.rs` around lines 46 -
78, The allow_empty parameter on parse_scss_expression_with_options is dead
(never passed true); either remove it and simplify the early-return check
(delete the allow_empty branch and update all call sites of
parse_scss_expression_with_options), or if it's intentional scaffolding for
upcoming binary-expression work, add a short explanatory comment above
parse_scss_expression_with_options stating its planned use and why callers
currently pass false so future reviewers won't remove it; update any
documentation/tests accordingly.
crates/biome_css_parser/src/syntax/value/function.rs (1)

337-351: parse_list_of_component_values_expression silently returns SCSS_EXPRESSION in SCSS mode.

The function name implies it always produces CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION, but in SCSS mode it delegates to parse_scss_expression and produces SCSS_EXPRESSION instead. Not a bug right now (callers treat the result as opaque ParsedSyntax), but the name will mislead future maintainers. Consider renaming or extracting a named SCSS overload — or at minimum add a brief comment.

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

In `@crates/biome_css_parser/src/syntax/value/function.rs` around lines 337 - 351,
The function parse_list_of_component_values_expression currently returns
CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION in CSS mode but delegates to
parse_scss_expression (returning SCSS_EXPRESSION) in SCSS mode, which is
misleading given the function name; update the implementation by adding a brief
comment above parse_list_of_component_values_expression that documents this dual
behavior (explicitly state that in SCSS mode it yields SCSS_EXPRESSION via
parse_scss_expression, while in non-SCSS it yields
CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION), and optionally consider renaming the
function or extracting an explicit
parse_list_of_component_values_expression_scss helper (e.g.,
parse_scss_expression is already present) if you prefer a clearer API—ensure the
comment references parse_scss_expression,
CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION, and SCSS_EXPRESSION so future
maintainers can locate the relevant code paths.
🤖 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 278-285: The mapping block is missing entries for
ScssListExpression and ScssListExpressionElement — add lines mapping the string
keys "ScssListExpression" and "ScssListExpressionElement" to their respective
kind singletons (use lang::ScssListExpression::KIND_SET.iter().next() and
lang::ScssListExpressionElement::KIND_SET.iter().next()) in the same mapping
where ScssMapExpression and ScssMapExpressionPair are defined so GritQL can
recognize these node kinds.

In `@xtask/codegen/css.ungram`:
- Around line 2556-2563: ScssMapExpressionPairList currently requires at least
one pair, so ScssMapExpression and ScssParenthesizedExpression cannot represent
an empty map `()`, but tests expect `fn(())`; update the grammar so
ScssMapExpressionPairList allows an empty list (make the inner pair-list
optional or change the production from (ScssMapExpressionPair (','
ScssMapExpressionPair)* ','?) to an optional group like (ScssMapExpressionPair
(',' ScssMapExpressionPair)* ','?)? or equivalent) so that ScssMapExpression and
ScssParenthesizedExpression can parse `()` without relying on parser
special-casing; adjust the production for ScssMapExpressionPairList accordingly.

---

Nitpick comments:
In `@crates/biome_css_formatter/src/scss/auxiliary/list_expression.rs`:
- Around line 3-5: The file list_expression.rs is missing a blank line between
the use statement and the item declaration (same issue as
map_expression_pair.rs); update the file so there is a single blank line
separating the use biome_formatter::{format_args, write}; line and the
#[derive(Debug, Clone, Default)] pub(crate) struct FormatScssListExpression
declaration, then run the formatter (just f) to ensure formatting passes.

In `@crates/biome_css_formatter/src/scss/auxiliary/map_expression_pair.rs`:
- Around line 3-5: Add a blank line between the import(s) and the
attribute/struct declaration in this file: separate the `use
biome_formatter::write;` line from the `#[derive(Debug, Clone, Default)]`
attribute for `pub(crate) struct FormatScssMapExpressionPair`, then run the
repository formatter (run `just f`) to apply formatting across the crate.

In `@crates/biome_css_formatter/src/scss/auxiliary/map_expression.rs`:
- Line 21: The current call to should_expand(!pairs.is_empty()) forces all
non-empty maps to be multiline; change it to compute expansion based on content
and print width instead of blindly expanding: replace the literal condition with
a boolean that checks pairs and whether they will exceed the configured line
width (e.g. !pairs.is_empty() && exceeds_print_width(&pairs,
context.print_width) or call an existing helper like needs_expansion(&pairs,
&context)); update the code around should_expand(...) so it only returns true
when there are pairs and the single-line representation would be too long or
otherwise require expansion, and add/ reuse a small helper
(needs_expansion/exceeds_print_width) to simulate single-line length using the
same formatting rules.

In `@crates/biome_css_formatter/src/scss/lists/map_expression_pair_list.rs`:
- Line 11: The separator currently uses hard_line_break() which forces a newline
and breaks enclosing groups; change it to soft_line_break() in
map_expression_pair_list.rs by replacing the hard_line_break() call used to set
separator (the variable named separator) so it relies on the surrounding
ScssMapExpression's should_expand(true) behavior and follows the formatter's
normal group mechanics.

In `@crates/biome_css_parser/src/syntax/scss/expression/mod.rs`:
- Around line 16-17: Replace the duplicated SCSS_BINARY_OPERATOR_TOKEN_SET with
the shared BINARY_OPERATION_TOKEN from the value/function module: remove the
local SCSS_BINARY_OPERATOR_TOKEN_SET constant, add an import for
crate::syntax::value::function::BINARY_OPERATION_TOKEN at the top of the file,
and replace all references to SCSS_BINARY_OPERATOR_TOKEN_SET with
BINARY_OPERATION_TOKEN so the SCSS parser reuses the canonical operator set.
- Around line 149-177: The progress.assert_progressing(p) call is currently
after p.bump(T![,]) which makes it a no-op; move progress.assert_progressing(p)
to immediately before p.bump(T![,]) in this loop so you assert progress before
consuming the separator; apply the same change in the analogous loop inside
complete_scss_list_expression (move its progress.assert_progressing(p) to just
before the p.bump(T![,]) there) to restore the canonical infinite-loop
protection pattern around parse_scss_inner_expression_until and
parse_scss_map_expression_pair_with_key.
- Around line 34-36: The call in parse_scss_expression_until currently unions
SCSS_BINARY_OPERATOR_TOKEN_SET into end_ts before passing to
parse_scss_expression_with_options, but END_OF_SCSS_EXPRESSION_TOKEN_SET already
contains SCSS_BINARY_OPERATOR_TOKEN_SET, so remove the redundant
.union(SCSS_BINARY_OPERATOR_TOKEN_SET) to simplify the call; update
parse_scss_expression_until to forward end_ts (or, if you want to keep the union
for an explicit contract, add a short comment explaining that it ensures binary
operators are always included for future narrower end sets) and reference
parse_scss_expression_with_options, END_OF_SCSS_EXPRESSION_TOKEN_SET, and
SCSS_BINARY_OPERATOR_TOKEN_SET when making the change.
- Around line 46-78: The allow_empty parameter on
parse_scss_expression_with_options is dead (never passed true); either remove it
and simplify the early-return check (delete the allow_empty branch and update
all call sites of parse_scss_expression_with_options), or if it's intentional
scaffolding for upcoming binary-expression work, add a short explanatory comment
above parse_scss_expression_with_options stating its planned use and why callers
currently pass false so future reviewers won't remove it; update any
documentation/tests accordingly.

In `@crates/biome_css_parser/src/syntax/value/function.rs`:
- Around line 337-351: The function parse_list_of_component_values_expression
currently returns CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION in CSS mode but
delegates to parse_scss_expression (returning SCSS_EXPRESSION) in SCSS mode,
which is misleading given the function name; update the implementation by adding
a brief comment above parse_list_of_component_values_expression that documents
this dual behavior (explicitly state that in SCSS mode it yields SCSS_EXPRESSION
via parse_scss_expression, while in non-SCSS it yields
CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION), and optionally consider renaming the
function or extracting an explicit
parse_list_of_component_values_expression_scss helper (e.g.,
parse_scss_expression is already present) if you prefer a clearer API—ensure the
comment references parse_scss_expression,
CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION, and SCSS_EXPRESSION so future
maintainers can locate the relevant code paths.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0385eb2 and 6300cb5.

⛔ Files ignored due to path filters (12)
  • 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/expression/list-map-paren.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/error/scss/expression/list-map-paren.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/namespaced-function.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/core.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/list-map-paren.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/qualified-names.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 (28)
  • crates/biome_css_formatter/src/css/any/expression.rs
  • crates/biome_css_formatter/src/generated.rs
  • crates/biome_css_formatter/src/scss/any/expression.rs
  • crates/biome_css_formatter/src/scss/any/expression_item.rs
  • crates/biome_css_formatter/src/scss/any/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/expression.rs
  • crates/biome_css_formatter/src/scss/auxiliary/list_expression.rs
  • crates/biome_css_formatter/src/scss/auxiliary/list_expression_element.rs
  • crates/biome_css_formatter/src/scss/auxiliary/map_expression.rs
  • crates/biome_css_formatter/src/scss/auxiliary/map_expression_pair.rs
  • crates/biome_css_formatter/src/scss/auxiliary/mod.rs
  • crates/biome_css_formatter/src/scss/auxiliary/parenthesized_expression.rs
  • crates/biome_css_formatter/src/scss/lists/expression_item_list.rs
  • crates/biome_css_formatter/src/scss/lists/list_expression_element_list.rs
  • crates/biome_css_formatter/src/scss/lists/map_expression_pair_list.rs
  • crates/biome_css_formatter/src/scss/lists/mod.rs
  • crates/biome_css_formatter/tests/quick_test.rs
  • crates/biome_css_formatter/tests/specs/css/scss/expression/list-map-paren.scss
  • crates/biome_css_parser/src/syntax/parse_error.rs
  • crates/biome_css_parser/src/syntax/scss/expression/mod.rs
  • crates/biome_css_parser/src/syntax/scss/mod.rs
  • crates/biome_css_parser/src/syntax/value/function.rs
  • crates/biome_css_parser/tests/css_test_suite/error/scss/expression/list-map-paren.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/core.scss
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/list-map-paren.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

Comment on lines +278 to +285
"ScssExpression" => lang::ScssExpression::KIND_SET.iter().next(),
"ScssIdentifier" => lang::ScssIdentifier::KIND_SET.iter().next(),
"ScssMapExpression" => lang::ScssMapExpression::KIND_SET.iter().next(),
"ScssMapExpressionPair" => lang::ScssMapExpressionPair::KIND_SET.iter().next(),
"ScssNamespacedIdentifier" => lang::ScssNamespacedIdentifier::KIND_SET.iter().next(),
"ScssNestingDeclaration" => lang::ScssNestingDeclaration::KIND_SET.iter().next(),
"ScssParentSelectorValue" => lang::ScssParentSelectorValue::KIND_SET.iter().next(),
"ScssParenthesizedExpression" => lang::ScssParenthesizedExpression::KIND_SET.iter().next(),
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 | 🟡 Minor

ScssListExpression (and ScssListExpressionElement) are missing from the GritQL kind mappings.

ScssListExpression is a first-class peer of ScssMapExpression and ScssParenthesizedExpression in AnyScssExpression, yet only the latter two were added here. ScssListExpressionElement is analogous to ScssMapExpressionPair (both are named list-element nodes with fields). Both are absent.

🔧 Proposed additions
 "ScssIdentifier" => lang::ScssIdentifier::KIND_SET.iter().next(),
+        "ScssListExpression" => lang::ScssListExpression::KIND_SET.iter().next(),
+        "ScssListExpressionElement" => lang::ScssListExpressionElement::KIND_SET.iter().next(),
         "ScssMapExpression" => lang::ScssMapExpression::KIND_SET.iter().next(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs`
around lines 278 - 285, The mapping block is missing entries for
ScssListExpression and ScssListExpressionElement — add lines mapping the string
keys "ScssListExpression" and "ScssListExpressionElement" to their respective
kind singletons (use lang::ScssListExpression::KIND_SET.iter().next() and
lang::ScssListExpressionElement::KIND_SET.iter().next()) in the same mapping
where ScssMapExpression and ScssMapExpressionPair are defined so GritQL can
recognize these node kinds.

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 23, 2026

Merging this PR will not alter performance

✅ 29 untouched benchmarks
⏩ 127 skipped benchmarks1


Comparing db/scss-expression-init (db5adc4) with main (272582c)

Open in CodSpeed

Footnotes

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

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 (4)
crates/biome_css_formatter/src/scss/auxiliary/map_expression.rs (1)

22-22: should_expand always breaks non-empty maps — intentional?

Unlike list_expression.rs, which lets the group decide based on line length, should_expand(!pairs.is_empty()) forces every non-empty map onto multiple lines, including trivial ones like (a: 1).

If that's the intended style (maps always expand, like object literals in some formatters), this is fine. If natural break-on-overflow behaviour is preferred instead, the pattern from list_expression.rs would be:

-        write!(
-            f,
-            [group(&format_args![
-                l_paren_token.format(),
-                indent(&format_args![soft_line_break(), pairs.format()]),
-                soft_line_break(),
-                r_paren_token.format()
-            ])
-            .should_expand(!pairs.is_empty())]
-        )
+        write!(
+            f,
+            [group(&format_args![
+                l_paren_token.format(),
+                soft_block_indent(&pairs.format()),
+                r_paren_token.format()
+            ])]
+        )
crates/biome_css_parser/src/syntax/scss/expression/mod.rs (3)

46-78: allow_empty is dead code — always false.

Every call-site passes false, so the branch on line 52 that gates on allow_empty is never taken with true. If the upcoming binary-expression work needs it, it can be added back then.

♻️ Proposed simplification
-#[inline]
-fn parse_scss_expression_with_options(
-    p: &mut CssParser,
-    end_ts: TokenSet<CssSyntaxKind>,
-    allow_empty: bool,
-) -> ParsedSyntax {
-    if !allow_empty && (p.at_ts(end_ts) || p.at(EOF)) {
-        return Absent;
-    }
+#[inline]
+fn parse_scss_expression_with_options(
+    p: &mut CssParser,
+    end_ts: TokenSet<CssSyntaxKind>,
+) -> ParsedSyntax {
+    if p.at_ts(end_ts) || p.at(EOF) {
+        return Absent;
+    }

Update the two call-sites (parse_scss_expression_until and parse_scss_inner_expression_until) to drop the false argument accordingly.

🤖 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/expression/mod.rs` around lines 46 -
78, The parameter allow_empty on parse_scss_expression_with_options is dead
(always passed false) and the branch guarding on it can be removed; update
parse_scss_expression_with_options to drop the allow_empty parameter and
simplify the early-return check to just if p.at_ts(end_ts) || p.at(EOF) then
return Absent, and update both call-sites parse_scss_expression_until and
parse_scss_inner_expression_until to call the new signature (remove the false
argument) and adjust imports/signatures accordingly.

60-74: Consider adding a comment explaining the intentional ParseNodeList deviation.

The repo's parser convention is to use ParseNodeList/ParseSeparatedList for list parsing. These manual loops are fine (and the PR description explains why), but a short inline comment would save future reviewers from wondering. Based on learnings, "Use ParseSeparatedList and ParseNodeList for parsing lists with error recovery to avoid infinite loops."

💬 Suggested comment
+    // Manual loop instead of `ParseNodeList` to support upcoming binary-expression
+    // parsing, where the list boundary changes depending on operator precedence.
     while !p.at(EOF) && !p.at_ts(end_ts) {
🤖 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/expression/mod.rs` around lines 60 -
74, Add a short inline comment above the manual loop that explains this is an
intentional deviation from the repo convention of using
ParseNodeList/ParseSeparatedList: clarify that parse_scss_expression_item is
being parsed in a manual while loop (using p.at(EOF), p.at_ts(end_ts),
progress.assert_progressing, and the ParseRecoveryTokenSet with
CSS_BOGUS_PROPERTY_VALUE and end_ts) to enable the specific error-recovery
behavior described in the PR (and to avoid the infinite-loop edge-cases that
ParseNodeList/ParseSeparatedList would cause here). Mention the relevant symbols
parse_scss_expression_item, ParseRecoveryTokenSet, expected_scss_expression,
ParseNodeList and ParseSeparatedList so future reviewers understand the
rationale.

19-23: SCSS_MAP_EXPRESSION_VALUE_END_TOKEN_SET and SCSS_LIST_EXPRESSION_ELEMENT_END_TOKEN_SET are identical.

Both expand to token_set![T![,], T![')']. A single shared constant would do the job — separate names only add confusion if the sets ever need to diverge (which could instead be an explicit duplication at that point).

♻️ Proposed consolidation
-const SCSS_MAP_EXPRESSION_VALUE_END_TOKEN_SET: TokenSet<CssSyntaxKind> = token_set![T![,], T![')']];
-const SCSS_LIST_EXPRESSION_ELEMENT_END_TOKEN_SET: TokenSet<CssSyntaxKind> =
-    token_set![T![,], T![')']];
+const SCSS_COMMA_PAREN_END_TOKEN_SET: TokenSet<CssSyntaxKind> = token_set![T![,], T![')']];

Then replace both usages with SCSS_COMMA_PAREN_END_TOKEN_SET.

🤖 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/expression/mod.rs` around lines 19 -
23, The two identical TokenSet constants SCSS_MAP_EXPRESSION_VALUE_END_TOKEN_SET
and SCSS_LIST_EXPRESSION_ELEMENT_END_TOKEN_SET should be consolidated: define a
single shared constant (e.g., SCSS_COMMA_PAREN_END_TOKEN_SET) with
token_set![T![,], T![')']] and replace all uses of
SCSS_MAP_EXPRESSION_VALUE_END_TOKEN_SET and
SCSS_LIST_EXPRESSION_ELEMENT_END_TOKEN_SET with that new constant; leave
SCSS_MAP_EXPRESSION_KEY_END_TOKEN_SET (token_set![T![,], T![:], T![')']]) as-is
because it differs.
🤖 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/map_expression.rs`:
- Around line 3-4: Add a blank line between the last use statement (use
biome_formatter::{format_args, write};) and the following item
attribute/definition (#[derive(Debug, Clone, Default)]) or simply run the
project formatter by executing "just f" so rustfmt inserts the required blank
line and normalizes formatting; ensure you run "just f" (and "just l") before
committing.

---

Nitpick comments:
In `@crates/biome_css_parser/src/syntax/scss/expression/mod.rs`:
- Around line 46-78: The parameter allow_empty on
parse_scss_expression_with_options is dead (always passed false) and the branch
guarding on it can be removed; update parse_scss_expression_with_options to drop
the allow_empty parameter and simplify the early-return check to just if
p.at_ts(end_ts) || p.at(EOF) then return Absent, and update both call-sites
parse_scss_expression_until and parse_scss_inner_expression_until to call the
new signature (remove the false argument) and adjust imports/signatures
accordingly.
- Around line 60-74: Add a short inline comment above the manual loop that
explains this is an intentional deviation from the repo convention of using
ParseNodeList/ParseSeparatedList: clarify that parse_scss_expression_item is
being parsed in a manual while loop (using p.at(EOF), p.at_ts(end_ts),
progress.assert_progressing, and the ParseRecoveryTokenSet with
CSS_BOGUS_PROPERTY_VALUE and end_ts) to enable the specific error-recovery
behavior described in the PR (and to avoid the infinite-loop edge-cases that
ParseNodeList/ParseSeparatedList would cause here). Mention the relevant symbols
parse_scss_expression_item, ParseRecoveryTokenSet, expected_scss_expression,
ParseNodeList and ParseSeparatedList so future reviewers understand the
rationale.
- Around line 19-23: The two identical TokenSet constants
SCSS_MAP_EXPRESSION_VALUE_END_TOKEN_SET and
SCSS_LIST_EXPRESSION_ELEMENT_END_TOKEN_SET should be consolidated: define a
single shared constant (e.g., SCSS_COMMA_PAREN_END_TOKEN_SET) with
token_set![T![,], T![')']] and replace all uses of
SCSS_MAP_EXPRESSION_VALUE_END_TOKEN_SET and
SCSS_LIST_EXPRESSION_ELEMENT_END_TOKEN_SET with that new constant; leave
SCSS_MAP_EXPRESSION_KEY_END_TOKEN_SET (token_set![T![,], T![:], T![')']]) as-is
because it differs.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6300cb5 and 705a74e.

📒 Files selected for processing (2)
  • crates/biome_css_formatter/src/scss/auxiliary/map_expression.rs
  • crates/biome_css_parser/src/syntax/scss/expression/mod.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.

🧹 Nitpick comments (1)
crates/biome_css_parser/src/syntax/value/function.rs (1)

288-289: else if is_at_parenthesized(p) is dead code in SCSS mode.

When SCSS is enabled, is_at_parenthesized(p) == true is consumed by the new branch above (line 284). The condition can only be true here in non-SCSS mode, which means the else if is_at_parenthesized branch silently does double duty as "SCSS off" guard. Consider making the intent explicit to avoid confusion for future readers:

-    } else if is_at_parenthesized(p) {
+    } else if !CssSyntaxFeatures::Scss.is_supported(p) && is_at_parenthesized(p) {

or, equivalently, restructure so CSS-only paths are nested under a single !scss arm.

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

In `@crates/biome_css_parser/src/syntax/value/function.rs` around lines 288 - 289,
The branch using else if is_at_parenthesized(p) is effectively dead when SCSS is
enabled and is currently serving as an implicit "SCSS off" guard; make the
intent explicit by guarding the CSS-only branch with a clear scss flag check
(e.g. if !scss { if is_at_parenthesized(p) { parse_parenthesized_expression(p) }
}) or by nesting CSS-only paths under a single !scss arm so that
is_at_parenthesized and parse_parenthesized_expression are only considered when
scss is false; update the conditional structure around is_at_parenthesized(p)
and parse_parenthesized_expression(p) to remove the implicit behavior and
document the intent with a short comment.
🤖 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/value/function.rs`:
- Around line 288-289: The branch using else if is_at_parenthesized(p) is
effectively dead when SCSS is enabled and is currently serving as an implicit
"SCSS off" guard; make the intent explicit by guarding the CSS-only branch with
a clear scss flag check (e.g. if !scss { if is_at_parenthesized(p) {
parse_parenthesized_expression(p) } }) or by nesting CSS-only paths under a
single !scss arm so that is_at_parenthesized and parse_parenthesized_expression
are only considered when scss is false; update the conditional structure around
is_at_parenthesized(p) and parse_parenthesized_expression(p) to remove the
implicit behavior and document the intent with a short comment.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 705a74e and 193d226.

⛔ Files ignored due to path filters (2)
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/namespaced-function.scss.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/value/qualified-names.scss.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (1)
  • crates/biome_css_parser/src/syntax/value/function.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.

🧹 Nitpick comments (1)
crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/unary-parenthesized.scss (1)

1-4: Consider adding a unary + and a comma-separated list variant for fuller coverage.

Right now only - is exercised. Throwing in a + case (e.g., fn(+(1 2))) and a negated comma-separated list (e.g., fn(-(1, 2, 3))) would give the parser a proper workout without much extra effort.

✨ Suggested additions
 .test {
   list: fn(-(1 2));
   map: fn(-(primary: red));
+  list-plus: fn(+(1 2));
+  list-comma: fn(-(1, 2, 3));
 }
🤖 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/ok/scss/expression/unary-parenthesized.scss`
around lines 1 - 4, Add test cases exercising a unary plus and a comma-separated
list variant to mirror the existing unary-minus tests: update the .test block
that uses fn(...) so it also includes e.g. fn(+(1 2)) and a negated
comma-separated list like fn(-(1, 2, 3)), and likewise add a map variant (e.g.,
map: fn(+(primary: red)) or map: fn(-(primary: red, secondary: blue))) to cover
both unary + and comma-separated list parsing for functions referenced as fn in
the existing test.
🤖 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/tests/css_test_suite/ok/scss/expression/unary-parenthesized.scss`:
- Around line 1-4: Add test cases exercising a unary plus and a comma-separated
list variant to mirror the existing unary-minus tests: update the .test block
that uses fn(...) so it also includes e.g. fn(+(1 2)) and a negated
comma-separated list like fn(-(1, 2, 3)), and likewise add a map variant (e.g.,
map: fn(+(primary: red)) or map: fn(-(primary: red, secondary: blue))) to cover
both unary + and comma-separated list parsing for functions referenced as fn in
the existing test.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff5f77e and db5adc4.

⛔ Files ignored due to path filters (2)
  • crates/biome_css_parser/tests/css_test_suite/ok/function/unary-parenthesized.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/unary-parenthesized.scss.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • crates/biome_css_parser/src/syntax/value/function.rs
  • crates/biome_css_parser/tests/css_test_suite/ok/function/unary-parenthesized.css
  • crates/biome_css_parser/tests/css_test_suite/ok/scss/expression/unary-parenthesized.scss

@denbezrukov denbezrukov merged commit de4c5f6 into main Feb 24, 2026
17 checks passed
@denbezrukov denbezrukov deleted the db/scss-expression-init branch February 24, 2026 08:38
@Netail Netail added the L-SCSS Language: SCSS label Feb 24, 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 L-Grit Language: GritQL L-SCSS Language: SCSS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants