diff --git a/crates/oxc_prettier/src/comment.rs b/crates/oxc_prettier/src/comment.rs index 9eda3703a..675b5d0e3 100644 --- a/crates/oxc_prettier/src/comment.rs +++ b/crates/oxc_prettier/src/comment.rs @@ -8,6 +8,7 @@ use oxc_ast::CommentKind; use oxc_span::Span; use crate::{ + array, doc::{Doc, DocBuilder, Separator}, hardline, indent, line, ss, Prettier, }; @@ -40,6 +41,30 @@ impl DanglingCommentsPrintOptions { } } +#[derive(Debug, Clone, Copy)] +struct Comment { + start: u32, + end: u32, + is_block: bool, + has_line_suffix: bool, +} + +impl Comment { + fn new(start: u32, end: u32, kind: CommentKind) -> Self { + // The comment span is for the comment value + // -2 for `//` and `/*` + let start = start - 2; + // +2 for `/*` + let end = if kind.is_multi_line() { end + 2 } else { end }; + Self { start, end, is_block: kind.is_multi_line(), has_line_suffix: false } + } + + fn with_line_suffix(mut self, yes: bool) -> Self { + self.has_line_suffix = yes; + self + } +} + impl<'a> Prettier<'a> { #[must_use] pub(crate) fn print_comments( @@ -71,34 +96,11 @@ impl<'a> Prettier<'a> { pub(crate) fn print_leading_comments(&mut self, range: Span) -> Option> { let mut parts = self.vec(); while let Some((start, end, kind)) = self.trivias.peek().copied() { + let comment = Comment::new(start, end, kind); // Comment before the span if end <= range.start { self.trivias.next(); - - parts.push(self.print_comment(start, end, kind)); - if kind.is_multi_line() { - let line_break = if self.has_newline(end) { - if self.has_newline(start) { - hardline!() - } else { - line!() - } - } else { - ss!(" ") - }; - parts.push(line_break); - } else { - parts.push(hardline!()); - } - - if self - .get_comment_end(kind, end) - .map(|end| self.skip_spaces(end)) - .and_then(|idx| self.skip_newline(idx)) - .is_some_and(|i| self.has_newline(i)) - { - parts.push(hardline!()); - } + self.print_leading_comment(&mut parts, comment); } else { break; } @@ -109,20 +111,109 @@ impl<'a> Prettier<'a> { Some(Doc::Array(parts)) } + fn print_leading_comment(&mut self, parts: &mut Vec<'a, Doc<'a>>, comment: Comment) { + let printed = self.print_comment(comment); + parts.push(printed); + + if comment.is_block { + let line_break = if self.has_newline(comment.end, /* backwards */ false) { + if self.has_newline(comment.start, /* backwards */ true) { + hardline!() + } else { + line!() + } + } else { + ss!(" ") + }; + parts.push(line_break); + } else { + parts.push(hardline!()); + } + + if self + .skip_spaces(comment.end, false) + .and_then(|idx| self.skip_newline(Some(idx), false)) + .is_some_and(|i| self.has_newline(i, /* backwards */ false)) + { + parts.push(hardline!()); + } + } + #[must_use] - #[allow(clippy::unused_self)] - pub(crate) fn print_trailing_comments(&mut self, _range: Span) -> Option> { - None + pub(crate) fn print_trailing_comments(&mut self, range: Span) -> Option> { + let mut parts = self.vec(); + let mut previous_comment: Option = None; + while let Some((start, end, kind)) = self.trivias.peek().copied() { + let comment = Comment::new(start, end, kind); + // Trailing comment if there is nothing in between. + if range.end < comment.start + && self.source_text[range.end as usize..comment.start as usize] + .chars() + .all(|c| c == ' ') + { + self.trivias.next(); + let previous = self.print_trailing_comment(&mut parts, comment, previous_comment); + previous_comment = Some(previous); + } else { + break; + } + } + if parts.is_empty() { + return None; + } + Some(Doc::Array(parts)) + } + + fn print_trailing_comment( + &mut self, + parts: &mut Vec<'a, Doc<'a>>, + comment: Comment, + previous: Option, + ) -> Comment { + let printed = self.print_comment(comment); + + if previous.is_some_and(|c| c.has_line_suffix && !c.is_block) + || self.has_newline(comment.start, /* backwards */ true) + { + parts.push(printed); + let suffix = { + let mut parts = self.vec(); + parts.push(hardline!()); + if self.is_previous_line_empty(comment.start) { + parts.push(hardline!()); + } + parts + }; + parts.push(Doc::LineSuffix(suffix)); + return comment.with_line_suffix(true); + } + + if !comment.is_block || previous.is_some_and(|c| c.has_line_suffix) { + let suffix = { + let mut parts = self.vec(); + parts.push(ss!(" ")); + parts.push(printed); + parts + }; + let doc = array![self, Doc::LineSuffix(suffix), Doc::BreakParent]; + parts.push(doc); + return comment.with_line_suffix(true); + } + + let doc = array![self, ss!(" "), printed]; + parts.push(doc); + comment.with_line_suffix(false) } #[must_use] pub(crate) fn print_inner_comment(&mut self, range: Span) -> Vec<'a, Doc<'a>> { let mut parts = self.vec(); while let Some((start, end, kind)) = self.trivias.peek().copied() { + let comment = Comment::new(start, end, kind); // Comment within the span - if start >= range.start && end <= range.end { + if comment.start >= range.start && comment.end <= range.end { self.trivias.next(); - parts.push(self.print_comment(start, end, kind)); + parts.push(self.print_comment(comment)); } else { break; } @@ -139,9 +230,10 @@ impl<'a> Prettier<'a> { ) -> Option> { let mut parts = vec![]; while let Some((start, end, kind)) = self.trivias.peek().copied() { + let comment = Comment::new(start, end, kind); // Comment within the span - if end <= range.end { - parts.push(self.print_comment(start, end, kind)); + if comment.end <= range.end { + parts.push(self.print_comment(comment)); self.trivias.next(); } else { break; @@ -157,18 +249,7 @@ impl<'a> Prettier<'a> { } #[must_use] - fn print_comment(&self, start: u32, end: u32, kind: CommentKind) -> Doc<'a> { - let end_offset = if kind.is_multi_line() { 2 } else { 0 }; - let comment = Span::new(start - 2, end + end_offset).source_text(self.source_text); - Doc::Str(comment) - } - - #[allow(clippy::cast_possible_truncation)] - fn get_comment_end(&self, kind: CommentKind, end: u32) -> Option { - if kind.is_single_line() { - self.source_text[end as usize..].chars().next().map(|c| end + c.len_utf8() as u32) - } else { - Some(end) - } + fn print_comment(&self, comment: Comment) -> Doc<'a> { + Doc::Str(Span::new(comment.start, comment.end).source_text(self.source_text)) } } diff --git a/crates/oxc_prettier/src/doc.rs b/crates/oxc_prettier/src/doc.rs index 1e0ae66ea..825d7ba3e 100644 --- a/crates/oxc_prettier/src/doc.rs +++ b/crates/oxc_prettier/src/doc.rs @@ -42,6 +42,8 @@ pub enum Doc<'a> { /// it's going to add a break whenever the next element doesn't fit in the line anymore. /// The difference with `group` is that it's not going to break all the separators, just the ones that are at the end of lines. Fill(Fill<'a>), + /// Include this anywhere to force all parent groups to break. + BreakParent, } #[derive(Debug)] @@ -225,6 +227,9 @@ fn print_doc_to_debug(doc: &Doc<'_>) -> std::string::String { } string.push(')'); } + Doc::BreakParent => { + string.push_str("BreakParent"); + } } string diff --git a/crates/oxc_prettier/src/format/mod.rs b/crates/oxc_prettier/src/format/mod.rs index d126fd72d..8192c83ed 100644 --- a/crates/oxc_prettier/src/format/mod.rs +++ b/crates/oxc_prettier/src/format/mod.rs @@ -26,7 +26,7 @@ use std::borrow::Cow; use oxc_allocator::{Box, Vec}; use oxc_ast::{ast::*, AstKind}; -use oxc_span::{GetSpan, Span}; +use oxc_span::GetSpan; use crate::{ array, @@ -150,13 +150,14 @@ impl<'a> Format<'a> for IfStatement<'a> { wrap!(p, self, IfStatement, { let mut parts = p.vec(); + let test_doc = format!(p, self.test); let consequent = format!(p, self.consequent); let consequent = misc::adjust_clause(p, &self.consequent, consequent, false); let opening = group![ p, ss!("if ("), - group!(p, indent!(p, softline!(), format!(p, self.test)), softline!()), + group!(p, indent!(p, softline!(), test_doc), softline!()), ss!(")"), consequent ]; @@ -1490,14 +1491,6 @@ impl<'a> Format<'a> for ObjectProperty<'a> { } } else { parts.push(format!(p, self.key)); - let comments = p.print_inner_comment(Span::new( - self.key.span().end, - self.value.span().start, - )); - if !comments.is_empty() { - parts.push(ss!(" ")); - parts.extend(comments); - } parts.push(ss!(": ")); parts.push(format!(p, self.value)); } @@ -1509,25 +1502,26 @@ impl<'a> Format<'a> for ObjectProperty<'a> { impl<'a> Format<'a> for PropertyKey<'a> { fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> { - match self { - PropertyKey::Identifier(ident) => ident.format(p), - PropertyKey::PrivateIdentifier(ident) => ident.format(p), - PropertyKey::Expression(expr) => match expr { - Expression::StringLiteral(literal) => { - let expr = format!(p, literal); - let value = literal.value.as_bytes(); - if !&value[0].is_ascii_digit() && !value.contains(&b'_') { - p.str(&literal.value) - } else { - literal.format(p) + wrap!(p, self, PropertyKey, { + match self { + PropertyKey::Identifier(ident) => ident.format(p), + PropertyKey::PrivateIdentifier(ident) => ident.format(p), + PropertyKey::Expression(expr) => match expr { + Expression::StringLiteral(literal) => { + let value = literal.value.as_bytes(); + if !&value[0].is_ascii_digit() && !value.contains(&b'_') { + p.str(&literal.value) + } else { + literal.format(p) + } } - } - Expression::Identifier(ident) => { - array!(p, ss!("["), ident.format(p), ss!("]")) - } - _ => expr.format(p), - }, - } + Expression::Identifier(ident) => { + array!(p, ss!("["), ident.format(p), ss!("]")) + } + _ => expr.format(p), + }, + } + }) } } diff --git a/crates/oxc_prettier/src/lib.rs b/crates/oxc_prettier/src/lib.rs index 2af0de294..67cfff351 100644 --- a/crates/oxc_prettier/src/lib.rs +++ b/crates/oxc_prettier/src/lib.rs @@ -124,26 +124,58 @@ impl<'a> Prettier<'a> { } #[allow(clippy::cast_possible_truncation)] - fn skip_newline(&self, start_index: u32) -> Option { - let c = self.source_text[start_index as usize..].chars().next()?; - is_line_terminator(c).then(|| start_index + c.len_utf8() as u32) + fn skip_newline(&self, start_index: Option, backwards: bool) -> Option { + let start_index = start_index?; + let c = if backwards { + self.source_text[..=start_index as usize].chars().next_back() + } else { + self.source_text[start_index as usize..].chars().next() + }?; + if is_line_terminator(c) { + let len = c.len_utf8() as u32; + return Some(if backwards { start_index - len } else { start_index + len }); + } + Some(start_index) } - fn skip_spaces(&self, start_index: u32) -> u32 { + fn skip_spaces(&self, start_index: u32, backwards: bool) -> Option { let mut index = start_index; - for c in self.source_text[start_index as usize..].chars() { - if matches!(c, ' ' | '\t') { - index += 1; - } else { - break; + if backwards { + for c in self.source_text[..=start_index as usize].chars().rev() { + if !matches!(c, ' ' | '\t') { + return Some(index); + } + index -= 1_u32; + } + } else { + for c in self.source_text[start_index as usize..].chars() { + if !matches!(c, ' ' | '\t') { + return Some(index); + } + index += 1_u32; } } - index + None } - fn has_newline(&self, start_index: u32) -> bool { - let idx = self.skip_spaces(start_index); - let idx2 = self.skip_newline(idx); - Some(idx) != idx2 + fn has_newline(&self, start_index: u32, backwards: bool) -> bool { + if (backwards && start_index == 0) + || (!backwards && start_index as usize == self.source_text.len()) + { + return false; + } + let start_index = if backwards { start_index - 1 } else { start_index }; + let idx = self.skip_spaces(start_index, backwards); + let idx2 = self.skip_newline(idx, backwards); + idx != idx2 + } + + fn is_previous_line_empty(&self, start_index: u32) -> bool { + let idx = start_index - 1; + let idx = self.skip_spaces(idx, true); + let Some(idx) = self.skip_newline(idx, true) else { return false }; + let idx = self.skip_spaces(idx, true); + let idx2 = self.skip_newline(idx, true); + idx != idx2 } } diff --git a/crates/oxc_prettier/src/printer/mod.rs b/crates/oxc_prettier/src/printer/mod.rs index 2540dc8f4..800f7b8d5 100644 --- a/crates/oxc_prettier/src/printer/mod.rs +++ b/crates/oxc_prettier/src/printer/mod.rs @@ -105,10 +105,11 @@ impl<'a> Printer<'a> { Doc::IndentIfBreak(docs) => self.handle_indent_if_break(indent, mode, docs), Doc::Line => self.handle_line(indent, mode), Doc::Softline => self.handle_softline(indent, mode), - Doc::Hardline => self.handle_hardline(indent), + Doc::Hardline => self.handle_line(indent, Mode::Break), Doc::LineSuffix(docs) => self.handle_line_suffix(indent, mode, docs), Doc::IfBreak(doc) => self.handle_if_break(doc.unbox(), indent, mode), Doc::Fill(fill) => self.handle_fill(indent, mode, fill), + Doc::BreakParent => {} // No op } if self.cmds.is_empty() && !self.line_suffix.is_empty() { @@ -192,6 +193,11 @@ impl<'a> Printer<'a> { fn handle_line(&mut self, indent: Indent, mode: Mode) { if mode.is_break() { + if !self.line_suffix.is_empty() { + self.cmds.extend(self.line_suffix.drain(..).rev()); + return; + } + self.handle_hardline(indent); } else { self.out.push(b' '); @@ -201,7 +207,7 @@ impl<'a> Printer<'a> { fn handle_softline(&mut self, indent: Indent, mode: Mode) { if mode.is_break() { - self.handle_hardline(indent); + self.handle_line(indent, Mode::Break); } } @@ -360,6 +366,7 @@ impl<'a> Printer<'a> { Doc::LineSuffix(_) => { break; } + Doc::BreakParent => {} } if remaining_width < 0 { diff --git a/tasks/prettier_conformance/prettier.snap.md b/tasks/prettier_conformance/prettier.snap.md index b7919df09..b2e311a7d 100644 --- a/tasks/prettier_conformance/prettier.snap.md +++ b/tasks/prettier_conformance/prettier.snap.md @@ -1,4 +1,8 @@ +<<<<<<< Updated upstream +Compatibility: 171/591 (28.93%) +======= Compatibility: 168/591 (28.43%) +>>>>>>> Stashed changes # Failed