fix(formatter_css): keep a same-line line comment on its line (#26321)

Consistent output with other language formatters.
This commit is contained in:
leaysgur committed 2026-09-04 07:14:53 +00:00
1 parent cafaf32de1
commit 172ddb67dc
10 files changed
+674 -77

No files matched your search

@@ -104,6 +104,8 @@ The invariants (placement only; losing a comment is the lossless contract under
- Terminator vs separator: a terminator cannot be replaced by another token (`;` after a JS statement); a separator can (`,` / `;` between TS interface members)
- The replaceability test only separates these two
- Comments may move behind a terminator (per-language compat tables decide when); they always stay before a separator
- Except a same-line line comment: it rides a `line_suffix` and lands just past the separator (`a // c\n, b` -> `a, // c`), the separator cannot follow it on the line;
- an own-line comment leads what follows the separator instead
- Grammar-fixed DELIMITER (braces, a head's parens) is neither: it bounds a region and stays user content, never crossed
- (JS/TS) Redundant expression parentheses are NOT delimiters: the formatter drops them and re-derives parens by its own rules, so any paren in the output is formatter-owned
- Trailing comment inside the dropped pair moves behind the terminator, even across a re-printed pair
+4
View File
@@ -79,6 +79,10 @@ The shared invariants (FORMATTER_POLICY.md "Comment placement invariants") apply
- Line-boundary rule in CSS terms: `//` comments force a hardline after;
- a `//` on a list `,`'s line stays there (`1, // c`, like JS), together with the block comments glued before it on that line;
Prettier's CSS moves it below as the next element's leading comment (DIVERGENCES.md "line-comment-after-comma")
- a `//` before a list `,` rides past it (`a // c\n, b` -> `a, // c`), an own-line comment there leads the next element;
a `//` glued to a prelude's end stays there and `{` starts the next line (DIVERGENCES.md "line-comment-before-block")
- a structured prelude printer places its own comments, word by word (`value::write_with_comments`: own-line ones lead the word, glued ones trail it),
and `write_at_rule` writes whatever is still pending before the `;` / `{`, so a prelude comment is never lost
- own-line comments stay own-line at statement and trailing level, but a value-level own-line BLOCK comment is a plain fill item (joins the line when it fits):
- it carries no line-based semantics, and freezing it own-line would pin a wrapped layout (= not idempotent)
+70 -5
View File
@@ -563,11 +563,16 @@ Its JS printer follows the option (`[1, 2\n // own]`), and so do all our formatt
);
```
An own-line trailing comment before a closing `)` keeps its own line:
in maps and `@use`/`@forward with (...)` configs (any comment kind),
in map-item lists (any kind, the body is already one item per line),
and in paren lists, call/`@include` arguments and `@mixin` parameters
(`//` only: an own-line block comment in a fill body is a fill item, see AGENTS.md).
An own-line trailing comment before a closing `)` keeps its own line
(so does an own-line comment before a list comma: it leads the next element, `a,\n // c\n b`, pinned in `line-comment-before-comma.scss`):
- maps
- `@use`/`@forward with (...)` configs (any comment kind)
- map-item lists (any kind, the body is already one item per line)
- paren lists
- call/`@include` arguments
- `@mixin` parameters
`//` only: an own-line block comment in a fill body is a fill item, see AGENTS.md.
Prettier's output changes line, own-line to the last item's line, a `lineSuffix` artifact of its comma-group printing.
Same-line trailing comments still glue (matching Prettier); moving an own-line comment up would destroy the author's visual grouping.
@@ -699,6 +704,66 @@ Applies at every comma site: values, function and `@include` arguments, maps, pa
`@import` paths and modifiers, `@layer`, `@custom-selector`, `@media` query lists and `selector()` lists.
For `@media` Prettier's move also swallows the next query (`@media a, // c b {`), a semantics bug on its side.
## line-comment-before-block
- Why: invariant
- Pin: `tests/fixtures/format/scss/at-rule-comment-before-block.scss`
```scss
/* input */
@supports (a: b) // c
{
color: red;
}
/* ours */
@supports (a: b) // c
{
color: red;
}
/* prettier */
@supports (a: b) { // c
color: red;
}
```
A `//` glued to the end of an at-rule prelude stays on that line and the `{` starts the next;
Prettier moves it past the `{` as the block's first comment for `@media` / `@supports` / `@mixin` / `@include`
(and keeps it before the `{` for `@page` / `@keyframes` / `@font-face` / `@if`):
the comment crosses the `{`, a grammar-fixed delimiter.
For `@media screen // c {` and `@else // c` Prettier's move also swallows the `{` / the comment text, bugs on its side.
## line-comment-before-comma-fill-head
- Why: uniform-rule (same construct, same output: `@each $k in a, // c` with the `//` after the comma)
- Pin: `tests/fixtures/format/scss/line-comment-before-comma.scss`
```scss
/* input */
@each $k in a // c
, b {
}
/* ours */
@each $k in a, // c
b
{
}
/* prettier */
@each $k
in a, // c
b
{
}
```
A `//` before a list comma rides past the comma and ends the line there; the fill entries before it stay on their line,
exactly as when the `//` follows the comma in the source.
Prettier attaches a `breakParent` to the deferred comment, which its fill measures as never fitting,
so the entry BEFORE the comment (`in a,`) breaks away from `$k` too; the same source with the `//` after the comma keeps `$k in a,`.
## semiless-custom-property-block
- Why: cost
+44 -38
View File
@@ -195,18 +195,11 @@ pub(super) fn write_at_rule<'a>(at_rule: &AtRule<'a>, f: &mut CssFormatter<'_, '
// Whatever the prelude printer left pending (before `region_end`: the `{` or the `;`)
// is written here, so nothing inside the prelude is lost to the statement loop's cursor guard.
if let Some(block) = &at_rule.block {
// An inline comment between the prelude and `{` keeps its own line
// (Prettier's `lastLineHasInlineComment` → line instead of space).
if f.context().comments().peek().is_some_and(|c| c.inline && c.span.end <= region_end) {
write!(f, hard_line_break());
for &comment in f.context().comments().take_before(region_end) {
write!(f, FormatCommentBeforeContent::new(comment, BlockCommentAfter::HardLine));
}
} else {
value::flush_trailing_value_comments(region_end, f);
if !is_control_directive {
write!(f, space());
}
let prelude_end =
at_rule.prelude.as_ref().map_or(name_span.end, |prelude| to_span(prelude.span()).end);
value::write_comments_before_block(prelude_end, region_end, f);
if !is_control_directive {
write!(f, space());
}
statement::write_block(block, f);
} else {
@@ -786,8 +779,10 @@ fn write_at_rule_prelude<'a>(prelude: &AtRulePrelude<'a>, f: &mut CssFormatter<'
let body = format_with(move |f: &mut CssFormatter<'_, 'a>| {
if let Some(prefix) = &namespace.prefix {
let span = to_span(prefix.span());
write!(f, text(source.text_for(&span)));
value::flush_glued_comments(span.end, to_span(namespace.span()).end, f);
let prelude_end = to_span(namespace.span()).end;
value::write_with_comments(span, prelude_end, f, |f| {
write!(f, text(source.text_for(&span)));
});
write!(f, space());
value::flush_value_comments(to_span(namespace.uri.span()).start, f);
}
@@ -1473,18 +1468,15 @@ fn write_import_prelude_inner<'a>(import: &ImportPrelude<'a>, f: &mut CssFormatt
}
// A `//` between the parts breaks the prelude either way
let prelude_end = to_span(import.span()).end;
let write_part = |span: oxc_span::Span,
f: &mut CssFormatter<'_, 'a>,
write: &dyn Fn(&mut CssFormatter<'_, 'a>)| {
value::flush_value_comments(span.start, f);
write(f);
value::flush_glued_comments(span.end, prelude_end, f);
};
write_part(to_span(import.href.span()), f, &|f| write_import_href(&import.href, f));
value::write_with_comments(to_span(import.href.span()), prelude_end, f, |f| {
write_import_href(&import.href, f);
});
if let Some(layer) = &import.layer {
write!(f, space());
let span = to_span(layer.span());
write_part(span, f, &|f| write!(f, text(source.text_for(&span))));
value::write_with_comments(span, prelude_end, f, |f| {
write!(f, text(source.text_for(&span)));
});
}
if let Some(supports) = &import.supports {
// `@import ... supports(<cond>)`.
@@ -1496,7 +1488,7 @@ fn write_import_prelude_inner<'a>(import: &ImportPrelude<'a>, f: &mut CssFormatt
// plus one of our own (a width-overflowing condition with no trailing media breaks INSIDE the parens, not before `supports`).
// Empty `supports()` was the prior data-loss stub.
write!(f, space());
write_part(to_span(supports.span()), f, &|f| {
value::write_with_comments(to_span(supports.span()), prelude_end, f, |f| {
write!(f, "supports(");
match &supports.kind {
// `supports(not (display: inline-grid))`, `supports(font-format(woff2))`
@@ -1649,10 +1641,12 @@ fn write_media_query_list<'a>(list: &MediaQueryList<'a>, f: &mut CssFormatter<'_
let body = format_with(move |f: &mut CssFormatter<'_, 'a>| {
write_media_query_list_inner(list, f);
});
// The indent only governs the inter-query `,`-breaks; a single query never breaks internally,
// so dropping it avoids leaking a level into an embedded `${}` placeholder break
// (a media value is flat text, like Prettier).
if list.queries.len() > 1 {
// The indent governs the inter-query `,`-breaks and a `//` break inside a query;
// without either a single query never breaks, and dropping the indent avoids
// leaking a level into an embedded `${}` placeholder break (a media value is flat text, like Prettier).
let breaks_inside =
f.context().comments().iter_before(to_span(list.span()).end).any(|c| c.inline);
if list.queries.len() > 1 || breaks_inside {
write!(f, group(&indent(&body)));
} else {
write!(f, group(&body));
@@ -1676,18 +1670,23 @@ fn write_media_query<'a>(query: &MediaQuery<'a>, f: &mut CssFormatter<'_, 'a>) {
match query {
MediaQuery::ConditionOnly(condition) => write_media_condition(condition, f),
MediaQuery::WithType(with_type) => {
// A `//` among the words breaks the query
let query_end = to_span(query.span()).end;
let write_word = |span: oxc_span::Span, f: &mut CssFormatter<'_, 'a>| {
value::write_with_comments(span, query_end, f, |f| {
write_maybe_lowercase(source.text_for(&span), f);
});
};
if let Some(modifier) = &with_type.modifier {
let span = to_span(modifier.span());
write_maybe_lowercase(source.text_for(&span), f);
write_word(to_span(modifier.span()), f);
write!(f, space());
}
let span = to_span(with_type.media_type.span());
write_maybe_lowercase(source.text_for(&span), f);
write_word(to_span(with_type.media_type.span()), f);
if let Some(condition) = &with_type.condition {
write!(f, space());
let and_span = to_span(condition.and.span());
write_maybe_lowercase(source.text_for(&and_span), f);
write_word(to_span(condition.and.span()), f);
write!(f, space());
value::flush_value_comments(to_span(condition.condition.span()).start, f);
write_media_condition(&condition.condition, f);
}
}
@@ -1700,11 +1699,12 @@ fn write_media_query<'a>(query: &MediaQuery<'a>, f: &mut CssFormatter<'_, 'a>) {
fn write_media_condition<'a>(condition: &MediaCondition<'a>, f: &mut CssFormatter<'_, 'a>) {
let source = f.context().source_text();
let condition_end = to_span(condition.span()).end;
for (i, kind) in condition.conditions.iter().enumerate() {
if i > 0 {
write!(f, space());
}
match kind {
value::write_with_comments(to_span(kind.span()), condition_end, f, |f| match kind {
MediaConditionKind::MediaInParens(in_parens) => {
write_media_in_parens(in_parens, f);
}
@@ -1726,7 +1726,7 @@ fn write_media_condition<'a>(condition: &MediaCondition<'a>, f: &mut CssFormatte
write!(f, space());
write_media_in_parens(&not.media_in_parens, f);
}
}
});
}
}
@@ -1866,6 +1866,7 @@ fn write_supports_condition<'a>(condition: &SupportsCondition<'a>, f: &mut CssFo
// A fill of keywords and parenthesized terms:
// a long condition breaks AFTER `and`/`or`, one indent in
// (`postcss-values` prints the params as a value group, so each word/paren is its own fill entry).
let condition_end = to_span(condition.span()).end;
let body = format_with(move |f: &mut CssFormatter<'_, 'a>| {
let mut filler = f.fill();
for kind in &condition.conditions {
@@ -1875,15 +1876,20 @@ fn write_supports_condition<'a>(condition: &SupportsCondition<'a>, f: &mut CssFo
SupportsConditionKind::Or(or) => (Some(&or.keyword), &or.condition),
SupportsConditionKind::Not(not) => (Some(&not.keyword), &not.condition),
};
// A `//` among the entries breaks the condition
if let Some(keyword) = keyword {
let kw = format_with(move |f: &mut CssFormatter<'_, 'a>| {
let span = to_span(keyword.span());
write_maybe_lowercase(f.context().source_text().text_for(&span), f);
value::write_with_comments(span, condition_end, f, |f| {
write_maybe_lowercase(f.context().source_text().text_for(&span), f);
});
});
filler.entry(&soft_line_break_or_space(), &kw);
}
let term = format_with(move |f: &mut CssFormatter<'_, 'a>| {
write_supports_in_parens(in_parens, f);
value::write_with_comments(to_span(in_parens.span()), condition_end, f, |f| {
write_supports_in_parens(in_parens, f);
});
});
filler.entry(&soft_line_break_or_space(), &term);
}
+19 -19
View File
@@ -5,7 +5,7 @@ use oxc_css_parser::ast::{
ComponentValue, InterpolableStr, SassEach, SassFor, SassForBoundaryKind, SassForward,
SassForwardVisibilityModifierKind, SassFunction, SassIfAtRule, SassInclude, SassList, SassMap,
SassMixin, SassModuleConfig, SassParameters, SassUnaryOperatorKind, SassUse,
SassUseNamespaceKind, SassVariableDeclaration,
SassUseNamespaceKind, SassVariableDeclaration, SimpleBlock,
};
use oxc_formatter_core::{
Buffer,
@@ -681,7 +681,7 @@ pub(super) fn write_sass_include<'a>(include: &SassInclude<'a>, f: &mut CssForma
pub(super) fn write_sass_if_at_rule<'a>(if_rule: &SassIfAtRule<'a>, f: &mut CssFormatter<'_, 'a>) {
write!(f, ["@if", space()]);
write_control_condition(&if_rule.if_clause.condition, f);
statement::write_block(&if_rule.if_clause.block, f);
write_block_after(to_span(if_rule.if_clause.condition.span()).end, &if_rule.if_clause.block, f);
for (clause, else_span) in if_rule.else_if_clauses.iter().zip(&if_rule.else_spans) {
write_else_join(to_span(else_span).start, f);
// `if` is a value word in postcss, so the condition may break after it
@@ -691,18 +691,23 @@ pub(super) fn write_sass_if_at_rule<'a>(if_rule: &SassIfAtRule<'a>, f: &mut CssF
} else {
write_condition_chain(Some("if"), &clause.condition, f);
}
statement::write_block(&clause.block, f);
write_block_after(to_span(clause.condition.span()).end, &clause.block, f);
}
if let Some(else_block) = &if_rule.else_clause {
let else_start = if_rule
.else_spans
.last()
.map_or_else(|| to_span(&else_block.span).start, |sp| to_span(sp).start);
write_else_join(else_start, f);
statement::write_block(else_block, f);
let block_start = to_span(&else_block.span).start;
let else_span = if_rule.else_spans.last().map_or(Span::empty(block_start), to_span);
write_else_join(else_span.start, f);
write_block_after(else_span.end, else_block, f);
}
}
/// A clause's block, with the comments between its head (ending at `head_end`)
/// and `{` placed first (`@if $a // c\n{`), so they do not fall into the block as its first leading comment.
fn write_block_after<'a>(head_end: u32, block: &SimpleBlock<'a>, f: &mut CssFormatter<'_, 'a>) {
value::write_comments_before_block(head_end, to_span(&block.span).start, f);
statement::write_block(block, f);
}
/// `} @else`: comments between them break the join; each keeps its line
/// (`} /* c */` stays on the brace line, an own-line comment stays own-line).
/// `else_start` (the keyword) bounds the take, so a comment after it leads the condition
@@ -904,10 +909,10 @@ pub(super) fn write_sass_forward<'a>(forward: &SassForward<'a>, f: &mut CssForma
let entry = format_with(move |f: &mut CssFormatter<'_, 'a>| {
value::write_text_with_leading_comments(to_span(member.span()), f);
if i + 1 < members.len() {
let comma = to_span(&visibility.comma_spans[i]).start;
write_same_line_trailing_comments(comma, f);
write!(f, ",");
value::flush_line_comment_after_comma(comma, f);
value::write_group_comma(
Some(to_span(&visibility.comma_spans[i]).start),
f,
);
}
});
filler.entry(&soft_line_break_or_space(), &entry);
@@ -990,12 +995,7 @@ fn write_sass_module_config<'a>(config: &SassModuleConfig<'a>, f: &mut CssFormat
write!(f, text(source.text_for(&span)));
}
if i + 1 < config.items.len() {
// An own-line comment before the comma stays pending and
// leads the next item instead.
let comma = to_span(&config.comma_spans[i]).start;
write_same_line_trailing_comments(comma, f);
write!(f, ",");
value::flush_line_comment_after_comma(comma, f);
value::write_group_comma(Some(to_span(&config.comma_spans[i]).start), f);
} else {
// Comments before `)` (past a trailing comma, which is dropped):
// same-line ones glue to the last item, own-line ones keep their line.
+93 -15
View File
@@ -37,7 +37,7 @@ use oxc_span::Span;
use crate::{
CssFormatOptions,
comments::{self, BlockCommentAfter, FormatCommentBeforeContent},
comments::{self, BlockCommentAfter, FormatCommentBeforeContent, FormatLineCommentSuffix},
format::to_span,
print::{CssFormatter, format_with, less, scss, statement},
};
@@ -596,17 +596,45 @@ pub(super) fn comma_group_is_multi(group: &[ComponentValue<'_>], is_first: bool)
}
/// A group's separator comma:
/// comments between the group and its comma stay before the comma (`a /* c */, b`);
/// a same-line `//` right after it stays on its line (`a, // c`, the line-boundary invariant),
/// only comments past those lead the next group.
/// a block comment between the group and its comma stays before the comma (`a /* c */, b`),
/// a same-line `//` there rides past it (`a // c\n,` -> `a, // c`: the comma cannot follow a `//`),
/// a same-line `//` right after it stays on its line (`a, // c`);
/// own-line comments and anything past those lead the next group (the line-boundary invariant).
pub(super) fn write_group_comma(comma_start: Option<u32>, f: &mut CssFormatter<'_, '_>) {
let Some(comma) = comma_start else {
write!(f, ",");
return;
};
flush_trailing_value_comments(comma, f);
let line_comment_deferred = flush_comments_before_separator(comma, f);
write!(f, ",");
flush_line_comment_after_comma(comma, f);
if line_comment_deferred {
// Flushes the deferred `//` right behind the comma and ends the line.
// A hardline, not `expand_parent`:
// inside a `fill` the latter would also break the separator BEFORE this entry (`@each $k\n in a, // c`),
// the former keeps it (`@each $k in a, // c`).
write!(f, hard_line_break());
}
}
/// The same-line comments before a separator at `separator_start`:
/// block comments glue before it, a `//` rides a `line_suffix` past it (its line ends after the separator).
/// Own-line comments stay pending, they lead what follows the separator.
/// Returns `true` when a `//` was deferred: the caller must end the line right after the separator.
fn flush_comments_before_separator(separator_start: u32, f: &mut CssFormatter<'_, '_>) -> bool {
flush_trailing_comments_while(0, separator_start, LineCommentAs::Suffix, f, |_, c, source| {
!comment_is_own_line(c, source)
})
}
/// How a trailing `//` is emitted by [`flush_trailing_comments_while`].
#[derive(Clone, Copy)]
enum LineCommentAs {
/// In place, ending the line (`FormatCommentBeforeContent`)
InPlace,
/// A `line_suffix`: the token written next (a separator) precedes it on the line;
/// the caller ends the line after that token
Suffix,
}
/// A `//` on the line of the comma just written (`a, // c`, `a, /* b */ // c`) stays on that line,
@@ -626,37 +654,83 @@ pub(super) fn flush_line_comment_after_comma(comma_start: u32, f: &mut CssFormat
/// as ` /* c */` / ` // c`, stopping after a `//` (it ends the line).
/// Unlike [`flush_same_line_comments`] a comment further along the line, past other tokens, stays pending.
pub(super) fn flush_glued_comments(prev_end: u32, upper_bound: u32, f: &mut CssFormatter<'_, '_>) {
flush_trailing_comments_while(prev_end, upper_bound, f, |prev_end, comment, source| {
source.all_bytes_match(prev_end, comment.span.start, |b| matches!(b, b' ' | b'\t'))
});
flush_trailing_comments_while(
prev_end,
upper_bound,
LineCommentAs::InPlace,
f,
|prev_end, comment, source| {
source.all_bytes_match(prev_end, comment.span.start, |b| matches!(b, b' ' | b'\t'))
},
);
}
/// A word with the comments around it: own-line ones before it lead it (`flush_value_comments`),
/// the run glued after it trails it (`flush_glued_comments`, bounded by `upper_bound`).
/// The shape of every structured prelude word (`@import` parts, `@media` words, `@supports` terms, ...).
pub(super) fn write_with_comments<'a>(
span: Span,
upper_bound: u32,
f: &mut CssFormatter<'_, 'a>,
write: impl FnOnce(&mut CssFormatter<'_, 'a>),
) {
flush_value_comments(span.start, f);
write(f);
flush_glued_comments(span.end, upper_bound, f);
}
/// Emits pending comments before `upper_bound` as ` /* c */` / ` // c` while `keep` accepts them
/// (`prev_end` is the previous comment's end from the second one on), stopping after a `//`.
/// Returns `true` when it stopped at a `//`.
fn flush_trailing_comments_while(
mut prev_end: u32,
upper_bound: u32,
line_comment_as: LineCommentAs,
f: &mut CssFormatter<'_, '_>,
keep: impl Fn(u32, comments::CssComment, SourceText<'_>) -> bool,
) {
) -> bool {
loop {
let Some(comment) = f.context().comments().peek() else { return };
let Some(comment) = f.context().comments().peek() else { return false };
let source = f.context().source_text();
if comment.span.start < prev_end
|| comment.span.end > upper_bound
|| !keep(prev_end, comment, source)
{
return;
return false;
}
f.context().comments().take_before(comment.span.end);
if comment.inline && matches!(line_comment_as, LineCommentAs::Suffix) {
write!(f, FormatLineCommentSuffix::new(comment).with_leading_space());
return true;
}
write!(f, [space(), FormatCommentBeforeContent::new(comment, BlockCommentAfter::None)]);
if comment.inline {
return;
return true;
}
prev_end = comment.span.end;
}
}
/// Comments between a prelude ending at `prelude_end` and its `{` at `block_start`:
/// a run glued to the prelude stays on its line (`@page :first // c`, the `//` ends it so `{` starts the next),
/// own-line ones keep their line before the `{`.
/// Either way the caller's space before `{` is safe: the printer drops it at the start of a line.
pub(super) fn write_comments_before_block(
prelude_end: u32,
block_start: u32,
f: &mut CssFormatter<'_, '_>,
) {
flush_glued_comments(prelude_end, block_start, f);
let own_line = f.context().comments().take_before(block_start);
if own_line.is_empty() {
return;
}
write!(f, hard_line_break());
for &comment in own_line {
write!(f, FormatCommentBeforeContent::new(comment, BlockCommentAfter::HardLine));
}
}
/// End of the comment run glued right after `prev_end` (only spaces/tabs between) that ends in a `//`;
/// `None` when no `//` is glued there (a lone block comment leads the next element instead).
/// `comments` are the pending comments in source order.
@@ -922,9 +996,13 @@ pub(super) fn flush_same_line_comments(
upper_bound: u32,
f: &mut CssFormatter<'_, '_>,
) {
flush_trailing_comments_while(prev_end, upper_bound, f, |_, comment, source| {
!comment_is_own_line(comment, source)
});
flush_trailing_comments_while(
prev_end,
upper_bound,
LineCommentAs::InPlace,
f,
|_, c, source| !comment_is_own_line(c, source),
);
}
/// Emits pending comments before `upper_bound` as ` /* c */` suffixes
@@ -0,0 +1,64 @@
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
// so `{` starts the next one. Own-line comments keep their line before the `{`.
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
@page :first // c
{
margin: 1px;
}
@page :first /* b */ // c
{
margin: 1px;
}
@page :first /* c */ {
margin: 1px;
}
@page :first
// c
{
margin: 1px;
}
@font-face // c
{
a: b;
}
@media screen // c
{
a {
b: c;
}
}
@supports (a: b) // c
{
a {
b: c;
}
}
@mixin m($a) // c
{
b: c;
}
.a {
@include m // c
{
b: c;
}
}
@if $a // c
{
b: c;
} @else // d
{
d: e;
}
@each $i in a, b // c
{
b: c;
}
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
@media screen // c
and (min-width: 1px) {
}
@supports (display: grid) // c
and (gap: 1px) {
}
@@ -0,0 +1,207 @@
---
source: crates/oxc_formatter_css/tests/fixtures/mod.rs
---
==================== Input ====================
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
// so `{` starts the next one. Own-line comments keep their line before the `{`.
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
@page :first // c
{
margin: 1px;
}
@page :first /* b */ // c
{
margin: 1px;
}
@page :first /* c */ {
margin: 1px;
}
@page :first
// c
{
margin: 1px;
}
@font-face // c
{
a: b;
}
@media screen // c
{
a {
b: c;
}
}
@supports (a: b) // c
{
a {
b: c;
}
}
@mixin m($a) // c
{
b: c;
}
.a {
@include m // c
{
b: c;
}
}
@if $a // c
{
b: c;
} @else // d
{
d: e;
}
@each $i in a, b // c
{
b: c;
}
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
@media screen // c
and (min-width: 1px) {
}
@supports (display: grid) // c
and (gap: 1px) {
}
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
// so `{` starts the next one. Own-line comments keep their line before the `{`.
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
@page :first // c
{
margin: 1px;
}
@page :first /* b */ // c
{
margin: 1px;
}
@page :first /* c */ {
margin: 1px;
}
@page :first
// c
{
margin: 1px;
}
@font-face // c
{
a: b;
}
@media screen // c
{
a {
b: c;
}
}
@supports (a: b) // c
{
a {
b: c;
}
}
@mixin m($a) // c
{
b: c;
}
.a {
@include m // c
{
b: c;
}
}
@if $a // c
{
b: c;
} @else // d
{
d: e;
}
@each $i in a, b // c
{
b: c;
}
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
@media screen // c
and (min-width: 1px) {
}
@supports (display: grid) // c
and (gap: 1px) {
}
-------------------
{ printWidth: 100 }
-------------------
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
// so `{` starts the next one. Own-line comments keep their line before the `{`.
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
@page :first // c
{
margin: 1px;
}
@page :first /* b */ // c
{
margin: 1px;
}
@page :first /* c */ {
margin: 1px;
}
@page :first
// c
{
margin: 1px;
}
@font-face // c
{
a: b;
}
@media screen // c
{
a {
b: c;
}
}
@supports (a: b) // c
{
a {
b: c;
}
}
@mixin m($a) // c
{
b: c;
}
.a {
@include m // c
{
b: c;
}
}
@if $a // c
{
b: c;
} @else // d
{
d: e;
}
@each $i in a, b // c
{
b: c;
}
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
@media screen // c
and (min-width: 1px) {
}
@supports (display: grid) // c
and (gap: 1px) {
}
===================== End =====================
@@ -0,0 +1,31 @@
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
// A block comment stays before the comma; an own-line comment leads the next element
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
a {
transition: a 1s // c
, b 2s;
transition: a 1s /* b */ // c
, b 2s;
transition: a 1s /* c */, b 2s;
transition: a 1s
// c
, b 2s;
b: f(a // c
, b);
@include m(a // c
, b);
}
$m: (a: 1 // c
, b: 2);
$l: (a // c
, b);
@each $k in a // c
, b {
}
@use "a" with ($a: 1 // c
, $b: 2);
@forward "b" show a // c
, b;
@@ -0,0 +1,140 @@
---
source: crates/oxc_formatter_css/tests/fixtures/mod.rs
---
==================== Input ====================
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
// A block comment stays before the comma; an own-line comment leads the next element
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
a {
transition: a 1s // c
, b 2s;
transition: a 1s /* b */ // c
, b 2s;
transition: a 1s /* c */, b 2s;
transition: a 1s
// c
, b 2s;
b: f(a // c
, b);
@include m(a // c
, b);
}
$m: (a: 1 // c
, b: 2);
$l: (a // c
, b);
@each $k in a // c
, b {
}
@use "a" with ($a: 1 // c
, $b: 2);
@forward "b" show a // c
, b;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
// A block comment stays before the comma; an own-line comment leads the next element
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
a {
transition:
a 1s, // c
b 2s;
transition:
a 1s /* b */, // c
b 2s;
transition:
a 1s /* c */,
b 2s;
transition:
a 1s,
// c
b 2s;
b: f(
a, // c
b
);
@include m(
a, // c
b
);
}
$m: (
a: 1, // c
b: 2,
);
$l: (
a, // c
b
);
@each $k in a, // c
b
{
}
@use "a" with (
$a: 1, // c
$b: 2
);
@forward "b" show a, // c
b;
-------------------
{ printWidth: 100 }
-------------------
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
// A block comment stays before the comma; an own-line comment leads the next element
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
a {
transition:
a 1s, // c
b 2s;
transition:
a 1s /* b */, // c
b 2s;
transition:
a 1s /* c */,
b 2s;
transition:
a 1s,
// c
b 2s;
b: f(
a, // c
b
);
@include m(
a, // c
b
);
}
$m: (
a: 1, // c
b: 2,
);
$l: (
a, // c
b
);
@each $k in a, // c
b
{
}
@use "a" with (
$a: 1, // c
$b: 2
);
@forward "b" show a, // c
b;
===================== End =====================