mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
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:
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:
|
||||
|
||||
|
||||
+38
@@ -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);
|
||||
+10
@@ -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))?.();
|
||||
}
|
||||
}
|
||||
+12
@@ -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))?.();
|
||||
}
|
||||
}
|
||||
+18
@@ -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();
|
||||
+7
@@ -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!!!)?.(); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["transform-class-properties", "transform-typescript"]
|
||||
}
|
||||
+18
@@ -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))?.();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user