fix(formatter): keep parens around private in expressions (#28096)

Should keep:

```ts
class C {
  #x;
  m(a, b, c) {
    x = (#x in b).c; // THIS parens with `PrivateInExpression`
```
This commit is contained in:
leaysgur committed 2026-10-22 07:36:09 +00:00
1 parent db55461552
commit a5a52cf5f7
4 files changed
+168 -25

No files matched your search

@@ -6,7 +6,7 @@ use oxc_span::GetSpan;
use crate::{
ast_nodes::{AstNode, AstNodes},
formatter::{JsFormatter, JsFormatterExt as _},
print::{BinaryLikeExpression, should_flatten, unary_argument_takes_comment_parens},
print::{BinaryLikeOperator, should_flatten, unary_argument_takes_comment_parens},
utils::{expression::ExpressionLeftSide, typecast::is_cast_target},
};
@@ -393,11 +393,11 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, BinaryExpression<'_>> {
// For `in` expressions in ForStatement: wrap to avoid ambiguity
// e.g., `for (var a = (b in c);;)`
if self.operator.is_in() && is_in_for_initializer(self) {
if self.operator.is_in() && is_in_for_initializer(self.span, parent) {
return true;
}
binary_like_needs_parens(BinaryLikeExpression::BinaryExpression(self), f)
binary_like_needs_parens(self.span, self.operator.into(), parent, f)
}
}
@@ -407,10 +407,8 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, BinaryExpression<'_>> {
/// separately at the VariableDeclarator level (see `is_for_in_statement_init`).
///
/// <https://github.com/prettier/prettier/issues/907#issuecomment-284304321>
fn is_in_for_initializer(expr: &AstNode<'_, '_, BinaryExpression<'_>>) -> bool {
let ancestors = expr.ancestors();
for parent in ancestors {
fn is_in_for_initializer(span: Span, parent: &AstNodes<'_, '_>) -> bool {
for parent in parent.ancestors() {
match parent {
AstNodes::ExpressionStatement(stmt) => {
// Block body: `() => { expr; }` or `function() { expr; }` - continue checking
@@ -422,10 +420,7 @@ fn is_in_for_initializer(expr: &AstNode<'_, '_, BinaryExpression<'_>>) -> bool {
return false;
}
AstNodes::ForStatement(stmt) => {
return stmt
.init
.as_ref()
.is_some_and(|init| init.span().contains_inclusive(expr.span));
return stmt.init.as_ref().is_some_and(|init| init.span().contains_inclusive(span));
}
// ForInStatement is handled at VariableDeclarator level, not here
AstNodes::ForInStatement(_) | AstNodes::Program(_) => return false,
@@ -464,8 +459,10 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, PrivateInExpression<'_>> {
return false;
}
is_class_extends(self.span, self.parent())
|| matches!(self.parent(), AstNodes::UnaryExpression(_))
// Same rules as a binary `in` expression
let parent = self.parent();
is_in_for_initializer(self.span, parent)
|| binary_like_needs_parens(self.span, BinaryOperator::In.into(), parent, f)
}
}
@@ -490,7 +487,7 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, LogicalExpression<'_>> {
{
true
} else {
binary_like_needs_parens(BinaryLikeExpression::LogicalExpression(self), f)
binary_like_needs_parens(self.span, self.operator.into(), parent, f)
}
}
}
@@ -513,6 +510,7 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, ConditionalExpression<'_>> {
| AstNodes::JSXSpreadAttribute(_)
| AstNodes::LogicalExpression(_)
| AstNodes::BinaryExpression(_)
| AstNodes::PrivateInExpression(_)
) {
return true;
}
@@ -747,6 +745,7 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, ArrowFunctionExpression<'_>> {
| AstNodes::AwaitExpression(_)
| AstNodes::LogicalExpression(_)
| AstNodes::BinaryExpression(_)
| AstNodes::PrivateInExpression(_)
) {
return true;
}
@@ -963,10 +962,12 @@ fn is_followed_by_comparison_or_sign(
}
fn binary_like_needs_parens(
binary_like: BinaryLikeExpression<'_, '_>,
span: Span,
operator: BinaryLikeOperator,
parent: &AstNodes<'_, '_>,
f: &JsFormatter<'_, '_>,
) -> bool {
let parent = match binary_like.parent() {
let (parent_operator, parent_right): (BinaryLikeOperator, Span) = match parent {
// NOTE: The unary adds the parentheses itself when comments are involved.
// Deliberately binary-like-only: Prettier keeps the inner parentheses for every other argument type (`!((x, y) /* c */)`)
// (Seems a leftover of prettier#18397's narrow scope, not a principle.)
@@ -986,22 +987,23 @@ fn binary_like_needs_parens(
| AstNodes::StaticMemberExpression(_)
| AstNodes::TaggedTemplateExpression(_) => return true,
AstNodes::ComputedMemberExpression(computed) => {
return computed.object.span() == binary_like.span();
return computed.object.span() == span;
}
AstNodes::Class(class) => {
return class.heritage().is_some_and(|heritage| {
let super_class = heritage.expression();
super_class.span().contains_inclusive(binary_like.span())
super_class.span().contains_inclusive(span)
});
}
AstNodes::BinaryExpression(binary) => BinaryLikeExpression::BinaryExpression(binary),
AstNodes::LogicalExpression(logical) => BinaryLikeExpression::LogicalExpression(logical),
parent if parent.is_call_like_callee_span(binary_like.span()) => return true,
AstNodes::BinaryExpression(binary) => (binary.operator.into(), binary.right.span()),
AstNodes::LogicalExpression(logical) => (logical.operator.into(), logical.right.span()),
AstNodes::PrivateInExpression(private_in) => {
(BinaryOperator::In.into(), private_in.right.span())
}
parent if parent.is_call_like_callee_span(span) => return true,
_ => return false,
};
let parent_operator = parent.operator();
let operator = binary_like.operator();
let parent_precedence = parent_operator.precedence();
let precedence = operator.precedence();
@@ -1011,7 +1013,7 @@ fn binary_like_needs_parens(
return true;
}
let is_right = parent.right().span() == binary_like.span();
let is_right = parent_right == span;
// `a ** b ** c`
if is_right && parent_precedence == precedence {
@@ -1187,6 +1189,7 @@ fn ts_as_or_satisfies_needs_parens(span: Span, parent: &AstNodes<'_, '_>) -> boo
AstNodes::ConditionalExpression(_)
| AstNodes::LogicalExpression(_)
| AstNodes::BinaryExpression(_)
| AstNodes::PrivateInExpression(_)
) || type_cast_like_needs_parens(span, parent)
}
+1 -1
View File
@@ -38,7 +38,7 @@ mod variable_declaration;
pub use arrow_function_expression::{
FormatJsArrowFunctionExpression, FormatJsArrowFunctionExpressionOptions,
};
pub use binary_like_expression::{BinaryLikeExpression, should_flatten};
pub use binary_like_expression::{BinaryLikeExpression, BinaryLikeOperator, should_flatten};
pub use fragment::{FormatFunctionParams, FormatTypeParameters};
pub use semicolon::write_comments_before_closing_paren;
pub use template::embed_hug;
@@ -6,3 +6,38 @@ class F {
+(#target in this);
}
}
// `#x in b` keeps the parens of a binary `in` expression
class C {
#x;
m(a, b, c) {
x = (#x in b).c;
x = (#x in b)[c];
x = (#x in b)();
x = new (#x in b)();
x = (#x in b)`t`;
x = (#x in b)?.c;
x = [...(#x in b)];
x = a + (#x in b);
x = (#x in b) + a;
x = a < (#x in b);
x = (#x in b) < a;
x = a in (#x in b);
x = (#x in b) & a;
x = a ** (#x in b);
x = (#x in b) && a;
x = (#x in b) ? a : c;
x = class extends (#x in b) {};
for (let y = (#x in b); ; );
x = #x in (a < b);
x = #x in (a in b);
x = #x in (a + b);
x = #x in (a && b);
x = #x in (a ? b : c);
x = #x in (() => a);
}
async n(b) {
x = await (#x in b);
}
}
@@ -11,6 +11,41 @@ class F {
}
}
// `#x in b` keeps the parens of a binary `in` expression
class C {
#x;
m(a, b, c) {
x = (#x in b).c;
x = (#x in b)[c];
x = (#x in b)();
x = new (#x in b)();
x = (#x in b)`t`;
x = (#x in b)?.c;
x = [...(#x in b)];
x = a + (#x in b);
x = (#x in b) + a;
x = a < (#x in b);
x = (#x in b) < a;
x = a in (#x in b);
x = (#x in b) & a;
x = a ** (#x in b);
x = (#x in b) && a;
x = (#x in b) ? a : c;
x = class extends (#x in b) {};
for (let y = (#x in b); ; );
x = #x in (a < b);
x = #x in (a in b);
x = #x in (a + b);
x = #x in (a && b);
x = #x in (a ? b : c);
x = #x in (() => a);
}
async n(b) {
x = await (#x in b);
}
}
==================== Output ====================
------------------
{ printWidth: 80 }
@@ -24,6 +59,41 @@ class F {
}
}
// `#x in b` keeps the parens of a binary `in` expression
class C {
#x;
m(a, b, c) {
x = (#x in b).c;
x = (#x in b)[c];
x = (#x in b)();
x = new (#x in b)();
x = (#x in b)`t`;
x = (#x in b)?.c;
x = [...(#x in b)];
x = a + (#x in b);
x = (#x in b) + a;
x = a < (#x in b);
x = #x in b < a;
x = a in (#x in b);
x = (#x in b) & a;
x = a ** (#x in b);
x = #x in b && a;
x = #x in b ? a : c;
x = class extends (#x in b) {};
for (let y = (#x in b); ;);
x = #x in (a < b);
x = #x in (a in b);
x = #x in a + b;
x = #x in (a && b);
x = #x in (a ? b : c);
x = #x in (() => a);
}
async n(b) {
x = await (#x in b);
}
}
-------------------
{ printWidth: 100 }
-------------------
@@ -36,4 +106,39 @@ class F {
}
}
// `#x in b` keeps the parens of a binary `in` expression
class C {
#x;
m(a, b, c) {
x = (#x in b).c;
x = (#x in b)[c];
x = (#x in b)();
x = new (#x in b)();
x = (#x in b)`t`;
x = (#x in b)?.c;
x = [...(#x in b)];
x = a + (#x in b);
x = (#x in b) + a;
x = a < (#x in b);
x = #x in b < a;
x = a in (#x in b);
x = (#x in b) & a;
x = a ** (#x in b);
x = #x in b && a;
x = #x in b ? a : c;
x = class extends (#x in b) {};
for (let y = (#x in b); ;);
x = #x in (a < b);
x = #x in (a in b);
x = #x in a + b;
x = #x in (a && b);
x = #x in (a ? b : c);
x = #x in (() => a);
}
async n(b) {
x = await (#x in b);
}
}
===================== End =====================