fix(transformer): preserve receivers in private optional chains (#26535)

Only bind a private field when it is the result of an optional chain being called. In `(this.#items.get(key)?.find(fn))?.()`, the intermediate `#items` field must not receive `.bind(this)`.

Normalize non-null assertions in the TypeScript transform before other transforms inspect the chain. This preserves the receiver for `(this?.#fn!)()` and `(this?.#fn!!)()` without mixing TypeScript assertion removal into the class transform.

Fixes #26528
This commit is contained in:
camc314 committed 2026-09-10 15:09:24 +00:00
1 parent b32d25d418
commit 75cd919ffb
11 files changed
+120 -8

No files matched your search

@@ -1219,8 +1219,9 @@ impl<'a> ClassProperties<'a> {
if matches!(element, ChainElement::PrivateFieldExpression(_)) {
// The PrivateFieldExpression must be transformed, so we can convert it to a normal expression here.
let mut chain_expr = Self::convert_chain_expression_to_expression(expr, ctx);
let bind_context = matches!(ctx.ancestor(1), Ancestor::CallExpressionCallee(_));
let result = self
.transform_private_field_expression_of_chain_expression(&mut chain_expr, ctx)
.transform_private_field_expression_of_chain_expression(&mut chain_expr, bind_context, ctx)
.expect("The ChainExpression must contain at least one optional expression, so it can never be `None` here.");
Some((result, chain_expr))
} else if let Some(result) = self.transform_chain_expression_element(element, ctx) {
@@ -1264,7 +1265,7 @@ impl<'a> ClassProperties<'a> {
) -> Option<Expression<'a>> {
match expr {
Expression::PrivateFieldExpression(_) => {
self.transform_private_field_expression_of_chain_expression(expr, ctx)
self.transform_private_field_expression_of_chain_expression(expr, false, ctx)
}
match_member_expression!(Expression) => self
.transform_member_expression_of_chain_expression(
@@ -1356,6 +1357,7 @@ impl<'a> ClassProperties<'a> {
fn transform_private_field_expression_of_chain_expression(
&mut self,
expr: &mut Expression<'a>,
bind_context: bool,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let Expression::PrivateFieldExpression(field_expr) = expr else { unreachable!() };
@@ -1369,9 +1371,9 @@ impl<'a> ClassProperties<'a> {
self.transform_first_optional_expression(object, ctx)
};
if matches!(ctx.ancestor(1), Ancestor::CallExpressionCallee(_)) {
if bind_context {
// `(Foo?.#m)();` -> `(Foo === null || Foo === void 0 ? void 0 : _m._.bind(Foo))();`
// ^^^^^^^^^^^^ is a call expression, we need to bind the proper context
// Only bind when the private field is the chain's result, not an intermediate object.
*expr = self.transform_bindable_private_field(field_expr, ctx);
} else {
self.transform_private_field_expression(expr, ctx);
@@ -265,10 +265,14 @@ impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptAnnotations<'a> {
.retain(|elem| !matches!(elem, ClassElement::PropertyDefinition(prop) if prop.declare));
}
fn enter_expression(&mut self, expr: &mut Expression<'a>, _ctx: &mut TraverseCtx<'a>) {
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if expr.is_typescript_syntax() {
expr.replace_with(Expression::into_inner_expression);
}
if let Expression::ChainExpression(chain) = expr {
// Other transforms inspect the chain before traversal reaches `enter_chain_element`.
self.enter_chain_element(&mut chain.expression, ctx);
}
}
fn enter_simple_assignment_target(
@@ -1,6 +1,6 @@
commit: 1eac4481
Passed: 277/405
Passed: 279/407
# All Passed:
* babel-plugin-transform-class-static-block
@@ -34,7 +34,7 @@ after transform: SymbolId(1) "C"
rebuilt : SymbolId(3) "C"
# babel-plugin-transform-class-properties (29/33)
# babel-plugin-transform-class-properties (31/35)
* private-field-resolve-to-method/input.js
x Output mismatch
@@ -2,7 +2,7 @@ commit: 1eac4481
node: v26.5.0
Passed: 20 of 22 (90.91%)
Passed: 22 of 24 (91.67%)
Failures:
@@ -0,0 +1,38 @@
class Cache {
#items = new Map();
#callback = function () { return this; };
constructor(items) {
this.#items.set("key", items);
}
runNext(key) {
return (this.#items.get(key)?.find(x => x.paused))?.continue()
?? Promise.resolve();
}
getCallbackReceiver() {
return (this?.#callback)();
}
callNext(key) {
return (this.#items.get(key)?.find(x => x.paused))?.();
}
}
const item = {
paused: true,
continue() { return this; },
};
const cache = new Cache([item]);
expect(cache.runNext("missing")).toBeInstanceOf(Promise);
expect(cache.runNext("key")).toBe(item);
expect(new Cache([]).runNext("key")).toBeInstanceOf(Promise);
expect(cache.getCallbackReceiver()).toBe(cache);
let calls = 0;
const callback = function () {
calls++;
return this;
};
callback.paused = true;
const callbacks = new Cache([callback]);
expect(callbacks.callNext("missing")).toBeUndefined();
expect(callbacks.callNext("key")).toBeUndefined();
expect(calls).toBe(1);
@@ -0,0 +1,10 @@
class Cache {
#items = new Map();
runNext(key) {
return (this.#items.get(key)?.find(x => x.paused))?.continue()
?? Promise.resolve();
}
callNext(key) {
return (this.#items.get(key)?.find(x => x.paused))?.();
}
}
@@ -0,0 +1,12 @@
var _items = /*#__PURE__*/ new WeakMap();
class Cache {
constructor() {
babelHelpers.classPrivateFieldInitSpec(this, _items, new Map());
}
runNext(key) {
return (babelHelpers.classPrivateFieldGet2(_items, this).get(key)?.find(x => x.paused))?.continue() ?? Promise.resolve();
}
callNext(key) {
return (babelHelpers.classPrivateFieldGet2(_items, this).get(key)?.find(x => x.paused))?.();
}
}
@@ -0,0 +1,18 @@
class C {
#fn = function () { return this; };
#items = new Map();
run() { return (this?.#fn!)(); }
runOptional() { return (this?.#fn!)?.(); }
runMultiple() { return (this?.#fn!!)(); }
runOptionalMultiple() { return (this?.#fn!!!)?.(); }
runMissing() { return (this.#items.get("missing")?.find(x => x.paused)!)?.(); }
runMissingMultiple() { return (this.#items.get("missing")?.find(x => x.paused)!!)?.(); }
}
const c = new C();
expect(c.run()).toBe(c);
expect(c.runOptional()).toBe(c);
expect(c.runMultiple()).toBe(c);
expect(c.runOptionalMultiple()).toBe(c);
expect(c.runMissing()).toBeUndefined();
expect(c.runMissingMultiple()).toBeUndefined();
@@ -0,0 +1,7 @@
class C {
#fn = function () { return this; };
run() { return (this?.#fn!)(); }
runOptional() { return (this?.#fn!)?.(); }
runMultiple() { return (this?.#fn!!)(); }
runOptionalMultiple() { return (this?.#fn!!!)?.(); }
}
@@ -0,0 +1,3 @@
{
"plugins": ["transform-class-properties", "transform-typescript"]
}
@@ -0,0 +1,18 @@
var _fn = /*#__PURE__*/ new WeakMap();
class C {
constructor() {
babelHelpers.classPrivateFieldInitSpec(this, _fn, function () { return this; });
}
run() {
return (this === null || this === void 0 ? void 0 : babelHelpers.classPrivateFieldGet2(_fn, this).bind(this))();
}
runOptional() {
return (this === null || this === void 0 ? void 0 : babelHelpers.classPrivateFieldGet2(_fn, this).bind(this))?.();
}
runMultiple() {
return (this === null || this === void 0 ? void 0 : babelHelpers.classPrivateFieldGet2(_fn, this).bind(this))();
}
runOptionalMultiple() {
return (this === null || this === void 0 ? void 0 : babelHelpers.classPrivateFieldGet2(_fn, this).bind(this))?.();
}
}