Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/fix-css-comment-placement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@biomejs/biome": patch
---

Fixed [#8409](https://github.com/biomejs/biome/issues/8409): CSS formatter now correctly places comments after the colon in property declarations.

Previously, comments that appeared after the colon in CSS property values were incorrectly moved before the property name:

```css
/* Before (incorrect) */
[lang]:lang(ja) {
/* system-ui,*/ font-family:
Hiragino Sans,
sans-serif;
}

/* After (correct) */
[lang]:lang(ja) {
font-family: /* system-ui,*/
Hiragino Sans,
sans-serif;
}
Comment on lines +17 to +22
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

Inconsistent indentation in the "After" example.

Line 21 has extra leading whitespace (sans-serif is indented more than Hiragino Sans). If this reflects actual formatter output, it may indicate the regression mentioned in the PR description. If it's a typo in the changeset, it should be corrected.

📝 If this is a typo, proposed fix
 /* After (correct) */
 [lang]:lang(ja) {
   font-family: /* system-ui,*/
     Hiragino Sans,
-     sans-serif;
+    sans-serif;
 }
📝 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
/* After (correct) */
[lang]:lang(ja) {
font-family: /* system-ui,*/
Hiragino Sans,
sans-serif;
}
/* After (correct) */
[lang]:lang(ja) {
font-family: /* system-ui,*/
Hiragino Sans,
sans-serif;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/fix-css-comment-placement.md around lines 17 - 22, The "After"
example has inconsistent indentation: the 'sans-serif' token is indented further
than 'Hiragino Sans' in the font-family block for the [lang]:lang(ja) selector;
fix by aligning 'sans-serif' to the same indentation level as 'Hiragino Sans' in
.changeset/fix-css-comment-placement.md (or, if this reflects real formatter
output from your CSS formatter, adjust the formatter/config or add a regression
test to ensure comment placement doesn't change indentation). Ensure the
font-family lines under the [lang]:lang(ja) block use consistent indentation.

```
53 changes: 51 additions & 2 deletions crates/biome_css_formatter/src/comments.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::prelude::*;
use biome_css_syntax::{
AnyCssDeclarationName, AnyCssRoot, CssComplexSelector, CssFunction, CssIdentifier, CssLanguage,
CssSyntaxKind, TextLen, TextSize,
AnyCssDeclarationName, AnyCssRoot, CssComplexSelector, CssFunction, CssGenericProperty,
CssIdentifier, CssLanguage, CssSyntaxKind, TextLen, TextSize,
};
use biome_diagnostics::category;
use biome_formatter::comments::{
Expand Down Expand Up @@ -99,21 +99,70 @@ impl CommentStyle for CssCommentStyle {
) -> CommentPlacement<Self::Language> {
match comment.text_position() {
CommentTextPosition::EndOfLine => handle_function_comment(comment)
.or_else(handle_generic_property_comment)
.or_else(handle_declaration_name_comment)
.or_else(handle_complex_selector_comment)
.or_else(handle_global_suppression),
CommentTextPosition::OwnLine => handle_function_comment(comment)
.or_else(handle_generic_property_comment)
.or_else(handle_declaration_name_comment)
.or_else(handle_complex_selector_comment)
.or_else(handle_global_suppression),
CommentTextPosition::SameLine => handle_function_comment(comment)
.or_else(handle_generic_property_comment)
.or_else(handle_declaration_name_comment)
.or_else(handle_complex_selector_comment)
.or_else(handle_global_suppression),
}
}
}

fn handle_generic_property_comment(
comment: DecoratedComment<CssLanguage>,
) -> CommentPlacement<CssLanguage> {
// Check if the comment is inside a CSS generic property (e.g., color: value)
let Some(generic_property) = comment
.enclosing_node()
.ancestors()
.find_map(CssGenericProperty::cast)
else {
return CommentPlacement::Default(comment);
};

let Ok(name) = generic_property.name() else {
return CommentPlacement::Default(comment);
};

let comment_piece = comment.piece();

// Check if the comment is in the name's trailing trivia (before colon)
// Example: `color /* comment */: value`
if let Some(name_token) = name.syntax().last_token() {
for piece in name_token.trailing_trivia().pieces() {
if piece.is_comments() && piece.text() == comment_piece.text() {
// Our placement is slightly better than Prettier because it adds some spacing
return CommentPlacement::trailing(name.into_syntax(), comment);
}
}
}

if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node())
{
// If preceding is the property name and following is in the value list
if preceding == name.syntax()
&& following
.parent()
.is_some_and(|p| p.kind() == CssSyntaxKind::CSS_GENERIC_COMPONENT_VALUE_LIST)
{
// Place comment as dangling on the property so it can be formatted inline
// between the colon and values
return CommentPlacement::trailing(generic_property.into_syntax(), comment);
}
}

CommentPlacement::Default(comment)
}

