From b8f4f34bba54567f46123740305d7089b9fddaad Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:09:36 +0000 Subject: [PATCH] fix(formatter): print `x.b` and `x.#b` samely (#28193) There is no reason to handle them separately. This change also fixes idempotency issue with comments. --- crates/oxc_formatter/DIVERGENCES.md | 88 ++++++++ .../src/ast_nodes/impls/ast_nodes.rs | 15 ++ .../src/parentheses/expression.rs | 6 +- .../src/print/as_or_satisfies_expression.rs | 11 +- .../src/print/binary_like_expression.rs | 7 +- .../print/call_like_expression/arguments.rs | 16 +- .../src/print/call_like_expression/mod.rs | 6 +- crates/oxc_formatter/src/print/decorators.rs | 18 +- crates/oxc_formatter/src/print/jsx/element.rs | 14 -- .../src/print/member_expression.rs | 127 ++++++----- crates/oxc_formatter/src/print/mod.rs | 7 +- .../src/print/return_or_throw_statement.rs | 29 +-- crates/oxc_formatter/src/utils/conditional.rs | 22 +- .../src/utils/member_chain/chain_member.rs | 97 +++++--- .../src/utils/member_chain/groups.rs | 10 +- .../src/utils/member_chain/mod.rs | 42 ++-- .../member-property-own-line-comment.js | 20 ++ .../member-property-own-line-comment.js.snap | 87 +++++++ .../tests/fixtures/js/jsx/optional-member.jsx | 4 + .../fixtures/js/jsx/optional-member.jsx.snap | 50 ++++ .../fixtures/js/private-fields/layout.js | 32 +++ .../fixtures/js/private-fields/layout.js.snap | 213 ++++++++++++++++++ .../private-member-like-static.js | 28 +++ .../private-member-like-static.js.snap | 115 ++++++++++ .../fixtures/ts/private-fields/layout.ts | 18 ++ .../fixtures/ts/private-fields/layout.ts.snap | 81 +++++++ .../allocs_formatter.yaml | 16 +- 27 files changed, 970 insertions(+), 209 deletions(-) create mode 100644 crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js create mode 100644 crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js.snap create mode 100644 crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx create mode 100644 crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx.snap create mode 100644 crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js create mode 100644 crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js.snap create mode 100644 crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js create mode 100644 crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js.snap create mode 100644 crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts create mode 100644 crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts.snap diff --git a/crates/oxc_formatter/DIVERGENCES.md b/crates/oxc_formatter/DIVERGENCES.md index f3a7bef2cd..9066f3fdbe 100644 --- a/crates/oxc_formatter/DIVERGENCES.md +++ b/crates/oxc_formatter/DIVERGENCES.md @@ -1079,3 +1079,91 @@ class A { A class member named `in` or `instanceof` would continue the previous property's value as a binary operator, so the `;` before it is kept. Prettier checks only an identifier key, but also prints the string key `"in"` unquoted, producing a syntax error. We check a string key by its value, which also keeps the `;` before a key that stays quoted (TypeScript, `quoteProps: "preserve"`), where Prettier's output stays valid. + +## private-member-like-static + +- Why: uniform-rule (same construct, same output: `a.b`) +- Pin: `tests/fixtures/js/private-fields/private-member-like-static.js` + +```js +// input (printWidth: 80) +foo(someIdentifierName.#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking); +this.#Factory.someMethodName(argumentOne).anotherMethodName(argumentTwo).third(); +const a = obj.#decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; +}); +@A.#decorator a() {} + +// ours: the same as `.b` +foo( + someIdentifierName.#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking, +); +this.#Factory.someMethodName(argumentOne) + .anotherMethodName(argumentTwo) + .third(); +const a = obj.#decorator(someArgument)(( + aaaaaaaaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccc, +) => { + return a; +}); +@A.#decorator a() {} + +// prettier +foo( + someIdentifierName + .#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking, +); +this.#Factory + .someMethodName(argumentOne) + .anotherMethodName(argumentTwo) + .third(); +const a = obj.#decorator(someArgument)( + (aaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }, +); +@(A.#decorator) a() {} +``` + +Both are a non-computed member access, so `.#b` follows every rule of `.b`. +Prettier prints `.b` like ours in all four, only `.#b` differently. +`@A.#decorator` is valid without parens (`DecoratorMemberExpression` accepts a `PrivateIdentifier`). + +## member-property-own-line-comment + +- Why: invariant +- Pin: `tests/fixtures/js/comments/member-property-own-line-comment.js` + +```js +// input +x = a + // comment + .b; +x = a + // comment + .#b; + +// ours +x = + a + // comment + .b; +x = + a + // comment + .#b; + +// prettier +x = + // comment + a.b; +x = + a + .// comment + #b; +``` + +An own-line comment before a member's `.` stays before it, for `.b` and `.#b` alike. +Prettier moves it before the object for `.b`, and after the `.` for `.#b`; both cross a token. diff --git a/crates/oxc_formatter/src/ast_nodes/impls/ast_nodes.rs b/crates/oxc_formatter/src/ast_nodes/impls/ast_nodes.rs index 214f13e07e..c45a0d3081 100644 --- a/crates/oxc_formatter/src/ast_nodes/impls/ast_nodes.rs +++ b/crates/oxc_formatter/src/ast_nodes/impls/ast_nodes.rs @@ -56,6 +56,21 @@ impl<'parent, 'a> AstNodes<'parent, 'a> { } } + /// Check if this is a member expression: `a.b`, `a[b]`, or `a.#b` + pub fn is_member_expression(&self) -> bool { + matches!( + self, + AstNodes::StaticMemberExpression(_) + | AstNodes::ComputedMemberExpression(_) + | AstNodes::PrivateFieldExpression(_) + ) + } + + /// Check if this is a non-computed member expression: `a.b` or `a.#b` + pub fn is_non_computed_member_expression(&self) -> bool { + matches!(self, AstNodes::StaticMemberExpression(_) | AstNodes::PrivateFieldExpression(_)) + } + /// Check if this is a binary-like expression: `a || b`, `a + b`, or `#x in b` pub fn is_binary_like(&self) -> bool { matches!( diff --git a/crates/oxc_formatter/src/parentheses/expression.rs b/crates/oxc_formatter/src/parentheses/expression.rs index c68abcfe28..1c3d1848df 100644 --- a/crates/oxc_formatter/src/parentheses/expression.rs +++ b/crates/oxc_formatter/src/parentheses/expression.rs @@ -1196,9 +1196,6 @@ fn jsx_element_or_fragment_needs_paren(span: Span, parent: &AstNodes<'_, '_>) -> AstNodes::TSAsExpression(_) | AstNodes::TSSatisfiesExpression(_) | AstNodes::AwaitExpression(_) - | AstNodes::StaticMemberExpression(_) - | AstNodes::ComputedMemberExpression(_) - | AstNodes::PrivateFieldExpression(_) | AstNodes::SequenceExpression(_) | AstNodes::UnaryExpression(_) | AstNodes::TSNonNullExpression(_) @@ -1206,8 +1203,7 @@ fn jsx_element_or_fragment_needs_paren(span: Span, parent: &AstNodes<'_, '_>) -> | AstNodes::TaggedTemplateExpression(_) | AstNodes::JSXSpreadAttribute(_) | AstNodes::JSXSpreadChild(_) => true, - _ if parent.is_call_like_callee_span(span) => true, - _ => false, + _ => parent.is_member_expression() || parent.is_call_like_callee_span(span), } } diff --git a/crates/oxc_formatter/src/print/as_or_satisfies_expression.rs b/crates/oxc_formatter/src/print/as_or_satisfies_expression.rs index 92c0d6e11e..9cb06d6e29 100644 --- a/crates/oxc_formatter/src/print/as_or_satisfies_expression.rs +++ b/crates/oxc_formatter/src/print/as_or_satisfies_expression.rs @@ -132,15 +132,10 @@ fn format_as_or_satisfies_expression<'a>( } fn is_callee_or_object_context(span: Span, parent: &AstNodes<'_, '_>) -> bool { - match parent { - // Static member - AstNodes::StaticMemberExpression(_) => true, - AstNodes::ComputedMemberExpression(member) => member.object.span() == span, + parent.is_member_object_span(span) // Or CallExpression callee (Not NewExpression, to align with Prettier) // https://github.com/prettier/prettier/blob/fdfa6701767f5140a85902ecc9fb6444f5b4e3f8/src/language-js/print/cast-expression.js#L28-L33 // NOTE: We may revert this if resolved: https://github.com/prettier/prettier/issues/18406 - // _ => parent.is_call_like_callee_span(span), - AstNodes::CallExpression(call) => call.callee.span() == span, - _ => false, - } + // || parent.is_call_like_callee_span(span) + || matches!(parent, AstNodes::CallExpression(call) if call.callee.span() == span) } diff --git a/crates/oxc_formatter/src/print/binary_like_expression.rs b/crates/oxc_formatter/src/print/binary_like_expression.rs index cfcd2deb0b..e76f1ac62b 100644 --- a/crates/oxc_formatter/src/print/binary_like_expression.rs +++ b/crates/oxc_formatter/src/print/binary_like_expression.rs @@ -230,8 +230,11 @@ impl<'a> Format<'a, JsFormatContext<'a>> for BinaryLikeExpression<'a, '_> { AstNodes::UnaryExpression(unary) if unary_argument_takes_comment_parens(unary, f) => { return format_flattened_logical_expression(*self, false, f); } - AstNodes::StaticMemberExpression(_) | AstNodes::UnaryExpression(_) => true, - _ => parent.is_call_like_callee_span(self.span()), + AstNodes::UnaryExpression(_) => true, + _ => { + parent.is_non_computed_member_expression() + || parent.is_call_like_callee_span(self.span()) + } }; if is_inside_parenthesis { diff --git a/crates/oxc_formatter/src/print/call_like_expression/arguments.rs b/crates/oxc_formatter/src/print/call_like_expression/arguments.rs index e116693834..0ee6a61924 100644 --- a/crates/oxc_formatter/src/print/call_like_expression/arguments.rs +++ b/crates/oxc_formatter/src/print/call_like_expression/arguments.rs @@ -1122,12 +1122,16 @@ fn is_decorated_function(argument: &AstNode<'_, '_, Argument<'_>>) -> bool { // Check if the decorator (callee.callee) is a simple identifier or member expression let decorator = &callee_call.callee; - let is_valid_decorator = matches!(decorator, Expression::Identifier(_)) - || matches!( - decorator, - Expression::StaticMemberExpression(member) - if matches!(&member.object, Expression::Identifier(_)) - ); + let is_valid_decorator = match decorator { + Expression::Identifier(_) => true, + Expression::StaticMemberExpression(member) => { + matches!(&member.object, Expression::Identifier(_)) + } + Expression::PrivateFieldExpression(member) => { + matches!(&member.object, Expression::Identifier(_)) + } + _ => false, + }; if !is_valid_decorator { return false; diff --git a/crates/oxc_formatter/src/print/call_like_expression/mod.rs b/crates/oxc_formatter/src/print/call_like_expression/mod.rs index 880d455ff6..fbe4bf5013 100644 --- a/crates/oxc_formatter/src/print/call_like_expression/mod.rs +++ b/crates/oxc_formatter/src/print/call_like_expression/mod.rs @@ -42,10 +42,8 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, CallExpression<'a>> { None }; - if matches!( - callee.as_ref(), - Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) - ) && !is_verbatim_multiline_template_sole_arg(arguments.as_slice(), Some(self), f) + if callee.is_member_expression() + && !is_verbatim_multiline_template_sole_arg(arguments.as_slice(), Some(self), f) && !is_simple_module_import(&self.arguments(), f.comments()) && !is_test_call_expression(self, f.comments()) { diff --git a/crates/oxc_formatter/src/print/decorators.rs b/crates/oxc_formatter/src/print/decorators.rs index a7fbce38ed..f6380f8144 100644 --- a/crates/oxc_formatter/src/print/decorators.rs +++ b/crates/oxc_formatter/src/print/decorators.rs @@ -52,7 +52,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for AstNode<'_, 'a, ArenaVec<'a, Decora } } -fn is_identifier_or_static_member_only(callee: &Expression) -> bool { +fn is_identifier_or_non_computed_member_only(callee: &Expression) -> bool { let mut expr = callee; loop { match expr { @@ -60,6 +60,9 @@ fn is_identifier_or_static_member_only(callee: &Expression) -> bool { Expression::StaticMemberExpression(static_member) => { expr = &static_member.object; } + Expression::PrivateFieldExpression(private_field) => { + expr = &private_field.object; + } _ => break, } } @@ -73,16 +76,13 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, Decorator<'a>> { // Determine if parentheses are required around decorator expressions let needs_parentheses = match &self.expression { - // Identifiers: `@decorator` needs no parens - Expression::Identifier(_) => false, // Call expressions: `@obj.method()` needs no parens, `@(complex().method)()` needs parens - Expression::CallExpression(call) => !is_identifier_or_static_member_only(&call.callee), - // Static member expressions: `@obj.prop` needs no parens, `@(complex[key])` needs parens - Expression::StaticMemberExpression(static_member) => { - !is_identifier_or_static_member_only(&static_member.object) + Expression::CallExpression(call) => { + !is_identifier_or_non_computed_member_only(&call.callee) } - // All other expressions need parentheses: `@(a + b)`, `@(condition ? a : b)` - _ => true, + // `@decorator`, `@obj.prop` and `@obj.#prop` need no parens, + // all other expressions need parens: `@(complex[key])`, `@(a + b)` + expression => !is_identifier_or_non_computed_member_only(expression), }; if needs_parentheses { diff --git a/crates/oxc_formatter/src/print/jsx/element.rs b/crates/oxc_formatter/src/print/jsx/element.rs index d99b4fe10e..a210101b73 100644 --- a/crates/oxc_formatter/src/print/jsx/element.rs +++ b/crates/oxc_formatter/src/print/jsx/element.rs @@ -95,25 +95,11 @@ impl<'a> AnyJsxTagWithChildren<'a, '_> { | AstNodes::JSXExpressionContainer(_) | AstNodes::ConditionalExpression(_) | AstNodes::ExpressionStatement(_) => WrapState::NoWrap, - AstNodes::StaticMemberExpression(member) => { - if member.optional { - WrapState::NoWrap - } else { - WrapState::WrapOnBreak - } - } // It is a argument of a call expression AstNodes::CallExpression(call) if call.is_argument_span(self.span()) => { WrapState::NoWrap } AstNodes::NewExpression(new) if new.is_argument_span(self.span()) => WrapState::NoWrap, - AstNodes::ComputedMemberExpression(member) => { - if member.optional { - WrapState::NoWrap - } else { - WrapState::WrapOnBreak - } - } // Concise arrow body: `() =>
` #[expect(clippy::match_same_arms)] AstNodes::ArrowFunctionExpression(_) => WrapState::WrapOnBreak, diff --git a/crates/oxc_formatter/src/print/member_expression.rs b/crates/oxc_formatter/src/print/member_expression.rs index 322ed3dbcd..bf63d5cb0b 100644 --- a/crates/oxc_formatter/src/print/member_expression.rs +++ b/crates/oxc_formatter/src/print/member_expression.rs @@ -26,48 +26,66 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, ComputedMemberExpression<'a>> { impl<'a> FormatWrite<'a> for AstNode<'_, 'a, StaticMemberExpression<'a>> { fn write(&self, f: &mut JsFormatter<'_, 'a>) { - // Before the recording: printing the object consumes its cast comment - let object_is_cast_target = is_cast_target(self.object.span(), f); - let is_member_chain = { - let start = f.elements().len(); - write!(f, [self.object()]); - f.elements()[start..].has_label(LabelId::of(JsLabels::MemberChain)) - }; + write_non_computed_member(self.object(), self.optional(), self.property(), f); + } +} - match layout(self, is_member_chain, object_is_cast_target, f) { - StaticMemberLayout::NoBreak => { - let format_no_break = - format_with(|f| write!(f, [operator_token(self.optional()), self.property()])); +/// `a.#b` is laid out the same as `a.b`. +impl<'a> FormatWrite<'a> for AstNode<'_, 'a, PrivateFieldExpression<'a>> { + fn write(&self, f: &mut JsFormatter<'_, 'a>) { + write_non_computed_member(self.object(), self.optional(), self.field(), f); + } +} - if is_member_chain { - write!(f, [labelled(LabelId::of(JsLabels::MemberChain), &format_no_break)]); - } else { - write!(f, [format_no_break]); - } - } - StaticMemberLayout::BreakAfterObject => { - write!( - f, - [group(&indent(&format_args!( - soft_line_break(), - &format_with(|f| { - if f.comments().has_leading_own_line_comment(self.property.span.start) { - let comments = f - .context() - .comments() - .comments_before(self.property.span.start); - write!( - f, - [FormatLeadingComments::Comments(comments), soft_line_break()] - ); - } - }), - operator_token(self.optional()), - self.property(), - )))] - ); +/// Writes `a.b` or `a.#b`. +fn write_non_computed_member<'b, 'a, P: GetSpan>( + object: AstNode<'b, 'a, Expression<'a>>, + optional: bool, + property: AstNode<'b, 'a, P>, + f: &mut JsFormatter<'_, 'a>, +) where + AstNode<'b, 'a, P>: Format<'a, JsFormatContext<'a>>, +{ + // Before the recording: printing the object consumes its cast comment + let object_is_cast_target = is_cast_target(object.span(), f); + let is_member_chain = { + let start = f.elements().len(); + write!(f, [object]); + f.elements()[start..].has_label(LabelId::of(JsLabels::MemberChain)) + }; + let property_start = property.span().start; + // The member expression itself + let member = property.parent(); + + match layout(&object, property_start, member, is_member_chain, object_is_cast_target, f) { + StaticMemberLayout::NoBreak => { + let format_no_break = format_with(|f| write!(f, [operator_token(optional), property])); + + if is_member_chain { + write!(f, [labelled(LabelId::of(JsLabels::MemberChain), &format_no_break)]); + } else { + write!(f, [format_no_break]); } } + StaticMemberLayout::BreakAfterObject => { + write!( + f, + [group(&indent(&format_args!( + soft_line_break(), + &format_with(|f| { + if f.comments().has_leading_own_line_comment(property_start) { + let comments = f.context().comments().comments_before(property_start); + write!( + f, + [FormatLeadingComments::Comments(comments), soft_line_break()] + ); + } + }), + operator_token(optional), + property, + )))] + ); + } } } @@ -87,23 +105,24 @@ fn operator_token(optional: bool) -> &'static str { /// `object_is_cast_target`: /// a cast-target object is neither a call nor an identifier to the rules below (see `is_cast_target`). fn layout<'a>( - node: &AstNode<'_, 'a, StaticMemberExpression<'a>>, + object: &Expression<'a>, + property_start: u32, + member: &AstNodes<'_, 'a>, is_member_chain: bool, object_is_cast_target: bool, f: &JsFormatter<'_, 'a>, ) -> StaticMemberLayout { - if f.comments().has_leading_own_line_comment(node.property.span.start) { + if f.comments().has_leading_own_line_comment(property_start) { return StaticMemberLayout::BreakAfterObject; } // `a.b.c!` and `a.b?.c` // `TSNonNullExpression` is a wrapper node for `!`, and `ChainExpression` is a wrapper node for `?.`, // so we need to skip them to find the real parent node. - let mut parent = node.parent(); + let mut parent = member.parent(); while matches!(parent, AstNodes::TSNonNullExpression(_) | AstNodes::ChainExpression(_)) { parent = parent.parent(); } - let object = &node.object; let is_nested = match parent { AstNodes::AssignmentExpression(_) | AstNodes::VariableDeclarator(_) => { @@ -117,8 +136,7 @@ fn layout<'a>( false } - AstNodes::StaticMemberExpression(_) | AstNodes::ComputedMemberExpression(_) => true, - _ => false, + _ => parent.is_member_expression(), }; if !is_nested && !object_is_cast_target && matches!(object, Expression::Identifier(_)) { @@ -126,18 +144,17 @@ fn layout<'a>( } let mut first_non_static_member_ancestor = parent; - while matches!( - first_non_static_member_ancestor, - AstNodes::StaticMemberExpression(_) - | AstNodes::ComputedMemberExpression(_) - | AstNodes::ChainExpression(_) - | AstNodes::TSNonNullExpression(_) - ) { + while first_non_static_member_ancestor.is_member_expression() + || matches!( + first_non_static_member_ancestor, + AstNodes::ChainExpression(_) | AstNodes::TSNonNullExpression(_) + ) + { first_non_static_member_ancestor = first_non_static_member_ancestor.parent(); } match first_non_static_member_ancestor { - AstNodes::NewExpression(expr) if expr.callee.span().contains_inclusive(node.span()) => { + AstNodes::NewExpression(expr) if expr.callee.span().contains_inclusive(member.span()) => { StaticMemberLayout::NoBreak } AstNodes::AssignmentExpression(assignment) => { @@ -150,9 +167,3 @@ fn layout<'a>( _ => StaticMemberLayout::BreakAfterObject, } } - -impl<'a> FormatWrite<'a> for AstNode<'_, 'a, PrivateFieldExpression<'a>> { - fn write(&self, f: &mut JsFormatter<'_, 'a>) { - write!(f, [self.object(), self.optional().then_some("?"), ".", self.field()]); - } -} diff --git a/crates/oxc_formatter/src/print/mod.rs b/crates/oxc_formatter/src/print/mod.rs index d1793a0d4d..84ae3f72d5 100644 --- a/crates/oxc_formatter/src/print/mod.rs +++ b/crates/oxc_formatter/src/print/mod.rs @@ -454,11 +454,8 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, AwaitExpression<'a>> { fn write(&self, f: &mut JsFormatter<'_, 'a>) { let format_inner = format_with(|f| write!(f, ["await", space(), self.argument()])); - let is_callee_or_object = match self.parent() { - AstNodes::StaticMemberExpression(_) => true, - AstNodes::ComputedMemberExpression(member) => member.object.span() == self.span(), - _ => self.parent().is_call_like_callee_span(self.span), - }; + let is_callee_or_object = self.parent().is_member_object_span(self.span) + || self.parent().is_call_like_callee_span(self.span); if !is_callee_or_object { write!(f, [format_inner]); diff --git a/crates/oxc_formatter/src/print/return_or_throw_statement.rs b/crates/oxc_formatter/src/print/return_or_throw_statement.rs index 4cc5f373fb..e1bdfc861e 100644 --- a/crates/oxc_formatter/src/print/return_or_throw_statement.rs +++ b/crates/oxc_formatter/src/print/return_or_throw_statement.rs @@ -210,25 +210,18 @@ fn has_argument_leading_comments(argument: &AstNode, f: &JsFormatter // This check is based on // if let LeftSide::Expression(left_side) = left_side { - let has_leading_own_line_comment = match left_side { - Expression::ChainExpression(chain) => { - if let ChainElement::StaticMemberExpression(member) = &chain.expression { - let comments = f - .comments() - .comments_in_range(member.object.span().end, member.property.span.end); - is_own_line_comment_or_multi_line_comment(comments) - } else { - false - } - } - Expression::StaticMemberExpression(member) => { - let comments = f - .comments() - .comments_in_range(member.object.span().end, member.property.span.end); - is_own_line_comment_or_multi_line_comment(comments) - } - _ => false, + let member = match left_side { + Expression::ChainExpression(chain) => chain.expression.as_member_expression(), + _ => left_side.as_member_expression(), }; + // `a.b` and `a.#b`, the comments after the object + let has_leading_own_line_comment = member.is_some_and(|member| { + !member.is_computed() + && is_own_line_comment_or_multi_line_comment( + f.comments() + .comments_in_range(member.object().span().end, member.span().end), + ) + }); if has_leading_own_line_comment { return true; diff --git a/crates/oxc_formatter/src/utils/conditional.rs b/crates/oxc_formatter/src/utils/conditional.rs index 84a9eee926..4f54b7bc77 100644 --- a/crates/oxc_formatter/src/utils/conditional.rs +++ b/crates/oxc_formatter/src/utils/conditional.rs @@ -270,21 +270,9 @@ impl<'a> FormatConditionalLike<'a, '_> { break; } } - AstNodes::StaticMemberExpression(member) => { - if member.object.span() == expression_span { - expression_span = member.span(); - parent = member.parent(); - } else { - break; - } - } - AstNodes::ComputedMemberExpression(member) => { - if member.object.span() == expression_span { - expression_span = member.span(); - parent = member.parent(); - } else { - break; - } + member if member.is_member_object_span(expression_span) => { + expression_span = member.span(); + parent = member.parent(); } AstNodes::CallExpression(call) => { if call.callee.span() == expression_span { @@ -351,12 +339,12 @@ impl<'a> FormatConditionalLike<'a, '_> { } } - /// Returns `true` if this is the root conditional expression and the parent is a [`StaticMemberExpression`]. + /// Returns `true` if this is the root conditional expression and the parent is a non-computed member: `.b` or `.#b` #[inline] fn is_parent_static_member_expression(&self, layout: ConditionalLayout) -> bool { layout.is_root() && self.is_conditional_expression() - && matches!(self.parent(), AstNodes::StaticMemberExpression(_)) + && self.parent().is_non_computed_member_expression() } /// Formats the test part of the conditional diff --git a/crates/oxc_formatter/src/utils/member_chain/chain_member.rs b/crates/oxc_formatter/src/utils/member_chain/chain_member.rs index 0899b86004..11db6cefe8 100644 --- a/crates/oxc_formatter/src/utils/member_chain/chain_member.rs +++ b/crates/oxc_formatter/src/utils/member_chain/chain_member.rs @@ -3,25 +3,27 @@ use std::ops::Deref; use crate::{ ast_nodes::{AstNode, AstNodes}, format_args, - formatter::{JsFormatter, prelude::*, trivia::FormatLeadingComments}, + formatter::{ + JsFormatter, + prelude::*, + trivia::{FormatLeadingComments, format_trailing_comments}, + }, utils::{call_expression::callee_opener, statement_body::write_comments_before_opener}, write, }; use oxc_ast::ast::*; use oxc_formatter_core::Format; -use oxc_span::GetSpan; +use oxc_span::{GetSpan, Span}; #[derive(Copy, Clone, Debug)] pub enum CallExpressionPosition { /// At the start of a call chain. /// `of` in `of().test` Start, - /// Somewhere in the middle. /// /// `b` in `a.b().c()` Middle, - /// At the end of a call chain (root) /// `c` in `a.b.c()` End, @@ -32,24 +34,22 @@ pub enum CallExpressionPosition { pub enum ChainMember<'a, 'b> { /// Holds onto a [oxc_ast::ast::StaticMemberExpression] StaticMember(&'b AstNode<'b, 'a, StaticMemberExpression<'a>>), - + /// Holds onto a [oxc_ast::ast::PrivateFieldExpression], treated the same as `StaticMember` + PrivateField(&'b AstNode<'b, 'a, PrivateFieldExpression<'a>>), /// Holds onto a [oxc_ast::ast::CallExpression] CallExpression { expression: &'b AstNode<'b, 'a, CallExpression<'a>>, position: CallExpressionPosition, }, - /// Holds onto a [oxc_ast::ast::ComputedMemberExpression] ComputedMember(&'b AstNode<'b, 'a, ComputedMemberExpression<'a>>), - TSNonNullExpression(&'b AstNode<'b, 'a, TSNonNullExpression<'a>>), - - /// Any other node that are not [oxc_ast::ast::CallExpression] or [oxc_ast::ast::StaticMemberExpression] + /// Any other node that is not one of the above /// Are tracked using this variant Node(&'b AstNode<'b, 'a, Expression<'a>>), } -impl ChainMember<'_, '_> { +impl<'a> ChainMember<'a, '_> { pub(crate) const fn is_call_expression(&self) -> bool { matches!(self, Self::CallExpression { .. }) } @@ -58,9 +58,25 @@ impl ChainMember<'_, '_> { matches!(self, Self::ComputedMember { .. }) } + /// The object, the property span and the property name of `a.b` or `a.#b` + pub fn as_non_computed_member(&self) -> Option<(&'a Expression<'a>, Span, &'a str)> { + match self { + Self::StaticMember(member) => { + let member = member.inner(); + Some((&member.object, member.property.span, member.property.name.as_str())) + } + Self::PrivateField(member) => { + let member = member.inner(); + Some((&member.object, member.field.span, member.field.name.as_str())) + } + _ => None, + } + } + pub fn span(&self) -> oxc_span::Span { match self { Self::StaticMember(member) => member.span(), + Self::PrivateField(member) => member.span(), Self::TSNonNullExpression(e) => e.span(), Self::CallExpression { expression, .. } => expression.span(), Self::ComputedMember(member) => member.span(), @@ -73,26 +89,10 @@ impl<'a> Format<'a, JsFormatContext<'a>> for ChainMember<'a, '_> { fn fmt(&self, f: &mut JsFormatter<'_, 'a>) { match self { Self::StaticMember(member) => { - write!( - f, - [ - FormatLeadingComments::Comments( - f.context().comments().comments_before(member.property().span().start) - ), - member.optional().then_some("?"), - ".", - member.property() - ] - ); - - // The callee of `A.b(c)` / `A.b(c)`: its comments keep their side of the opener - if let AstNodes::CallExpression(call) = member.parent() - && !call.optional - { - write_comments_before_opener(member.span().end, callee_opener(call), f); - } else { - member.format_trailing_comments(f); - } + write_member_lookup(member.optional(), member.property(), f); + } + Self::PrivateField(member) => { + write_member_lookup(member.optional(), member.field(), f); } Self::TSNonNullExpression(e) => { e.format_leading_comments(f); @@ -135,6 +135,43 @@ impl<'a> Format<'a, JsFormatContext<'a>> for ChainMember<'a, '_> { } } +/// Writes `.b` of `a.b` or `.#b` of `a.#b` with its comments. +fn write_member_lookup<'b, 'a, P: GetSpan>( + optional: bool, + property: AstNode<'b, 'a, P>, + f: &mut JsFormatter<'_, 'a>, +) where + AstNode<'b, 'a, P>: Format<'a, JsFormatContext<'a>>, +{ + write!( + f, + [ + FormatLeadingComments::Comments( + f.context().comments().comments_before(property.span().start) + ), + optional.then_some("?"), + ".", + property + ] + ); + + // The member expression itself + let member = property.parent(); + // The callee of `A.b(c)` / `A.b(c)`: its comments keep their side of the opener + if let AstNodes::CallExpression(call) = member.parent() + && !call.optional + { + write_comments_before_opener(member.span().end, callee_opener(call), f); + } else { + format_trailing_comments( + member.parent().span(), + member.span(), + property.following_span_start(), + ) + .fmt(f); + } +} + pub struct FormatComputedMemberExpressionWithoutObject<'a, 'b>( pub &'b AstNode<'b, 'a, ComputedMemberExpression<'a>>, ); diff --git a/crates/oxc_formatter/src/utils/member_chain/groups.rs b/crates/oxc_formatter/src/utils/member_chain/groups.rs index 82b2a31ad0..a5bba1dd48 100644 --- a/crates/oxc_formatter/src/utils/member_chain/groups.rs +++ b/crates/oxc_formatter/src/utils/member_chain/groups.rs @@ -189,14 +189,16 @@ impl<'a, 'b> MemberChainGroup<'a, 'b> { } fn needs_empty_line_before(&self, f: &JsFormatter) -> bool { - let Some(ChainMember::StaticMember(expression)) = self.members.first() else { + let Some((object, property_span, _)) = + self.members.first().and_then(ChainMember::as_non_computed_member) + else { return false; }; // Prettier doesn't preserve blank lines after a terminal call like `fn()` // whose callee is not part of the chain: `fn()\n\n.bar()` becomes `fn().bar()`. // - if let Expression::CallExpression(call) = expression.object().as_ref() + if let Expression::CallExpression(call) = object && !call.callee.is_member_expression() && !call.callee.is_call_expression() { @@ -208,8 +210,8 @@ impl<'a, 'b> MemberChainGroup<'a, 'b> { // `A \n\n/* comment . */ . / comment . */ B` // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Check whether has more than 1 continuous new lines before the operator (`.`) - let start = expression.object().span().end; - let mut end = expression.property().span().start; + let start = object.span().end; + let mut end = property_span.start; // We should found the real `end` position that is before comments and it is not after the operator `.` if let Some(printed_comment) = f diff --git a/crates/oxc_formatter/src/utils/member_chain/mod.rs b/crates/oxc_formatter/src/utils/member_chain/mod.rs index c387fd6e79..e133f743d0 100644 --- a/crates/oxc_formatter/src/utils/member_chain/mod.rs +++ b/crates/oxc_formatter/src/utils/member_chain/mod.rs @@ -107,8 +107,10 @@ impl<'a, 'b> MemberChain<'a, 'b> { Expression::ThisExpression(_) => true, _ => false, } - } else if let Some(ChainMember::StaticMember(expression)) = self.head.members().last() { - has_computed_property || is_factory(&expression.property().name) + } else if let Some((_, _, name)) = + self.head.members().last().and_then(ChainMember::as_non_computed_member) + { + has_computed_property || is_factory(name) } else { false } @@ -185,11 +187,10 @@ impl<'a, 'b> MemberChain<'a, 'b> { /// Check if a member has comments between object and property or end-of-line comments after it fn has_comment_in_member(member: &ChainMember, comments: &Comments) -> bool { - matches!( - member, - ChainMember::StaticMember(member) - if comments.has_comment_in_range(member.object().span().end, member.property().span.start) || comments.has_end_of_line_comment_after(member.span.end) - ) + member.as_non_computed_member().is_some_and(|(object, property_span, _)| { + comments.has_comment_in_range(object.span().end, property_span.start) + || comments.has_end_of_line_comment_after(member.span().end) + }) } fn has_comment(&self, f: &JsFormatter<'_, 'a>) -> bool { @@ -291,7 +292,9 @@ fn get_split_index_of_head_and_tail_groups(members: &[ChainMember<'_, '_>]) -> u .position(|member| { !matches!( member, - ChainMember::StaticMember { .. } | ChainMember::ComputedMember { .. } + ChainMember::StaticMember { .. } + | ChainMember::PrivateField { .. } + | ChainMember::ComputedMember { .. } ) }) .map_or(rest.len(), |index| { @@ -324,7 +327,9 @@ fn compute_remaining_groups<'a, 'b>( ChainMember::ComputedMember(_) if is_computed_array_member_access(&member) => { groups_builder.start_or_continue_group(member); } - ChainMember::StaticMember(_) | ChainMember::ComputedMember(_) => { + ChainMember::StaticMember(_) + | ChainMember::PrivateField(_) + | ChainMember::ComputedMember(_) => { // if we have seen a CallExpression, we want to close the group. // The resultant group will be something like: [ . , then, () ]; // `.` and `then` belong to the previous StaticMemberExpression, @@ -420,12 +425,7 @@ fn chain_members_iter<'a, 'b>( next: &mut Option>>| { let callee = expr.callee(); - let is_chain = matches!( - callee.as_ref(), - Expression::StaticMemberExpression(_) - | Expression::ComputedMemberExpression(_) - | Expression::CallExpression(_) - ); + let is_chain = callee.is_member_expression() || callee.is_call_expression(); if is_chain { *next = Some(callee); @@ -462,12 +462,7 @@ fn chain_members_iter<'a, 'b>( let member = match node { AstNodeKind::CallExpression(expr) => { let callee = expr.callee(); - let is_chain = matches!( - callee.as_ref(), - Expression::StaticMemberExpression(_) - | Expression::ComputedMemberExpression(_) - | Expression::CallExpression(_) - ); + let is_chain = callee.is_member_expression() || callee.is_call_expression(); let position = if is_chain { CallExpressionPosition::Middle } else { @@ -480,6 +475,11 @@ fn chain_members_iter<'a, 'b>( next = Some(expr.object()); ChainMember::StaticMember(expr) } + AstNodeKind::PrivateFieldExpression(expr) => { + let expr = expr.in_arena(allocator); + next = Some(expr.object()); + ChainMember::PrivateField(expr) + } AstNodeKind::ComputedMemberExpression(expr) => { let expr = expr.in_arena(allocator); next = Some(expr.object()); diff --git a/crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js b/crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js new file mode 100644 index 0000000000..7c04268006 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js @@ -0,0 +1,20 @@ +// DIVERGENCES.md#member-property-own-line-comment +class A { + #b; + m() { + x = a + // comment + .b; + x = a + // comment + .#b; + return a + // comment + .#b; + } + n() { + return a + // comment + .b; + } +} diff --git a/crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js.snap b/crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js.snap new file mode 100644 index 0000000000..b2ede57c96 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/comments/member-property-own-line-comment.js.snap @@ -0,0 +1,87 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +// DIVERGENCES.md#member-property-own-line-comment +class A { + #b; + m() { + x = a + // comment + .b; + x = a + // comment + .#b; + return a + // comment + .#b; + } + n() { + return a + // comment + .b; + } +} + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +// DIVERGENCES.md#member-property-own-line-comment +class A { + #b; + m() { + x = + a + // comment + .b; + x = + a + // comment + .#b; + return ( + a + // comment + .#b + ); + } + n() { + return ( + a + // comment + .b + ); + } +} + +------------------- +{ printWidth: 100 } +------------------- +// DIVERGENCES.md#member-property-own-line-comment +class A { + #b; + m() { + x = + a + // comment + .b; + x = + a + // comment + .#b; + return ( + a + // comment + .#b + ); + } + n() { + return ( + a + // comment + .b + ); + } +} + +===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx b/crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx new file mode 100644 index 0000000000..3a1a528600 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx @@ -0,0 +1,4 @@ +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
)?.b; +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
)?.[0]; +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
)?.b(); +x = (
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnn}
)?.b; diff --git a/crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx.snap b/crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx.snap new file mode 100644 index 0000000000..22affabaee --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/jsx/optional-member.jsx.snap @@ -0,0 +1,50 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
)?.b; +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
)?.[0]; +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
)?.b(); +x = (
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnn}
)?.b; + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +( +
+ {someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn} +
+)?.b; +( +
+ {someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn} +
+)?.[0]; +( +
+ {someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn} +
+)?.b(); +x = ( +
+ {someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnn} +
+)?.b; + +------------------- +{ printWidth: 100 } +------------------- +(
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
) + ?.b; +( +
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
+)?.[0]; +( +
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnnn}
+)?.b(); +x = ( +
{someVeryLongContentExpressionnnnnnnnnnnnnnnnnnnnnn}
+)?.b; + +===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js b/crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js new file mode 100644 index 0000000000..89838a5a73 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js @@ -0,0 +1,32 @@ +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + #c; + async m() { + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb).#b; + (await someVeryLongFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThree)).#b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeeeeeeee).#b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee).#b.method(); + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeeeeeee.#b; + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeee.#b.#c; + this.#b.someMethodName().anotherMethodName().yetAnotherMethodName().finalMethodNameeeeeeeeeeeeee(); + this.#b(argumentNumberOne, argumentNumberTwo, argumentNumberThree, argumentNumberFourrrrrrrrrr); + const value = someFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThreeeeeeee).#b; + } +} + +class B { + _b; + _c; + async m() { + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)._b; + (await someVeryLongFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThree))._b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeeeeeeee)._b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee)._b.method(); + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeeeeeee._b; + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeee._b._c; + this._b.someMethodName().anotherMethodName().yetAnotherMethodName().finalMethodNameeeeeeeeeeeeee(); + this._b(argumentNumberOne, argumentNumberTwo, argumentNumberThree, argumentNumberFourrrrrrrrrr); + const value = someFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThreeeeeeee)._b; + } +} diff --git a/crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js.snap b/crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js.snap new file mode 100644 index 0000000000..c29ff08af8 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/private-fields/layout.js.snap @@ -0,0 +1,213 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + #c; + async m() { + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb).#b; + (await someVeryLongFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThree)).#b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeeeeeeee).#b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee).#b.method(); + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeeeeeee.#b; + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeee.#b.#c; + this.#b.someMethodName().anotherMethodName().yetAnotherMethodName().finalMethodNameeeeeeeeeeeeee(); + this.#b(argumentNumberOne, argumentNumberTwo, argumentNumberThree, argumentNumberFourrrrrrrrrr); + const value = someFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThreeeeeeee).#b; + } +} + +class B { + _b; + _c; + async m() { + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)._b; + (await someVeryLongFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThree))._b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeeeeeeee)._b; + (someCondition ? someVeryLongConsequentValueeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee)._b.method(); + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeeeeeee._b; + result = someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeee._b._c; + this._b.someMethodName().anotherMethodName().yetAnotherMethodName().finalMethodNameeeeeeeeeeeeee(); + this._b(argumentNumberOne, argumentNumberTwo, argumentNumberThree, argumentNumberFourrrrrrrrrr); + const value = someFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThreeeeeeee)._b; + } +} + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + #c; + async m() { + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ).#b; + ( + await someVeryLongFunctionName( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThree, + ) + ).#b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeeeeeeee + ).#b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeee + ).#b.method(); + result = + someVeryLongObjectName.someVeryLongPropertyName + .anotherVeryLongPropertyNameeeeeeeeeee.#b; + result = + someVeryLongObjectName.someVeryLongPropertyName + .anotherVeryLongPropertyNameeeeeee.#b.#c; + this.#b + .someMethodName() + .anotherMethodName() + .yetAnotherMethodName() + .finalMethodNameeeeeeeeeeeeee(); + this.#b( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThree, + argumentNumberFourrrrrrrrrr, + ); + const value = someFunctionName( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThreeeeeeee, + ).#b; + } +} + +class B { + _b; + _c; + async m() { + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + )._b; + ( + await someVeryLongFunctionName( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThree, + ) + )._b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeeeeeeee + )._b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeee + )._b.method(); + result = + someVeryLongObjectName.someVeryLongPropertyName + .anotherVeryLongPropertyNameeeeeeeeeee._b; + result = + someVeryLongObjectName.someVeryLongPropertyName + .anotherVeryLongPropertyNameeeeeee._b._c; + this._b + .someMethodName() + .anotherMethodName() + .yetAnotherMethodName() + .finalMethodNameeeeeeeeeeeeee(); + this._b( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThree, + argumentNumberFourrrrrrrrrr, + ); + const value = someFunctionName( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThreeeeeeee, + )._b; + } +} + +------------------- +{ printWidth: 100 } +------------------- +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + #c; + async m() { + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ).#b; + (await someVeryLongFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThree)).#b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeeeeeeee + ).#b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeee + ).#b.method(); + result = + someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeeeeeee.#b; + result = + someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeee.#b.#c; + this.#b + .someMethodName() + .anotherMethodName() + .yetAnotherMethodName() + .finalMethodNameeeeeeeeeeeeee(); + this.#b(argumentNumberOne, argumentNumberTwo, argumentNumberThree, argumentNumberFourrrrrrrrrr); + const value = someFunctionName( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThreeeeeeee, + ).#b; + } +} + +class B { + _b; + _c; + async m() { + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + )._b; + (await someVeryLongFunctionName(argumentNumberOne, argumentNumberTwo, argumentNumberThree))._b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeeeeeeee + )._b; + (someCondition + ? someVeryLongConsequentValueeeeeeeeeeee + : someVeryLongAlternateValueeeeeeeeeee + )._b.method(); + result = + someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeeeeeee._b; + result = + someVeryLongObjectName.someVeryLongPropertyName.anotherVeryLongPropertyNameeeeeee._b._c; + this._b + .someMethodName() + .anotherMethodName() + .yetAnotherMethodName() + .finalMethodNameeeeeeeeeeeeee(); + this._b(argumentNumberOne, argumentNumberTwo, argumentNumberThree, argumentNumberFourrrrrrrrrr); + const value = someFunctionName( + argumentNumberOne, + argumentNumberTwo, + argumentNumberThreeeeeeee, + )._b; + } +} + +===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js b/crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js new file mode 100644 index 0000000000..04acb8bc7b --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js @@ -0,0 +1,28 @@ +// DIVERGENCES.md#private-member-like-static +class A { + static #decorator; + static decorator; + static #Factory; + static Factory; + #privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking; + publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX; + + m() { + foo(someIdentifierName.#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking); + foo(someIdentifierName.publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX); + this.#Factory.someMethodName(argumentOne).anotherMethodName(argumentTwo).third(); + this.Factory.someMethodName(argumentOneX).anotherMethodName(argumentTwo).third(); + } + + n(obj) { + const a = obj.#decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }); + const b = obj.decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }); + } + + @A.#decorator a() {} + @A.decorator b() {} +} diff --git a/crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js.snap b/crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js.snap new file mode 100644 index 0000000000..618859088f --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/private-fields/private-member-like-static.js.snap @@ -0,0 +1,115 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +// DIVERGENCES.md#private-member-like-static +class A { + static #decorator; + static decorator; + static #Factory; + static Factory; + #privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking; + publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX; + + m() { + foo(someIdentifierName.#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking); + foo(someIdentifierName.publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX); + this.#Factory.someMethodName(argumentOne).anotherMethodName(argumentTwo).third(); + this.Factory.someMethodName(argumentOneX).anotherMethodName(argumentTwo).third(); + } + + n(obj) { + const a = obj.#decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }); + const b = obj.decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }); + } + + @A.#decorator a() {} + @A.decorator b() {} +} + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +// DIVERGENCES.md#private-member-like-static +class A { + static #decorator; + static decorator; + static #Factory; + static Factory; + #privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking; + publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX; + + m() { + foo( + someIdentifierName.#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking, + ); + foo( + someIdentifierName.publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX, + ); + this.#Factory.someMethodName(argumentOne) + .anotherMethodName(argumentTwo) + .third(); + this.Factory.someMethodName(argumentOneX) + .anotherMethodName(argumentTwo) + .third(); + } + + n(obj) { + const a = obj.#decorator(someArgument)(( + aaaaaaaaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccc, + ) => { + return a; + }); + const b = obj.decorator(someArgument)(( + aaaaaaaaaaaa, + bbbbbbbbbbbbbbbbb, + cccccccccccccccc, + ) => { + return a; + }); + } + + @A.#decorator a() {} + @A.decorator b() {} +} + +------------------- +{ printWidth: 100 } +------------------- +// DIVERGENCES.md#private-member-like-static +class A { + static #decorator; + static decorator; + static #Factory; + static Factory; + #privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking; + publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX; + + m() { + foo(someIdentifierName.#privateFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreaking); + foo(someIdentifierName.publicFieldWithAVeryLongNameThatDoesNotFitOnTheLineEvenAfterBreakingXX); + this.#Factory.someMethodName(argumentOne).anotherMethodName(argumentTwo).third(); + this.Factory.someMethodName(argumentOneX).anotherMethodName(argumentTwo).third(); + } + + n(obj) { + const a = obj.#decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }); + const b = obj.decorator(someArgument)((aaaaaaaaaaaa, bbbbbbbbbbbbbbbbb, cccccccccccccccc) => { + return a; + }); + } + + @A.#decorator a() {} + @A.decorator b() {} +} + +===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts b/crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts new file mode 100644 index 0000000000..638c365bc9 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts @@ -0,0 +1,18 @@ +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + m() { + (someVeryLongExpressionName as unknown as SomeVeryLongTypeName).#b; + (someVeryLongExpressionName satisfies SomeVeryLongTypeName).#b; + (>someVeryLongExpressionNameeeeeeeeeeeeeeeeeee).#b; + } +} + +class B { + _b; + m() { + (someVeryLongExpressionName as unknown as SomeVeryLongTypeName)._b; + (someVeryLongExpressionName satisfies SomeVeryLongTypeName)._b; + (>someVeryLongExpressionNameeeeeeeeeeeeeeeeeee)._b; + } +} diff --git a/crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts.snap b/crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts.snap new file mode 100644 index 0000000000..fcf5d49f71 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/ts/private-fields/layout.ts.snap @@ -0,0 +1,81 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + m() { + (someVeryLongExpressionName as unknown as SomeVeryLongTypeName).#b; + (someVeryLongExpressionName satisfies SomeVeryLongTypeName).#b; + (>someVeryLongExpressionNameeeeeeeeeeeeeeeeeee).#b; + } +} + +class B { + _b; + m() { + (someVeryLongExpressionName as unknown as SomeVeryLongTypeName)._b; + (someVeryLongExpressionName satisfies SomeVeryLongTypeName)._b; + (>someVeryLongExpressionNameeeeeeeeeeeeeeeeeee)._b; + } +} + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + m() { + ( + someVeryLongExpressionName as unknown as SomeVeryLongTypeName + ).#b; + ( + someVeryLongExpressionName satisfies SomeVeryLongTypeName + ).#b; + (>( + someVeryLongExpressionNameeeeeeeeeeeeeeeeeee + )).#b; + } +} + +class B { + _b; + m() { + ( + someVeryLongExpressionName as unknown as SomeVeryLongTypeName + )._b; + ( + someVeryLongExpressionName satisfies SomeVeryLongTypeName + )._b; + (>( + someVeryLongExpressionNameeeeeeeeeeeeeeeeeee + ))._b; + } +} + +------------------- +{ printWidth: 100 } +------------------- +// `a.#b` (class A) is laid out the same as `a._b` (class B) +class A { + #b; + m() { + (someVeryLongExpressionName as unknown as SomeVeryLongTypeName).#b; + (someVeryLongExpressionName satisfies SomeVeryLongTypeName).#b; + (>someVeryLongExpressionNameeeeeeeeeeeeeeeeeee).#b; + } +} + +class B { + _b; + m() { + (someVeryLongExpressionName as unknown as SomeVeryLongTypeName)._b; + (someVeryLongExpressionName satisfies SomeVeryLongTypeName)._b; + (>someVeryLongExpressionNameeeeeeeeeeeeeeeeeee)._b; + } +} + +===================== End ===================== diff --git a/tasks/track_memory_allocations/allocs_formatter.yaml b/tasks/track_memory_allocations/allocs_formatter.yaml index 1283b82339..61ba7b9b27 100644 --- a/tasks/track_memory_allocations/allocs_formatter.yaml +++ b/tasks/track_memory_allocations/allocs_formatter.yaml @@ -33,14 +33,14 @@ RadixUIAdoptionSection.jsx: pdf.mjs: file size: 567296 # 567.30 kB - sys allocs: 9736 + sys allocs: 9755 sys reallocs: 167 - sys deallocs: 9735 + sys deallocs: 9754 sys alloc bytes: 2384856 # 2.38 MB sys peak growth: 1156488 # 1.16 MB - arena allocs: 20583 - arena reallocs: 428 - arena size: 11029152 # 11.03 MB + arena allocs: 21673 + arena reallocs: 446 + arena size: 11090656 # 11.09 MB antd.js: file size: 6686316 # 6.69 MB @@ -71,6 +71,6 @@ kitchen-sink.tsx: sys deallocs: 12609 sys alloc bytes: 3737938 # 3.74 MB sys peak growth: 1491884 # 1.49 MB - arena allocs: 30666 - arena reallocs: 1041 - arena size: 14614680 # 14.61 MB + arena allocs: 30688 + arena reallocs: 1043 + arena size: 14615800 # 14.62 MB