diff --git a/crates/rome_formatter/src/buffer.rs b/crates/rome_formatter/src/buffer.rs index d9e2e184bc5..23007de7d4d 100644 --- a/crates/rome_formatter/src/buffer.rs +++ b/crates/rome_formatter/src/buffer.rs @@ -199,6 +199,11 @@ impl<'a, Context> VecBuffer<'a, Context> { FormatElement::List(List::new(std::mem::take(&mut self.elements))) } } + + /// Returns a reference to the current context + pub fn context(&self) -> &Context { + &self.state.context + } } impl Deref for VecBuffer<'_, Context> { diff --git a/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern.rs b/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern.rs index 1cb5ba0bc49..706f63785d6 100644 --- a/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern.rs +++ b/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern.rs @@ -1,8 +1,7 @@ use crate::prelude::*; - +use crate::utils::JsObjectPatternLike; use rome_formatter::write; use rome_js_syntax::JsObjectAssignmentPattern; -use rome_js_syntax::JsObjectAssignmentPatternFields; #[derive(Debug, Clone, Default)] pub struct FormatJsObjectAssignmentPattern; @@ -13,18 +12,6 @@ impl FormatNodeRule for FormatJsObjectAssignmentPatte node: &JsObjectAssignmentPattern, f: &mut JsFormatter, ) -> FormatResult<()> { - let JsObjectAssignmentPatternFields { - l_curly_token, - properties, - r_curly_token, - } = node.as_fields(); - - write!( - f, - [ - format_delimited(&l_curly_token?, &properties.format(), &r_curly_token?,) - .soft_block_spaces() - ] - ) + write!(f, [JsObjectPatternLike::from(node.clone())]) } } diff --git a/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern_property.rs b/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern_property.rs index efe0f2deb8a..fcd8eb21d0f 100644 --- a/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern_property.rs +++ b/crates/rome_js_formatter/src/js/assignments/object_assignment_pattern_property.rs @@ -1,9 +1,7 @@ use crate::prelude::*; +use crate::utils::JsAnyAssignmentLike; use rome_formatter::write; - -use crate::utils::FormatMemberName; use rome_js_syntax::JsObjectAssignmentPatternProperty; -use rome_js_syntax::JsObjectAssignmentPatternPropertyFields; #[derive(Debug, Clone, Default)] pub struct FormatJsObjectAssignmentPatternProperty; @@ -14,27 +12,6 @@ impl FormatNodeRule for FormatJsObjectAssignm node: &JsObjectAssignmentPatternProperty, f: &mut JsFormatter, ) -> FormatResult<()> { - let JsObjectAssignmentPatternPropertyFields { - member, - colon_token, - pattern, - init, - } = node.as_fields(); - - write!( - f, - [ - FormatMemberName::from(member?), - colon_token.format(), - space_token(), - pattern.format(), - ] - )?; - - if let Some(init) = init { - write!(f, [space_token(), init.format()])?; - } - - Ok(()) + write!(f, [JsAnyAssignmentLike::from(node.clone())]) } } diff --git a/crates/rome_js_formatter/src/js/bindings/object_binding_pattern.rs b/crates/rome_js_formatter/src/js/bindings/object_binding_pattern.rs index 5cd82875a0b..1bb8d5bb2c2 100644 --- a/crates/rome_js_formatter/src/js/bindings/object_binding_pattern.rs +++ b/crates/rome_js_formatter/src/js/bindings/object_binding_pattern.rs @@ -1,26 +1,13 @@ use crate::prelude::*; - +use crate::utils::JsObjectPatternLike; use rome_formatter::write; use rome_js_syntax::JsObjectBindingPattern; -use rome_js_syntax::JsObjectBindingPatternFields; #[derive(Debug, Clone, Default)] pub struct FormatJsObjectBindingPattern; impl FormatNodeRule for FormatJsObjectBindingPattern { fn fmt_fields(&self, node: &JsObjectBindingPattern, f: &mut JsFormatter) -> FormatResult<()> { - let JsObjectBindingPatternFields { - l_curly_token, - properties, - r_curly_token, - } = node.as_fields(); - - write!( - f, - [ - format_delimited(&l_curly_token?, &properties.format(), &r_curly_token?,) - .soft_block_spaces() - ] - ) + write!(f, [JsObjectPatternLike::from(node.clone())]) } } diff --git a/crates/rome_js_formatter/src/lib.rs b/crates/rome_js_formatter/src/lib.rs index 6988a852adc..069cf6a7bf5 100644 --- a/crates/rome_js_formatter/src/lib.rs +++ b/crates/rome_js_formatter/src/lib.rs @@ -511,8 +511,9 @@ mod test { // use this test check if your snippet prints as you wish, without using a snapshot fn quick_test() { let src = r#" -bifornCringerMoshedPerplex = - bifornCringerMoshedPerplexSawder = arrayOfNumb = a = "test"; "#; +({aaaaaaaaaa,bbbbbbbbbb=cccccccccc,dddddddddd:eeeeeeeeee,ffffffffff:gggggggggg=hhhhhhhhhh,...jjjjjjjjjj} = x) + + "#; let syntax = SourceType::ts(); let tree = parse(src, 0, syntax); let result = format_node(JsFormatContext::default(), &tree.syntax()) diff --git a/crates/rome_js_formatter/src/utils/assignment_like.rs b/crates/rome_js_formatter/src/utils/assignment_like.rs index 039bf72816d..de3e84d4fd8 100644 --- a/crates/rome_js_formatter/src/utils/assignment_like.rs +++ b/crates/rome_js_formatter/src/utils/assignment_like.rs @@ -1,16 +1,20 @@ use crate::prelude::*; use crate::utils::object::write_member_name; use crate::utils::JsAnyBinaryLikeExpression; -use rome_formatter::{format_args, write}; +use rome_formatter::{format_args, write, VecBuffer}; use rome_js_syntax::{ - JsAnyAssignmentPattern, JsAnyExpression, JsAnyObjectMemberName, JsAssignmentExpression, + JsAnyAssignmentPattern, JsAnyExpression, JsAnyFunctionBody, JsAnyObjectAssignmentPatternMember, + JsAnyObjectBindingPatternMember, JsAnyObjectMemberName, JsAssignmentExpression, + JsObjectAssignmentPattern, JsObjectAssignmentPatternProperty, JsObjectBindingPattern, JsPropertyObjectMember, JsSyntaxKind, }; use rome_js_syntax::{JsAnyLiteralExpression, JsSyntaxNode}; use rome_rowan::{declare_node_union, AstNode, SyntaxResult}; declare_node_union! { - pub(crate) JsAnyAssignmentLike = JsPropertyObjectMember | JsAssignmentExpression + pub(crate) JsAnyAssignmentLike = JsPropertyObjectMember | + JsAssignmentExpression | + JsObjectAssignmentPatternProperty } @@ -18,12 +22,100 @@ declare_node_union! { pub(crate) LeftAssignmentLike = JsAnyAssignmentPattern | JsAnyObjectMemberName } +declare_node_union! { + pub(crate) RightAssignmentLike = JsAnyExpression | JsAnyAssignmentPattern +} + +declare_node_union! { + /// This is a convenient enum to map object patterns. + pub(crate) AnyObjectPattern = JsObjectAssignmentPattern | JsObjectBindingPattern +} + +impl AnyObjectPattern { + fn is_complex(&self) -> SyntaxResult { + match self { + AnyObjectPattern::JsObjectAssignmentPattern(assignment_pattern) => { + let properties_len = assignment_pattern.properties().len(); + if properties_len <= 2 { + return Ok(false); + } + // A binding is complex when we have at least one [JsObjectBindingPatternProperty] + // e.g. a = { a: c = f } = a + // The `c = f` will trigger the complex binding + let has_at_least_a_complex_binding = assignment_pattern + .properties() + .iter() + .map(|p| p.ok()) + .any(|property| { + let property = property; + + matches!( + property, + Some( + JsAnyObjectAssignmentPatternMember::JsObjectAssignmentPatternProperty(_), + ) + ) + }); + Ok(has_at_least_a_complex_binding) + } + AnyObjectPattern::JsObjectBindingPattern(binding_pattern) => { + let properties_len = binding_pattern.properties().len(); + if properties_len <= 2 { + return Ok(false); + } + // A binding is complex when we have at least one [JsObjectBindingPatternProperty] + // e.g. const a = { a: c = f } = a + // The `c = f` will trigger the complex binding + let has_at_least_a_complex_binding = binding_pattern + .properties() + .iter() + .map(|p| p.ok()) + .any(|property| { + let property = property; + + matches!( + property, + Some( + JsAnyObjectBindingPatternMember::JsObjectBindingPatternProperty(_), + ) + ) + }); + Ok(has_at_least_a_complex_binding) + } + } + } +} + +impl LeftAssignmentLike { + fn as_object_assignment_pattern(&self) -> Option { + match self { + LeftAssignmentLike::JsAnyAssignmentPattern( + JsAnyAssignmentPattern::JsObjectAssignmentPattern(node), + ) => Some(AnyObjectPattern::from(node.clone())), + _ => None, + } + } +} + +impl Format for RightAssignmentLike { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + match self { + RightAssignmentLike::JsAnyExpression(expression) => { + write!(f, [expression.format()]) + } + RightAssignmentLike::JsAnyAssignmentPattern(assignment) => { + write!(f, [assignment.format()]) + } + } + } +} + /// Determines how a assignment like be formatted /// /// Assignment like are: /// - Assignment /// - Object property member -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub(crate) enum AssignmentLikeLayout { /// First break right-hand side, then after operator. /// ```js @@ -82,13 +174,60 @@ pub(crate) enum AssignmentLikeLayout { /// /// Given the previous snippet, then `"foo"` formatted using the [ChainTail] layout. ChainTail, + + /// This layout is used in cases where we want to "break" the left hand side + /// of assignment like expression, but only when the group decides to do it. + /// + /// ```js + /// const a { + /// loreum: { ipsum }, + /// something_else, + /// happy_days: { fonzy } + /// } = obj; + /// ``` + /// + /// The snippet triggers the layout because the left hand side contains a "complex destructuring" + /// which requires having the properties broke on different lines. + BreakLeftHandSide, + + /// This is a special case of the "chain" layout collection. This is triggered when there's + /// a series of simple assignments (at least three) and in the middle we have an arrow function + /// and this function followed by two more arrow functions. + /// + /// This layout will break the right hand side of the tail on a new line and add a new level + /// of indentation + /// + /// ```js + /// lorem = + /// fff = + /// ee = + /// () => (fff) => () => (fefef) => () => fff; + /// ``` + ChainTailArrowFunction, } impl JsAnyAssignmentLike { - fn right(&self) -> SyntaxResult { + fn right(&self) -> SyntaxResult { match self { - JsAnyAssignmentLike::JsPropertyObjectMember(property) => property.value(), - JsAnyAssignmentLike::JsAssignmentExpression(assignment) => assignment.right(), + JsAnyAssignmentLike::JsPropertyObjectMember(property) => Ok(property.value()?.into()), + JsAnyAssignmentLike::JsAssignmentExpression(assignment) => { + Ok(assignment.right()?.into()) + } + JsAnyAssignmentLike::JsObjectAssignmentPatternProperty(assignment_pattern) => { + Ok(assignment_pattern.pattern()?.into()) + } + } + } + + fn left(&self) -> SyntaxResult { + match self { + JsAnyAssignmentLike::JsPropertyObjectMember(property) => Ok(property.name()?.into()), + JsAnyAssignmentLike::JsAssignmentExpression(assignment) => { + Ok(assignment.left()?.into()) + } + JsAnyAssignmentLike::JsObjectAssignmentPatternProperty(property) => { + Ok(property.pattern()?.into()) + } } } } @@ -96,19 +235,25 @@ impl JsAnyAssignmentLike { const MIN_OVERLAP_FOR_BREAK: u8 = 3; impl JsAnyAssignmentLike { - fn write_left(&self, f: &mut JsFormatter) -> FormatResult { + fn write_left(&self, buffer: &mut VecBuffer) -> FormatResult { match self { JsAnyAssignmentLike::JsPropertyObjectMember(property) => { - let width = write_member_name(&property.name()?, f)?; + let width = write_member_name(&property.name()?, buffer)?; let text_width_for_break = - (f.context().tab_width() + MIN_OVERLAP_FOR_BREAK) as usize; + (buffer.context().tab_width() + MIN_OVERLAP_FOR_BREAK) as usize; Ok(width < text_width_for_break) } JsAnyAssignmentLike::JsAssignmentExpression(assignment) => { let left = assignment.left()?; - write!(f, [group_elements(&left.format())])?; + write!(buffer, [&left.format()])?; Ok(false) } + JsAnyAssignmentLike::JsObjectAssignmentPatternProperty(property) => { + let width = write_member_name(&property.member()?, buffer)?; + let text_width_for_break = + (buffer.context().tab_width() + MIN_OVERLAP_FOR_BREAK) as usize; + Ok(width < text_width_for_break) + } } } @@ -122,6 +267,32 @@ impl JsAnyAssignmentLike { let operator_token = assignment.operator_token()?; write!(f, [space_token(), operator_token.format()]) } + JsAnyAssignmentLike::JsObjectAssignmentPatternProperty(property) => { + let colon_token = property.colon_token()?; + write!(f, [colon_token.format()]) + } + } + } + + fn write_right(&self, f: &mut JsFormatter) -> FormatResult<()> { + match self { + JsAnyAssignmentLike::JsPropertyObjectMember(property) => { + let value = property.value()?; + write!(f, [value.format()]) + } + JsAnyAssignmentLike::JsAssignmentExpression(assignment) => { + let right = assignment.right()?; + write!(f, [space_token(), right.format()]) + } + JsAnyAssignmentLike::JsObjectAssignmentPatternProperty(property) => { + let pattern = property.pattern()?; + let init = property.init(); + write!(f, [pattern.format()])?; + if let Some(init) = init { + write!(f, [space_token(), init.format()])?; + } + Ok(()) + } } } @@ -130,30 +301,45 @@ impl JsAnyAssignmentLike { fn layout(&self, is_left_short: bool) -> FormatResult { let right = self.right()?; if let Some(layout) = self.chain_formatting_layout()? { - Ok(layout) - } else if is_break_after_operator(&right)? { - Ok(AssignmentLikeLayout::BreakAfterOperator) - } else if is_left_short { - Ok(AssignmentLikeLayout::NeverBreakAfterOperator) - } else if matches!( + return Ok(layout); + } + + if self.should_break_left_hand_side()? { + return Ok(AssignmentLikeLayout::BreakLeftHandSide); + } + + if let RightAssignmentLike::JsAnyExpression(expression) = &right { + if is_break_after_operator(expression)? { + return Ok(AssignmentLikeLayout::BreakAfterOperator); + } + } + if is_left_short { + return Ok(AssignmentLikeLayout::NeverBreakAfterOperator); + } + + if matches!( right, - JsAnyExpression::JsAnyLiteralExpression( + RightAssignmentLike::JsAnyExpression(JsAnyExpression::JsAnyLiteralExpression( JsAnyLiteralExpression::JsStringLiteralExpression(_) - ) + )), ) { - Ok(AssignmentLikeLayout::BreakAfterOperator) - } else if self.is_never_break_after_operator()? { - Ok(AssignmentLikeLayout::NeverBreakAfterOperator) - } else { - Ok(AssignmentLikeLayout::Fluid) + return Ok(AssignmentLikeLayout::BreakAfterOperator); + } + + if self.is_never_break_after_operator()? { + return Ok(AssignmentLikeLayout::NeverBreakAfterOperator); } + Ok(AssignmentLikeLayout::Fluid) } /// Checks if the right node is entitled of the chain formatting, /// and if so, it return the layout type fn chain_formatting_layout(&self) -> SyntaxResult> { let right = self.right()?; - let right_is_tail = !matches!(right, JsAnyExpression::JsAssignmentExpression(_)); + let right_is_tail = !matches!( + right, + RightAssignmentLike::JsAnyExpression(JsAnyExpression::JsAssignmentExpression(_)) + ); // The chain goes up two levels, by checking up to the great parent if all the conditions // are correctly met. let upper_chain_is_eligible = @@ -188,10 +374,28 @@ impl JsAnyAssignmentLike { }; let result = if upper_chain_is_eligible { - if right_is_tail { - Some(AssignmentLikeLayout::ChainTail) - } else { + if !right_is_tail { Some(AssignmentLikeLayout::Chain) + } else { + match right { + RightAssignmentLike::JsAnyExpression( + JsAnyExpression::JsArrowFunctionExpression(arrow), + ) => { + let this_body = arrow.body()?; + if matches!( + this_body, + JsAnyFunctionBody::JsAnyExpression( + JsAnyExpression::JsArrowFunctionExpression(_) + ) + ) { + Some(AssignmentLikeLayout::ChainTailArrowFunction) + } else { + Some(AssignmentLikeLayout::ChainTail) + } + } + + _ => Some(AssignmentLikeLayout::ChainTail), + } } } else { None @@ -202,28 +406,47 @@ impl JsAnyAssignmentLike { fn is_never_break_after_operator(&self) -> SyntaxResult { let right = self.right()?; - if let JsAnyExpression::JsCallExpression(call_expression) = &right { - if call_expression.callee()?.syntax().text() == "require" { - return Ok(true); + if let RightAssignmentLike::JsAnyExpression(right) = &right { + if let JsAnyExpression::JsCallExpression(call_expression) = &right { + if call_expression.callee()?.syntax().text() == "require" { + return Ok(true); + } } - } - if matches!( - right, - JsAnyExpression::JsClassExpression(_) - | JsAnyExpression::JsTemplate(_) - | JsAnyExpression::JsAnyLiteralExpression( - JsAnyLiteralExpression::JsBooleanLiteralExpression(_), - ) - | JsAnyExpression::JsAnyLiteralExpression( - JsAnyLiteralExpression::JsNumberLiteralExpression(_) - ) - ) { - return Ok(true); + if matches!( + right, + JsAnyExpression::JsClassExpression(_) + | JsAnyExpression::JsTemplate(_) + | JsAnyExpression::JsAnyLiteralExpression( + JsAnyLiteralExpression::JsBooleanLiteralExpression(_), + ) + | JsAnyExpression::JsAnyLiteralExpression( + JsAnyLiteralExpression::JsNumberLiteralExpression(_) + ) + ) { + return Ok(true); + } } Ok(false) } + + /// Particular function that checks if the left hand side of a [JsAnyAssignmentLike] should + /// be broken on multiple lines + fn should_break_left_hand_side(&self) -> SyntaxResult { + // TODO: here we have to add the check for variable declarator too + let is_complex_destructuring = if let JsAnyAssignmentLike::JsAssignmentExpression(_) = self + { + self.left()? + .as_object_assignment_pattern() + .and_then(|pattern| pattern.is_complex().ok()) + .unwrap_or(false) + } else { + false + }; + + Ok(is_complex_destructuring) + } } /// Checks if the function is entitled to be printed with layout [AssignmentLikeLayout::BreakAfterOperator] @@ -278,22 +501,50 @@ pub(crate) fn has_new_line_before_comment(node: &JsSyntaxNode) -> bool { impl Format for JsAnyAssignmentLike { fn fmt(&self, f: &mut JsFormatter) -> FormatResult<()> { - let right = self.right()?; let format_content = format_with(|f| { + // We create a temporary buffer because the left hand side has to conditionally add + // a group based on the layout, but the layout can only be computed by knowing the + // width of the left hand side. The left hand side can be a member, and that has a width + // can can be known only when it's formatted (it can incur in some transformation, + // like removing some escapes, etc.). + // + // 1. we crate a temporary buffer + // 2. we write the left hand side into the buffer and retrieve the `is_left_short` info + // which is computed only when we format it + // 3. we compute the layout + // 4. we write the left node inside the main buffer based on the layout + let mut buffer = VecBuffer::new(f.state_mut()); + let is_left_short = self.write_left(&mut buffer)?; + // Compare name only if we are in a position of computing it. // If not (for example, left is not an identifier), then let's fallback to false, // so we can continue the chain of checks - let is_left_short = self.write_left(f)?; + let layout = self.layout(is_left_short)?; + + let formatted_element = buffer.into_element(); + + if layout == AssignmentLikeLayout::BreakLeftHandSide { + write!( + f, + [&format_once(|f| { f.write_element(formatted_element) })] + )?; + } else { + write!( + f, + [group_elements(&format_once(|f| { + f.write_element(formatted_element) + }))] + )?; + } + self.write_operator(f)?; - let layout = self.layout(is_left_short)?; + let right = &format_with(|f| self.write_right(f)).memoized(); let inner_content = format_with(|f| match &layout { AssignmentLikeLayout::Fluid => { let group_id = f.group_id("assignment_like"); - let right = right.format().memoized(); - write![ f, [ @@ -310,25 +561,39 @@ impl Format for JsAnyAssignmentLike { f, [group_elements(&indent(&format_args![ soft_line_break_or_space(), - right.format() + right, ])),] ] } AssignmentLikeLayout::NeverBreakAfterOperator => { - write![f, [space_token(), right.format(),]] + write![f, [space_token(), right,]] + } + + AssignmentLikeLayout::BreakLeftHandSide => { + write![f, [space_token(), group_elements(right),]] } AssignmentLikeLayout::Chain => { - write!(f, [soft_line_break_or_space(), right.format()]) + write!(f, [soft_line_break_or_space(), right,]) } AssignmentLikeLayout::ChainTail => { write!( f, - [&indent(&format_args![ - soft_line_break_or_space(), - right.format() - ])] + [&indent(&format_args![soft_line_break_or_space(), right,])] + ) + } + + AssignmentLikeLayout::ChainTailArrowFunction => { + let group_id = f.group_id("arrow_chain"); + + write!( + f, + [ + space_token(), + group_elements(&indent(&format_args![hard_line_break(), right])) + .with_group_id(Some(group_id)), + ] ) } }); diff --git a/crates/rome_js_formatter/src/utils/mod.rs b/crates/rome_js_formatter/src/utils/mod.rs index 4b0f68bbe18..1008f9c3a00 100644 --- a/crates/rome_js_formatter/src/utils/mod.rs +++ b/crates/rome_js_formatter/src/utils/mod.rs @@ -8,6 +8,7 @@ pub mod string_utils; pub(crate) mod format_class; mod member_chain; mod object; +mod object_pattern_like; #[cfg(test)] mod quickcheck_utils; @@ -16,6 +17,7 @@ pub(crate) use assignment_like::{is_break_after_operator, JsAnyAssignmentLike}; pub(crate) use binary_like_expression::{format_binary_like_expression, JsAnyBinaryLikeExpression}; pub(crate) use format_conditional::{format_conditional, Conditional}; pub(crate) use member_chain::format_call_expression; +pub(crate) use object_pattern_like::JsObjectPatternLike; use rome_formatter::{format_args, normalize_newlines, write, Buffer, VecBuffer}; use rome_js_syntax::suppression::{has_suppressions_category, SuppressionCategory}; use rome_js_syntax::JsSyntaxKind::JS_STRING_LITERAL; diff --git a/crates/rome_js_formatter/src/utils/object.rs b/crates/rome_js_formatter/src/utils/object.rs index 425d135419d..738a21bc30e 100644 --- a/crates/rome_js_formatter/src/utils/object.rs +++ b/crates/rome_js_formatter/src/utils/object.rs @@ -2,7 +2,7 @@ use crate::prelude::*; use crate::utils::FormatLiteralStringToken; use crate::utils::StringLiteralParentKind; -use rome_formatter::write; +use rome_formatter::{write, VecBuffer}; use rome_js_syntax::JsAnyObjectMemberName; use rome_js_syntax::JsSyntaxKind::JS_STRING_LITERAL; use rome_rowan::AstNode; @@ -10,7 +10,7 @@ use unicode_width::UnicodeWidthStr; pub(crate) fn write_member_name( name: &JsAnyObjectMemberName, - f: &mut JsFormatter, + buffer: &mut VecBuffer, ) -> FormatResult { match name { name @ JsAnyObjectMemberName::JsLiteralMemberName(literal) => { @@ -18,19 +18,19 @@ pub(crate) fn write_member_name( if value.kind() == JS_STRING_LITERAL { let format = FormatLiteralStringToken::new(&value, StringLiteralParentKind::Member); - let cleaned = format.clean_text(f.context()); + let cleaned = format.clean_text(buffer.context()); - cleaned.fmt(f)?; + write!(buffer, [cleaned])?; Ok(cleaned.width()) } else { - name.format().fmt(f)?; + write!(buffer, [name.format()])?; Ok(value.text_trimmed().width()) } } name => { - write!(f, [group_elements(&name.format())])?; + write!(buffer, [&name.format()])?; Ok(name.text().width()) } } diff --git a/crates/rome_js_formatter/src/utils/object_pattern_like.rs b/crates/rome_js_formatter/src/utils/object_pattern_like.rs new file mode 100644 index 00000000000..7a7e8d1ea42 --- /dev/null +++ b/crates/rome_js_formatter/src/utils/object_pattern_like.rs @@ -0,0 +1,158 @@ +use crate::prelude::*; +use crate::JsFormatContext; +use rome_formatter::formatter::Formatter; +use rome_formatter::write; +use rome_formatter::{Format, FormatResult}; +use rome_js_syntax::{ + JsAnyAssignmentPattern, JsAnyBindingPattern, JsAnyObjectAssignmentPatternMember, + JsAnyObjectBindingPatternMember, JsObjectAssignmentPattern, JsObjectBindingPattern, + JsSyntaxKind, JsSyntaxToken, +}; +use rome_rowan::{declare_node_union, AstNode, SyntaxResult}; + +declare_node_union! { + pub (crate) JsObjectPatternLike = JsObjectAssignmentPattern | JsObjectBindingPattern +} + +impl JsObjectPatternLike { + fn l_curly_token(&self) -> SyntaxResult { + match self { + JsObjectPatternLike::JsObjectAssignmentPattern(node) => node.l_curly_token(), + JsObjectPatternLike::JsObjectBindingPattern(node) => node.l_curly_token(), + } + } + + fn r_curly_token(&self) -> SyntaxResult { + match self { + JsObjectPatternLike::JsObjectAssignmentPattern(node) => node.r_curly_token(), + JsObjectPatternLike::JsObjectBindingPattern(node) => node.r_curly_token(), + } + } + + fn properties_len(&self) -> usize { + match self { + JsObjectPatternLike::JsObjectAssignmentPattern(node) => node.properties().len(), + JsObjectPatternLike::JsObjectBindingPattern(node) => node.properties().len(), + } + } + + fn write_properties(&self, f: &mut JsFormatter) -> FormatResult<()> { + match self { + JsObjectPatternLike::JsObjectAssignmentPattern(node) => { + write!(f, [node.properties().format()]) + } + JsObjectPatternLike::JsObjectBindingPattern(node) => { + write!(f, [node.properties().format()]) + } + } + } + + fn should_break_properties(&self) -> SyntaxResult { + let has_at_least_a_complex_property = match self { + JsObjectPatternLike::JsObjectAssignmentPattern(node) => { + node.properties().iter().any(|property| { + if let Ok( + JsAnyObjectAssignmentPatternMember::JsObjectAssignmentPatternProperty(node), + ) = property + { + let pattern = node.pattern(); + matches!( + pattern, + Ok(JsAnyAssignmentPattern::JsObjectAssignmentPattern(_) + | JsAnyAssignmentPattern::JsArrayAssignmentPattern(_)) + ) + } else { + false + } + }) + } + JsObjectPatternLike::JsObjectBindingPattern(node) => { + node.properties().iter().any(|property| { + if let Ok(JsAnyObjectBindingPatternMember::JsObjectBindingPatternProperty( + node, + )) = property + { + let pattern = node.pattern(); + matches!( + pattern, + Ok(JsAnyBindingPattern::JsObjectBindingPattern(_) + | JsAnyBindingPattern::JsArrayBindingPattern(_)) + ) + } else { + false + } + }) + } + }; + + let parent_kind = self.syntax().parent().map(|p| p.kind()); + + let parent_where_not_to_break = !matches!( + parent_kind, + Some( + // These parents are the kinds where we want to prevent + // to go to multiple lines. + JsSyntaxKind::JS_FUNCTION_EXPRESSION + | JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION + | JsSyntaxKind::JS_OBJECT_ASSIGNMENT_PATTERN_PROPERTY + | JsSyntaxKind::JS_CATCH_DECLARATION + | JsSyntaxKind::JS_FUNCTION_DECLARATION + | JsSyntaxKind::JS_OBJECT_BINDING_PATTERN_PROPERTY + | JsSyntaxKind::JS_FORMAL_PARAMETER + ) + ); + + Ok(parent_where_not_to_break && has_at_least_a_complex_property) + } + + fn is_in_assignment_like(&self) -> bool { + if let JsObjectPatternLike::JsObjectAssignmentPattern(pattern) = self { + let parent_kind = pattern.syntax().parent().map(|p| p.kind()); + matches!( + parent_kind, + Some(JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION | JsSyntaxKind::JS_VARIABLE_DECLARATOR) + ) + } else { + false + } + } +} + +impl Format for JsObjectPatternLike { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let should_break_properties = self.should_break_properties()?; + let right = &format_with(|f| self.write_properties(f)); + let is_in_assignment_like = self.is_in_assignment_like(); + let properties_len = self.properties_len(); + + if should_break_properties { + write!( + f, + [ + &self.l_curly_token()?.format(), + &block_indent(right), + &self.r_curly_token()?.format(), + ] + ) + } else if !should_break_properties && is_in_assignment_like { + // no need to add a group if we know the parent already does that + write!(f, [&self.l_curly_token()?.format()])?; + if properties_len > 0 { + write!(f, [soft_line_break_or_space()])?; + write!(f, [soft_block_indent(right)])?; + write!(f, [soft_line_break_or_space()])?; + } else { + write!(f, [right])?; + } + write!(f, [&self.r_curly_token()?.format()]) + } else { + write!( + f, + [ + format_delimited(&self.l_curly_token()?, right, &self.r_curly_token()?,) + .soft_block_spaces() + ] + ) + } + } +} diff --git a/crates/rome_js_formatter/tests/specs/js/module/arrow/params.js.snap b/crates/rome_js_formatter/tests/specs/js/module/arrow/params.js.snap index 9720d5d8eb1..d4e8ff87935 100644 --- a/crates/rome_js_formatter/tests/specs/js/module/arrow/params.js.snap +++ b/crates/rome_js_formatter/tests/specs/js/module/arrow/params.js.snap @@ -364,9 +364,21 @@ foo(({ a: { b: { c, d } } }) => a); foo(({ a: { b: { c: { d, e } } } }) => a); -foo(([{ a: { b: { c: { d, e } } } }]) => {}); +foo(( + [ + { + a: { b: { c: { d, e } } }, + }, + ], +) => {}); -foo(([...{ a: { b: { c: { d, e } } } }]) => {}); +foo(( + [ + ...{ + a: { b: { c: { d, e } } }, + } + ], +) => {}); foo(( n = { @@ -398,7 +410,20 @@ foo( foo(([[{ a, b }]]) => {}); -foo(([[[[{ a, b: { c, d: { e, f } } }]]]]) => {}); +foo(( + [ + [ + [ + [ + { + a, + b: { c, d: { e, f } }, + }, + ], + ], + ], + ], +) => {}); foo((...{ a, b }) => {}); diff --git a/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js b/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js index f6089fd55cb..1d81229f2e3 100644 --- a/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js +++ b/crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js @@ -116,3 +116,23 @@ bifornCringerMoshedPerplex = bifornCringerMoshedPerplexSawder = arrayOfNumb = a // chain and chain tail where it doesn't break loreum = ipsum = arrayOfNumb = a = "test" +// chain tail arrow function +lorem = fff = ee = () => (fff) => () => (fefef) => () => fff; + +// complex destructuring, break left hand +a = { + a: { t: c = b }, loreum, ipsurm +} = {} + +a = +// rome-ignore format: test +{ + a: { t: c = b }, loreum, ipsurm +} = {} + +a = + { +// rome-ignore format: test + a: { t: c = b }, loreum, ipsurm + } = {} + 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 63ff3fd6c8d..888472af010 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 @@ -122,6 +122,26 @@ bifornCringerMoshedPerplex = bifornCringerMoshedPerplexSawder = arrayOfNumb = a // chain and chain tail where it doesn't break loreum = ipsum = arrayOfNumb = a = "test" +// chain tail arrow function +lorem = fff = ee = () => (fff) => () => (fefef) => () => fff; + +// complex destructuring, break left hand +a = { + a: { t: c = b }, loreum, ipsurm +} = {} + +a = +// rome-ignore format: test +{ + a: { t: c = b }, loreum, ipsurm +} = {} + +a = + { +// rome-ignore format: test + a: { t: c = b }, loreum, ipsurm + } = {} + ============================= # Outputs @@ -299,6 +319,32 @@ bifornCringerMoshedPerplex = // chain and chain tail where it doesn't break loreum = ipsum = arrayOfNumb = a = "test"; +// chain tail arrow function +lorem = + fff = + ee = + () => (fff) => () => (fefef) => () => fff; + +// complex destructuring, break left hand +a = { + a: { t: c = b }, + loreum, + ipsurm, +} = {}; + +a = + // rome-ignore format: test +{ + a: { t: c = b }, loreum, ipsurm +} = {}; + +a = { + // rome-ignore format: test + a: { t: c = b }, + loreum, + ipsurm, +} = {}; + ## Lines exceeding width of 80 characters diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/destructuring-private-fields/nested-bindings.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/destructuring-private-fields/nested-bindings.js.snap index 2f4575a3c37..a01268740a9 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/destructuring-private-fields/nested-bindings.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/destructuring-private-fields/nested-bindings.js.snap @@ -1,5 +1,6 @@ --- source: crates/rome_js_formatter/tests/prettier_tests.rs +assertion_line: 184 expression: nested-bindings.js --- # Input @@ -18,7 +19,10 @@ class C { class C { #x = 1; m() { - const { x: { #x: [x] }, y: [...{ #x: y }] } = this; + const { + x: { #x: [x] }, + y: [...{ #x: y }], + } = this; } } diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/destructuring/destructuring.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/destructuring/destructuring.js.snap index 8cce4e73034..771bca59a6f 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/destructuring/destructuring.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/destructuring/destructuring.js.snap @@ -1,5 +1,6 @@ --- source: crates/rome_js_formatter/tests/prettier_tests.rs +assertion_line: 184 expression: destructuring.js --- # Input @@ -58,7 +59,9 @@ const [one, two = null, three = null] = arr; a = ([s = 1]) => 1; const { children, ...props } = this.props; -const { user: { firstName, lastName } } = this.props; +const { + user: { firstName, lastName }, +} = this.props; const { name: { first, last }, @@ -80,7 +83,12 @@ const UserComponent = function ( return; }; -const { a, b, c, d: { e } } = someObject; +const { + a, + b, + c, + d: { e }, +} = someObject; try { // code