fn handle_declaration_name_comment(
comment: DecoratedComment<CssLanguage>,
) -> CommentPlacement<CssLanguage> {
Expand Down
37 changes: 32 additions & 5 deletions crates/biome_css_formatter/src/css/properties/generic_property.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::comments::FormatCssLeadingComment;
use crate::prelude::*;
use biome_css_syntax::{CssGenericProperty, CssGenericPropertyFields};
use biome_formatter::write;
use biome_formatter::{CstFormatContext, FormatRefWithRule, format_args, write};

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatCssGenericProperty;
Expand All @@ -12,9 +13,35 @@ impl FormatNodeRule<CssGenericProperty> for FormatCssGenericProperty {
value,
} = node.as_fields();

write!(
f,
[name.format(), colon_token.format(), space(), value.format()]
)
write!(f, [name.format(), colon_token.format()])?;

// Format trailing comments inline after the colon
let comments = f.context().comments().clone();
let trailing_comments = comments.trailing_comments(node.syntax());

if !trailing_comments.is_empty() {
for comment in trailing_comments {
write!(f, [space()])?;
let format_comment =
FormatRefWithRule::new(comment, FormatCssLeadingComment::default());
write!(f, [format_comment])?;
comment.mark_formatted();
}
write!(
f,
[indent(&format_args![hard_line_break(), &value.format()])]
)
} else {
write!(f, [space(), value.format()])
}
}

fn fmt_trailing_comments(
&self,
_node: &CssGenericProperty,
_f: &mut CssFormatter,
) -> FormatResult<()> {
// Trailing comments are formatted inline in fmt_fields
Ok(())
}
}
42 changes: 38 additions & 4 deletions crates/biome_css_formatter/src/utils/component_value_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,13 @@ where
let has_leading_newline = element.syntax().has_leading_newline();

