fix(transformer/async-to-generator): move parameters to the inner generator function when they could throw errors (#8500)

The new implementation port from [esbuild](df815ac27b/internal/js_parser/js_parser_lower.go (L355-L467)), before from `Babel`.

Babel's transform implementation for the async method is incorrect because the async method should return a rejecting promise when it throws an error. Everything is good if the errors are thrown in the async method body, but the following case will throw an error in the parameters which causes the whole program crushed not a rejecting promise. So we should move the parameters to the inner generator function when the parameters could throw an error.

Input:
```js
class Cls {
  // ReferenceError: Cannot access 'b' before initialization
  async method(a = b, b = 0) {}
}
```

Before output
```js
class Cls {
  method(a = b, b = 0) {
    return babelHelpers.asyncToGenerator(function* () {})();
  }
}
```

After output:
```js
class Cls {
  method() {
     // ReferenceError: Cannot access 'b' before initialization
    return babelHelpers.asyncToGenerator(function* (a = b, b = 0) {}).apply(this, arguments);
  }
}
```

No override tests because Babel doesn't cover this case.
This commit is contained in:
Dunqing 2025-01-16 00:01:26 +00:00 committed by overlookmotel
parent 3789d2faf9
commit 06ccb51fae
6 changed files with 288 additions and 2477 deletions

View file

@ -220,23 +220,70 @@ impl<'a, 'ctx> AsyncGeneratorExecutor<'a, 'ctx> {
return;
};
// If parameters could throw errors, we need to move them to the inner function,
// because it is an async function, which should return a rejecting promise if
// there is an error.
let needs_move_parameters_to_inner_function =
Self::could_throw_errors_parameters(&func.params);
let (generator_scope_id, wrapper_scope_id) = {
let new_scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function);
let scope_id = func.scope_id.replace(Some(new_scope_id)).unwrap();
// We need to change the parent id to new scope id because we need to this function's body inside the wrapper function,
// and then the new scope id will be wrapper function's scope id.
ctx.scopes_mut().change_parent_id(scope_id, Some(new_scope_id));
// We need to transform formal parameters change back to the original scope,
// because we only move out the function body.
Self::move_formal_parameters_to_target_scope(new_scope_id, &func.params, ctx);
if !needs_move_parameters_to_inner_function {
// We need to change formal parameters's scope back to the original scope,
// because we only move out the function body.
Self::move_formal_parameters_to_target_scope(new_scope_id, &func.params, ctx);
}
(scope_id, new_scope_id)
};
let params = Self::create_empty_params(ctx);
let expression = self.create_async_to_generator_call(params, body, generator_scope_id, ctx);
// Construct the IIFE
let expression = ctx.ast.expression_call(SPAN, expression, NONE, ctx.ast.vec(), false);
let params = if needs_move_parameters_to_inner_function {
// Make sure to not change the value of the "length" property. This is
// done by generating dummy arguments for the outer function equal to
// the expected length of the function:
//
// async function foo(a, b, c = d, ...e) {
// }
//
// This turns into:
//
// function foo(_x, _x1) {
// return _asyncToGenerator(function* (a, b, c = d, ...e) {
// }).call(this, arguments);
// }
//
// The "_x" and "_x1" are dummy variables to ensure "foo.length" is 2.
let new_params = Self::create_placeholder_params(&func.params, wrapper_scope_id, ctx);
mem::replace(&mut func.params, new_params)
} else {
Self::create_empty_params(ctx)
};
let callee = self.create_async_to_generator_call(params, body, generator_scope_id, ctx);
let (callee, arguments) = if needs_move_parameters_to_inner_function {
// callee.apply(this, arguments)
let property = ctx.ast.identifier_name(SPAN, "apply");
let callee =
Expression::from(ctx.ast.member_expression_static(SPAN, callee, property, false));
// this, arguments
let this_argument = Argument::from(ctx.ast.expression_this(SPAN));
let arguments_argument = Argument::from(ctx.create_unbound_ident_expr(
SPAN,
Atom::new_const("arguments"),
ReferenceFlags::Read,
));
(callee, ctx.ast.vec_from_iter([this_argument, arguments_argument]))
} else {
// callee()
(callee, ctx.ast.vec())
};
let expression = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false);
let statement = ctx.ast.statement_return(SPAN, Some(expression));
// Modify the wrapper function
@ -788,6 +835,32 @@ impl<'a, 'ctx> AsyncGeneratorExecutor<'a, 'ctx> {
params.items.first().is_some_and(|param| !param.pattern.kind.is_assignment_pattern())
}
/// Check whether the function parameters could throw errors.
#[inline]
fn could_throw_errors_parameters(params: &FormalParameters<'a>) -> bool {
params.items.iter().any(|param|
matches!(
&param.pattern.kind,
BindingPatternKind::AssignmentPattern(pattern) if Self::could_potentially_throw_error_expression(&pattern.right)
)
)
}
/// Check whether the expression could potentially throw an error.
#[inline]
fn could_potentially_throw_error_expression(expr: &Expression<'a>) -> bool {
!(matches!(
expr,
Expression::NullLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NumericLiteral(_)
| Expression::StringLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::ArrowFunctionExpression(_)
| Expression::FunctionExpression(_)
) || expr.is_undefined())
}
#[inline]
fn move_formal_parameters_to_target_scope(
target_scope_id: ScopeId,

View file

@ -2,7 +2,7 @@ commit: c4317b0c
runtime Summary:
AST Parsed : 18055/18055 (100.00%)
Positive Passed: 17718/18055 (98.13%)
Positive Passed: 17736/18055 (98.23%)
tasks/coverage/test262/test/language/expressions/assignment/fn-name-lhs-cover.js
codegen error: Test262Error: descriptor value should be ; object value should be
@ -213,24 +213,6 @@ transform error: Test262Error: reject reason Expected SameValue(«TypeError: The
tasks/coverage/test262/test/language/expressions/class/async-gen-method-static/yield-star-sync-throw.js
transform error: throw-arg-1
tasks/coverage/test262/test/language/expressions/class/async-method/dflt-params-abrupt.js
transform error: Test262Error:
tasks/coverage/test262/test/language/expressions/class/async-method/dflt-params-ref-later.js
transform error: ReferenceError: Cannot access 'y' before initialization
tasks/coverage/test262/test/language/expressions/class/async-method/dflt-params-ref-self.js
transform error: ReferenceError: Cannot access 'x' before initialization
tasks/coverage/test262/test/language/expressions/class/async-method-static/dflt-params-abrupt.js
transform error: Test262Error:
tasks/coverage/test262/test/language/expressions/class/async-method-static/dflt-params-ref-later.js
transform error: ReferenceError: Cannot access 'y' before initialization
tasks/coverage/test262/test/language/expressions/class/async-method-static/dflt-params-ref-self.js
transform error: ReferenceError: Cannot access 'x' before initialization
tasks/coverage/test262/test/language/expressions/class/elements/arrow-body-derived-cls-direct-eval-err-contains-supercall-1.js
transform error: Test262Error: Expected a SyntaxError but got a ReferenceError
@ -549,21 +531,6 @@ transform error: Test262Error: reject reason Expected SameValue(«TypeError: The
tasks/coverage/test262/test/language/expressions/object/method-definition/async-gen-yield-star-sync-throw.js
transform error: throw-arg-1
tasks/coverage/test262/test/language/expressions/object/method-definition/async-meth-dflt-params-abrupt.js
transform error: Test262Error:
tasks/coverage/test262/test/language/expressions/object/method-definition/async-meth-dflt-params-ref-later.js
transform error: ReferenceError: Cannot access 'y' before initialization
tasks/coverage/test262/test/language/expressions/object/method-definition/async-meth-dflt-params-ref-self.js
transform error: ReferenceError: Cannot access 'x' before initialization
tasks/coverage/test262/test/language/expressions/object/method-definition/async-meth-eval-var-scope-syntax-err.js
transform error: SyntaxError: Identifier 'a' has already been declared
tasks/coverage/test262/test/language/expressions/object/method-definition/async-super-call-param.js
transform error: ReferenceError: _superprop_getMethod is not defined
tasks/coverage/test262/test/language/expressions/object/object-spread-proxy-get-not-called-on-dontenum-keys.js
transform error: Test262Error: Actual [dontEnumString, 0, enumerableString, 1, Symbol(dont_enum_symbol), Symbol(enumerable_symbol)] and expected [Symbol(dont_enum_symbol), dontEnumString, 0, Symbol(enumerable_symbol), enumerableString, 1] should have the same contents.
@ -702,27 +669,6 @@ transform error: Test262Error: reject reason Expected SameValue(«TypeError: The
tasks/coverage/test262/test/language/statements/class/async-gen-method-static/yield-star-sync-throw.js
transform error: throw-arg-1
tasks/coverage/test262/test/language/statements/class/async-method/dflt-params-abrupt.js
transform error: Test262Error:
tasks/coverage/test262/test/language/statements/class/async-method/dflt-params-ref-later.js
transform error: ReferenceError: Cannot access 'y' before initialization
tasks/coverage/test262/test/language/statements/class/async-method/dflt-params-ref-self.js
transform error: ReferenceError: Cannot access 'x' before initialization
tasks/coverage/test262/test/language/statements/class/async-method-static/dflt-params-abrupt.js
transform error: Test262Error:
tasks/coverage/test262/test/language/statements/class/async-method-static/dflt-params-ref-later.js
transform error: ReferenceError: Cannot access 'y' before initialization
tasks/coverage/test262/test/language/statements/class/async-method-static/dflt-params-ref-self.js
transform error: ReferenceError: Cannot access 'x' before initialization
tasks/coverage/test262/test/language/statements/class/definition/methods-async-super-call-param.js
transform error: ReferenceError: _superprop_getMethod is not defined
tasks/coverage/test262/test/language/statements/class/elements/arrow-body-derived-cls-direct-eval-err-contains-supercall-1.js
transform error: Test262Error: Expected a SyntaxError but got a ReferenceError

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
commit: acbc09a8
Passed: 131/153
Passed: 132/154
# All Passed:
* babel-plugin-transform-class-static-block

View file

@ -0,0 +1,4 @@
class Cls {
// ReferenceError: Cannot access 'b' before initialization
async method(a = b, b = 0) {}
}

View file

@ -0,0 +1,5 @@
class Cls {
method() {
return babelHelpers.asyncToGenerator(function* (a = b, b = 0) {}).apply(this, arguments);
}
}