Skip to content

Commit 16b6c49

Browse files
authored
fix(css/formatter): comment placement in lists (#9261)
1 parent 78bce77 commit 16b6c49

10 files changed

Lines changed: 311 additions & 107 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#8409](https://github.com/biomejs/biome/issues/8409): CSS formatter now correctly places comments after the colon in property declarations.
6+
7+
Previously, comments that appeared after the colon in CSS property values were incorrectly moved before the property name:
8+
9+
```diff
10+
[lang]:lang(ja) {
11+
- /* system-ui,*/ font-family:
12+
+ font-family: /* system-ui,*/
13+
Hiragino Sans,
14+
sans-serif;
15+
}
16+
```

crates/biome_css_formatter/src/comments.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::prelude::*;
22
use biome_css_syntax::{
3-
AnyCssDeclarationName, AnyCssRoot, CssComplexSelector, CssFunction, CssIdentifier, CssLanguage,
4-
CssSyntaxKind, TextLen, TextSize,
3+
AnyCssDeclarationName, AnyCssRoot, CssComplexSelector, CssFunction, CssGenericProperty,
4+
CssIdentifier, CssLanguage, CssSyntaxKind, TextLen, TextSize,
55
};
66
use biome_diagnostics::category;
77
use biome_formatter::comments::{
@@ -99,21 +99,70 @@ impl CommentStyle for CssCommentStyle {
9999
) -> CommentPlacement<Self::Language> {
100100
match comment.text_position() {
101101
CommentTextPosition::EndOfLine => handle_function_comment(comment)
102+
.or_else(handle_generic_property_comment)
102103
.or_else(handle_declaration_name_comment)
103104
.or_else(handle_complex_selector_comment)
104105
.or_else(handle_global_suppression),
105106
CommentTextPosition::OwnLine => handle_function_comment(comment)
107+
.or_else(handle_generic_property_comment)
106108
.or_else(handle_declaration_name_comment)
107109
.or_else(handle_complex_selector_comment)
108110
.or_else(handle_global_suppression),
109111
CommentTextPosition::SameLine => handle_function_comment(comment)
112+
.or_else(handle_generic_property_comment)
110113
.or_else(handle_declaration_name_comment)
111114
.or_else(handle_complex_selector_comment)
112115
.or_else(handle_global_suppression),
113116
}
114117
}
115118
}
116119

120+
fn handle_generic_property_comment(
121+
comment: DecoratedComment<CssLanguage>,
122+
) -> CommentPlacement<CssLanguage> {
123+
// Check if the comment is inside a CSS generic property (e.g., color: value)
124+
let Some(generic_property) = comment
125+
.enclosing_node()
126+
.ancestors()
127+
.find_map(CssGenericProperty::cast)
128+
else {
129+
return CommentPlacement::Default(comment);
130+
};
131+
132+
let Ok(name) = generic_property.name() else {
133+
return CommentPlacement::Default(comment);
134+
};
135+
136+
let comment_piece = comment.piece();
137+
138+
// Check if the comment is in the name's trailing trivia (before colon)
139+
// Example: `color /* comment */: value`
140+
if let Some(name_token) = name.syntax().last_token() {
141+
for piece in name_token.trailing_trivia().pieces() {
142+
if piece.is_comments() && piece.text() == comment_piece.text() {
143+
// Our placement is slightly better than Prettier because it adds some spacing
144+
return CommentPlacement::trailing(name.into_syntax(), comment);
145+
}
146+
}
147+
}
148+
149+
if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node())
150+
{
151+
// If preceding is the property name and following is in the value list
152+
if preceding == name.syntax()
153+
&& following
154+
.parent()
155+
.is_some_and(|p| p.kind() == CssSyntaxKind::CSS_GENERIC_COMPONENT_VALUE_LIST)
156+
{
157+
// Place comment as dangling on the property so it can be formatted inline
158+
// between the colon and values
159+
return CommentPlacement::trailing(generic_property.into_syntax(), comment);
160+
}
161+
}
162+
163+
CommentPlacement::Default(comment)
164+
}
165+
117166
fn handle_declaration_name_comment(
118167
comment: DecoratedComment<CssLanguage>,
119168
) -> CommentPlacement<CssLanguage> {
Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use crate::comments::FormatCssLeadingComment;
12
use crate::prelude::*;
23
use biome_css_syntax::{CssGenericProperty, CssGenericPropertyFields};
3-
use biome_formatter::write;
4+
use biome_formatter::{CstFormatContext, FormatRefWithRule, format_args, write};
45

56
#[derive(Debug, Clone, Default)]
67
pub(crate) struct FormatCssGenericProperty;
@@ -12,9 +13,34 @@ impl FormatNodeRule<CssGenericProperty> for FormatCssGenericProperty {
1213
value,
1314
} = node.as_fields();
1415

15-
write!(
16-
f,
17-
[name.format(), colon_token.format(), space(), value.format()]
18-
)
16+
write!(f, [name.format(), colon_token.format()])?;
17+
18+
// Format trailing comments inline after the colon
19+
let comments = f.context().comments().clone();
20+
let trailing_comments = comments.trailing_comments(node.syntax());
21+
22+
if !trailing_comments.is_empty() {
23+
for comment in trailing_comments {
24+
write!(f, [space()])?;
25+
let format_comment = FormatRefWithRule::new(comment, FormatCssLeadingComment);
26+
write!(f, [format_comment])?;
27+
comment.mark_formatted();
28+
}
29+
write!(
30+
f,
31+
[indent(&format_args![hard_line_break(), &value.format()])]
32+
)
33+
} else {
34+
write!(f, [space(), value.format()])
35+
}
36+
}
37+
38+
fn fmt_trailing_comments(
39+
&self,
40+
_node: &CssGenericProperty,
41+
_f: &mut CssFormatter,
42+
) -> FormatResult<()> {
43+
// Trailing comments are formatted inline in fmt_fields
44+
Ok(())
1945
}
2046
}

