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