diff --git a/crates/oxc_formatter/src/generated/format.rs b/crates/oxc_formatter/src/generated/format.rs index fd4f7ee6dec31..5cafc7e714c0c 100644 --- a/crates/oxc_formatter/src/generated/format.rs +++ b/crates/oxc_formatter/src/generated/format.rs @@ -171,10 +171,7 @@ impl<'a> Format<'a> for AstNode<'a, TaggedTemplateExpression<'a>> { impl<'a> Format<'a> for AstNode<'a, TemplateElement<'a>> { fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - self.format_leading_comments(f)?; - let result = self.write(f); - self.format_trailing_comments(f)?; - result + self.write(f) } } diff --git a/crates/oxc_formatter/src/parentheses/expression.rs b/crates/oxc_formatter/src/parentheses/expression.rs index 8a4dad6179540..46b2fce1ce895 100644 --- a/crates/oxc_formatter/src/parentheses/expression.rs +++ b/crates/oxc_formatter/src/parentheses/expression.rs @@ -651,7 +651,6 @@ fn update_or_lower_expression_needs_parens(span: Span, parent: &AstNodes<'_>) -> | AstNodes::CallExpression(_) | AstNodes::NewExpression(_) | AstNodes::StaticMemberExpression(_) - | AstNodes::TemplateLiteral(_) | AstNodes::TaggedTemplateExpression(_) ) || is_class_extends(span, parent) { diff --git a/crates/oxc_formatter/src/utils/call_expression.rs b/crates/oxc_formatter/src/utils/call_expression.rs index e1cd108b28109..d8e7cdc946077 100644 --- a/crates/oxc_formatter/src/utils/call_expression.rs +++ b/crates/oxc_formatter/src/utils/call_expression.rs @@ -78,7 +78,7 @@ pub fn is_test_call_expression(call: &AstNode>) -> bool { _ => return false, }; - parameter_count == 2 || (parameter_count <= 1 && has_block_body) + arguments.len() == 2 || (parameter_count <= 1 && has_block_body) } _ => false, } @@ -123,7 +123,7 @@ fn is_unit_test_set_up_callee(callee: &Expression) -> bool { /// Same as . pub fn callee_name_iterator<'b>(expr: &'b Expression<'_>) -> impl Iterator { let mut current = Some(expr); - std::iter::from_fn(move || match current { + let mut names = std::iter::from_fn(move || match current { Some(Expression::Identifier(ident)) => { current = None; Some(ident.name.as_str()) @@ -133,7 +133,12 @@ pub fn callee_name_iterator<'b>(expr: &'b Expression<'_>) -> impl Iterator None, - }) + }); + + [names.next(), names.next(), names.next(), names.next(), names.next()] + .into_iter() + .rev() + .flatten() } /// This function checks if a call expressions has one of the following members: @@ -193,3 +198,41 @@ pub fn contains_a_test_pattern(expr: &Expression<'_>) -> bool { _ => false, } } + +pub fn is_test_each_pattern(expr: &Expression<'_>) -> bool { + let mut names = callee_name_iterator(expr); + + let first = names.next(); + let second = names.next(); + let third = names.next(); + let fourth = names.next(); + let fifth = names.next(); + + match first { + Some("describe" | "xdescribe" | "fdescribe") => match second { + Some("each") => third.is_none(), + Some("skip" | "only") => match third { + Some("each") => fourth.is_none(), + _ => false, + }, + _ => false, + }, + Some("test" | "xtest" | "ftest" | "it" | "xit" | "fit") => match second { + Some("each") => third.is_none(), + Some("skip" | "only" | "failing") => match third { + Some("each") => fourth.is_none(), + _ => false, + }, + Some("concurrent") => match third { + Some("each") => fourth.is_none(), + Some("only" | "skip") => match fourth { + Some("each") => fifth.is_none(), + _ => false, + }, + _ => false, + }, + _ => false, + }, + _ => false, + } +} diff --git a/crates/oxc_formatter/src/write/jsx/mod.rs b/crates/oxc_formatter/src/write/jsx/mod.rs index add472b1c187d..667eb691cec99 100644 --- a/crates/oxc_formatter/src/write/jsx/mod.rs +++ b/crates/oxc_formatter/src/write/jsx/mod.rs @@ -177,8 +177,8 @@ impl<'a> FormatWrite<'a> for AstNode<'a, JSXExpressionContainer<'a>> { | JSXExpression::BinaryExpression(_) ); - let should_inline = (is_conditional_or_binary - || should_inline_jsx_expression(&self.expression, f.comments())); + let should_inline = + (is_conditional_or_binary || should_inline_jsx_expression(self, f.comments())); if should_inline { write!(f, ["{", self.expression(), line_suffix_boundary(), "}"]) @@ -195,7 +195,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, JSXExpressionContainer<'a>> { } } } else { - let should_inline = should_inline_jsx_expression(&self.expression, f.comments()); + let should_inline = should_inline_jsx_expression(self, f.comments()); if should_inline { write!(f, ["{", self.expression(), line_suffix_boundary(), "}"]) @@ -241,14 +241,17 @@ impl<'a> FormatWrite<'a> for AstNode<'a, JSXExpressionContainer<'a>> { /// } /> /// ``` pub fn should_inline_jsx_expression( - expression: &JSXExpression<'_>, + container: &JSXExpressionContainer<'_>, comments: &Comments<'_>, ) -> bool { - if comments.has_comments_before(expression.span().start) { + let expression = &container.expression; + let span = expression.span(); + if comments.has_comments_before(span.start) + || comments.has_comments_between(span.end, container.span().end) + { return false; } - - match expression { + match &expression { JSXExpression::ArrayExpression(_) | JSXExpression::ObjectExpression(_) | JSXExpression::ArrowFunctionExpression(_) diff --git a/crates/oxc_formatter/src/write/mod.rs b/crates/oxc_formatter/src/write/mod.rs index bf60284e9dacc..6e034101faa83 100644 --- a/crates/oxc_formatter/src/write/mod.rs +++ b/crates/oxc_formatter/src/write/mod.rs @@ -19,6 +19,7 @@ mod parameter_list; mod return_or_throw_statement; mod semicolon; mod switch_statement; +mod template; mod try_statement; mod type_parameters; mod utils; @@ -56,7 +57,7 @@ use crate::{ parentheses::NeedsParentheses, utils::{ assignment_like::AssignmentLike, - call_expression::is_test_call_expression, + call_expression::{contains_a_test_pattern, is_test_call_expression, is_test_each_pattern}, conditional::ConditionalLike, member_chain::MemberChain, object::format_property_key, @@ -259,34 +260,6 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ObjectProperty<'a>> { } } -impl<'a> FormatWrite<'a> for AstNode<'a, TemplateLiteral<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - write!(f, "`")?; - let mut expressions = self.expressions().iter(); - - for quasi in self.quasis() { - write!(f, quasi); - if let Some(expr) = expressions.next() { - write!(f, ["${", expr, "}"]); - } - } - - write!(f, "`") - } -} - -impl<'a> FormatWrite<'a> for AstNode<'a, TaggedTemplateExpression<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - write!(f, [self.tag(), self.type_arguments(), self.quasi()]) - } -} - -impl<'a> FormatWrite<'a> for AstNode<'a, TemplateElement<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - write!(f, dynamic_text(self.value().raw.as_str())) - } -} - impl<'a> FormatWrite<'a> for AstNode<'a, CallExpression<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { let callee = self.callee(); @@ -1961,21 +1934,6 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSMappedType<'a>> { } } -impl<'a> FormatWrite<'a> for AstNode<'a, TSTemplateLiteralType<'a>> { - fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { - write!(f, "`")?; - let mut quasis = self.quasis().iter(); - let quasi = quasis.next().unwrap(); - write!(f, dynamic_text(quasi.value().raw.as_str())); - - for (index, (quasi, types)) in quasis.zip(self.types().iter()).enumerate() { - write!(f, ["${", types, "}"])?; - write!(f, dynamic_text(quasi.value().raw.as_str())); - } - write!(f, "`") - } -} - impl<'a> FormatWrite<'a> for AstNode<'a, TSAsExpression<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { write!(f, [self.expression(), " as ", self.type_annotation()]) diff --git a/crates/oxc_formatter/src/write/template.rs b/crates/oxc_formatter/src/write/template.rs new file mode 100644 index 0000000000000..7401420ba6dea --- /dev/null +++ b/crates/oxc_formatter/src/write/template.rs @@ -0,0 +1,709 @@ +use unicode_width::UnicodeWidthStr; + +use std::cmp; + +use oxc_allocator::{StringBuilder, Vec as ArenaVec}; +use oxc_ast::ast::*; +use oxc_span::{GetSpan, Span}; +use oxc_syntax::identifier::is_line_terminator; + +use crate::{ + IndentWidth, format, format_args, + formatter::{ + Format, FormatElement, FormatResult, Formatter, VecBuffer, + buffer::RemoveSoftLinesBuffer, + prelude::{document::Document, *}, + printer::Printer, + trivia::{FormatLeadingComments, FormatTrailingComments}, + }, + generated::ast_nodes::{AstNode, AstNodeIterator}, + utils::{ + call_expression::is_test_each_pattern, expression::FormatExpressionWithoutTrailingComments, + }, + write, +}; + +use super::FormatWrite; + +impl<'a> FormatWrite<'a> for AstNode<'a, TemplateLiteral<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + let template = TemplateLike::TemplateLiteral(self); + write!(f, template) + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TaggedTemplateExpression<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + // Format the tag and type arguments + write!(f, [self.tag(), self.type_arguments(), line_suffix_boundary()])?; + + let quasi = self.quasi(); + + quasi.format_leading_comments(f); + + if is_test_each_pattern(&self.tag) { + let template = &EachTemplateTable::from_template(quasi, f)?; + // Use table formatting + write!(f, template) + } else { + let template = TemplateLike::TemplateLiteral(quasi); + write!(f, template) + } + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TemplateElement<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + write!(f, dynamic_text(self.value.raw.as_str())) + } +} + +impl<'a> FormatWrite<'a> for AstNode<'a, TSTemplateLiteralType<'a>> { + fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + let template = TemplateLike::TSTemplateLiteralType(self); + write!(f, template) + } +} + +/// Layout strategy for template element expressions +#[derive(Debug, Clone, Copy)] +enum TemplateElementLayout { + /// Tries to format the expression on a single line regardless of the print width. + SingleLine, + + /// Tries to format the expression on a single line but may break the expression if the line otherwise exceeds the print width. + Fit, +} + +/// The indentation derived from a position in the source document. Consists of indentation level and spaces +#[derive(Debug, Copy, Clone, Default)] +pub struct TemplateElementIndention(u32); + +impl TemplateElementIndention { + /// Returns the indentation level + pub(crate) fn level(self, indent_width: IndentWidth) -> u32 { + self.0 / u32::from(indent_width.value()) + } + + /// Returns the number of space indents on top of the indent level + pub(crate) fn align(self, indent_width: IndentWidth) -> u8 { + let remainder = self.0 % u32::from(indent_width.value()); + remainder.try_into().unwrap_or(u8::MAX) + } + + /// Computes the indentation after the last new line character. + pub(crate) fn after_last_new_line( + text: &str, + tab_width: u32, + previous_indention: Self, + ) -> Self { + let by_new_line = text.rsplit_once('\n'); + + let size = match by_new_line { + None => previous_indention.0, + Some((_, after_new_line)) => { + let mut size: u32 = 0; + + for byte in after_new_line.bytes() { + match byte { + b'\t' => { + // Tabs behave in a way that they are aligned to the nearest + // multiple of tab_width: + // number of spaces -> added size + // 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4 + // 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 .. + // Or in other words, it clips the size to the next multiple of tab width. + size = size + tab_width - (size % tab_width); + } + b' ' => { + size += 1; + } + _ => break, + } + } + + size + } + }; + + Self(size) + } +} + +/// Unified enum for handling both JS template literals and TS template literal types +pub enum TemplateLike<'a, 'b> { + TemplateLiteral(&'b AstNode<'a, TemplateLiteral<'a>>), + TSTemplateLiteralType(&'b AstNode<'a, TSTemplateLiteralType<'a>>), +} + +impl<'a> TemplateLike<'a, '_> { + #[inline] + pub fn span(&self) -> Span { + match self { + Self::TemplateLiteral(t) => t.span, + Self::TSTemplateLiteralType(t) => t.span, + } + } + + #[inline] + pub fn quasis(&self) -> &AstNode<'a, ArenaVec<'a, TemplateElement<'a>>> { + match self { + Self::TemplateLiteral(t) => t.quasis(), + Self::TSTemplateLiteralType(t) => t.quasis(), + } + } +} + +/// Iterator that yields template expressions without allocation +enum TemplateExpressionIterator<'a> { + Expression(AstNodeIterator<'a, Expression<'a>>), + TSType(AstNodeIterator<'a, TSType<'a>>), +} + +impl<'a> Iterator for TemplateExpressionIterator<'a> { + type Item = TemplateExpression<'a, 'a>; + + fn next(&mut self) -> Option { + match self { + Self::Expression(iter) => iter.next().map(TemplateExpression::Expression), + Self::TSType(iter) => iter.next().map(TemplateExpression::TSType), + } + } +} + +impl<'a> Format<'a> for TemplateLike<'a, '_> { + fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + write!(f, "`")?; + + let quasis = self.quasis(); + let mut indention = TemplateElementIndention::default(); + let mut after_new_line = false; + + let mut expression_iterator = match self { + Self::TemplateLiteral(t) => { + TemplateExpressionIterator::Expression(t.expressions().iter()) + } + Self::TSTemplateLiteralType(t) => TemplateExpressionIterator::TSType(t.types().iter()), + }; + + for quasi in quasis { + write!(f, quasi)?; + + let quasi_text = quasi.value.raw.as_str(); + + if let Some(expr) = expression_iterator.next() { + let tab_width = u32::from(f.options().indent_width.value()); + indention = + TemplateElementIndention::after_last_new_line(quasi_text, tab_width, indention); + after_new_line = quasi_text.ends_with('\n'); + let options = FormatTemplateExpressionOptions { indention, after_new_line }; + FormatTemplateExpression::new(&expr, options).fmt(f)?; + } + } + + write!(f, "`") + } +} + +#[derive(Debug, Copy, Clone, Default)] +pub struct FormatTemplateExpressionOptions { + /// The indentation to use for this element + pub(crate) indention: TemplateElementIndention, + + /// Does the last template chunk (text element) end with a new line? + pub(crate) after_new_line: bool, +} + +pub(super) enum TemplateExpression<'a, 'b> { + Expression(&'b AstNode<'a, Expression<'a>>), + TSType(&'b AstNode<'a, TSType<'a>>), +} + +pub struct FormatTemplateExpression<'a, 'b> { + expression: &'b TemplateExpression<'a, 'b>, + options: FormatTemplateExpressionOptions, +} + +impl<'a, 'b> FormatTemplateExpression<'a, 'b> { + pub fn new( + expression: &'b TemplateExpression<'a, 'b>, + options: FormatTemplateExpressionOptions, + ) -> Self { + Self { expression, options } + } +} + +impl<'a> Format<'a> for FormatTemplateExpression<'a, '_> { + fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + let options = self.options; + + let mut has_comment_in_expression = false; + + // First, format the expression to check if it will break + // Special handling for array expressions - force flat mode + let format_expression = format_once(|f| match self.expression { + TemplateExpression::Expression(e) => { + let leading_comments = f.context().comments().comments_before(e.span().start); + FormatLeadingComments::Comments(leading_comments).fmt(f)?; + FormatExpressionWithoutTrailingComments(e).fmt(f)?; + let trailing_comments = + f.context().comments().comments_before_character(e.span().start, b'}'); + has_comment_in_expression = + !leading_comments.is_empty() || !trailing_comments.is_empty(); + FormatTrailingComments::Comments(trailing_comments).fmt(f) + } + TemplateExpression::TSType(t) => write!(f, t), + }); + + // Intern the expression to check if it will break + let interned_expression = f.intern(&format_expression)?; + + let layout = if self.expression.has_new_line_in_range(f) { + TemplateElementLayout::Fit + } else { + let will_break = interned_expression.as_ref().is_some_and(FormatElement::will_break); + + // Make sure the expression won't break to prevent reformat issue + if will_break { TemplateElementLayout::Fit } else { TemplateElementLayout::SingleLine } + }; + + // We don't need to calculate indentation here as it's already tracked in options + + // Format based on layout + let format_inner = format_with(|f: &mut Formatter<'_, 'a>| match layout { + TemplateElementLayout::SingleLine => { + // Remove soft line breaks for single-line layout + let mut buffer = RemoveSoftLinesBuffer::new(f); + match &interned_expression { + Some(element) => buffer.write_element(element.clone()), + None => Ok(()), + } + } + TemplateElementLayout::Fit => { + // Determine if we should add indentation based on expression complexity + let indent = match self.expression { + TemplateExpression::Expression(e) => { + has_comment_in_expression + || matches!( + e.as_ref(), + Expression::StaticMemberExpression(_) + | Expression::ComputedMemberExpression(_) + | Expression::ConditionalExpression(_) + | Expression::SequenceExpression(_) + | Expression::TSAsExpression(_) + | Expression::TSSatisfiesExpression(_) + | Expression::BinaryExpression(_) + | Expression::LogicalExpression(_) + | Expression::Identifier(_) + ) + } + TemplateExpression::TSType(t) => { + self.options.after_new_line || is_complex_type(t.as_ref()) + } + }; + + match &interned_expression { + Some(element) if indent => { + write!( + f, + [soft_block_indent(&format_with(|f| f.write_element(element.clone())))] + ) + } + Some(element) => f.write_element(element.clone()), + None => Ok(()), + } + } + }); + + let format_indented = format_with(|f: &mut Formatter<'_, 'a>| { + if options.after_new_line { + // Apply dedent_to_root for expressions after newlines + write!(f, [dedent_to_root(&format_inner)]) + } else { + write_with_indention(&format_inner, options.indention, f.options().indent_width, f) + } + }); + + // Wrap in ${...} with group + write!(f, [group(&format_args!("${", format_indented, line_suffix_boundary(), "}"))]) + } +} + +impl<'a> TemplateExpression<'a, '_> { + fn has_new_line_in_range(&self, f: &Formatter<'_, 'a>) -> bool { + match self { + TemplateExpression::Expression(e) => { + // Has potential newlines + matches!( + e.as_ref(), + Expression::ConditionalExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::FunctionExpression(_) + ) + // TODO: improve this + || f.source_text()[..e.span().start as usize] + .chars() + .rev() + .take_while(|c| *c != '{') + .any(is_line_terminator) + || f.source_text()[e.span().end as usize..] + .chars() + .take_while(|c| *c != '}') + .any(is_line_terminator) + || e.span().source_text(f.source_text()).chars().any(is_line_terminator) + } + TemplateExpression::TSType(t) => { + matches!( + t.as_ref(), + TSType::TSConditionalType(_) + | TSType::TSMappedType(_) + | TSType::TSTypeLiteral(_) + | TSType::TSIntersectionType(_) + | TSType::TSUnionType(_) + ) + } + } + } +} + +/// Writes `content` with the specified `indention`. +fn write_with_indention<'a, Content>( + content: &Content, + indention: TemplateElementIndention, + indent_width: IndentWidth, + f: &mut Formatter<'_, 'a>, +) -> FormatResult<()> +where + Content: Format<'a>, +{ + let level = indention.level(indent_width); + let spaces = indention.align(indent_width); + + if level == 0 && spaces == 0 { + return write!(f, [content]); + } + + // Adds as many nested `indent` elements until it reaches the desired indention level. + let format_indented = format_with(|f| { + for _ in 0..level { + f.write_element(FormatElement::Tag(Tag::StartIndent))?; + } + + write!(f, [content])?; + + for _ in 0..level { + f.write_element(FormatElement::Tag(Tag::EndIndent))?; + } + + Ok(()) + }); + + // Adds any necessary `align` for spaces not covered by indent level. + let format_aligned = format_with(|f| { + if spaces == 0 { + write!(f, [format_indented]) + } else { + write!(f, [align(spaces, &format_indented)]) + } + }); + + write!(f, [dedent_to_root(&format_aligned)]) +} + +/// Check if a TypeScript type is complex enough to warrant line breaks +#[inline] +fn is_complex_type(ts_type: &TSType) -> bool { + matches!( + ts_type, + TSType::TSConditionalType(_) + | TSType::TSMappedType(_) + | TSType::TSTypeLiteral(_) + | TSType::TSIntersectionType(_) + | TSType::TSUnionType(_) + | TSType::TSTupleType(_) + | TSType::TSArrayType(_) + | TSType::TSIndexedAccessType(_) + ) +} + +#[derive(Debug)] +enum EachTemplateElement<'a> { + /// A significant value in the test each table. It's a row element. + Column(EachTemplateColumn<'a>), + /// Indicates the end of the current row. + LineBreak, +} + +/// Row element containing the column information. +#[derive(Debug)] +struct EachTemplateColumn<'a> { + /// Formatted text of the column. + text: &'a str, + /// Formatted text width. + width: usize, + /// Indicates the line break in the text. + will_break: bool, +} + +impl<'a> EachTemplateColumn<'a> { + fn new(text: &'a str, will_break: bool) -> Self { + let width = text.width(); + + Self { text, width, will_break } + } +} + +struct EachTemplateTableBuilder<'a> { + /// Holds information about the current row. + current_row: EachTemplateCurrentRow, + /// Information about all rows. + rows: Vec, + /// Contains the maximum length of each column of all rows. + columns_width: Vec, + /// Elements for formatting. + elements: Vec>, +} + +impl<'a> EachTemplateTableBuilder<'a> { + fn new() -> Self { + Self { + current_row: EachTemplateCurrentRow::new(), + rows: Vec::new(), + columns_width: Vec::new(), + elements: Vec::new(), + } + } + + fn entry(&mut self, element: EachTemplateElement<'a>) { + match &element { + EachTemplateElement::Column(column) => { + if column.will_break { + self.current_row.has_line_break_column = true; + } + + if !self.current_row.has_line_break_column { + self.current_row.column_widths.push(column.width); + } + } + EachTemplateElement::LineBreak => { + self.next_row(); + } + } + self.elements.push(element); + } + + /// Advance the table state to a new row. + /// Merge the current row columns width with the table ones if row doesn't contain a line break column. + fn next_row(&mut self) { + if !self.current_row.has_line_break_column { + let table_column_width_iter = self.columns_width.iter_mut(); + let mut row_column_width_iter = self.current_row.column_widths.iter(); + + for table_column_width in table_column_width_iter { + let Some(row_column_width) = row_column_width_iter.next() else { break }; + *table_column_width = cmp::max(*table_column_width, *row_column_width); + } + + self.columns_width.extend(row_column_width_iter); + } + + self.rows.push(EachTemplateRow { + has_line_break_column: self.current_row.has_line_break_column, + }); + + self.current_row.reset(); + } + + fn finish(mut self) -> EachTemplateTable<'a> { + self.next_row(); + + EachTemplateTable { + rows: self.rows, + columns_width: self.columns_width, + elements: self.elements, + } + } +} + +#[derive(Debug)] +pub struct EachTemplateTable<'a> { + /// Information about all rows. + rows: Vec, + /// Contains the maximum length of each column of all rows. + columns_width: Vec, + /// Elements for formatting. + elements: Vec>, +} + +#[derive(Debug)] +struct EachTemplateCurrentRow { + /// Contains the maximum length of the current column. + column_widths: Vec, + /// Whether the current row contains a column with a line break. + has_line_break_column: bool, +} + +impl EachTemplateCurrentRow { + fn new() -> Self { + Self { column_widths: Vec::new(), has_line_break_column: false } + } + + fn reset(&mut self) { + self.column_widths.clear(); + self.has_line_break_column = false; + } +} + +#[derive(Debug)] +struct EachTemplateRow { + /// Whether the current row contains a column with a line break. + has_line_break_column: bool, +} + +/// Separator between columns in a row. +struct EachTemplateSeparator; + +impl<'a> Format<'a> for EachTemplateSeparator { + fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + write!(f, [text("|")]) + } +} + +impl<'a> EachTemplateTable<'a> { + pub(crate) fn from_template( + quasi: &AstNode<'a, TemplateLiteral<'a>>, + f: &mut Formatter<'_, 'a>, + ) -> FormatResult { + let mut builder = EachTemplateTableBuilder::new(); + + let mut quasi_iter = quasi.quasis.iter(); + // `unwrap()` is okay, because the template literal is guaranteed to have at least one quasi + let header = quasi_iter.next().unwrap(); + let header_text = header.value.raw.as_str(); + + for column in header_text.split_terminator('|') { + let trimmed = column.trim(); + let text = f.context().allocator().alloc_str(trimmed); + let column = EachTemplateColumn::new(text, false); + builder.entry(EachTemplateElement::Column(column)); + } + + builder.entry(EachTemplateElement::LineBreak); + + for (i, expr) in quasi.expressions().iter().enumerate() { + let mut vec_buffer = VecBuffer::new(f.state_mut()); + + // The softline buffer replaces all softline breaks with a space or removes it entirely + // to "mimic" an infinite print width + let mut buffer = RemoveSoftLinesBuffer::new(&mut vec_buffer); + + let options = FormatTemplateExpressionOptions { + after_new_line: false, + indention: TemplateElementIndention::default(), + }; + + let mut recording = buffer.start_recording(); + write!( + recording, + [FormatTemplateExpression::new(&TemplateExpression::Expression(expr), options)] + )?; + + recording.stop(); + + let root = Document::from(vec_buffer.into_vec()); + + // let range = element.range(); + let print_options = f.options().as_print_options(); + let printed = Printer::new(print_options).print(&root)?; + let text = f.context().allocator().alloc_str(&printed.into_code()); + let will_break = text.contains('\n'); + + let column = EachTemplateColumn::new(text, will_break); + builder.entry(EachTemplateElement::Column(column)); + + if let Some(quasi) = quasi_iter.next() { + let quasi_text = quasi.value.raw.as_str(); + + // go to the next line if the current element contains a line break + if quasi_text.contains('\n') { + builder.entry(EachTemplateElement::LineBreak); + } + } + } + + let table = builder.finish(); + + Ok(table) + } +} + +impl<'a> Format<'a> for EachTemplateTable<'a> { + fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> { + let table_content = format_with(|f| { + let mut current_column: usize = 0; + let mut current_row: usize = 0; + + let mut iter = self.elements.iter().peekable(); + + write!(f, [hard_line_break()])?; + + while let Some(element) = iter.next() { + let next_item = iter.peek(); + let is_last = next_item.is_none(); + let is_last_in_row = + matches!(next_item, Some(EachTemplateElement::LineBreak)) || is_last; + + match element { + EachTemplateElement::Column(column) => { + let mut text = if current_column != 0 + && (!is_last_in_row || !column.text.is_empty()) + { + StringBuilder::from_strs_array_in( + [" ", column.text], + f.context().allocator(), + ) + } else { + StringBuilder::from_str_in(column.text, f.context().allocator()) + }; + + // align the column based on the maximum column width in the table + if !is_last_in_row { + if !self.rows[current_row].has_line_break_column { + let column_width = self + .columns_width + .get(current_column) + .copied() + .unwrap_or_default(); + + let padding = " ".repeat(column_width.saturating_sub(column.width)); + + text.push_str(&padding); + } + + text.push(' '); + } + + write!(f, [dynamic_text(text.into_str())])?; + + if !is_last_in_row { + write!(f, [EachTemplateSeparator])?; + } + + current_column += 1; + } + EachTemplateElement::LineBreak => { + current_column = 0; + current_row += 1; + + if !is_last { + write!(f, [hard_line_break()])?; + } + } + } + } + Ok(()) + }); + + write!(f, ["`", indent(&format_args!(table_content)), hard_line_break(), "`"]) + } +} diff --git a/tasks/ast_tools/src/generators/formatter/format.rs b/tasks/ast_tools/src/generators/formatter/format.rs index aaf69213e9cae..8a31fc59c5649 100644 --- a/tasks/ast_tools/src/generators/formatter/format.rs +++ b/tasks/ast_tools/src/generators/formatter/format.rs @@ -28,6 +28,8 @@ const AST_NODE_WITHOUT_PRINTING_COMMENTS_LIST: &[&str] = &[ // "JSXElement", "JSXFragment", + // + "TemplateElement", ]; const NEEDS_PARENTHESES: &[&str] = &[ diff --git a/tasks/prettier_conformance/snapshots/prettier.js.snap.md b/tasks/prettier_conformance/snapshots/prettier.js.snap.md index 13d844f2d5fb9..7c406cd71474a 100644 --- a/tasks/prettier_conformance/snapshots/prettier.js.snap.md +++ b/tasks/prettier_conformance/snapshots/prettier.js.snap.md @@ -1,4 +1,4 @@ -js compatibility: 583/699 (83.40%) +js compatibility: 603/699 (86.27%) # Failed @@ -6,9 +6,7 @@ js compatibility: 583/699 (83.40%) | :-------- | :--------------: | :---------: | | js/arrows/curried.js | 💥💥 | 92.55% | | js/arrows/semi/semi.js | 💥✨ | 0.00% | -| js/assignment/issue-15534.js | 💥 | 25.00% | | js/assignment/sequence.js | 💥 | 71.43% | -| js/chain-expression/issue-15785-3.js | 💥 | 50.00% | | js/class-comment/misc.js | 💥 | 72.73% | | js/comments/15661.js | 💥💥 | 55.81% | | js/comments/16398.js | 💥💥 | 80.00% | @@ -18,7 +16,6 @@ js compatibility: 583/699 (83.40%) | js/comments/empty-statements.js | 💥💥 | 90.91% | | js/comments/export.js | 💥💥 | 97.37% | | js/comments/function-declaration.js | 💥💥 | 89.60% | -| js/comments/issue-3532.js | 💥💥 | 97.30% | | js/comments/issues.js | 💥✨ | 48.53% | | js/comments/jsdoc-nestled-dangling.js | 💥💥 | 93.02% | | js/comments/jsdoc-nestled.js | 💥💥 | 89.29% | @@ -26,8 +23,7 @@ js compatibility: 583/699 (83.40%) | js/comments/jsx.js | 💥✨ | 40.74% | | js/comments/last-arg.js | 💥💥 | 80.65% | | js/comments/return-statement.js | 💥💥 | 98.27% | -| js/comments/tagged-template-literal.js | 💥💥 | 69.23% | -| js/comments/template-literal.js | 💥💥 | 30.43% | +| js/comments/template-literal.js | 💥✨ | 46.43% | | js/comments/trailing_space.js | 💥💥 | 60.00% | | js/comments/variable_declarator.js | 💥✨ | 49.31% | | js/comments/html-like/comment.js | 💥 | 0.00% | @@ -67,7 +63,6 @@ js compatibility: 583/699 (83.40%) | js/import-attributes/long-sources.js | 💥 | 75.00% | | js/label/comment.js | 💥 | 53.33% | | js/last-argument-expansion/dangling-comment-in-arrow-function.js | 💥 | 22.22% | -| js/line-suffix-boundary/boundary.js | 💥 | 36.73% | | js/logical_expressions/issue-7024.js | 💥 | 66.67% | | js/method-chain/comment.js | 💥 | 97.56% | | js/method-chain/conditional.js | 💥 | 85.19% | @@ -88,35 +83,20 @@ js compatibility: 583/699 (83.40%) | js/sequence-break/break.js | 💥 | 53.45% | | js/sequence-expression/ignore.js | 💥 | 42.86% | | js/strings/escaped.js | 💥💥 | 73.68% | -| js/strings/template-literals.js | 💥💥 | 62.96% | -| js/template/comment.js | 💥 | 24.00% | -| js/template/graphql.js | 💥 | 81.25% | -| js/template/indent.js | 💥 | 85.71% | -| js/template-align/indent.js | 💥💥 | 14.47% | -| js/template-literals/binary-exporessions.js | 💥 | 0.00% | -| js/template-literals/conditional-expressions.js | 💥 | 0.00% | -| js/template-literals/expressions.js | 💥 | 67.67% | -| js/template-literals/indention.js | 💥 | 51.16% | -| js/template-literals/logical-expressions.js | 💥 | 0.00% | -| js/template-literals/sequence-expressions.js | 💥 | 0.00% | -| js/ternaries/binary.js | 💥💥💥💥💥💥✨✨ | 20.36% | -| js/ternaries/func-call.js | 💥💥💥💥💥💥✨✨ | 28.33% | -| js/ternaries/indent-after-paren.js | 💥💥💥💥💥💥✨✨ | 30.97% | -| js/ternaries/indent.js | 💥💥💥💥💥💥💥✨ | 6.16% | -| js/ternaries/nested-in-condition.js | 💥💥💥💥💥💥💥✨ | 30.62% | -| js/ternaries/nested.js | 💥💥💥💥💥💥💥✨ | 23.60% | -| js/ternaries/parenthesis.js | 💥💥💥💥💥💥💥✨ | 29.31% | -| js/ternaries/test.js | 💥💥💥💥💥💥💥✨ | 32.79% | +| js/ternaries/binary.js | 💥💥💥💥✨✨✨✨ | 18.42% | +| js/ternaries/func-call.js | 💥💥💥💥✨✨✨✨ | 25.00% | +| js/ternaries/indent-after-paren.js | 💥💥💥💥✨✨✨✨ | 24.59% | +| js/ternaries/indent.js | 💥💥💥💥💥✨✨✨ | 6.65% | +| js/ternaries/nested-in-condition.js | 💥💥💥💥💥✨✨✨ | 31.61% | +| js/ternaries/nested.js | 💥💥💥💥💥💥💥✨ | 47.40% | +| js/ternaries/parenthesis.js | 💥💥💥💥💥✨✨✨ | 22.22% | +| js/ternaries/test.js | 💥💥💥💥💥✨✨✨ | 34.24% | | js/ternaries/parenthesis/await-expression.js | 💥✨ | 14.29% | | js/test-declarations/angularjs_inject.js | 💥💥 | 91.53% | -| js/test-declarations/jest-each-template-string.js | 💥💥 | 27.78% | -| js/test-declarations/jest-each.js | 💥💥 | 67.65% | -| js/test-declarations/test_declarations.js | 💥💥 | 75.81% | | js/trailing-comma/dynamic-import.js | 💥💥💥 | 0.00% | | jsx/expression-with-types/expression.js | 💥💥💥💥 | 0.00% | | jsx/fbt/test.js | 💥 | 84.06% | | jsx/ignore/jsx_ignore.js | 💥 | 82.57% | | jsx/jsx/quotes.js | 💥💥💥💥 | 79.41% | -| jsx/jsx/template-literal-in-attr.js | 💥💥💥💥 | 85.71% | | jsx/single-attribute-per-line/single-attribute-per-line.js | 💥✨ | 43.37% | -| jsx/text-wrap/test.js | 💥 | 98.85% | +| jsx/text-wrap/test.js | 💥 | 99.56% | diff --git a/tasks/prettier_conformance/snapshots/prettier.ts.snap.md b/tasks/prettier_conformance/snapshots/prettier.ts.snap.md index 11c83f78bbb49..09b971b1d7881 100644 --- a/tasks/prettier_conformance/snapshots/prettier.ts.snap.md +++ b/tasks/prettier_conformance/snapshots/prettier.ts.snap.md @@ -1,4 +1,4 @@ -ts compatibility: 321/573 (56.02%) +ts compatibility: 326/573 (56.89%) # Failed @@ -8,9 +8,8 @@ ts compatibility: 321/573 (56.02%) | jsx/fbt/test.js | 💥 | 84.06% | | jsx/ignore/jsx_ignore.js | 💥 | 82.57% | | jsx/jsx/quotes.js | 💥💥💥💥 | 79.41% | -| jsx/jsx/template-literal-in-attr.js | 💥💥💥💥 | 85.71% | | jsx/single-attribute-per-line/single-attribute-per-line.js | 💥✨ | 43.37% | -| jsx/text-wrap/test.js | 💥 | 98.85% | +| jsx/text-wrap/test.js | 💥 | 99.56% | | typescript/ambient/ambient.ts | 💥 | 88.24% | | typescript/angular-component-examples/15934-computed.component.ts | 💥💥 | 76.92% | | typescript/angular-component-examples/15934.component.ts | 💥💥 | 53.85% | @@ -212,13 +211,10 @@ ts compatibility: 321/573 (56.02%) | typescript/satisfies-operators/nested-await-and-satisfies.ts | 💥💥 | 42.86% | | typescript/satisfies-operators/non-null.ts | 💥💥 | 66.67% | | typescript/satisfies-operators/satisfies.ts | 💥💥 | 68.18% | -| typescript/satisfies-operators/template-literal.ts | 💥💥 | 14.29% | | typescript/satisfies-operators/ternary.ts | 💥💥 | 82.00% | | typescript/satisfies-operators/types-comments.ts | 💥✨ | 33.33% | | typescript/semi/no-semi.ts | 💥 | 88.89% | | typescript/template-literal-types/template-literal-types.ts | 💥 | 73.33% | -| typescript/template-literals/as-expression.ts | 💥 | 14.29% | -| typescript/template-literals/expressions.ts | 💥 | 0.00% | | typescript/test-declarations/test_declarations.ts | 💥💥 | 66.67% | | typescript/trailing-comma/arrow-functions.tsx | 💥💥💥 | 25.00% | | typescript/trailing-comma/trailing.ts | 💥💥💥 | 87.66% | @@ -242,7 +238,6 @@ ts compatibility: 321/573 (56.02%) | typescript/typeparams/line-breaking-after-extends-2.ts | 💥 | 21.74% | | typescript/typeparams/line-breaking-after-extends.ts | 💥 | 17.14% | | typescript/typeparams/long-function-arg.ts | 💥 | 66.67% | -| typescript/typeparams/tagged-template-expression.ts | 💥 | 75.00% | | typescript/typeparams/empty-parameters-with-arrow-function/issue-13817.ts | 💥 | 66.67% | | typescript/typeparams/trailing-comma/type-paramters.ts | 💥💥💥 | 28.57% | | typescript/union/comments.ts | 💥 | 15.38% | diff --git a/tasks/prettier_conformance/src/spec.rs b/tasks/prettier_conformance/src/spec.rs index ad890cdeb9b56..45d29952d137e 100644 --- a/tasks/prettier_conformance/src/spec.rs +++ b/tasks/prettier_conformance/src/spec.rs @@ -7,8 +7,9 @@ use oxc_ast::ast::{ }; use oxc_ast_visit::VisitMut; use oxc_formatter::{ - ArrowParentheses, BracketSameLine, BracketSpacing, FormatOptions, IndentWidth, LineEnding, - LineWidth, OperatorPosition, QuoteProperties, QuoteStyle, Semicolons, TrailingCommas, + ArrowParentheses, BracketSameLine, BracketSpacing, FormatOptions, IndentStyle, IndentWidth, + LineEnding, LineWidth, OperatorPosition, QuoteProperties, QuoteStyle, Semicolons, + TrailingCommas, }; use oxc_parser::Parser; use oxc_span::{GetSpan, SourceType}; @@ -143,6 +144,12 @@ impl VisitMut<'_> for SpecParser { } else { QuoteStyle::Double }; + } else if name == "useTabs" { + options.indent_style = if literal.value { + IndentStyle::Tab + } else { + IndentStyle::Space + }; } } #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]