crates/biome_css_formatter/src/utils/component_value_list.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,12 @@ where
227227
// This is also why `at_group_boundary` is initialized to `false` even when
228228
// the layout is OneGroupPerLine: because the line break would be ignored
229229
// if `at_group_boundary` were set to `true` initially.
230-
at_group_boundary =
231-
is_comma && matches!(layout, ValueListLayout::OneGroupPerLine);
230+
at_group_boundary = is_comma
231+
&& matches!(
232+
layout,
233+
ValueListLayout::OneGroupPerLine
234+
| ValueListLayout::OneGroupPerLineWithDanglingComments
235+
);
232236

233237
Ok(())
234238
}),
@@ -272,6 +276,9 @@ where
272276

273277
write!(f, [group(&indent(&content))])
274278
}
279+
ValueListLayout::OneGroupPerLineWithDanglingComments => {
280+
write!(f, [group(&values)])
281+
}
275282
}
276283
}
277284

@@ -354,6 +361,15 @@ pub(crate) enum ValueListLayout {
354361
/// These conditions are inherited from Prettier,
355362
/// see https://github.com/biomejs/biome/pull/5334 for a detailed explanation
356363
OneGroupPerLine,
364+
365+
/// Similar to OneGroupPerLine, but formats dangling comments on the property inline
366+
/// before the line break. Used when comments appear between the colon and values.
367+
/// ```css
368+
/// font-family: /* comment */
369+
/// Hiragino Sans,
370+
/// sans-serif;
371+
/// ```
372+
OneGroupPerLineWithDanglingComments,
357373
}
358374

