fix(codegen): preserve private-in right operand precedence (#26411)

Private-in expressions can lose required parentheses around their right-hand operand in both default and minified code generation. When the operand is another relational expression, removing the grouping changes which value the private-field check receives and can change observable behavior.

For example:

```js
class D {
  #x;
  test(a, b) {
    return #x in (a instanceof b);
  }
}

new D().test({}, Object);
```

The original throws a `TypeError`: `a instanceof b` evaluates to `true`, and the private-field check requires an object as its right-hand operand. Previously, codegen emitted:

```js
return #x in a instanceof b;
```

Relational operators associate to the left, so this is parsed as `(#x in a) instanceof b`. The private-field check on `{}` produces `false`, and `false instanceof Object` then returns `false` instead of throwing.

The corrected output preserves the grouping in both modes:

```js
// Default output
return #x in (a instanceof b);

// Minified output
return#x in (a instanceof b)
```

`PrivateInExpression::gen_expr` now prints its right-hand operand with `Precedence::Compare` instead of `Precedence::Equals`. The printer wraps expressions whose precedence is equal to or lower than the requested precedence. Using `Compare` therefore preserves parentheses around relational operands, including `<`, `<=`, `>`, `>=`, `instanceof`, and nested private-in expressions. Equality and lower-precedence expressions continue to retain their required grouping, while shift, arithmetic, and exponentiation operands can still omit unnecessary parentheses.

This addresses the RHS grouping issue separately from #26383, which fixed the precedence passed to private-in expressions appearing as the left operand of another binary expression. The change is confined to the Rust `oxc_codegen` crate.
This commit is contained in:
camc314 committed 2026-09-07 17:10:19 +00:00
1 parent 81fd4825a1
commit 42ac916b21
2 files changed
+62 -1

No files matched your search

+1 -1
View File
@@ -2006,7 +2006,7 @@ impl GenExpr for PrivateInExpression<'_> {
p.add_source_mapping(self.span);
self.left.print(p, ctx);
p.print_str(" in ");
self.right.print_expr(p, Precedence::Equals, Context::FORBID_IN);
self.right.print_expr(p, Precedence::Compare, Context::FORBID_IN);
});
}
}
@@ -164,6 +164,67 @@ fn private_in() {
);
}
#[test]
fn private_in_binary_right() {
fn wrap_case_equal_lower_precedence(op: &str) -> (String, String, String) {
let minified_op =
if matches!(op, "in" | "instanceof") { format!(" {op} ") } else { op.to_string() };
(
format!("class C {{ #x; test(a, b) {{ return #x in (a {op} b); }} }}"),
format!("class C {{\n\t#x;\n\ttest(a, b) {{\n\t\treturn #x in (a {op} b);\n\t}}\n}}\n"),
format!("class C{{#x;test(a,b){{return#x in (a{minified_op}b)}}}}"),
)
}
fn wrap_case_higher_precedence(op: &str) -> (String, String, String) {
(
format!("class C {{ #x; test(a, b) {{ return #x in (a {op} b); }} }}"),
format!("class C {{\n\t#x;\n\ttest(a, b) {{\n\t\treturn #x in a {op} b;\n\t}}\n}}\n"),
format!("class C{{#x;test(a,b){{return#x in a{op}b}}}}"),
)
}
for (source, expected, expected_minified) in [
"instanceof",
"in",
"<",
"<=",
">",
">=",
"==",
"!=",
"===",
"!==",
"&",
"^",
"|",
"&&",
"||",
"??",
"=",
]
.map(wrap_case_equal_lower_precedence)
{
test(&source, &expected);
test_minify(&source, &expected_minified);
}
for (source, expected, expected_minified) in
["<<", ">>", ">>>", "+", "-", "*", "/", "%", "**"].map(wrap_case_higher_precedence)
{
test(&source, &expected);
test_minify(&source, &expected_minified);
}
for (rhs, minified_rhs) in [("#x in a", "#x in a"), ("a ? a : b", "a?a:b"), ("a, b", "a,b")] {
let source = format!("class C {{ #x; test(a, b) {{ return #x in ({rhs}); }} }}");
test(
&source,
&format!("class C {{\n\t#x;\n\ttest(a, b) {{\n\t\treturn #x in ({rhs});\n\t}}\n}}\n"),
);
test_minify(&source, &format!("class C{{#x;test(a,b){{return#x in ({minified_rhs})}}}}"));
}
}
#[test]
fn private_in_binary_left() {
fn wrap_case_equal_higher_precedence(op: &str) -> (String, String, String) {