fix(formatter): break parens of (a + b)[c] like (a + b).c (#28199)

```js
// before
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
  bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)[c];

// after
(
  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
  bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)[c];
```
This commit is contained in:
leaysgur committed 2026-10-27 06:09:37 +00:00
1 parent b8f4f34bba
commit 72a54cf447
8 files changed
+129 -26

No files matched your search

+33
View File
@@ -1167,3 +1167,36 @@ x =
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.
## computed-member-object-paren-break
- Why: uniform-rule (same construct, same output: `(a + b).c` and `(await a)[c]`)
- Pin: `tests/fixtures/js/parenthesis/computed-member-object-paren-break.js`
- Conformance: `js/ternaries/indent-after-paren.js`
```js
// input (printWidth: 80)
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)[c];
(someCondition ? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee)[c];
// ours: the same as `.c`
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)[c];
(someCondition
? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee
: someVeryLongAlternateValueeeeeeeeeee
)[c];
// prettier
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)[c];
(someCondition
? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee
: someVeryLongAlternateValueeeeeeeeeee)[c];
```
A binary-like or conditional object of a member breaks its parens the same way for `.c`, `[c]` and `?.[c]`.
Prettier does so for `.c` only, while an `await` / `as` object breaks them for `[c]` too.
The computed property (`a[b + c]`) keeps its own layout.
@@ -66,11 +66,6 @@ impl<'parent, 'a> AstNodes<'parent, 'a> {
)
}
/// 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!(
@@ -224,7 +224,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for BinaryLikeExpression<'a, '_> {
}
// Add a group with a soft block indent in cases where it is necessary to parenthesize the binary expression.
// For example, `(a+b)(call)`, `!(a + b)`, `(a + b).test`.
// For example, `(a+b)(call)`, `!(a + b)`, `(a + b).test`, `(a + b)[test]`.
let is_inside_parenthesis = match parent {
// The unary's own comment parens already provide the group and indent
AstNodes::UnaryExpression(unary) if unary_argument_takes_comment_parens(unary, f) => {
@@ -232,7 +232,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for BinaryLikeExpression<'a, '_> {
}
AstNodes::UnaryExpression(_) => true,
_ => {
parent.is_non_computed_member_expression()
parent.is_member_object_span(self.span())
|| parent.is_call_like_callee_span(self.span())
}
};
+3 -16
View File
@@ -36,11 +36,6 @@ impl<'a> ConditionalLike<'a, '_> {
ConditionalLike::TSConditionalType(ty) => ty.parent(),
}
}
#[inline]
fn is_conditional_expression(&self) -> bool {
matches!(self, ConditionalLike::ConditionalExpression(_))
}
}
/// Layout information for a conditional expression to determine formatting strategy
@@ -339,14 +334,6 @@ impl<'a> FormatConditionalLike<'a, '_> {
}
}
/// 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()
&& self.parent().is_non_computed_member_expression()
}
/// Formats the test part of the conditional
fn format_test<'f>(&self, f: &mut JsFormatter<'f, 'a>, layout: ConditionalLayout) {
let format_inner = format_with(|f| {
@@ -550,16 +537,16 @@ impl<'a> Format<'a, JsFormatContext<'a>> for FormatConditionalLike<'a, '_> {
format_tail_with_indent.fmt(f);
// Add a soft line break in front of the closing `)` in case the parent is a static member expression
// Add a soft line break in front of the closing `)` in case it is the object of a member expression
// ```text
// (veryLongCondition
// ? a
// : b // <- enforce line break here if the conditional breaks
// ).more
// ).more // or `)[more]`
// ```
if !should_extra_indent
&& !is_jsx_chain
&& self.is_parent_static_member_expression(layout)
&& self.parent().is_member_object_span(self.span())
{
write!(f, [soft_line_break()]);
}
@@ -0,0 +1,9 @@
// DIVERGENCES.md#computed-member-object-paren-break
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb).c;
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)[c];
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa || bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)?.[c];
(someCondition ? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee).c;
(someCondition ? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee)[c];
// The property keeps its own layout
xxxxxxxxxx[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb];
@@ -0,0 +1,78 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// DIVERGENCES.md#computed-member-object-paren-break
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb).c;
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)[c];
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa || bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)?.[c];
(someCondition ? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee).c;
(someCondition ? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee : someVeryLongAlternateValueeeeeeeeeee)[c];
// The property keeps its own layout
xxxxxxxxxx[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb];
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// DIVERGENCES.md#computed-member-object-paren-break
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
).c;
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)[c];
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)?.[c];
(someCondition
? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee
: someVeryLongAlternateValueeeeeeeeeee
).c;
(someCondition
? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee
: someVeryLongAlternateValueeeeeeeeeee
)[c];
// The property keeps its own layout
xxxxxxxxxx[
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
];
-------------------
{ printWidth: 100 }
-------------------
// DIVERGENCES.md#computed-member-object-paren-break
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
).c;
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)[c];
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)?.[c];
(someCondition
? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee
: someVeryLongAlternateValueeeeeeeeeee
).c;
(someCondition
? someVeryLongConsequentValueeeeeeeeeeeeeeeeeeee
: someVeryLongAlternateValueeeeeeeeeee
)[c];
// The property keeps its own layout
xxxxxxxxxx[
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
];
===================== End =====================
@@ -2,7 +2,7 @@
source: crates/oxc_formatter/tests/conformance.rs
expression: report
---
js compatibility: 1033/1123 (91.99%), 23 files skipped
js compatibility: 1029/1123 (91.63%), 23 files skipped
# Failed
@@ -55,6 +55,7 @@ js compatibility: 1033/1123 (91.99%), 23 files skipped
| js/switch/comments.js | 💥 | 94.74% |
| js/template-literals/expression-break.js | 💥 | 80.00% |
| js/template-literals/expressions.js | 💥 | 97.64% |
| js/ternaries/indent-after-paren.js | 💥💥💥💥 | 99.14% |
| js/test-declarations/jest-each.js | 💥💥 | 95.71% |
| js/trailing-comma/trailing_whitespace.js | 💥💥💥 | 96.55% |
| js/try/try.js | 💥 | 44.44% |
@@ -44,9 +44,9 @@ pdf.mjs:
antd.js:
file size: 6686316 # 6.69 MB
sys allocs: 74785
sys allocs: 74784
sys reallocs: 351
sys deallocs: 74784
sys deallocs: 74783
sys alloc bytes: 23137418 # 23.14 MB
sys peak growth: 13710144 # 13.71 MB
arena allocs: 120676