fix(codegen): preserve private-in left operand precedence (#26383)

Private-in expressions lose required parentheses when they appear on the left of a higher-precedence binary operator. This affects both default and minified code generation and can change the result or cause a runtime error.

For example:

```js
class C {
  #x;
  test(o) {
    return (#x in o) + 1;
  }
}
console.log(new C().test(new C()));
```

The input prints `2`. Previously, codegen emitted this return expression:

```js
return #x in o + 1;
```

Because `+` binds more tightly than `in`, this is parsed as `#x in (o + 1)`. The addition converts the object to a primitive, so the private-field check throws a `TypeError` instead of producing the boolean used by the original addition.

With this change, the required grouping is preserved:

```js
// Default output
return (#x in o) + 1;

// Minified output
return(#x in o)+1
```

The binary expression visitor already computes the precedence required for its left operand. However, its `PrivateInExpression` special case discarded that value and called `gen_expr` with `Precedence::Lowest` and an empty context. Passing `self.left_precedence` and `self.ctx` lets the existing private-in printer apply its parentheses rule using the same inputs as other left operands.

This preserves parentheses for arithmetic, exponentiation, and shift operators. Equal- and lower-precedence operators, such as `<`, `===`, and `&&`, continue to omit unnecessary parentheses.
This commit is contained in:
camc314 committed 2026-09-07 09:30:28 +00:00
1 parent 2ca7d29b7a
commit 0a81d293f9
2 files changed
+34 -1

No files matched your search

@@ -226,7 +226,7 @@ impl<'a> BinaryExpressionVisitor<'a> {
}
if let Expression::PrivateInExpression(e) = self.e.left() {
e.gen_expr(p, Precedence::Lowest, Context::empty());
e.gen_expr(p, self.left_precedence, self.ctx);
self.visit_right_and_finish(p);
return false;
}
@@ -142,6 +142,39 @@ fn private_in() {
);
}
#[test]
fn private_in_binary_left() {
fn wrap_case_equal_higher_precedence(op: &str) -> (String, String, String) {
(
format!("class C {{ #x; test(o) {{ return (#x in o) {op} 1; }} }}"),
format!("class C {{\n\t#x;\n\ttest(o) {{\n\t\treturn (#x in o) {op} 1;\n\t}}\n}}\n"),
format!("class C{{#x;test(o){{return(#x in o){op}1}}}}"),
)
}
fn wrap_case_equal_lower_precedence(op: &str) -> (String, String, String) {
(
format!("class C {{ #x; test(o) {{ return (#x in o) {op} 1; }} }}"),
format!("class C {{\n\t#x;\n\ttest(o) {{\n\t\treturn #x in o {op} 1;\n\t}}\n}}\n"),
format!("class C{{#x;test(o){{return#x in o{op}1}}}}"),
)
}
for (source, expected, expected_minified) in
["+", "-", "*", "/", "%", "**", "<<", ">>", ">>>"].map(wrap_case_equal_higher_precedence)
{
test(&source, &expected);
test_minify(&source, &expected_minified);
}
for (source, expected, expected_minified) in
["<", "<=", ">", ">=", "==", "!=", "===", "!==", "&", "^", "|", "&&", "||", "??"]
.map(wrap_case_equal_lower_precedence)
{
test(&source, &expected);
test_minify(&source, &expected_minified);
}
}
#[test]
fn class() {
test(