fix(transformer): limit arguments capture to lowered async functions (#26317)

For targets that support async functions but not async generators, plain async methods and arrows remain native. Async-only converter mode still captured `arguments` for all of them, exposing generated `_arguments` bindings even though their bodies never move.

Gate `arguments` capture on the transform that owns the function body. Native async functions now remain untouched, while lowered async generator methods still capture their original arguments.

## Example

This native async method previously changed from:

```js
async method() {
  void arguments;
  return eval("typeof _arguments");
}
```

to:

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

The generated binding changed the return value from `"undefined"` to `"object"`. The method now remains unchanged.

Verified with focused output/runtime conformance, full transformer output conformance, and `cargo test -p oxc_transformer`.

Stacked on #26227.

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 789969f8a1
commit f45a9ee58b
7 files changed
+91 -11

No files matched your search

@@ -282,7 +282,8 @@ impl<'a> Traverse<'a, TransformState<'a>> for ArrowFunctionConverter<'a> {
) {
if self.is_async_only() {
let previous = *self.arguments_needs_transform_stack.last();
self.arguments_needs_transform_stack.push(previous || arrow.r#async);
self.arguments_needs_transform_stack
.push(previous || self.will_transform_async_function(arrow.r#async, false));
if Self::in_class_property_definition_value(ctx) {
self.this_var_stack.push(None);
@@ -322,8 +323,9 @@ impl<'a> Traverse<'a, TransformState<'a>> for ArrowFunctionConverter<'a> {
if self.is_async_only() {
// Ignore arrow functions
if let Ancestor::FunctionBody(func) = ctx.parent() {
let is_async_method =
*func.r#async() && Self::is_class_method_like_ancestor(ctx.ancestor(1));
let is_async_method = self
.will_transform_async_function(*func.r#async(), *func.generator())
&& Self::is_class_method_like_ancestor(ctx.ancestor(1));
self.arguments_needs_transform_stack.push(is_async_method);
}
}
@@ -527,6 +529,16 @@ impl<'a> ArrowFunctionConverter<'a> {
self.mode == ArrowFunctionConverterMode::AsyncOnly
}
#[inline]
fn will_transform_async_function(&self, r#async: bool, generator: bool) -> bool {
r#async
&& if generator {
self.async_generator_functions_enabled
} else {
self.async_to_generator_enabled
}
}
fn get_this_identifier(
&mut self,
span: Span,
@@ -641,12 +653,10 @@ impl<'a> ArrowFunctionConverter<'a> {
// `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.async_to_generator_enabled
|| (self.async_generator_functions_enabled && *func.generator()))
&& *func.r#async()
&& Self::is_class_method_like_ancestor(
ancestors.next().unwrap()
) {
return if self
.will_transform_async_function(*func.r#async(), *func.generator())
&& Self::is_class_method_like_ancestor(ancestors.next().unwrap())
{
Some(func.scope_id().get().unwrap())
} else {
None
@@ -1,6 +1,6 @@
commit: 1eac4481
Passed: 272/400
Passed: 273/401
# All Passed:
* babel-plugin-transform-class-static-block
@@ -2,7 +2,7 @@ commit: 1eac4481
node: v26.5.0
Passed: 16 of 18 (88.89%)
Passed: 17 of 19 (89.47%)
Failures:
@@ -0,0 +1,18 @@
const object = {
async method() {
void arguments;
return eval("typeof _arguments");
},
};
function outer() {
return (async () => {
void arguments;
return eval("typeof _arguments2");
})();
}
return Promise.all([object.method(), outer()]).then(function (results) {
expect(results[0]).toBe("undefined");
expect(results[1]).toBe("undefined");
});
@@ -0,0 +1,22 @@
const object = {
async method() {
return arguments;
},
async *generatorMethod() {
return arguments;
},
};
class Example {
async method() {
return arguments;
}
async *generatorMethod() {
return arguments;
}
}
function outer() {
return async () => arguments;
}
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-async-generator-functions"
]
}
@@ -0,0 +1,25 @@
const object = {
async method() {
return arguments;
},
generatorMethod() {
var _arguments = arguments;
return babelHelpers.wrapAsyncGenerator(function* () {
return _arguments;
})();
}
};
class Example {
async method() {
return arguments;
}
generatorMethod() {
var _arguments2 = arguments;
return babelHelpers.wrapAsyncGenerator(function* () {
return _arguments2;
})();
}
}
function outer() {
return async () => arguments;
}