359375
fn should_preceded_by_softline<N, I>(node: &N) -> bool
@@ -371,7 +387,7 @@ where
371387
/// printed compactly.
372388
pub(crate) fn get_value_list_layout<N, I>(
373389
list: &N,
374-
_: &CssComments,
390+
comments: &CssComments,
375391
f: &CssFormatter,
376392
) -> ValueListLayout
377393
where
@@ -402,13 +418,27 @@ where
402418
.iter()
403419
.any(|x| CssGenericDelimiter::cast_ref(x.syntax()).is_some());
404420

421+
// Check if the property name has trailing comments (comments between name and values)
422+
// If so, we don't need to change the layout since the comments will be formatted
423+
// inline with the property name, outside the value indent block
424+
425+
// Check if the parent property has trailing comments (comments between colon and values)
426+
let parent_property = list.parent::<CssGenericProperty>();
427+
let has_trailing_comments = parent_property
428+
.as_ref()
429+
.is_some_and(|prop| !comments.trailing_comments(prop.syntax()).is_empty());
430+
405431
// TODO: Check for comments, check for the types of elements in the list, etc.
406432
if is_grid_property {
407433
ValueListLayout::PreserveInline
408434
} else if list.len() == 1 {
409435
ValueListLayout::SingleValue
410436
} else if use_one_group_per_line(css_property.as_deref(), list) {
411-
ValueListLayout::OneGroupPerLine
437+
if has_trailing_comments {
438+
ValueListLayout::OneGroupPerLineWithDanglingComments
439+
} else {
440+
ValueListLayout::OneGroupPerLine
441+
}
412442
} else if is_comma_separated
413443
&& value_count > 12
414444
&& text_size >= TextSize::from(f.options().line_width().value() as u32)

crates/biome_css_formatter/tests/specs/css/atrule/import.css.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ st.css");
328328
@import url("./test.css") /* Comment */
329329
layer(/* Comment */ /* Comment */ default) /* Comment */
330330
supports(
331-
/* Comment */ /* Comment */ /* Comment */ display: flex /* Comment */
331+
/* Comment */ display /* Comment */ /* Comment */: flex /* Comment */
332332
) /* Comment */
333333
screen /* Comment */ and /* Comment */ (
334334
/* Comment */ /* Comment */ /* Comment */ min-width: 400px /* Comment */
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Comment after colon in property value */
2+
[lang]:lang(ja) {
3+
font-family: /* system-ui,*/ Hiragino Sans, sans-serif;
4+
}
5+
6+
/* Another case */
7+
.selector {
8+
color: /* red, */ blue;
9+
}
10+
11+
/* Another case */
12+
.selector {
13+
color/* red, */: blue;
14+
}
15+
16+
/* Comment before property name should still work */
17+
.selector {
18+
/* comment */
19+
color: red;
20+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
source: crates/biome_formatter_test/src/snapshot_builder.rs
3+
info: css/comments/property-value-comment.css
4+
---
5+
6+
# Input
7+
8+
```css
9+
/* Comment after colon in property value */
10+
[lang]:lang(ja) {
11+
font-family: /* system-ui,*/ Hiragino Sans, sans-serif;
12+
}
13+
14+
/* Another case */
15+
.selector {
16+
color: /* red, */ blue;
17+
}
18+
19+
/* Another case */
20+
.selector {
21+
color/* red, */: blue;
22+
}
23+
24+
/* Comment before property name should still work */
25+
.selector {
26+
/* comment */
27+
color: red;
28+
}
29+
30+
```
31+
32+
33+
# Formatted
34+
35+
```css
36+
/* Comment after colon in property value */
37+
[lang]:lang(ja) {
38+
font-family: /* system-ui,*/
39+
Hiragino Sans,
40+
sans-serif;
41+
}
42+
43+
/* Another case */
44+
.selector {
45+
color: /* red, */
46+
blue;
47+
}
48+
49+
/* Another case */
50+
.selector {
51+
color /* red, */: blue;
52+
}
53+
54+
/* Comment before property name should still work */
55+
.selector {
56+
/* comment */
57+
color: red;
58+
}
59+
60+
```

0 commit comments

Comments
 (0)