diff --git a/crates/rome_formatter/src/builders.rs b/crates/rome_formatter/src/builders.rs index cbf70a7a75cb..d9545cc08562 100644 --- a/crates/rome_formatter/src/builders.rs +++ b/crates/rome_formatter/src/builders.rs @@ -1231,6 +1231,7 @@ pub fn group(content: &impl Format) -> Group { Group { content: Argument::new(content), group_id: None, + should_expand: false, } } @@ -1238,6 +1239,7 @@ pub fn group(content: &impl Format) -> Group { pub struct Group<'a, Context> { content: Argument<'a, Context>, group_id: Option, + should_expand: bool, } impl Group<'_, Context> { @@ -1245,14 +1247,35 @@ impl Group<'_, Context> { self.group_id = group_id; self } + + /// Setting the value to `true` forces the group to expand regardless if it otherwise would fit on the + /// line or contains any hard line breaks. + /// + /// It omits the group if `should_expand` is true and instead writes an [FormatElement::ExpandParent] to + /// force any enclosing group to break as well **except** if the group has a group id, in which case the group + /// gets emitted but its first containing element is a [FormatElement::ExpandParent] to force it into expanded mode. + pub fn should_expand(mut self, should_expand: bool) -> Self { + self.should_expand = should_expand; + self + } } impl Format for Group<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + if self.group_id.is_none() && self.should_expand { + write!(f, [expand_parent()])?; + return f.write_fmt(Arguments::from(&self.content)); + } + let mut buffer = GroupBuffer::new(f); + buffer.write_fmt(Arguments::from(&self.content))?; - let content = buffer.into_vec(); + if self.should_expand { + write!(buffer, [expand_parent()])?; + } + + let content = buffer.into_vec(); if content.is_empty() && self.group_id.is_none() { return Ok(()); } @@ -1269,6 +1292,7 @@ impl std::fmt::Debug for Group<'_, Context> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GroupElements") .field("group_id", &self.group_id) + .field("should_break", &self.should_expand) .field("content", &"{{content}}") .finish() } diff --git a/crates/rome_js_formatter/src/js/auxiliary/initializer_clause.rs b/crates/rome_js_formatter/src/js/auxiliary/initializer_clause.rs index ab2ffc9c6439..0209342a9536 100644 --- a/crates/rome_js_formatter/src/js/auxiliary/initializer_clause.rs +++ b/crates/rome_js_formatter/src/js/auxiliary/initializer_clause.rs @@ -1,11 +1,28 @@ use crate::prelude::*; -use rome_formatter::write; +use crate::utils::{with_assignment_layout, AssignmentLikeLayout}; +use rome_formatter::{write, FormatRuleWithOptions}; use rome_js_syntax::JsInitializerClause; use rome_js_syntax::JsInitializerClauseFields; #[derive(Debug, Clone, Default)] -pub struct FormatJsInitializerClause; +pub struct FormatJsInitializerClause { + assignment_layout: Option, +} + +#[derive(Default, Debug)] +pub struct FormatJsInitializerClauseOptions { + pub(crate) assignment_layout: Option, +} + +impl FormatRuleWithOptions for FormatJsInitializerClause { + type Options = FormatJsInitializerClauseOptions; + + fn with_options(mut self, options: Self::Options) -> Self { + self.assignment_layout = options.assignment_layout; + self + } +} impl FormatNodeRule for FormatJsInitializerClause { fn fmt_fields(&self, node: &JsInitializerClause, f: &mut JsFormatter) -> FormatResult<()> { @@ -14,6 +31,13 @@ impl FormatNodeRule for FormatJsInitializerClause { expression, } = node.as_fields(); - write![f, [eq_token.format(), space(), expression.format()]] + write![ + f, + [ + eq_token.format(), + space(), + with_assignment_layout(&expression?, self.assignment_layout) + ] + ] } } diff --git a/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs b/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs index 7fc6fbc101d2..c2670c0de8b6 100644 --- a/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs +++ b/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs @@ -1,19 +1,34 @@ use crate::prelude::*; -use rome_formatter::{format_args, write}; +use rome_formatter::{format_args, write, Comments, CstFormatContext, FormatRuleWithOptions}; +use std::iter::once; use crate::parentheses::{ - is_binary_like_left_or_right, is_conditional_test, + is_binary_like_left_or_right, is_callee, is_conditional_test, update_or_lower_expression_needs_parentheses, NeedsParentheses, }; -use crate::utils::{resolve_left_most_expression, JsAnyBinaryLikeLeftExpression}; +use crate::utils::{ + resolve_left_most_expression, AssignmentLikeLayout, JsAnyBinaryLikeLeftExpression, +}; use rome_js_syntax::{ - JsAnyArrowFunctionParameters, JsAnyExpression, JsAnyFunctionBody, JsAnyTemplateElement, - JsArrowFunctionExpression, JsArrowFunctionExpressionFields, JsSyntaxKind, JsSyntaxNode, - JsTemplate, + JsAnyArrowFunctionParameters, JsAnyBindingPattern, JsAnyExpression, JsAnyFormalParameter, + JsAnyFunctionBody, JsAnyParameter, JsAnyTemplateElement, JsArrowFunctionExpression, JsLanguage, + JsSyntaxKind, JsSyntaxNode, JsTemplate, }; +use rome_rowan::SyntaxResult; #[derive(Debug, Clone, Default)] -pub struct FormatJsArrowFunctionExpression; +pub struct FormatJsArrowFunctionExpression { + assignment_layout: Option, +} + +impl FormatRuleWithOptions for FormatJsArrowFunctionExpression { + type Options = Option; + + fn with_options(mut self, options: Self::Options) -> Self { + self.assignment_layout = options; + self + } +} impl FormatNodeRule for FormatJsArrowFunctionExpression { fn fmt_fields( @@ -21,140 +36,432 @@ impl FormatNodeRule for FormatJsArrowFunctionExpressi node: &JsArrowFunctionExpression, f: &mut JsFormatter, ) -> FormatResult<()> { - use self::JsAnyExpression::*; - use JsAnyFunctionBody::*; - - let JsArrowFunctionExpressionFields { - async_token, - type_parameters, - parameters, - return_type_annotation, - fat_arrow_token, - body, - } = node.as_fields(); - - let format_signature = format_with(|f| { - if let Some(async_token) = &async_token { - write!(f, [async_token.format(), space()])?; + let layout = ArrowFunctionLayout::for_arrow( + node.clone(), + &f.context().comments(), + self.assignment_layout, + )?; + + match layout { + ArrowFunctionLayout::Chain(chain) => { + write!(f, [chain]) + } + ArrowFunctionLayout::Single(arrow) => { + use self::JsAnyExpression::*; + use JsAnyFunctionBody::*; + + let body = arrow.body()?; + + let format_signature = format_with(|f| { + write!( + f, + [ + format_signature(&arrow), + space(), + arrow.fat_arrow_token().format() + ] + ) + }); + + // With arrays, arrow selfs and objects, they have a natural line breaking strategy: + // Arrays and objects become blocks: + // + // [ + // 100000, + // 200000, + // 300000 + // ] + // + // Arrow selfs get line broken after the `=>`: + // + // (foo) => (bar) => + // (foo + bar) * (foo + bar) + // + // Therefore if our body is an arrow self, array, or object, we + // do not have a soft line break after the arrow because the body is + // going to get broken anyways. + let body_has_soft_line_break = match &body { + JsFunctionBody(_) => true, + JsAnyExpression(expr) => match expr { + JsArrowFunctionExpression(_) + | JsArrayExpression(_) + | JsObjectExpression(_) + | JsxTagExpression(_) => true, + JsTemplate(template) => { + is_multiline_template_starting_on_same_line(template) + } + JsSequenceExpression(_) => { + return write!( + f, + [group(&format_args![ + format_signature, + group(&format_args![ + space(), + text("("), + soft_block_indent(&body.format()), + text(")") + ]) + ])] + ); + } + _ => false, + }, + }; + + // Add parentheses to avoid confusion between `a => b ? c : d` and `a <= b ? c : d` + // but only if the body isn't an object/function or class expression because parentheses are always required in that + // case and added by the object expression itself + let should_add_parens = match &body { + JsAnyExpression(expression) => { + let is_conditional = matches!(expression, JsConditionalExpression(_)); + let are_parentheses_mandatory = matches!( + resolve_left_most_expression(expression), + JsAnyBinaryLikeLeftExpression::JsAnyExpression( + JsObjectExpression(_) + | JsFunctionExpression(_) + | JsClassExpression(_) + ) + ); + + is_conditional && !are_parentheses_mandatory + } + _ => false, + }; + + if body_has_soft_line_break && !should_add_parens { + write![f, [format_signature, space(), body.format()]] + } else { + write!( + f, + [ + format_signature, + group(&soft_line_indent_or_space(&format_with(|f| { + if should_add_parens { + write!(f, [if_group_fits_on_line(&text("("))])?; + } + + write!(f, [body.format()])?; + + if should_add_parens { + write!(f, [if_group_fits_on_line(&text(")"))])?; + } + + Ok(()) + }))) + ] + ) + } } + } + } + + fn needs_parentheses(&self, item: &JsArrowFunctionExpression) -> bool { + item.needs_parentheses() + } +} - write!(f, [type_parameters.format()])?; +/// writes the arrow function type parameters, parameters, and return type annotation +fn format_signature<'a>(arrow: &'a JsArrowFunctionExpression) -> impl Format + 'a { + format_with(|f| { + if let Some(async_token) = arrow.async_token() { + write!(f, [async_token.format(), space()])?; + } + + let format_parameters = format_with(|f| { + write!(f, [arrow.type_parameters().format()])?; - match parameters.as_ref()? { + match arrow.parameters()? { JsAnyArrowFunctionParameters::JsAnyBinding(binding) => write!( f, [format_parenthesize( binding.syntax().first_token().as_ref(), - &format_args![binding.format(), if_group_breaks(&text(",")),], + &soft_block_indent(&format_args![ + binding.format(), + if_group_breaks(&text(",")) + ]), binding.syntax().last_token().as_ref(), - ) - .grouped_with_soft_block_indent()] + )] )?, JsAnyArrowFunctionParameters::JsParameters(params) => { - write![f, [group(¶ms.format())]]? + write!(f, [params.format()])?; } - } + }; - write![ - f, - [ - return_type_annotation.format(), - space(), - fat_arrow_token.format(), - ] - ] + write!(f, [arrow.return_type_annotation().format()]) }); - let body = body?; - - // With arrays, arrow selfs and objects, they have a natural line breaking strategy: - // Arrays and objects become blocks: - // - // [ - // 100000, - // 200000, - // 300000 - // ] - // - // Arrow selfs get line broken after the `=>`: - // - // (foo) => (bar) => - // (foo + bar) * (foo + bar) - // - // Therefore if our body is an arrow self, array, or object, we - // do not have a soft line break after the arrow because the body is - // going to get broken anyways. - let body_has_soft_line_break = match &body { - JsFunctionBody(_) => true, - JsAnyExpression(expr) => match expr { - JsArrowFunctionExpression(_) - | JsArrayExpression(_) - | JsObjectExpression(_) - | JsxTagExpression(_) => true, - JsTemplate(template) => is_multiline_template_starting_on_same_line(template), - JsSequenceExpression(_) => { - return write!( - f, - [group(&format_args![ - format_signature, - group(&format_args![ + write!(f, [group(&format_parameters)]) + }) +} + +fn should_break_chain(arrow: &JsArrowFunctionExpression) -> SyntaxResult { + if arrow.type_parameters().is_some() { + return Ok(true); + } + + let parameters = arrow.parameters()?; + + let has_parameters = match ¶meters { + JsAnyArrowFunctionParameters::JsAnyBinding(_) => true, + JsAnyArrowFunctionParameters::JsParameters(parameters) => !parameters.items().is_empty(), + }; + + if arrow.return_type_annotation().is_some() && has_parameters { + return Ok(true); + } + + // Break if the function has any rest, object, or array parameter + let result = match parameters { + JsAnyArrowFunctionParameters::JsAnyBinding(_) => false, + JsAnyArrowFunctionParameters::JsParameters(parameters) => parameters + .items() + .iter() + .flatten() + .any(|parameter| match parameter { + JsAnyParameter::JsAnyFormalParameter(JsAnyFormalParameter::JsFormalParameter( + parameter, + )) => { + matches!( + parameter.binding(), + Ok(JsAnyBindingPattern::JsArrayBindingPattern(_) + | JsAnyBindingPattern::JsObjectBindingPattern(_)) + ) + } + JsAnyParameter::JsAnyFormalParameter(JsAnyFormalParameter::JsUnknownParameter( + _, + )) => false, + JsAnyParameter::TsThisParameter(_) => false, + JsAnyParameter::JsRestParameter(_) => true, + }), + }; + + Ok(result) +} + +#[derive(Clone, Debug)] +enum ArrowFunctionLayout { + /// Arrow function with a non-arrow function body + Single(JsArrowFunctionExpression), + + /// A chain of at least two arrow functions. + /// + /// An arrow function is part of the chain when it is the body of the parent arrow function. + /// + /// The idea of arrow chains is that they break after the `=>` token + /// + /// ```javascript + /// const x = + /// (a): string => + /// (b) => + /// (c) => + /// (d) => + /// (e) => + /// f; + /// ``` + Chain(ArrowChain), +} + +#[derive(Clone, Debug)] +struct ArrowChain { + /// The top most arrow function in the chain + head: JsArrowFunctionExpression, + + /// The arrow functions in the chain that are neither the first nor the last. + /// Empty for chains consisting only of two arrow functions. + middle: Vec, + + /// The last arrow function in the chain + tail: JsArrowFunctionExpression, + + /// The layout of the assignment this arrow function is the right hand side of or `None` + assignment_layout: Option, + + /// Whether the group wrapping the signatures should be expanded or not. + expand_signatures: bool, +} + +impl ArrowChain { + /// Returns an iterator over all arrow functions in this chain + fn arrows(&self) -> impl Iterator { + once(&self.head) + .chain(self.middle.iter()) + .chain(once(&self.tail)) + } +} + +impl Format for ArrowChain { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let ArrowChain { + head, + tail, + expand_signatures, + assignment_layout, + .. + } = self; + + let head_parent = head.syntax().parent(); + let tail_body = tail.body()?; + + let is_assignment_rhs = assignment_layout.is_some(); + + let is_callee = head_parent + .as_ref() + .map_or(false, |parent| is_callee(head.syntax(), parent)); + + let body_on_separate_line = !matches!( + tail_body, + JsAnyFunctionBody::JsFunctionBody(_) + | JsAnyFunctionBody::JsAnyExpression( + JsAnyExpression::JsObjectExpression(_) + | JsAnyExpression::JsSequenceExpression(_) + ) + ); + + let break_before_chain = (is_callee && body_on_separate_line) + || matches!( + assignment_layout, + Some(AssignmentLikeLayout::ChainTailArrowFunction) + ); + + let format_arrow_signatures = format_with(|f| { + if is_callee || is_assignment_rhs { + write!(f, [soft_line_break()])?; + } + + let join_signatures = format_with(|f| { + for arrow in self.arrows() { + write!(f, [format_signature(arrow)])?; + + // The arrow of the tail is formatted outside of the group to ensure it never + // breaks from the body + if arrow != tail { + write!( + f, + [ space(), - text("("), - soft_block_indent(&body.format()), - text(")") - ]) - ])] - ); + arrow.fat_arrow_token().format(), + soft_line_break_or_space() + ] + )?; + } } - _ => false, - }, - }; - // Add parentheses to avoid confusion between `a => b ? c : d` and `a <= b ? c : d` - // but only if the body isn't an object/function or class expression because parentheses are always required in that - // case and added by the object expression itself - let should_add_parens = match &body { - JsAnyExpression(expression) => { - let is_conditional = matches!(expression, JsConditionalExpression(_)); - let are_parentheses_mandatory = matches!( - resolve_left_most_expression(expression), - JsAnyBinaryLikeLeftExpression::JsAnyExpression( - JsObjectExpression(_) | JsFunctionExpression(_) | JsClassExpression(_) - ) - ); + Ok(()) + }); - is_conditional && !are_parentheses_mandatory + write!( + f, + [group(&join_signatures).should_expand(*expand_signatures)] + ) + }); + + let format_tail_body_inner = format_with(|f| { + // Ensure that the parens of sequence expressions end up on their own line if the + // body breaks + if matches!( + tail_body, + JsAnyFunctionBody::JsAnyExpression(JsAnyExpression::JsSequenceExpression(_)) + ) { + write!( + f, + [group(&format_args![ + format_inserted(JsSyntaxKind::L_PAREN,), + soft_block_indent(&tail_body.format()), + format_inserted(JsSyntaxKind::R_PAREN) + ])] + ) + } else { + write!(f, [tail_body.format()]) } - _ => false, - }; + }); + + let format_tail_body = format_with(|f| { + if body_on_separate_line { + write!( + f, + [indent(&format_args![ + soft_line_break_or_space(), + format_tail_body_inner + ])] + ) + } else { + write!(f, [space(), format_tail_body_inner]) + } + }); + + let group_id = f.group_id("arrow-chain"); - if body_has_soft_line_break && !should_add_parens { - write![f, [format_signature, space(), body.format()]] - } else { + let format_inner = format_once(|f| { write!( f, [ - format_signature, - group(&soft_line_indent_or_space(&format_with(|f| { - if should_add_parens { - write!(f, [if_group_fits_on_line(&text("("))])?; - } + group(&indent(&format_arrow_signatures)) + .with_group_id(Some(group_id)) + .should_expand(break_before_chain), + space(), + tail.fat_arrow_token().format(), + indent_if_group_breaks(&format_tail_body, group_id) + ] + )?; - write!(f, [body.format()])?; + if is_callee { + write!( + f, + [if_group_breaks(&soft_line_break()).with_group_id(Some(group_id))] + )?; + } - if should_add_parens { - write!(f, [if_group_fits_on_line(&text(")"))])?; - } + Ok(()) + }); - Ok(()) - }))) - ] - ) - } + write!(f, [group(&format_inner)]) } +} - fn needs_parentheses(&self, item: &JsArrowFunctionExpression) -> bool { - item.needs_parentheses() +impl ArrowFunctionLayout { + /// Determines the layout for the passed arrow function. See [ArrowFunctionLayout] for a description + /// of the different layouts. + fn for_arrow( + arrow: JsArrowFunctionExpression, + comments: &Comments, + assignment_layout: Option, + ) -> SyntaxResult { + let mut head = None; + let mut middle = Vec::new(); + let mut current = arrow; + let mut should_break = false; + + let result = loop { + match current.body()? { + JsAnyFunctionBody::JsAnyExpression(JsAnyExpression::JsArrowFunctionExpression( + next, + )) if !comments.is_suppressed(next.syntax()) => { + should_break = should_break || should_break_chain(¤t)?; + + if head.is_none() { + head = Some(current); + } else { + middle.push(current); + } + + current = next; + } + _ => { + break match head { + None => ArrowFunctionLayout::Single(current), + Some(head) => ArrowFunctionLayout::Chain(ArrowChain { + head, + middle, + tail: current, + expand_signatures: should_break, + assignment_layout, + }), + } + } + } + }; + + Ok(result) } } @@ -175,40 +482,6 @@ impl NeedsParentheses for JsArrowFunctionExpression { } } -#[cfg(test)] -mod tests { - - use crate::{assert_needs_parentheses, assert_not_needs_parentheses}; - use rome_js_syntax::{JsArrowFunctionExpression, SourceType}; - - #[test] - fn needs_parentheses() { - assert_needs_parentheses!("new (a => test)()`", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => test)()", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => test).member", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => test)[member]", JsArrowFunctionExpression); - assert_not_needs_parentheses!("object[a => a]", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a) as Function", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a)!", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a)`template`", JsArrowFunctionExpression); - assert_needs_parentheses!("+(a => a)", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a) && b", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a) instanceof b", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a) in b", JsArrowFunctionExpression); - assert_needs_parentheses!("(a => a) + b", JsArrowFunctionExpression); - assert_needs_parentheses!("await (a => a)", JsArrowFunctionExpression); - assert_needs_parentheses!( - "(a => a)", - JsArrowFunctionExpression, - SourceType::ts() - ); - assert_needs_parentheses!("(a => a) ? b : c", JsArrowFunctionExpression); - assert_not_needs_parentheses!("a ? b => b : c", JsArrowFunctionExpression); - assert_not_needs_parentheses!("a ? b : c => c", JsArrowFunctionExpression); - assert_needs_parentheses!("class Test extends (a => a) {}", JsArrowFunctionExpression); - } -} - /// Returns `true` if the template contains any new lines inside of its text chunks. fn template_literal_contains_new_line(template: &JsTemplate) -> bool { template.elements().iter().any(|element| match element { @@ -264,3 +537,37 @@ fn is_multiline_template_starting_on_same_line(template: &JsTemplate) -> bool { contains_new_line && starts_on_same_line } + +#[cfg(test)] +mod tests { + + use crate::{assert_needs_parentheses, assert_not_needs_parentheses}; + use rome_js_syntax::{JsArrowFunctionExpression, SourceType}; + + #[test] + fn needs_parentheses() { + assert_needs_parentheses!("new (a => test)()`", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => test)()", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => test).member", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => test)[member]", JsArrowFunctionExpression); + assert_not_needs_parentheses!("object[a => a]", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a) as Function", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a)!", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a)`template`", JsArrowFunctionExpression); + assert_needs_parentheses!("+(a => a)", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a) && b", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a) instanceof b", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a) in b", JsArrowFunctionExpression); + assert_needs_parentheses!("(a => a) + b", JsArrowFunctionExpression); + assert_needs_parentheses!("await (a => a)", JsArrowFunctionExpression); + assert_needs_parentheses!( + "(a => a)", + JsArrowFunctionExpression, + SourceType::ts() + ); + assert_needs_parentheses!("(a => a) ? b : c", JsArrowFunctionExpression); + assert_not_needs_parentheses!("a ? b => b : c", JsArrowFunctionExpression); + assert_not_needs_parentheses!("a ? b : c => c", JsArrowFunctionExpression); + assert_needs_parentheses!("class Test extends (a => a) {}", JsArrowFunctionExpression); + } +} diff --git a/crates/rome_js_formatter/src/lib.rs b/crates/rome_js_formatter/src/lib.rs index 2f877af77b4d..e21179ed0ca7 100644 --- a/crates/rome_js_formatter/src/lib.rs +++ b/crates/rome_js_formatter/src/lib.rs @@ -748,9 +748,9 @@ function() { // use this test check if your snippet prints as you wish, without using a snapshot fn quick_test() { let src = r#" -test.expect(t => { - t.true(a); -}, false); +const getIconEngagementTypeFrom = (engagementTypes: Array) => + (iconEngagementType) => + engagementTypes.includes(iconEngagementType); "#; let syntax = SourceType::tsx(); let tree = parse(src, 0, syntax); diff --git a/crates/rome_js_formatter/src/parentheses.rs b/crates/rome_js_formatter/src/parentheses.rs index 45ff5eb30dbf..654bad30e361 100644 --- a/crates/rome_js_formatter/src/parentheses.rs +++ b/crates/rome_js_formatter/src/parentheses.rs @@ -723,7 +723,7 @@ fn debug_assert_is_expression(node: &JsSyntaxNode) { ) } -fn debug_assert_is_parent(node: &JsSyntaxNode, parent: &JsSyntaxNode) { +pub(crate) fn debug_assert_is_parent(node: &JsSyntaxNode, parent: &JsSyntaxNode) { debug_assert!( node.parent().as_ref() == Some(parent), "Node {node:#?} is not a child of ${parent:#?}" diff --git a/crates/rome_js_formatter/src/utils/assignment_like.rs b/crates/rome_js_formatter/src/utils/assignment_like.rs index aab1fb48184c..5c241deec396 100644 --- a/crates/rome_js_formatter/src/utils/assignment_like.rs +++ b/crates/rome_js_formatter/src/utils/assignment_like.rs @@ -1,3 +1,4 @@ +use crate::js::auxiliary::initializer_clause::FormatJsInitializerClauseOptions; use crate::parentheses::get_expression_left_side; use crate::prelude::*; use crate::utils::member_chain::is_member_call_chain; @@ -197,7 +198,7 @@ impl Format for RightAssignmentLike { /// - Object property member /// - Variable declaration #[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub(crate) enum AssignmentLikeLayout { +pub enum AssignmentLikeLayout { /// This is a special layout usually used for variable declarations. /// This layout is hit, usually, when a [variable declarator](JsVariableDeclarator) doesn't have initializer: /// ```js @@ -504,29 +505,41 @@ impl JsAnyAssignmentLike { } } - fn write_right(&self, f: &mut JsFormatter) -> FormatResult<()> { + fn write_right(&self, f: &mut JsFormatter, layout: AssignmentLikeLayout) -> FormatResult<()> { match self { JsAnyAssignmentLike::JsPropertyObjectMember(property) => { let value = property.value()?; - write!(f, [value.format()]) + write!(f, [with_assignment_layout(&value, Some(layout))]) } JsAnyAssignmentLike::JsAssignmentExpression(assignment) => { let right = assignment.right()?; - write!(f, [space(), right.format()]) + write!(f, [space(), with_assignment_layout(&right, Some(layout))]) } JsAnyAssignmentLike::JsObjectAssignmentPatternProperty(property) => { let pattern = property.pattern()?; let init = property.init(); write!(f, [pattern.format()])?; if let Some(init) = init { - write!(f, [space(), init.format()])?; + write!( + f, + [ + space(), + init.format() + .with_options(FormatJsInitializerClauseOptions { + assignment_layout: Some(layout) + }) + ] + )?; } Ok(()) } JsAnyAssignmentLike::JsVariableDeclarator(variable_declarator) => { if let Some(initializer) = variable_declarator.initializer() { let expression = initializer.expression()?; - write!(f, [space(), expression.format()])?; + write!( + f, + [space(), with_assignment_layout(&expression, Some(layout))] + )?; } Ok(()) } @@ -537,7 +550,10 @@ impl JsAnyAssignmentLike { JsAnyAssignmentLike::JsPropertyClassMember(property_class_member) => { if let Some(initializer) = property_class_member.value() { let expression = initializer.expression()?; - write!(f, [space(), expression.format()])?; + write!( + f, + [space(), with_assignment_layout(&expression, Some(layout))] + )?; } Ok(()) } @@ -929,7 +945,7 @@ impl Format for JsAnyAssignmentLike { } }); - let right = format_with(|f| self.write_right(f)); + let right = format_with(|f| self.write_right(f, layout)); let inner_content = format_with(|f| { write!(f, [left])?; @@ -982,16 +998,7 @@ impl Format for JsAnyAssignmentLike { } AssignmentLikeLayout::ChainTailArrowFunction => { - let group_id = f.group_id("arrow_chain"); - - write!( - f, - [ - space(), - group(&indent(&format_args![hard_line_break(), right])) - .with_group_id(Some(group_id)), - ] - ) + write!(f, [space(), right]) } AssignmentLikeLayout::SuppressedInitializer => { self.write_suppressed_initializer(f) @@ -1191,3 +1198,28 @@ fn is_complex_type_arguments(type_arguments: TsTypeArguments) -> SyntaxResult { + expression: &'a JsAnyExpression, + layout: Option, +} + +pub(crate) fn with_assignment_layout( + expression: &JsAnyExpression, + layout: Option, +) -> WithAssignmentLayout { + WithAssignmentLayout { expression, layout } +} + +impl Format for WithAssignmentLayout<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + match self.expression { + JsAnyExpression::JsArrowFunctionExpression(arrow) => { + arrow.format().with_options(self.layout).fmt(f) + } + expression => expression.format().fmt(f), + } + } +} diff --git a/crates/rome_js_formatter/src/utils/mod.rs b/crates/rome_js_formatter/src/utils/mod.rs index b098a254238b..1e3efa14d6ce 100644 --- a/crates/rome_js_formatter/src/utils/mod.rs +++ b/crates/rome_js_formatter/src/utils/mod.rs @@ -16,7 +16,9 @@ mod typescript; pub(crate) use crate::parentheses::resolve_left_most_expression; use crate::prelude::*; -pub(crate) use assignment_like::JsAnyAssignmentLike; +pub(crate) use assignment_like::{ + with_assignment_layout, AssignmentLikeLayout, JsAnyAssignmentLike, +}; pub(crate) use binary_like_expression::{ needs_binary_like_parentheses, JsAnyBinaryLikeExpression, JsAnyBinaryLikeLeftExpression, }; diff --git a/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js.snap b/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js.snap index ee5e340ae02b..1059370ade49 100644 --- a/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js.snap +++ b/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js.snap @@ -369,7 +369,8 @@ loreum = ipsum = arrayOfNumb = a = "test"; lorem = fff = ee = - () => (fff) => () => (fefef) => () => fff; + () => (fff) => () => (fefef) => () => + fff; // complex destructuring, break left hand a = { diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/argument-expansion/arrow-with-return-type.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/argument-expansion/arrow-with-return-type.ts.snap index d6de920ad6be..5d4ec11b8edb 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/argument-expansion/arrow-with-return-type.ts.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/typescript/argument-expansion/arrow-with-return-type.ts.snap @@ -47,17 +47,6 @@ longfunctionWithCall1("bla", foo, (thing: string): complex - > => { ``` # Output @@ -98,7 +87,9 @@ longfunctionWithCallBack( longfunctionWithCall1( "bla", foo, - (thing: string): complex< + ( + thing: string, + ): complex< type<` `> > => { diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/arrow/issue-6107-curry.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/arrow/issue-6107-curry.ts.snap deleted file mode 100644 index 7e8b78da0387..000000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/arrow/issue-6107-curry.ts.snap +++ /dev/null @@ -1,87 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -const getIconEngagementTypeFrom = (engagementTypes: Array) => - iconEngagementType => engagementTypes.includes(iconEngagementType); - -const getIconEngagementTypeFrom2 = - ( - engagementTypes: Array, - secondArg: Something - ) => - iconEngagementType => - engagementTypes.includes(iconEngagementType); - -const getIconEngagementTypeFrom2 = - ( - engagementTypes: Array, - secondArg: Something, - thirArg: SomethingElse - ) => - iconEngagementType => - engagementTypes.includes(iconEngagementType); -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -1,17 +1,14 @@ --const getIconEngagementTypeFrom = -- (engagementTypes: Array) => (iconEngagementType) => -- engagementTypes.includes(iconEngagementType); -+const getIconEngagementTypeFrom = (engagementTypes: Array) => ( -+ iconEngagementType, -+) => engagementTypes.includes(iconEngagementType); - --const getIconEngagementTypeFrom2 = -- (engagementTypes: Array, secondArg: Something) => -- (iconEngagementType) => -- engagementTypes.includes(iconEngagementType); -+const getIconEngagementTypeFrom2 = ( -+ engagementTypes: Array, -+ secondArg: Something, -+) => (iconEngagementType) => engagementTypes.includes(iconEngagementType); - --const getIconEngagementTypeFrom2 = -- ( -- engagementTypes: Array, -- secondArg: Something, -- thirArg: SomethingElse, -- ) => -- (iconEngagementType) => -- engagementTypes.includes(iconEngagementType); -+const getIconEngagementTypeFrom2 = ( -+ engagementTypes: Array, -+ secondArg: Something, -+ thirArg: SomethingElse, -+) => (iconEngagementType) => engagementTypes.includes(iconEngagementType); -``` - -# Output - -```js -const getIconEngagementTypeFrom = (engagementTypes: Array) => ( - iconEngagementType, -) => engagementTypes.includes(iconEngagementType); - -const getIconEngagementTypeFrom2 = ( - engagementTypes: Array, - secondArg: Something, -) => (iconEngagementType) => engagementTypes.includes(iconEngagementType); - -const getIconEngagementTypeFrom2 = ( - engagementTypes: Array, - secondArg: Something, - thirArg: SomethingElse, -) => (iconEngagementType) => engagementTypes.includes(iconEngagementType); -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/compiler/contextualSignatureInstantiation2.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/compiler/contextualSignatureInstantiation2.ts.snap index 99f5dca786e6..fa2f4f86167d 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/compiler/contextualSignatureInstantiation2.ts.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/typescript/compiler/contextualSignatureInstantiation2.ts.snap @@ -17,18 +17,15 @@ var r23 = dot(id)(id);``` ```diff --- Prettier +++ Rome -@@ -1,9 +1,6 @@ - // dot f g x = f(g(x)) +@@ -2,7 +2,7 @@ var dot: (f: (_: T) => S) => (g: (_: U) => T) => (_: U) => S; --dot = -- (f: (_: T) => S) => + dot = + (f: (_: T) => S) => - (g: (_: U) => T): ((r: U) => S) => -- (x) => -- f(g(x)); -+dot = (f: (_: T) => S) => (g: (_: U) => T): (r: U) => S => (x) => -+ f(g(x)); ++ (g: (_: U) => T): (r: U) => S => + (x) => + f(g(x)); var id: (x: T) => T; - var r23 = dot(id)(id); ``` # Output @@ -36,8 +33,11 @@ var r23 = dot(id)(id);``` ```js // dot f g x = f(g(x)) var dot: (f: (_: T) => S) => (g: (_: U) => T) => (_: U) => S; -dot = (f: (_: T) => S) => (g: (_: U) => T): (r: U) => S => (x) => - f(g(x)); +dot = + (f: (_: T) => S) => + (g: (_: U) => T): (r: U) => S => + (x) => + f(g(x)); var id: (x: T) => T; var r23 = dot(id)(id); ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/conformance/classes/mixinClassesAnnotated.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/conformance/classes/mixinClassesAnnotated.ts.snap deleted file mode 100644 index 107a769f01ac..000000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/conformance/classes/mixinClassesAnnotated.ts.snap +++ /dev/null @@ -1,162 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -// @declaration: true - -type Constructor = new(...args: any[]) => T; - -class Base { - constructor(public x: number, public y: number) {} -} - -class Derived extends Base { - constructor(x: number, y: number, public z: number) { - super(x, y); - } -} - -const Printable = >(superClass: T): Constructor & { message: string } & T => - class extends superClass { - static message = "hello"; - print() { - const output = this.x + "," + this.y; - } - } - - -function Tagged>(superClass: T): Constructor & T { - class C extends superClass { - _tag: string; - constructor(...args: any[]) { - super(...args); - this._tag = "hello"; - } - } - return C; -} - -const Thing1 = Tagged(Derived); -const Thing2 = Tagged(Printable(Derived)); -Thing2.message; - -function f1() { - const thing = new Thing1(1, 2, 3); - thing.x; - thing._tag; -} - -function f2() { - const thing = new Thing2(1, 2, 3); - thing.x; - thing._tag; - thing.print(); -} - -class Thing3 extends Thing2 { - constructor(tag: string) { - super(10, 20, 30); - this._tag = tag; - } - test() { - this.print(); - } -} -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -12,9 +12,10 @@ - } - } - --const Printable = >( -- superClass: T, --): Constructor & { message: string } & T => -+const Printable = >(superClass: T): -+ & Constructor -+ & { message: string } -+ & T => - class extends superClass { - static message = "hello"; - print() { -``` - -# Output - -```js -// @declaration: true - -type Constructor = new (...args: any[]) => T; - -class Base { - constructor(public x: number, public y: number) {} -} - -class Derived extends Base { - constructor(x: number, y: number, public z: number) { - super(x, y); - } -} - -const Printable = >(superClass: T): - & Constructor - & { message: string } - & T => - class extends superClass { - static message = "hello"; - print() { - const output = this.x + "," + this.y; - } - }; - -function Tagged>( - superClass: T, -): Constructor & T { - class C extends superClass { - _tag: string; - constructor(...args: any[]) { - super(...args); - this._tag = "hello"; - } - } - return C; -} - -const Thing1 = Tagged(Derived); -const Thing2 = Tagged(Printable(Derived)); -Thing2.message; - -function f1() { - const thing = new Thing1(1, 2, 3); - thing.x; - thing._tag; -} - -function f2() { - const thing = new Thing2(1, 2, 3); - thing.x; - thing._tag; - thing.print(); -} - -class Thing3 extends Thing2 { - constructor(tag: string) { - super(10, 20, 30); - this._tag = tag; - } - test() { - this.print(); - } -} -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/generic/arrow-return-type.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/generic/arrow-return-type.ts.snap deleted file mode 100644 index 735adde7a02e..000000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/generic/arrow-return-type.ts.snap +++ /dev/null @@ -1,209 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise< - Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -> => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributes"] | undefined -> => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributes"] & undefined -> => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributessssssssssssssssssssssss"] -> => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise< - keyof Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -> => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string -): Promise< - Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy[] -> => {}; -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -6,52 +6,42 @@ - vehicleId: string, - ): Promise => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise => {}; -+export const getVehicleDescriptor = async (vehicleId: string): Promise< -+ Descriptor | undefined -+> => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise< -+export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributes"] | undefined - > => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise => {}; -+export const getVehicleDescriptor = async (vehicleId: string): Promise< -+ Descriptor & undefined -+> => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise< -+export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributes"] & undefined - > => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise => {}; -+export const getVehicleDescriptor = async (vehicleId: string): Promise< -+ Descriptor["attributes"] -+> => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise< -+export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributessssssssssssssssssssssss"] - > => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise => {}; -+export const getVehicleDescriptor = async (vehicleId: string): Promise< -+ keyof Descriptor -+> => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise< -+export const getVehicleDescriptor = async (vehicleId: string): Promise< - keyof Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy - > => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise => {}; -+export const getVehicleDescriptor = async (vehicleId: string): Promise< -+ Descriptor[] -+> => {}; - --export const getVehicleDescriptor = async ( -- vehicleId: string, --): Promise< -+export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy[] - > => {}; -``` - -# Output - -```js -export const getVehicleDescriptor = async ( - vehicleId: string, -): Promise => {}; - -export const getVehicleDescriptor = async ( - vehicleId: string, -): Promise => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Descriptor | undefined -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributes"] | undefined -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Descriptor & undefined -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributes"] & undefined -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Descriptor["attributes"] -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssembly["attributessssssssssssssssssssssss"] -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - keyof Descriptor -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - keyof Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Descriptor[] -> => {}; - -export const getVehicleDescriptor = async (vehicleId: string): Promise< - Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy[] -> => {}; -``` - - -# Lines exceeding max width of 80 characters -``` - 7: ): Promise => {}; - 30: Collections.Parts.PrintedCircuitBoardAssembly["attributessssssssssssssssssssssss"] - 38: keyof Collections.Parts.PrintedCircuitBoardAssemblyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -``` - diff --git a/crates/rome_js_formatter/tests/specs/ts/arrow_chain.ts b/crates/rome_js_formatter/tests/specs/ts/arrow_chain.ts new file mode 100644 index 000000000000..06f8a829280f --- /dev/null +++ b/crates/rome_js_formatter/tests/specs/ts/arrow_chain.ts @@ -0,0 +1,11 @@ +// chain is-callee +const x = ((a) => (b) => c)(test); + +// chain should break +const x = ({prop}) => (b) => { c }; +const x = (a): string => b => c => d => e => f; +const x = (a): string => b => ({test}); + + +// break sequence body on new line +const x = a => b => (aLongSequenceExpression, thatContinuesFurtherOnUntilItBreaks, expands); diff --git a/crates/rome_js_formatter/tests/specs/ts/arrow_chain.ts.snap b/crates/rome_js_formatter/tests/specs/ts/arrow_chain.ts.snap new file mode 100644 index 000000000000..7cb2f6e31cf8 --- /dev/null +++ b/crates/rome_js_formatter/tests/specs/ts/arrow_chain.ts.snap @@ -0,0 +1,54 @@ +--- +source: crates/rome_js_formatter/tests/spec_test.rs +expression: arrow_chain.ts +--- +# Input +// chain is-callee +const x = ((a) => (b) => c)(test); + +// chain should break +const x = ({prop}) => (b) => { c }; +const x = (a): string => b => c => d => e => f; +const x = (a): string => b => ({test}); + + +// break sequence body on new line +const x = a => b => (aLongSequenceExpression, thatContinuesFurtherOnUntilItBreaks, expands); + +============================= +# Outputs +## Output 1 +----- +Indent style: Tab +Line width: 80 +Quote style: Double Quotes +Quote properties: As needed +----- +// chain is-callee +const x = ( + (a) => (b) => + c +)(test); + +// chain should break +const x = + ({ prop }) => + (b) => { + c; + }; +const x = + (a): string => + (b) => + (c) => + (d) => + (e) => + f; +const x = + (a): string => + (b) => ({ test }); + +// break sequence body on new line +const x = (a) => (b) => ( + aLongSequenceExpression, thatContinuesFurtherOnUntilItBreaks, expands +); +