From d83bd048563a821902c116452872b1b5e2e35527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 28 Jun 2026 08:53:33 +0200 Subject: [PATCH] =?UTF-8?q?fix(hir):=20#5703=20=E2=80=94=20apply=20default?= =?UTF-8?q?-parameter=20prologue=20for=20class=20setters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A setter with a defaulted parameter (`set a(_ = expr)`) dropped its default: `lower_setter_method_with_name` built the `Param` with `default: None` and emitted no `if (param === undefined) param = ` prologue (only destructuring was handled). So invoking the setter with `undefined` (`C.a = undefined`) never ran the default expression — test262 `language/expressions/class/scope-static-setter-paramsbody-var-open` left `probeParams` undefined ("value is not a function"). Fix: mirror the constructor/method path — lower the default via `get_param_default` in the param loop (BEFORE `define_local`, so the default's references resolve to the enclosing scope rather than the param or the body's hoisted `var`s — the separate parameter/body VariableEnvironment the spec mandates) and prepend `build_default_param_stmts` to the body. The default's `x` closure then binds the OUTER `x` ("outside") while the body's `var x = "inside"` closure binds the body `x`, matching node byte-for-byte. Applies to instance and static setters alike; affects only setters that actually declare a default (`build_default_param_stmts` is inert otherwise). Regression test: class_expr_static_setter_default_param_scope. --- .../perry-hir/src/lower_decl/class_members.rs | 24 +++++++++++++- ...ssue_5703_class_expr_static_dflt_params.rs | 32 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/perry-hir/src/lower_decl/class_members.rs b/crates/perry-hir/src/lower_decl/class_members.rs index 6ada92826c..5870d60718 100644 --- a/crates/perry-hir/src/lower_decl/class_members.rs +++ b/crates/perry-hir/src/lower_decl/class_members.rs @@ -805,6 +805,17 @@ pub fn lower_setter_method_with_name( continue; } let param_type = extract_param_type_with_ctx(¶m.pat, Some(ctx)); + // #5703: a setter with a defaulted parameter (`set a(_ = expr)`) must + // apply the default when invoked with `undefined` (e.g. `C.a = + // undefined`). Previously the setter param dropped its default + // (`default: None`) and emitted no prologue, so the default expression + // never ran. Mirror the constructor/method path: lower the default + // BEFORE defining the param local so its references resolve to the + // enclosing scope rather than the param or the body's hoisted `var`s — + // i.e. the separate parameter/body VariableEnvironment the spec + // mandates (test262 scope-*-setter-paramsbody-var-open) — then emit the + // `if (param === undefined) param = ` prologue below. + let param_default = get_param_default(ctx, ¶m.pat)?; let param_id = ctx.define_local(param_name.clone(), param_type.clone()); ctx.shadow_native_instance_if_present(¶m_name); ctx.shadow_native_module_if_present(¶m_name); @@ -812,7 +823,7 @@ pub fn lower_setter_method_with_name( id: param_id, name: param_name, ty: param_type, - default: None, + default: param_default, decorators: Vec::new(), is_rest: false, arguments_object: None, @@ -846,6 +857,17 @@ pub fn lower_setter_method_with_name( body = destructuring_stmts; } + // #5703: prepend `if (param === undefined) param = ` for a + // defaulted setter parameter, so an under-supplied / `undefined` assignment + // (`C.a = undefined`) applies the default. Runs before any destructuring + // prologue (which destructures the already-defaulted param). + let default_stmts = build_default_param_stmts(¶ms); + if !default_stmts.is_empty() { + let mut new_body = default_stmts; + new_body.append(&mut body); + body = new_body; + } + ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); diff --git a/crates/perry/tests/issue_5703_class_expr_static_dflt_params.rs b/crates/perry/tests/issue_5703_class_expr_static_dflt_params.rs index 262256a80e..65d0e8e004 100644 --- a/crates/perry/tests/issue_5703_class_expr_static_dflt_params.rs +++ b/crates/perry/tests/issue_5703_class_expr_static_dflt_params.rs @@ -184,3 +184,35 @@ var C = class { "default applies inside a static async generator" ); } + +/// A class-expression static SETTER with a defaulted parameter, invoked via +/// assignment of `undefined` (`C.a = undefined`). Pre-fix the setter dropped +/// its default (`default: None`, no prologue), so the default expression never +/// ran and `probeParams` stayed `undefined`. The default must also evaluate in +/// the parameter VariableEnvironment — its `x` closure sees the OUTER `x` +/// ("outside"), distinct from the setter body's `var x = "inside"` — matching +/// test262 `language/expressions/class/scope-static-setter-paramsbody-var-open`. +#[test] +fn class_expr_static_setter_default_param_scope() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + r#" +var x = "outside"; +var probeParams: any, probeBody: any; +var C: any = class { + static set a(_ = (probeParams = function () { return x; })) { + var x = "inside"; + probeBody = function () { return x; }; + } +}; +C.a = undefined; +console.log(probeParams()); +console.log(probeBody()); +"#, + ); + assert_eq!( + stdout, "outside\ninside\n", + "defaulted setter param runs its default in the parameter scope" + ); +}