if has_leading_newline {
dbg!("here1");
write!(f, [hard_line_break()])?;
} else {
write!(f, [space()])?;
}
} else if at_group_boundary {
dbg!("here2");
write!(f, [hard_line_break()])?;
Comment on lines +197 to 204
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

Remove debug statements before merge.

These dbg!() calls appear to be temporary debugging hooks left in the code. While the coding guidelines do mention using dbg!() for debugging, these should be removed before the PR is merged to avoid noisy output during formatting.

🧹 Proposed fix
                                if has_leading_newline {
-                                    dbg!("here1");
                                    write!(f, [hard_line_break()])?;
                                } else {
                                    write!(f, [space()])?;
                                }
                            } else if at_group_boundary {
-                                dbg!("here2");
                                write!(f, [hard_line_break()])?;
📝 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
dbg!("here1");
write!(f, [hard_line_break()])?;
} else {
write!(f, [space()])?;
}
} else if at_group_boundary {
dbg!("here2");
write!(f, [hard_line_break()])?;
write!(f, [hard_line_break()])?;
} else {
write!(f, [space()])?;
}
} else if at_group_boundary {
write!(f, [hard_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/utils/component_value_list.rs` around lines
197 - 204, Remove the temporary dbg!() calls left in component_value_list.rs
around the conditional that chooses between hard_line_break() and space():
delete the dbg!("here1") and dbg!("here2") invocations so they no longer print
to stdout; keep the surrounding logic that calls write!(f, [hard_line_break()])?
and write!(f, [space()])? intact and run tests/formatting to confirm no behavior
change. Use the nearby identifiers (hard_line_break(), space(),
at_group_boundary, and the write!(f, ...) calls) to locate the exact spots to
clean up.

} else {
write!(f, [soft_line_break_or_space()])?
Expand Down Expand Up @@ -227,8 +229,12 @@ where
// This is also why `at_group_boundary` is initialized to `false` even when
// the layout is OneGroupPerLine: because the line break would be ignored
// if `at_group_boundary` were set to `true` initially.
at_group_boundary =
is_comma && matches!(layout, ValueListLayout::OneGroupPerLine);
at_group_boundary = is_comma
&& matches!(
layout,
ValueListLayout::OneGroupPerLine
| ValueListLayout::OneGroupPerLineWithDanglingComments
);

Ok(())
}),
Expand Down Expand Up @@ -272,6 +278,11 @@ where

write!(f, [group(&indent(&content))])
}
ValueListLayout::OneGroupPerLineWithDanglingComments => {
// Dangling comments are formatted inline by the property's fmt_dangling_comments
// We only need to indent the values, no hard line break here
write!(f, [group(&indent(&values))])
}
}
}

Expand Down Expand Up @@ -354,6 +365,15 @@ pub(crate) enum ValueListLayout {
/// These conditions are inherited from Prettier,
/// see https://github.com/biomejs/biome/pull/5334 for a detailed explanation
OneGroupPerLine,

/// Similar to OneGroupPerLine, but formats dangling comments on the property inline
/// before the line break. Used when comments appear between the colon and values.
/// ```css
/// font-family: /* comment */
/// Hiragino Sans,
/// sans-serif;
/// ```
OneGroupPerLineWithDanglingComments,
}

fn should_preceded_by_softline<N, I>(node: &N) -> bool
Expand All @@ -371,7 +391,7 @@ where
/// printed compactly.
pub(crate) fn get_value_list_layout<N, I>(
list: &N,
_: &CssComments,
comments: &CssComments,
f: &CssFormatter,
) -> ValueListLayout
where
Expand Down Expand Up @@ -402,13 +422,27 @@ where
.iter()
.any(|x| CssGenericDelimiter::cast_ref(x.syntax()).is_some());

// Check if the property name has trailing comments (comments between name and values)
// If so, we don't need to change the layout since the comments will be formatted
// inline with the property name, outside the value indent block

// Check if the parent property has trailing comments (comments between colon and values)
let parent_property = list.parent::<CssGenericProperty>();
let has_trailing_comments = parent_property
.as_ref()
.is_some_and(|prop| !comments.trailing_comments(prop.syntax()).is_empty());

// TODO: Check for comments, check for the types of elements in the list, etc.
if is_grid_property {
ValueListLayout::PreserveInline
} else if list.len() == 1 {
ValueListLayout::SingleValue
} else if use_one_group_per_line(css_property.as_deref(), list) {
ValueListLayout::OneGroupPerLine
if has_trailing_comments {
ValueListLayout::OneGroupPerLineWithDanglingComments
} else {
ValueListLayout::OneGroupPerLine
}
} else if is_comma_separated
&& value_count > 12
&& text_size >= TextSize::from(f.options().line_width().value() as u32)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ st.css");
@import url("./test.css") /* Comment */
layer(/* Comment */ /* Comment */ default) /* Comment */
supports(
/* Comment */ /* Comment */ /* Comment */ display: flex /* Comment */
/* Comment */ display /* Comment */ /* Comment */: flex /* Comment */
) /* Comment */
screen /* Comment */ and /* Comment */ (
/* Comment */ /* Comment */ /* Comment */ min-width: 400px /* Comment */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Comment after colon in property value */
[lang]:lang(ja) {
font-family: /* system-ui,*/ Hiragino Sans, sans-serif;
}

/* Another case */
.selector {
color: /* red, */ blue;
}

/* Another case */
.selector {
color/* red, */: blue;
}

/* Comment before property name should still work */
.selector {
/* comment */
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: css/comments/property-value-comment.css
---

# Input

```css
/* Comment after colon in property value */
[lang]:lang(ja) {
font-family: /* system-ui,*/ Hiragino Sans, sans-serif;
}
/* Another case */
.selector {
color: /* red, */ blue;
}
/* Another case */
.selector {
color/* red, */: blue;
}
/* Comment before property name should still work */
.selector {
/* comment */
color: red;
}
```


=============================

# Outputs

## Output 1

-----
Indent style: Tab
Indent width: 2
Line ending: LF
Line width: 80
Quote style: Double Quotes
Trailing newline: true
-----

```css
/* Comment after colon in property value */
[lang]:lang(ja) {
font-family: /* system-ui,*/
Hiragino Sans,
sans-serif;
}
/* Another case */
.selector {
color: /* red, */
blue;
}
/* Another case */
.selector {
color /* red, */: blue;
}
/* Comment before property name should still work */
.selector {
/* comment */
color: red;
}
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
assertion_line: 212
info: css/scss/declaration/mixed.scss
---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
assertion_line: 212
info: css/scss/declaration/nested-properties-empty-value.scss
---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
assertion_line: 212
info: css/scss/declaration/parent-and-colon-values.scss
---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
assertion_line: 212
info: css/scss/expression/list-map-paren.scss
---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
assertion_line: 212
info: css/scss/expression/precedence.scss
---

# Input

```scss
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
assertion_line: 212
info: css/unary-precedence.css
---

Expand Down
Loading
Loading