From da891b29460f53c3d3773b3aae0cafafa78b5c43 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:08:07 +0000 Subject: [PATCH] js_parser: declare decorator lowering temporaries per iteration inside loops The standard decorator / accessor lowering declared a class's temporaries (_init, _dec, _computedKey, _base, the _ WeakMaps and WeakSets of lowered members, _class in expression mode) with var. Inside a loop body that is a single binding shared by every iteration, while the class each iteration creates keeps reading it afterwards: its constructor reads _init, _computedKey and the WeakMaps on every instantiation, and the accessors installed by __decorateElement hold the WeakMap of their own iteration. So instances of an earlier iteration's class ran the last iteration's initializers, got its computed key, or threw "Cannot read from private field" for accessor fields and private members. When the class is lowered inside a loop body (is_inside_loop), declare the temporaries with let, in both statement mode and expression mode (where the declaration is hoisted to the top of the innermost statement list, which is inside the loop body), so each iteration gets its own set. Outside loops the output is unchanged. Unlike var declarations, initialized let declarations take part in the single-use inlining of visit_stmts, which trusts use_count_estimate, so the references to temporaries that the lowering created as bare identifiers (the _base argument of __decoratorStart, _init in the __decorateElement calls, the storage WeakMaps handed to __privateAdd, the second reference to a _computedKey) now go through use_ref. Without that, `@dec class K extends Base {}` in a loop had _base inlined into __decoratorStart and dropped while `extends _base` still read it. --- src/js_parser/lower/lower_decorators.rs | 143 ++++----- test/bundler/bundler_edgecase.test.ts | 61 ++++ test/bundler/transpiler/es-decorators.test.ts | 271 ++++++++++++++++++ 3 files changed, 411 insertions(+), 64 deletions(-) diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 056c77a83e6e..1ae8bd3d80d2 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -179,11 +179,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } /// A temporary (`_init`, `_dec`, a member's `_name` WeakMap, ...) that the - /// lowering declares in the statement list it is lowering into, i.e. a - /// binding of the enclosing var-hoisting scope. The bundler's renamers - /// rename it from there; without a renamer, symbols print under their - /// original names, so `base` is only a request and `name_decorator_temps` - /// picks the final name once the whole file has been visited. + /// lowering declares in the statement list it is lowering into, with the + /// keyword `temp_decl_kind` picks. Either way it is registered with the + /// enclosing var-hoisting scope: a `let` lands in a block of that scope, + /// where a name unique across the scope is unique too. The bundler's + /// renamers rename it from there; without a renamer, symbols print under + /// their original names, so `base` is only a request and + /// `name_decorator_temps` picks the final name once the whole file has + /// been visited. fn new_temp(&mut self, base: &'a [u8]) -> Ref { debug_assert!( base.starts_with(b"_"), @@ -265,12 +268,38 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O b"_accessor_storage" } - /// Single var declaration statement. - fn var_decl(&mut self, ref_: Ref, value: Option, l: bun_ast::Loc) -> Stmt { + /// Keyword for the declarations of a class's temporaries. + /// + /// A class keeps reading them after it has been created: its constructor + /// runs `__runInitializers(_init, ..)` and `__privateAdd(this, _x, ..)`, + /// and the accessors `__decorateElement` installed hold the `_x` WeakMap + /// that was current when the class was decorated. Inside a loop body a + /// `var` is one binding for all iterations, so every class the loop creates + /// ends up reading the last iteration's temporaries; a `let` gives each + /// iteration its own. Outside a loop the statement list runs once per + /// function invocation either way, and `var` stays hoisted, so it is kept. + /// + /// Unlike a `var`, an initialized `let` is a candidate for the single-use + /// inlining in `visit_stmts`, which goes by `use_count_estimate`: with a + /// count of 1, `let _base = Base; let _init = __decoratorStart(_base)` + /// becomes `__decoratorStart(Base)` and the declaration `extends _base` + /// still needs is dropped. So every reference to a temporary is created + /// with `use_ref`, never as a bare `E::Identifier`. + fn temp_decl_kind(&self) -> S::Kind { + if self.fn_or_arrow_data_visit.is_inside_loop { + S::Kind::KLet + } else { + S::Kind::KVar + } + } + + /// Declaration statement for one temporary. + fn temp_decl(&mut self, ref_: Ref, value: Option, l: bun_ast::Loc) -> Stmt { let binding = self.b(B::Identifier { r#ref: ref_ }, l); let decls = DeclList::from_slice(&[G::Decl { binding, value }]); self.s( S::Local { + kind: self.temp_decl_kind(), decls, ..Default::default() }, @@ -278,8 +307,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ) } - /// Two-variable declaration statement. - fn var_decl2( + /// Declaration statement for two temporaries. + fn temp_decl2( &mut self, r1: Ref, v1: Option, @@ -301,6 +330,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ]); self.s( S::Local { + kind: self.temp_decl_kind(), decls, ..Default::default() }, @@ -987,8 +1017,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } /// Drain receiver-capture temporaries created past `baseline` into a - /// single `var` declaration statement; `None` if none were created. - fn drain_capture_temp_decls(&mut self, baseline: usize, loc: bun_ast::Loc) -> Option { + /// single declaration statement; `None` if none were created. + fn drain_capture_temp_decls( + &mut self, + baseline: usize, + kind: S::Kind, + loc: bun_ast::Loc, + ) -> Option { let total = self.temp_refs_to_declare.len(); if total == baseline { return None; @@ -1006,6 +1041,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.temp_refs_to_declare.truncate(baseline); Some(self.s( S::Local { + kind, decls: DeclList::from_bump_vec(capture_decls), ..Default::default() }, @@ -1025,7 +1061,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O temps_before: usize, body_loc: bun_ast::Loc, ) -> js_ast::StmtNodeList { - let Some(decl_stmt) = self.drain_capture_temp_decls(temps_before, body_loc) else { + let Some(decl_stmt) = self.drain_capture_temp_decls(temps_before, S::Kind::KVar, body_loc) + else { return stmts; }; let old_stmts = stmts.slice(); @@ -1325,7 +1362,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }); class_dec_assign_expr = Some(p.assign_to(cdr, arr, loc)); } else { - class_dec_stmt = p.var_decl(cdr, Some(arr), loc); + class_dec_stmt = p.temp_decl(cdr, Some(arr), loc); } } @@ -1351,7 +1388,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, loc, ); - pre_eval_stmts.push(p.var_decl(dec_ref, Some(arr), loc)); + pre_eval_stmts.push(p.temp_decl(dec_ref, Some(arr), loc)); } if prop.flags.contains(Flags::Property::IsComputed) && prop.key.is_some() @@ -1360,7 +1397,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let key_ref = p.new_temp(b"_computedKey"); computed_key_refs.insert(prop_idx, key_ref); let key_loc = prop.key.expect("infallible: prop has key").loc; - pre_eval_stmts.push(p.var_decl(key_ref, prop.key, loc)); + pre_eval_stmts.push(p.temp_decl(key_ref, prop.key, loc)); prop.key = Some(p.use_ref(key_ref, key_loc)); } } @@ -1408,13 +1445,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // ── Phase 3: __decoratorStart + base decls ─────── let init_start_expr: Expr = { let base_expr = if let Some(br) = base_ref { - p.new_expr( - E::Identifier { - ref_: br, - ..Default::default() - }, - loc, - ) + p.use_ref(br, loc) } else { p.new_expr(E::Undefined {}, loc) }; @@ -1424,7 +1455,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut base_decl_stmt: Stmt = Stmt::empty(); if !is_expr { if let Some(br) = base_ref { - base_decl_stmt = p.var_decl(br, class.extends, loc); + base_decl_stmt = p.temp_decl(br, class.extends, loc); } } @@ -1439,7 +1470,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } let init_decl_stmt: Stmt = if !is_expr { - p.var_decl(init_ref, Some(init_start_expr), loc) + p.temp_decl(init_ref, Some(init_start_expr), loc) } else { Stmt::empty() }; @@ -1553,9 +1584,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if existing.is_none() { let wse = p.new_weak_set_expr(loc); - prefix_stmts.push(p.var_decl2(ws_ref, Some(wse), fn_ref, None, loc)); + prefix_stmts.push(p.temp_decl2(ws_ref, Some(wse), fn_ref, None, loc)); } else { - prefix_stmts.push(p.var_decl(fn_ref, None, loc)); + prefix_stmts.push(p.temp_decl(fn_ref, None, loc)); } // Assign function: _fn = function() { ... } @@ -1590,7 +1621,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let wm_ref = p.new_temp(wm_nm); private_lowered_map.insert(npriv_inner, PrivateLoweredInfo::new(wm_ref)); let wme = p.new_weak_map_expr(loc); - prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); + prefix_stmts.push(p.temp_decl(wm_ref, Some(wme), loc)); let init_val = prop .initializer @@ -1617,7 +1648,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let accessor_name = p.accessor_storage_base(prop.key); let wm_ref = p.new_temp(accessor_name); let wme = p.new_weak_map_expr(loc); - prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); + prefix_stmts.push(p.temp_decl(wm_ref, Some(wme), loc)); // Getter: get foo() { return __privateGet(this, _foo); } let this_e = p.new_expr(E::This {}, loc); @@ -1810,9 +1841,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if existing.is_none() { let wse = p.new_weak_set_expr(loc); - prefix_stmts.push(p.var_decl2(ws_ref, Some(wse), fn_ref, None, loc)); + prefix_stmts.push(p.temp_decl2(ws_ref, Some(wse), fn_ref, None, loc)); } else { - prefix_stmts.push(p.var_decl(fn_ref, None, loc)); + prefix_stmts.push(p.temp_decl(fn_ref, None, loc)); } dec_arg_count = 6; } else if k == 5 { @@ -1821,7 +1852,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O private_storage_ref = Some(wm_ref); private_lowered_map.insert(priv_inner, PrivateLoweredInfo::new(wm_ref)); let wme = p.new_weak_map_expr(loc); - prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); + prefix_stmts.push(p.temp_decl(wm_ref, Some(wme), loc)); dec_arg_count = 5; } else if k == 4 { let nm = p.bump_name2(b"_", &private_orig[1..]); @@ -1847,7 +1878,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, ); let wme = p.new_weak_map_expr(loc); - prefix_stmts.push(p.var_decl2(wm_ref, Some(wme), acc_ref, None, loc)); + prefix_stmts.push(p.temp_decl2(wm_ref, Some(wme), acc_ref, None, loc)); dec_arg_count = 6; } } else if k == 4 { @@ -1856,7 +1887,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let wm_ref = p.new_temp(accessor_name); private_extra_ref = Some(wm_ref); let wme = p.new_weak_map_expr(loc); - prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); + prefix_stmts.push(p.temp_decl(wm_ref, Some(wme), loc)); dec_arg_count = 6; } @@ -1867,13 +1898,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O class_name_ref }; let mut dec_args = BumpVec::with_capacity_in(dec_arg_count, bump); - dec_args.push(p.new_expr( - E::Identifier { - ref_: init_ref, - ..Default::default() - }, - loc, - )); + dec_args.push(p.use_ref(init_ref, loc)); dec_args.push(p.new_expr(E::Number::new(flags), loc)); dec_args.push(if is_private { let priv_ref = match &key_expr.data { @@ -1893,6 +1918,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, loc, ) + } else if let Some(key_ref) = computed_key_refs.get(&prop_idx).copied() { + // `key_expr` is the reference Phase 2 recorded; this call is + // another one. + p.use_ref(key_ref, key_expr.loc) } else { key_expr }); @@ -1943,23 +1972,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut prop_shallow = prop_copy(prop); if is_private { if let Some(ps_ref) = private_storage_ref { - prop_shallow.key = Some(p.new_expr( - E::Identifier { - ref_: ps_ref, - ..Default::default() - }, - loc, - )); + prop_shallow.key = Some(p.use_ref(ps_ref, loc)); } } if let Some(pe_ref) = private_extra_ref { - prop_shallow.value = Some(p.new_expr( - E::Identifier { - ref_: pe_ref, - ..Default::default() - }, - loc, - )); + prop_shallow.value = Some(p.use_ref(pe_ref, loc)); } let is_accessor = k == 4; @@ -2094,13 +2111,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }; let mut cls_dec_args = BumpVec::with_capacity_in(5, bump); - cls_dec_args.push(p.new_expr( - E::Identifier { - ref_: init_ref, - ..Default::default() - }, - loc, - )); + cls_dec_args.push(p.use_ref(init_ref, loc)); cls_dec_args.push(p.new_expr(E::Number::new(0.0), loc)); cls_dec_args.push(p.new_expr( E::EString { @@ -2485,9 +2496,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // already declared there by `declare_capture_temps_in_fn_body`. Static // blocks, static initializers, and decorate expressions run at most // once per class evaluation; instance field initializers run once per - // construction and share the hoisted binding across constructions, + // construction and share the class's binding across constructions, // matching where esbuild declares these temps. - if let Some(decl_stmt) = p.drain_capture_temp_decls(temp_refs_before, loc) { + if let Some(decl_stmt) = + p.drain_capture_temp_decls(temp_refs_before, p.temp_decl_kind(), loc) + { prefix_stmts.push(decl_stmt); } @@ -2571,11 +2584,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ); } - // Emit var declarations + // Declare the temporaries at the top of the enclosing statement + // list; the comma chain above assigns them. if !expr_var_decls.is_empty() { let decls = DeclList::from_bump_vec(expr_var_decls); let var_decl_stmt = p.s( S::Local { + kind: p.temp_decl_kind(), decls, ..Default::default() }, diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 6df2d864865c..c1e45a2c5ab2 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -3340,6 +3340,67 @@ describe("bundler", () => { }, run: { stdout: "1 2" }, }); + // Inside a loop body the temporaries are declared with `let`, one set per + // iteration, while the symbols are still registered with the module or + // function scope for renaming. Every class is instantiated after its loop + // has finished, which reads the iteration's own temporaries. `E` has no + // members, so its `let _base = Base` is directly followed by + // `let _init = __decoratorStart(_base)`: --minify-syntax must not inline + // `_base` there, the class's extends clause reads it too. + const decoratorTempsInLoops = { + "/entry.js": /* js */ ` + function dec(value, ctx) {} + class Base {} + const classes = []; + const subclasses = []; + for (let i = 0; i < 2; i++) { + class A { @dec accessor x = "a" + i; } + @dec class E extends Base {} + classes.push(A, class { @dec accessor x = "b" + i; }); + subclasses.push(E); + } + function inner() { + const out = []; + for (const tag of ["c", "d"]) out.push(class { @dec accessor x = tag; }); + return out; + } + classes.push(...inner()); + console.log(classes.map(C => new C().x).join(","), subclasses.every(E => new E() instanceof Base)); + `, + }; + itBundled("edgecase/DecoratorLoweringTempsInLoops", { + files: decoratorTempsInLoops, + run: { stdout: "a0,b0,a1,b1,c,d true" }, + }); + itBundled("edgecase/DecoratorLoweringTempsInLoopsMinifyIdentifiers", { + files: decoratorTempsInLoops, + minifyIdentifiers: true, + run: { stdout: "a0,b0,a1,b1,c,d true" }, + }); + itBundled("edgecase/DecoratorLoweringTempsInLoopsMinifySyntax", { + files: decoratorTempsInLoops, + minifySyntax: true, + run: { stdout: "a0,b0,a1,b1,c,d true" }, + }); + // require() of an ES module wraps the module in an __esm closure, which + // hoists the module's top-level declarations out of the closure. + itBundled("edgecase/DecoratorLoweringTempsInLoopsInWrappedModule", { + files: { + "/entry.js": /* js */ ` + const { classes } = require("./classes.js"); + console.log(classes.map(C => new C().x).join(",")); + `, + "/classes.js": /* js */ ` + function dec(value, ctx) {} + export const classes = []; + for (let i = 0; i < 2; i++) { + class A { @dec accessor x = "a" + i; } + classes.push(A, class { @dec accessor x = "b" + i; }); + } + `, + }, + run: { stdout: "a0,b0,a1,b1" }, + }); }); for (const backend of ["api", "cli"] as const) { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index 82a7fe5fd15e..c7e5f99f8d8c 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -1516,4 +1516,275 @@ describe("ES Decorators", () => { expect(exitCode).toBe(0); }); }); + + // A class keeps using its temporaries after it has been created: the + // constructor reads _init, _computedKey and the _ WeakMaps whenever an + // instance is made, and the getters/setters installed by __decorateElement + // hold the WeakMap that existed when the class was decorated. A class + // created by a loop body must therefore get its own temporaries on every + // iteration instead of sharing one binding with the classes of the other + // iterations. + describe.concurrent("lowering temporaries of a class created in a loop", () => { + test("class statement: initializers registered for each iteration's class", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + let n = 0; + function dec(value, ctx) { + const id = ++n; + ctx.addInitializer(function () { this.id = id; }); + } + const classes = []; + for (let i = 0; i < 2; i++) { + class K { @dec m() {} } + classes.push(K); + } + console.log(new classes[0]().id, new classes[1]().id); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("1 2\n"); + expect(exitCode).toBe(0); + }); + + test("class statement: accessor storage of instances created during the loop", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const objs = []; + for (let i = 0; i < 2; i++) { + class K { accessor x = i; } + objs.push(new K()); + } + console.log(objs[0].x, objs[1].x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("0 1\n"); + expect(exitCode).toBe(0); + }); + + test("class statement: private members of a class lowered for a decorated method", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) {} + const objs = []; + for (let i = 0; i < 2; i++) { + class K { + @dec m() {} + #v = i; + get v() { return this.#v; } + } + objs.push(new K()); + } + console.log(objs[0].v, objs[1].v); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("0 1\n"); + expect(exitCode).toBe(0); + }); + + test("class statement: two classes in one loop body", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) {} + const classes = []; + for (let i = 0; i < 2; i++) { + class A { @dec accessor x = "a" + i; } + class B { @dec accessor x = "b" + i; } + classes.push(A, B); + } + console.log(classes.map(C => new C().x).join(",")); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a0,b0,a1,b1\n"); + expect(exitCode).toBe(0); + }); + + test("class statement: every kind of lowered member", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + const classes = []; + for (const tag of ["a", "b"]) { + class Base {} + @dec class K extends Base { + @dec accessor decorated = tag; + accessor plain = tag; + #field = tag; + #method() { return this.#field + "()"; } + get #pair() { return this.#field + "!"; } + set #pair(v) { this.#field = v; } + @dec #decoratedField = tag + "#"; + @dec [tag + "Key"] = tag; + static #s = tag; + static readStatic() { return K.#s; } + read() { + this.#pair = this.#field; + return [this.#method(), this.#pair, this.#decoratedField, this.decorated, this.plain, this.viaTemp]; + } + // A receiver that is not an identifier or this is captured in a + // temporary that is declared together with the other temporaries. + @dec viaTemp = [this][0].#method(); + } + classes.push(K); + } + const a = new classes[0](); + const b = new classes[1](); + console.log(JSON.stringify([ + a.read(), b.read(), + Object.keys(a), Object.keys(b), + classes[0].readStatic(), classes[1].readStatic(), + ])); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([ + ["a()", "a!", "a#", "a", "a", "a()"], + ["b()", "b!", "b#", "b", "b", "b()"], + ["aKey", "viaTemp"], + ["bKey", "viaTemp"], + "a", + "b", + ]); + expect(exitCode).toBe(0); + }); + + test("class statement: class decorator and extends clause only", async () => { + // With no members, nothing is declared between `let _base = Base` and + // `let _init = __decoratorStart(_base)`, which is the shape the + // single-use inlining of the runtime transpiler (minify-syntax is on) + // looks at. _base is also read by the extends clause, so inlining it + // would leave that read dangling. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + class Base {} + const classes = []; + for (let i = 0; i < 2; i++) { + @dec class K extends Base {} + classes.push(K); + } + console.log(new classes[0]() instanceof Base, new classes[1]() instanceof Base); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true true\n"); + expect(exitCode).toBe(0); + }); + + test("class expression: decorated accessor of instances created after the loop", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) {} + const classes = []; + for (let i = 0; i < 2; i++) classes.push(class { @dec accessor x = i; }); + console.log(new classes[0]().x, new classes[1]().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("0 1\n"); + expect(exitCode).toBe(0); + }); + + test("class expression: accessor storage of instances created during the loop", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const objs = []; + for (let i = 0; i < 2; i++) objs.push(new (class { accessor x = i; })()); + console.log(objs[0].x, objs[1].x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("0 1\n"); + expect(exitCode).toBe(0); + }); + + // The accessor decorator gives each class's accessor the initial value of + // the class's position in the loop. Block bodies and single-statement + // bodies alternate on purpose: the temporaries of a class expression are + // declared at the top of the statement list the expression is in, which + // for a single-statement body is the block the transpiler wraps around it. + test.each([ + ["for", "for (let k = 0; k < 2; k++) { BODY }"], + ["for-of", "for (const k of [0, 1]) BODY"], + ["for-in", "for (const k in { a: 0, b: 0 }) { BODY }"], + ["while", "while (classes.length < 2) BODY"], + ["do-while", "do { BODY } while (classes.length < 2);"], + ])("class expression in the body of a %s loop", async (_kind, loop) => { + const { stdout, stderr, exitCode } = await runDecorator(` + let decorated = 0; + function dec(value, ctx) { + const position = decorated++; + return { init() { return position; } }; + } + const classes = []; + ${loop.replace("BODY", "classes.push(class { @dec accessor x; });")} + console.log(classes.length, new classes[0]().x, new classes[1]().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("2 0 1\n"); + expect(exitCode).toBe(0); + }); + + test("class expression in a block nested in the loop body", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) {} + const classes = []; + for (let i = 0; i < 2; i++) { + if (classes.length === i) { + classes.push(class { @dec accessor x = i; }); + } + } + console.log(new classes[0]().x, new classes[1]().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("0 1\n"); + expect(exitCode).toBe(0); + }); + + test("class expression: class decorator, extends clause and computed key", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + const classes = []; + for (const name of ["a", "b"]) { + class Base { static tag = name; } + classes.push(@dec class extends Base { @dec [name] = name; }); + } + console.log(JSON.stringify([ + new classes[0](), new classes[1](), + classes[0].tag, classes[1].tag, + ])); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([{ a: "a" }, { b: "b" }, "a", "b"]); + expect(exitCode).toBe(0); + }); + + test("class expressions in the cases of a switch inside a loop", async () => { + // The declarations land in the case clauses, which share the scope of + // the switch block. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) {} + const classes = []; + for (let i = 0; i < 4; i++) { + switch (i % 2) { + case 0: + classes.push(class { @dec accessor x = "even" + i; }); + break; + default: + classes.push(class { @dec accessor x = "odd" + i; }); + } + } + console.log(classes.map(C => new C().x).join(",")); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("even0,odd1,even2,odd3\n"); + expect(exitCode).toBe(0); + }); + + test("class expression in a static field of a class created in the loop", async () => { + // The static initializer runs once per evaluation of the outer class, + // i.e. once per iteration, and the inner class's temporaries are + // declared in the loop body: a class body does not end the loop. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) {} + const outers = []; + for (let i = 0; i < 2; i++) { + class Outer { + static Inner = class { @dec accessor x = i; }; + } + outers.push(Outer); + } + console.log(new outers[0].Inner().x, new outers[1].Inner().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("0 1\n"); + expect(exitCode).toBe(0); + }); + }); });