fix(transformer): gate this capture by owning async transform (#26227)

For targets that support async functions but not async generators, plain async methods remain native. The shared arrow converter still captured `this` whenever either async transform was active, injecting an observable `_this` binding into methods whose bodies never move.

Track async-to-generator and async-generator lowering separately. Plain async methods are now left untouched when only async generators require lowering, while transformed async generator methods still preserve their receiver.

## Example

With a Chrome 55 target, this method previously gained a generated binding:

```js
async method() {
  var _this = this;
  void _this;
  return eval("typeof _this");
}
```

It now remains unchanged, so the direct evaluation returns `"undefined"` as it does in the source program.

Verified with a Chrome 55 target output/runtime fixture, full transformer output conformance, and `cargo test -p oxc_transformer`.

AI assistance was used to diagnose the transform boundary, implement the fix, and add tests. I remain responsible for reviewing and understanding the changes before merge.
This commit is contained in:
Dunqing committed 2026-09-07 07:05:01 +00:00
1 parent affeb14f5a
commit 789969f8a1
7 files changed
+66 -8

No files matched your search

@@ -145,6 +145,8 @@ struct SuperMethodInfo<'a> {
pub struct ArrowFunctionConverter<'a> {
mode: ArrowFunctionConverterMode,
async_to_generator_enabled: bool,
async_generator_functions_enabled: bool,
this_var_stack: SparseStack<BoundIdentifier<'a>>,
arguments_var_stack: SparseStack<BoundIdentifier<'a>>,
constructor_super_stack: NonEmptyStack<bool>,
@@ -158,9 +160,11 @@ pub struct ArrowFunctionConverter<'a> {
impl ArrowFunctionConverter<'_> {
pub fn new(env: &EnvOptions) -> Self {
let async_to_generator_enabled = env.es2017.async_to_generator;
let async_generator_functions_enabled = env.es2018.async_generator_functions;
let mode = if env.es2015.arrow_function.is_some() {
ArrowFunctionConverterMode::Enabled
} else if env.es2017.async_to_generator || env.es2018.async_generator_functions {
} else if async_to_generator_enabled || async_generator_functions_enabled {
ArrowFunctionConverterMode::AsyncOnly
} else {
ArrowFunctionConverterMode::Disabled
@@ -168,6 +172,8 @@ impl ArrowFunctionConverter<'_> {
// `SparseStack`s are created with 1 empty entry, for `Program`
Self {
mode,
async_to_generator_enabled,
async_generator_functions_enabled,
this_var_stack: SparseStack::new(),
arguments_var_stack: SparseStack::new(),
constructor_super_stack: NonEmptyStack::new(false),
@@ -630,13 +636,13 @@ impl<'a> ArrowFunctionConverter<'a> {
}
// Function body (includes class method or object method)
Ancestor::FunctionBody(func) => {
// If we're inside a class async method or an object async method, and `is_async_only` is true,
// the `AsyncToGenerator` or `AsyncGeneratorFunctions` plugin will move the body
// of the method into a new generator function. This transformation can cause `this`
// to point to the wrong context.
// If we're inside a class async method or an object async method, and an
// async transform will move the method body into a new generator function,
// `this` will point to the wrong context.
// To prevent this issue, we replace `this` with `_this`, treating it similarly
// to how we handle arrow functions. Therefore, we return the `ScopeId` of the function.
return if self.is_async_only()
return if (self.async_to_generator_enabled
|| (self.async_generator_functions_enabled && *func.generator()))
&& *func.r#async()
&& Self::is_class_method_like_ancestor(
ancestors.next().unwrap()
@@ -1,6 +1,6 @@
commit: 1eac4481
Passed: 271/399
Passed: 272/400
# All Passed:
* babel-plugin-transform-class-static-block
@@ -2,7 +2,7 @@ commit: 1eac4481
node: v26.5.0
Passed: 15 of 17 (88.24%)
Passed: 16 of 18 (88.89%)
Failures:
@@ -0,0 +1,17 @@
const component = {
value: 42,
async method() {
this.value;
return eval("typeof _this");
},
async *generatorMethod() {
yield this.value;
},
};
return component.method().then(function (value) {
expect(value).toBe("undefined");
return component.generatorMethod().next();
}).then(function (result) {
expect(result.value).toBe(42);
});
@@ -0,0 +1,10 @@
const component = {
value: 42,
async method() {
this.value;
return eval("typeof _this");
},
async *generatorMethod() {
yield this.value;
},
};
@@ -0,0 +1,12 @@
{
"presets": [
[
"env",
{
"targets": {
"chrome": "55"
}
}
]
]
}
@@ -0,0 +1,13 @@
const component = {
value: 42,
async method() {
this.value;
return eval("typeof _this");
},
generatorMethod() {
var _this = this;
return babelHelpers.wrapAsyncGenerator(function* () {
yield _this.value;
})();
}
};