fix(packages/codegen): preserve private-in right operand precedence (#26420)

`packages/codegen` drops required parentheses around the right operand of a private-in expression when that operand is another relational expression. This changes which value the private-field check receives and can change observable behavior.

For example, this input throws a `TypeError`:

```js
class C {
  #x;
  test(a, b) {
    return #x in (a instanceof b);
  }
}
new C().test({}, Object);
```

`a instanceof b` evaluates to `true`, but a private-field check requires an object on its right-hand side.

The JavaScript printer previously emitted:

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

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

The printer now preserves the original grouping:

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

`printPrivateInExpression` now prints its right operand with `PREC_COMPARE` instead of `PREC_EQUALS`. The expression printers wrap operands whose precedence is equal to or lower than the requested precedence, so the comparison threshold retains parentheses around `<`, `<=`, `>`, `>=`, `instanceof`, and nested private-in expressions. The now-unused `PREC_EQUALS` import is removed.

Equality and lower-precedence operands continue to retain their required grouping. Shift, arithmetic, and exponentiation operands can still omit unnecessary parentheses. The existing `CTX_FORBID_IN` handling remains in place.

This applies to both ordinary and source-map-enabled printer builds and brings the JavaScript implementation in line with the Rust fix in #26411. It addresses grouping of the private-in right operand, separately from the left-operand correction in #26403.
This commit is contained in:
camc314 committed 2026-09-07 17:54:59 +00:00
1 parent f8e6c6ccf8
commit 598618793c
2 files changed
+40 -4

No files matched your search

+4 -4
View File
@@ -56,7 +56,6 @@ import {
PREC_COMMA,
PREC_COMPARE,
PREC_CONDITIONAL,
PREC_EQUALS,
PREC_EXPONENTIATION,
PREC_LOWEST,
PREC_NEW,
@@ -382,8 +381,9 @@ function printArguments(
* `#field in obj`, which arrives as a `BinaryExpression` with a `PrivateIdentifier` on the left
* rather than as a node type of its own - hence the extra test in `printExpression`.
*
* It sits at the `in` operator's own level, so it wraps from `PREC_COMPARE` upwards, and the right
* operand prints one level tighter with `CTX_FORBID_IN` set.
* It sits at the `in` operator's own level, so it wraps from `PREC_COMPARE` upwards. The right
* operand also prints at `PREC_COMPARE`, preserving parentheses around relational and
* lower-precedence expressions, with `CTX_FORBID_IN` set.
*/
export function printPrivateInExpression(
node: ESTree.PrivateInExpression,
@@ -396,7 +396,7 @@ export function printPrivateInExpression(
markMapStart(state, node.start, node.end, node);
writeWithMapNamedPrivate(state, node.left.name, node.left.start, node.left.end, node.left);
write(state, " in ", CAT_OTHER);
printExpression(node.right, state, PREC_EQUALS, CTX_FORBID_IN);
printExpression(node.right, state, PREC_COMPARE, CTX_FORBID_IN);
if (wrap) write(state, ")", CAT_CLOSE_BRACKET);
}
@@ -0,0 +1,36 @@
class C {
#x;
test(a, b) {
return [
#x in (a instanceof b),
#x in (a in b),
#x in (a < b),
#x in (a <= b),
#x in (a > b),
#x in (a >= b),
#x in (a == b),
#x in (a != b),
#x in (a === b),
#x in (a !== b),
#x in (a & b),
#x in (a ^ b),
#x in (a | b),
#x in (a && b),
#x in (a || b),
#x in (a ?? b),
#x in (a = b),
#x in (a << b),
#x in (a >> b),
#x in (a >>> b),
#x in (a + b),
#x in (a - b),
#x in (a * b),
#x in (a / b),
#x in (a % b),
#x in (a ** b),
#x in (#x in a),
#x in (a ? a : b),
#x in (a, b),
];
}
}