From 499d2d31f01fecdb8ecf4484ea8f2737992dc54d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 07:49:39 +0100 Subject: [PATCH 01/12] feat(transpiler): implement TC39 standard ES decorators lowering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement complete lowering of TC39 stage-3 standard decorators (the non-legacy variant used when tsconfig has no `experimentalDecorators`). Passes all 147 esbuild decorator tests plus 22 additional Bun-specific decorator tests. Features implemented: - Method, getter, setter decorators (static and instance) - Field decorators with initializer replacement and extra initializers - Auto-accessor lowering (accessor keyword → WeakMap + getter/setter) - Private member decorators (methods, fields, accessors via WeakMap/WeakSet) - Class decorators (statement and expression positions) - Class expression decorators with comma-expression lowering - Decorator metadata (Symbol.metadata) - Proper decorator evaluation order (source order per TC39 spec) - Class binding semantics (inner vs outer class name bindings) - Static block extraction with this-replacement - Computed property key pre-evaluation Runtime helpers added to src/runtime.js: __decoratorStart, __decorateElement, __decoratorMetadata, __runInitializers Fixes #4122 Fixes #20206 Fixes #14529 Fixes #6051 Co-Authored-By: Claude --- src/ast/G.zig | 2 + src/ast/P.zig | 2040 ++++++++++++++++- src/ast/parse.zig | 7 +- src/ast/parsePrefix.zig | 54 +- src/ast/parseProperty.zig | 14 +- src/ast/parseStmt.zig | 7 +- src/ast/parseTypescript.zig | 108 +- src/ast/visit.zig | 27 + src/ast/visitBinaryExpression.zig | 9 + src/ast/visitExpr.zig | 39 + src/ast/visitStmt.zig | 5 +- src/bun.js/ModuleLoader.zig | 1 + src/bun.js/RuntimeTranspilerStore.zig | 1 + src/bundler/ParseTask.zig | 3 + src/js_lexer_tables.zig | 2 + src/js_printer.zig | 7 +- src/options.zig | 1 + src/resolver/resolver.zig | 4 +- src/resolver/tsconfig_json.zig | 8 + src/runtime.js | 117 + src/runtime.zig | 23 + src/transpiler.zig | 4 + .../transpiler/es-decorators-esbuild.test.ts | 199 ++ test/bundler/transpiler/es-decorators.test.ts | 475 ++++ 24 files changed, 3135 insertions(+), 22 deletions(-) create mode 100644 test/bundler/transpiler/es-decorators-esbuild.test.ts create mode 100644 test/bundler/transpiler/es-decorators.test.ts diff --git a/src/ast/G.zig b/src/ast/G.zig index 41f141ef2375..0dee1eb1c6ab 100644 --- a/src/ast/G.zig +++ b/src/ast/G.zig @@ -32,6 +32,7 @@ pub const Class = struct { close_brace_loc: logger.Loc = logger.Loc.Empty, properties: []Property = &([_]Property{}), has_decorators: bool = false, + should_lower_standard_decorators: bool = false, pub fn canBeMoved(this: *const Class) bool { if (this.extends != null) @@ -134,6 +135,7 @@ pub const Property = struct { declare, abstract, class_static_block, + auto_accessor, pub fn jsonStringify(self: @This(), writer: anytype) !void { return try writer.write(@tagName(self)); diff --git a/src/ast/P.zig b/src/ast/P.zig index 5dbcb1b03bce..188dc2897a4e 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -99,6 +99,7 @@ pub fn NewParser_( pub const parseStmtsUpTo = parse_zig.parseStmtsUpTo; pub const parseAsyncPrefixExpr = parse_zig.parseAsyncPrefixExpr; pub const parseTypeScriptDecorators = parse_zig.parseTypeScriptDecorators; + pub const parseStandardDecorator = parse_zig.parseStandardDecorator; pub const parseTypeScriptNamespaceStmt = parse_zig.parseTypeScriptNamespaceStmt; pub const parseTypeScriptImportEqualsStmt = parse_zig.parseTypeScriptImportEqualsStmt; pub const parseTypescriptEnumStmt = parse_zig.parseTypescriptEnumStmt; @@ -486,6 +487,10 @@ pub fn NewParser_( /// Used for react refresh, it must be able to insert `const _s = $RefreshSig$();` nearest_stmt_list: ?*ListManaged(Stmt) = null, + /// Name from assignment context for anonymous decorated class expressions. + /// Set before visitExpr, consumed by lowerStandardDecoratorsImpl. + decorator_class_name: ?[]const u8 = null, + const RecentlyVisitedTSNamespace = struct { expr: Expr.Data = Expr.empty.data, map: ?*js_ast.TSNamespaceMemberMap = null, @@ -4851,12 +4856,2045 @@ pub fn NewParser_( stmts.append(closure) catch unreachable; } + /// Info about a lowered private member. + const PrivateLoweredInfo = struct { + storage_ref: Ref, // WeakMap (fields) or WeakSet (methods/getters/setters) ref + method_fn_ref: ?Ref = null, // For k=1 (method): ref to decorated function variable + getter_fn_ref: ?Ref = null, // For k=2 (getter): ref to decorated getter function variable + setter_fn_ref: ?Ref = null, // For k=3 (setter): ref to decorated setter function variable + accessor_desc_ref: ?Ref = null, // For k=4 private accessor: ref to captured descriptor + }; + + /// Map type for tracking private members that need lowering to WeakMap/WeakSet. + /// Maps private identifier inner_index -> storage info. + const PrivateLoweredMap = std.AutoHashMapUnmanaged(u32, PrivateLoweredInfo); + + /// Recursively walk an expression tree and rewrite private member accesses + /// that have been lowered to WeakMap/WeakSet storage. + fn rewritePrivateAccessesInExpr(p: *P, expr: *Expr, map: *const PrivateLoweredMap) void { + switch (expr.data) { + .e_index => |e| { + // Recurse into target first + p.rewritePrivateAccessesInExpr(&e.target, map); + // Check if index is a private identifier that needs lowering + if (e.index.data == .e_private_identifier) { + if (map.get(e.index.data.e_private_identifier.ref.innerIndex())) |info| { + if (info.accessor_desc_ref) |desc_ref| { + // Private auto-accessor: obj.#prop → __privateGet(obj, _wm, _acc.get) + const args = bun.handleOom(p.allocator.alloc(Expr, 3)); + args[0] = e.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + p.recordUsage(desc_ref); + args[2] = p.newExpr(E.Dot{ + .target = p.newExpr(E.Identifier{ .ref = desc_ref }, expr.loc), + .name = "get", + .name_loc = expr.loc, + }, expr.loc); + expr.* = p.callRuntime(expr.loc, "__privateGet", args); + } else if (info.getter_fn_ref) |fn_ref| { + // Private getter: obj.#prop → __privateGet(obj, _ws, _getter_fn) + const args = bun.handleOom(p.allocator.alloc(Expr, 3)); + args[0] = e.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + p.recordUsage(fn_ref); + args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); + expr.* = p.callRuntime(expr.loc, "__privateGet", args); + } else if (info.method_fn_ref) |fn_ref| { + // Private method: obj.#method → __privateMethod(obj, _ws, _fn) + const args = bun.handleOom(p.allocator.alloc(Expr, 3)); + args[0] = e.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + p.recordUsage(fn_ref); + args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); + expr.* = p.callRuntime(expr.loc, "__privateMethod", args); + } else { + // Private field/accessor: obj.#name → __privateGet(obj, _wm) + const args = bun.handleOom(p.allocator.alloc(Expr, 2)); + args[0] = e.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + expr.* = p.callRuntime(expr.loc, "__privateGet", args); + } + return; + } + } + p.rewritePrivateAccessesInExpr(&e.index, map); + }, + .e_binary => |e| { + // Check for assignment to private member: obj.#name = val + if (e.op == .bin_assign and e.left.data == .e_index) { + if (e.left.data.e_index.index.data == .e_private_identifier) { + if (map.get(e.left.data.e_index.index.data.e_private_identifier.ref.innerIndex())) |info| { + p.rewritePrivateAccessesInExpr(&e.left.data.e_index.target, map); + p.rewritePrivateAccessesInExpr(&e.right, map); + if (info.accessor_desc_ref) |desc_ref| { + // Private auto-accessor: obj.#prop = val → __privateSet(obj, _wm, val, _acc.set) + const args = bun.handleOom(p.allocator.alloc(Expr, 4)); + args[0] = e.left.data.e_index.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + args[2] = e.right; + p.recordUsage(desc_ref); + args[3] = p.newExpr(E.Dot{ + .target = p.newExpr(E.Identifier{ .ref = desc_ref }, expr.loc), + .name = "set", + .name_loc = expr.loc, + }, expr.loc); + expr.* = p.callRuntime(expr.loc, "__privateSet", args); + } else if (info.setter_fn_ref) |fn_ref| { + // Private setter: obj.#prop = val → __privateSet(obj, _ws, val, _setter_fn) + const args = bun.handleOom(p.allocator.alloc(Expr, 4)); + args[0] = e.left.data.e_index.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + args[2] = e.right; + p.recordUsage(fn_ref); + args[3] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); + expr.* = p.callRuntime(expr.loc, "__privateSet", args); + } else { + // Private field: obj.#name = val → __privateSet(obj, _wm, val) + const args = bun.handleOom(p.allocator.alloc(Expr, 3)); + args[0] = e.left.data.e_index.target; + p.recordUsage(info.storage_ref); + args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + args[2] = e.right; + expr.* = p.callRuntime(expr.loc, "__privateSet", args); + } + return; + } + } + } + // Check for #name in obj + if (e.op == .bin_in and e.left.data == .e_private_identifier) { + if (map.get(e.left.data.e_private_identifier.ref.innerIndex())) |info| { + p.rewritePrivateAccessesInExpr(&e.right, map); + const args = bun.handleOom(p.allocator.alloc(Expr, 2)); + p.recordUsage(info.storage_ref); + args[0] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + args[1] = e.right; + expr.* = p.callRuntime(expr.loc, "__privateIn", args); + return; + } + } + p.rewritePrivateAccessesInExpr(&e.left, map); + p.rewritePrivateAccessesInExpr(&e.right, map); + }, + .e_unary => |e| { + p.rewritePrivateAccessesInExpr(&e.value, map); + }, + .e_call => |e| { + // Check for obj.#method(args) → __privateMethod(obj, _ws, _fn).call(obj, args) + // or obj.#field(args) → __privateGet(obj, _wm).call(obj, args) + if (e.target.data == .e_index) { + if (e.target.data.e_index.index.data == .e_private_identifier) { + if (map.get(e.target.data.e_index.index.data.e_private_identifier.ref.innerIndex())) |info| { + p.rewritePrivateAccessesInExpr(&e.target.data.e_index.target, map); + const obj_expr = e.target.data.e_index.target; + // Build the accessor call + const private_access = brk: { + if (info.getter_fn_ref) |fn_ref| { + // Private getter called as function: __privateGet(obj, _ws, _getter_fn) + const get_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + get_args[0] = obj_expr; + p.recordUsage(info.storage_ref); + get_args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + p.recordUsage(fn_ref); + get_args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); + break :brk p.callRuntime(expr.loc, "__privateGet", get_args); + } else if (info.method_fn_ref) |fn_ref| { + // Private method: __privateMethod(obj, _ws, _fn) + const method_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + method_args[0] = obj_expr; + p.recordUsage(info.storage_ref); + method_args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + p.recordUsage(fn_ref); + method_args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); + break :brk p.callRuntime(expr.loc, "__privateMethod", method_args); + } else { + // Private field/accessor: __privateGet(obj, _wm) + const get_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + get_args[0] = obj_expr; + p.recordUsage(info.storage_ref); + get_args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); + break :brk p.callRuntime(expr.loc, "__privateGet", get_args); + } + }; + // Build .call(obj, ...original_args) + const call_target = p.newExpr(E.Dot{ + .target = private_access, + .name = "call", + .name_loc = expr.loc, + }, expr.loc); + const orig_args = e.args.slice(); + const new_args = bun.handleOom(p.allocator.alloc(Expr, 1 + orig_args.len)); + new_args[0] = obj_expr; // 'this' for .call + for (orig_args, 0..) |*arg, ai| { + p.rewritePrivateAccessesInExpr(arg, map); + new_args[1 + ai] = arg.*; + } + e.target = call_target; + e.args = ExprNodeList.fromOwnedSlice(new_args); + return; + } + } + } + p.rewritePrivateAccessesInExpr(&e.target, map); + for (e.args.slice()) |*arg| { + p.rewritePrivateAccessesInExpr(arg, map); + } + }, + .e_dot => |e| { + p.rewritePrivateAccessesInExpr(&e.target, map); + }, + .e_spread => |e| { + p.rewritePrivateAccessesInExpr(&e.value, map); + }, + .e_if => |e| { + p.rewritePrivateAccessesInExpr(&e.test_, map); + p.rewritePrivateAccessesInExpr(&e.yes, map); + p.rewritePrivateAccessesInExpr(&e.no, map); + }, + .e_await => |e| { + p.rewritePrivateAccessesInExpr(&e.value, map); + }, + .e_yield => |e| { + if (e.value) |*v| p.rewritePrivateAccessesInExpr(v, map); + }, + .e_new => |e| { + p.rewritePrivateAccessesInExpr(&e.target, map); + for (e.args.slice()) |*arg| { + p.rewritePrivateAccessesInExpr(arg, map); + } + }, + .e_array => |e| { + for (e.items.slice()) |*item| { + p.rewritePrivateAccessesInExpr(item, map); + } + }, + .e_object => |e| { + for (e.properties.slice()) |*prop| { + if (prop.value) |*v| p.rewritePrivateAccessesInExpr(v, map); + if (prop.initializer) |*ini| p.rewritePrivateAccessesInExpr(ini, map); + } + }, + .e_template => |e| { + if (e.tag) |*t| p.rewritePrivateAccessesInExpr(t, map); + for (e.parts) |*part| { + p.rewritePrivateAccessesInExpr(&part.value, map); + } + }, + .e_function => |e| { + p.rewritePrivateAccessesInStmts(e.func.body.stmts, map); + }, + .e_arrow => |e| { + p.rewritePrivateAccessesInStmts(e.body.stmts, map); + }, + else => {}, + } + } + + /// Recursively walk a statement list and rewrite private member accesses. + fn rewritePrivateAccessesInStmts(p: *P, stmts: []Stmt, map: *const PrivateLoweredMap) void { + for (stmts) |*stmt_item| { + switch (stmt_item.data) { + .s_expr => |data| p.rewritePrivateAccessesInExpr(&data.value, map), + .s_return => |data| { + if (data.value) |*v| p.rewritePrivateAccessesInExpr(v, map); + }, + .s_throw => |data| p.rewritePrivateAccessesInExpr(&data.value, map), + .s_local => |data| { + for (data.decls.slice()) |*decl| { + if (decl.value) |*v| p.rewritePrivateAccessesInExpr(v, map); + } + }, + .s_if => |data| { + p.rewritePrivateAccessesInExpr(&data.test_, map); + p.rewritePrivateAccessesInStmts((&data.yes)[0..1], map); + if (data.no) |*no| p.rewritePrivateAccessesInStmts(no[0..1], map); + }, + .s_block => |data| p.rewritePrivateAccessesInStmts(data.stmts, map), + .s_for => |data| { + if (data.init) |*for_init| p.rewritePrivateAccessesInStmts(for_init[0..1], map); + if (data.test_) |*t| p.rewritePrivateAccessesInExpr(t, map); + if (data.update) |*u| p.rewritePrivateAccessesInExpr(u, map); + p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); + }, + .s_for_in => |data| { + p.rewritePrivateAccessesInExpr(&data.value, map); + p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); + }, + .s_for_of => |data| { + p.rewritePrivateAccessesInExpr(&data.value, map); + p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); + }, + .s_while => |data| { + p.rewritePrivateAccessesInExpr(&data.test_, map); + p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); + }, + .s_do_while => |data| { + p.rewritePrivateAccessesInExpr(&data.test_, map); + p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); + }, + .s_switch => |data| { + p.rewritePrivateAccessesInExpr(&data.test_, map); + for (data.cases) |*case| { + if (case.value) |*v| p.rewritePrivateAccessesInExpr(v, map); + p.rewritePrivateAccessesInStmts(case.body, map); + } + }, + .s_try => |data| { + p.rewritePrivateAccessesInStmts(data.body, map); + if (data.catch_) |c| p.rewritePrivateAccessesInStmts(c.body, map); + if (data.finally) |f| p.rewritePrivateAccessesInStmts(f.stmts, map); + }, + .s_label => |data| p.rewritePrivateAccessesInStmts((&data.stmt)[0..1], map), + .s_with => |data| { + p.rewritePrivateAccessesInExpr(&data.value, map); + p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); + }, + else => {}, + } + } + } + + /// Lower standard (TC39) decorators on a class statement. + /// Emits: var _init = __decoratorStart(base); class Foo { ... } __decorateElement(...); ... + fn lowerStandardDecoratorsStmt(p: *P, stmt: Stmt) []Stmt { + return p.lowerStandardDecoratorsImpl(stmt, .stmt); + } + + pub fn lowerStandardDecoratorsExpr(p: *P, class: *G.Class, loc: logger.Loc, name_from_context: ?[]const u8) Expr { + const result = p.lowerStandardDecoratorsImpl(Stmt.empty(), .{ .expr = .{ .class = class, .loc = loc, .name_from_context = name_from_context } }); + if (result.len == 0) return p.newExpr(E.Missing{}, loc); + // For expressions, result[0] contains the comma expression + return result[0].data.s_expr.value; + } + + const StdDecMode = union(enum) { + stmt, + expr: struct { + class: *G.Class, + loc: logger.Loc, + name_from_context: ?[]const u8 = null, + }, + }; + + /// Core lowering logic for standard (TC39) decorators. + /// Handles both statement and expression forms. + fn lowerStandardDecoratorsImpl(p: *P, stmt: Stmt, mode: StdDecMode) []Stmt { + const is_expr = mode == .expr; + var class = switch (mode) { + .stmt => &stmt.data.s_class.class, + .expr => |e| e.class, + }; + const loc = switch (mode) { + .stmt => stmt.loc, + .expr => |e| e.loc, + }; + const name_from_context: ?[]const u8 = switch (mode) { + .expr => |e| e.name_from_context, + .stmt => null, + }; + + // For expressions, create a temp variable to capture the class + var class_name_ref: Ref = undefined; + var class_name_loc: logger.Loc = undefined; + var expr_class_ref: ?Ref = null; + var expr_class_is_anonymous = false; + + // For expressions, collect refs that need var declarations + var expr_var_decls = ListManaged(G.Decl).init(p.allocator); + + if (is_expr) { + // Create a temp ref for the class expression value + expr_class_ref = p.newSymbol(.other, "_class") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, expr_class_ref.?)); + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = expr_class_ref.? }, loc) }) catch unreachable; + + // For class expressions, create or use the class name binding + if (class.class_name) |cn| { + class_name_ref = cn.ref.?; + class_name_loc = cn.loc; + // Don't declare class_name_ref (e.g., Foo) as var here. + // For class binding semantics, Foo must remain undeclared in the + // outer scope so that class decorator closures over Foo get + // ReferenceError. We'll swap class_name_ref to expr_class_ref + // after pre-eval replacement for all suffix operations. + } else { + // Anonymous class: use _class ref for both class value and name target + class_name_ref = expr_class_ref.?; + class_name_loc = loc; + expr_class_is_anonymous = true; + + // If we have a name from assignment context (e.g., { Foo: @dec class {} }), + // set the class name so the class expression preserves .name property + if (name_from_context) |name| { + const name_ref = p.newSymbol(.other, name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, name_ref)); + class.class_name = .{ .ref = name_ref, .loc = loc }; + } + } + } else { + class_name_ref = class.class_name.?.ref.?; + class_name_loc = class.class_name.?.loc; + } + + // For proper class binding semantics (TC39 15.7.14): element decorators + // should capture an inner class name binding that survives mutation of the + // outer class name binding. In statement mode, we create an inner `let _Foo = Foo` + // after the suffix. In expression mode, element decorators use expr_class_ref. + // Element decorator pre-eval expressions will have class_name_ref replaced + // with inner_class_ref so their closures capture the inner binding. + var inner_class_ref: Ref = class_name_ref; + if (!is_expr) { + const class_name_str = p.symbols.items[class_name_ref.innerIndex()].original_name; + const inner_name = std.fmt.allocPrint(p.allocator, "_{s}", .{class_name_str}) catch unreachable; + inner_class_ref = p.newSymbol(.other, inner_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, inner_class_ref)); + } + + // Save and clear class-level decorators + const class_decorators = class.ts_decorators; + class.ts_decorators = .{}; + + // Create the _init symbol for the decorator context + const init_ref = p.newSymbol(.other, "_init") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, init_ref)); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = init_ref }, loc) }) catch unreachable; + } + + // If class extends a base, extract the extends expression into a temp variable + // to avoid double evaluation (once for __decoratorStart, once for class extends). + // Input: class Foo extends (expr) { ... } + // Output: var _base = expr; var _init = __decoratorStart(_base); class Foo extends _base { ... } + var base_ref: ?Ref = null; + if (class.extends != null) { + base_ref = p.newSymbol(.other, "_base") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, base_ref.?)); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = base_ref.? }, loc) }) catch unreachable; + } + } + + // === Pre-evaluate decorator expressions in source order === + // TC39 spec requires all decorator expressions and computed property keys + // to be evaluated in source order before the class body. We extract them + // into temp variables in the prefix. + var dec_counter: usize = 0; + + // Pre-evaluate class decorator expressions (evaluated before extends) + var class_dec_ref: ?Ref = null; + var class_dec_stmt: Stmt = Stmt.empty(); + var class_dec_assign_expr: ?Expr = null; + if (class_decorators.len > 0) { + dec_counter += 1; + class_dec_ref = p.newSymbol(.other, "_dec") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, class_dec_ref.?)); + const class_dec_array = p.newExpr(E.Array{ .items = class_decorators }, loc); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = class_dec_ref.? }, loc) }) catch unreachable; + p.recordUsage(class_dec_ref.?); + class_dec_assign_expr = Expr.assign( + p.newExpr(E.Identifier{ .ref = class_dec_ref.? }, loc), + class_dec_array, + ); + } else { + const decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = class_dec_ref.? }, loc), + .value = class_dec_array, + }; + class_dec_stmt = p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(decls) }, loc); + } + } + + // Pre-evaluate property decorator expressions and computed keys + // Maps property index (in class.properties) to its pre-evaluated decorator ref + var prop_dec_refs = std.AutoHashMapUnmanaged(usize, Ref){}; + var computed_key_refs = std.AutoHashMapUnmanaged(usize, Ref){}; + // Collect all pre-evaluation declarations in source order + var pre_eval_stmts = ListManaged(Stmt).init(p.allocator); + var computed_key_counter: usize = 0; + for (class.properties, 0..) |*prop, prop_idx| { + if (prop.kind == .class_static_block) continue; + + // Pre-evaluate decorator expressions for this property + if (prop.ts_decorators.len > 0) { + dec_counter += 1; + const dec_name = if (dec_counter == 1) + "_dec" + else + std.fmt.allocPrint(p.allocator, "_dec{d}", .{dec_counter}) catch unreachable; + const dec_ref = p.newSymbol(.other, dec_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, dec_ref)); + prop_dec_refs.put(p.allocator, prop_idx, dec_ref) catch unreachable; + + const dec_array = p.newExpr(E.Array{ .items = prop.ts_decorators }, loc); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = dec_ref }, loc) }) catch unreachable; + } + const dec_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + dec_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = dec_ref }, loc), + .value = dec_array, + }; + pre_eval_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(dec_decls) }, loc)) catch unreachable; + } + + // Pre-evaluate computed property keys (for decorated properties or properties + // in decorated classes where source-order evaluation matters) + if (prop.flags.contains(.is_computed) and prop.key != null and prop.ts_decorators.len > 0) { + computed_key_counter += 1; + const key_name = if (computed_key_counter == 1) + "_computedKey" + else + std.fmt.allocPrint(p.allocator, "_computedKey{d}", .{computed_key_counter}) catch unreachable; + const key_ref = p.newSymbol(.other, key_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, key_ref)); + computed_key_refs.put(p.allocator, prop_idx, key_ref) catch unreachable; + + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = key_ref }, loc) }) catch unreachable; + } + const key_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + key_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = key_ref }, loc), + .value = prop.key.?, + }; + pre_eval_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(key_decls) }, loc)) catch unreachable; + + // Replace the property key with the temp variable reference + p.recordUsage(key_ref); + prop.key = p.newExpr(E.Identifier{ .ref = key_ref }, prop.key.?.loc); + // Computed keys referenced via temp var are no longer "computed" in the + // traditional sense (they're simple identifier lookups), but we keep the + // flag so downstream code still treats them as dynamic keys. + } + } + + // Replace class name refs in element decorator pre-eval expressions. + // Element decorators should capture the inner class binding, not the outer one. + { + const replacement_ref = if (is_expr) (expr_class_ref orelse class_name_ref) else inner_class_ref; + if (!replacement_ref.eql(class_name_ref)) { + for (pre_eval_stmts.items) |*pre_stmt| { + switch (pre_stmt.data) { + .s_local => |local| { + for (local.decls.slice()) |*decl| { + if (decl.value) |*v| { + p.replaceRefInExpr(v, class_name_ref, replacement_ref); + } + } + }, + else => {}, + } + } + } + } + + // For named class expressions: swap class_name_ref to expr_class_ref so that + // all suffix operations (class decorator assignment, __runInitializers, metadata) + // use expr_class_ref (_class) instead of the original class name (Foo). + // This keeps Foo undeclared in the outer scope, so class decorator closures + // over Foo correctly throw ReferenceError. + var original_class_name_for_decorator: ?[]const u8 = null; + if (is_expr and !expr_class_is_anonymous and expr_class_ref != null) { + original_class_name_for_decorator = p.symbols.items[class_name_ref.inner_index].original_name; + class_name_ref = expr_class_ref.?; + class_name_loc = loc; + } + + // Build: var _init = __decoratorStart(base) (for statements) + // or: _init = __decoratorStart(base) (for expressions, part of comma chain) + const init_start_expr: Expr = brk: { + const base_expr = if (base_ref) |br| + p.newExpr(E.Identifier{ .ref = br }, loc) + else + p.newExpr(E.Undefined{}, loc); + + const start_args = bun.handleOom(p.allocator.alloc(Expr, 1)); + start_args[0] = base_expr; + break :brk p.callRuntime(loc, "__decoratorStart", start_args); + }; + + // For statement mode: base decl goes before init decl + var base_decl_stmt: Stmt = Stmt.empty(); + if (!is_expr) { + if (base_ref) |br| { + const base_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + base_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = br }, loc), + .value = class.extends, + }; + base_decl_stmt = p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(base_decls) }, loc); + } + } + + // For expressions: store base assignment to be added to comma chain later + const base_assign_expr: ?Expr = if (is_expr and base_ref != null) brk: { + p.recordUsage(base_ref.?); + break :brk Expr.assign( + p.newExpr(E.Identifier{ .ref = base_ref.? }, loc), + class.extends.?, + ); + } else null; + + // Update class.extends to use the temp variable + if (base_ref) |br| { + p.recordUsage(br); + class.extends = p.newExpr(E.Identifier{ .ref = br }, loc); + } + + const init_decl_stmt: Stmt = if (!is_expr) brk: { + const decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = init_ref }, loc), + .value = init_start_expr, + }; + break :brk p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(decls) }, loc); + } else Stmt.empty(); + + // Collect suffix expressions to be emitted as statements after the class + var suffix_exprs = ListManaged(Expr).init(p.allocator); + + // Collect statements to inject into the constructor (for instance fields/methods) + var constructor_inject_stmts = ListManaged(Stmt).init(p.allocator); + + // New class properties (decorated fields removed from class body) + var new_properties = ListManaged(Property).init(p.allocator); + + // Categorize decorated elements for __decorateElement ordering + // Per TC39 spec, the order is: static non-fields → instance non-fields → static fields → instance fields + // Accessors (k=4) go in the NON-FIELD group for __decorateElement ordering, + // but need field-like init/extra-init tracking. + var static_non_field_elements = ListManaged(Expr).init(p.allocator); + var instance_non_field_elements = ListManaged(Expr).init(p.allocator); + // Track private methods separately (they go in prefix, not suffix) + var has_static_private_methods = false; + var has_instance_private_methods = false; + + // Separate field-only __decorateElement lists (k=5, NOT accessors) + var static_field_decorate = ListManaged(Expr).init(p.allocator); + var instance_field_decorate = ListManaged(Expr).init(p.allocator); + + // Accessor counts for field_idx computation + var static_accessor_count: usize = 0; + var instance_accessor_count: usize = 0; + + // Combined field/accessor init entries in source order (for init/extra-init) + const FieldInitEntry = struct { + prop: Property, + is_private: bool, + is_accessor: bool, + }; + var static_init_entries = ListManaged(FieldInitEntry).init(p.allocator); + var instance_init_entries = ListManaged(FieldInitEntry).init(p.allocator); + + // Track source order of static elements (blocks + field/accessor inits) + const StaticElementKind = enum { block, field_or_accessor }; + const StaticElement = struct { + kind: StaticElementKind, + index: usize, // index into static_init_entries (for field_or_accessor) or extracted_static_blocks + }; + var static_element_order = ListManaged(StaticElement).init(p.allocator); + var extracted_static_blocks = ListManaged(*G.ClassStaticBlock).init(p.allocator); + + // For private members, track WeakMap/WeakSet refs and prefix statements + var prefix_stmts = ListManaged(Stmt).init(p.allocator); + + // Map private identifier refs to their WeakMap/WeakSet storage refs + // Used to rewrite this.#x → __privateGet(this, _x) in the class body + var private_lowered_map = PrivateLoweredMap{}; + + // Counter for unique accessor storage names (used for computed keys) + var accessor_storage_counter: usize = 0; + + // Track which private names already had __privateAdd emitted (for getter+setter pairs) + var emitted_private_adds = std.AutoHashMapUnmanaged(u32, void){}; + // Static private __privateAdd calls need to be static blocks at the START of the class body + var static_private_add_blocks = ListManaged(Property).init(p.allocator); + + // Pre-scan: if any private member is decorated OR if the class has both + // private members and decorated members, ALL private members need lowering. + // This is because: + // 1. Extracted decorator functions may reference other private members + // 2. Decorator expressions pre-evaluated outside the class may reference + // private names (e.g., `@(dec(() => new Foo().#priv)) method() {}`) + var lower_all_private = false; + { + var has_any_private = false; + var has_any_decorated = false; + for (class.properties) |cprop| { + if (cprop.kind == .class_static_block) continue; + if (cprop.ts_decorators.len > 0) { + has_any_decorated = true; + if (cprop.key != null and cprop.key.?.data == .e_private_identifier) { + // If a private member itself is decorated, always lower + lower_all_private = true; + break; + } + } + if (cprop.key != null and cprop.key.?.data == .e_private_identifier) { + has_any_private = true; + } + } + // Also lower if the class has both private members and decorated members, + // since decorator expressions may reference private names + if (!lower_all_private and has_any_private and has_any_decorated) { + lower_all_private = true; + } + } + + // Process each property + for (class.properties, 0..) |*prop, prop_idx| { + if (prop.ts_decorators.len == 0) { + // Non-decorated property + if (lower_all_private and prop.key != null and prop.key.?.data == .e_private_identifier and prop.kind != .class_static_block) { + // Lower this non-decorated private member to WeakMap/WeakSet + // so that references from extracted decorated functions can be rewritten + const nk_expr = prop.key.?; + const npriv_orig = p.symbols.items[nk_expr.data.e_private_identifier.ref.inner_index].original_name; + const npriv_inner = nk_expr.data.e_private_identifier.ref.innerIndex(); + + if (prop.flags.contains(.is_method)) { + // Non-decorated private method/getter/setter: lower to WeakSet + const nk: u8 = switch (prop.kind) { + .get => 2, + .set => 3, + else => 1, + }; + const existing2 = private_lowered_map.get(npriv_inner); + const ws_ref2 = if (existing2) |ex| ex.storage_ref else brk2: { + const ws_nm = std.fmt.allocPrint(p.allocator, "_{s}", .{npriv_orig[1..]}) catch unreachable; + const ref2 = p.newSymbol(.other, ws_nm) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, ref2)); + break :brk2 ref2; + }; + const fn_sfx = if (nk == 2) "_get" else if (nk == 3) "_set" else "_fn"; + const fn_nm = std.fmt.allocPrint(p.allocator, "_{s}{s}", .{ npriv_orig[1..], fn_sfx }) catch unreachable; + const fn_ref2 = p.newSymbol(.other, fn_nm) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, fn_ref2)); + + var new_info2 = if (existing2) |ex| ex else PrivateLoweredInfo{ .storage_ref = ws_ref2 }; + if (nk == 1) { + new_info2.method_fn_ref = fn_ref2; + } else if (nk == 2) { + new_info2.getter_fn_ref = fn_ref2; + } else { + new_info2.setter_fn_ref = fn_ref2; + } + private_lowered_map.put(p.allocator, npriv_inner, new_info2) catch unreachable; + + if (existing2 == null) { + // Create WeakSet declaration + const ws_gr = p.newSymbol(.unbound, "WeakSet") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, ws_gr)); + const ws_new2 = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = ws_gr }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const ws_d = bun.handleOom(p.allocator.alloc(G.Decl, 2)); + ws_d[0] = .{ .binding = p.b(B.Identifier{ .ref = ws_ref2 }, loc), .value = ws_new2 }; + ws_d[1] = .{ .binding = p.b(B.Identifier{ .ref = fn_ref2 }, loc), .value = null }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(ws_d) }, loc)) catch unreachable; + } else { + const fn_d = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + fn_d[0] = .{ .binding = p.b(B.Identifier{ .ref = fn_ref2 }, loc), .value = null }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(fn_d) }, loc)) catch unreachable; + } + + // Assign the function: _fn = function() { ... } + p.recordUsage(fn_ref2); + const fn_assign = Expr.assign( + p.newExpr(E.Identifier{ .ref = fn_ref2 }, loc), + if (prop.value) |v| v else p.newExpr(E.Undefined{}, loc), + ); + prefix_stmts.append(p.s(S.SExpr{ .value = fn_assign }, loc)) catch unreachable; + + // Add __privateAdd (once per name) + if (!emitted_private_adds.contains(npriv_inner)) { + emitted_private_adds.put(p.allocator, npriv_inner, {}) catch unreachable; + if (!prop.flags.contains(.is_static)) { + const pa_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + pa_args[0] = p.newExpr(E.This{}, loc); + p.recordUsage(ws_ref2); + pa_args[1] = p.newExpr(E.Identifier{ .ref = ws_ref2 }, loc); + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", pa_args) }, loc), + ) catch unreachable; + } else { + const sa_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + sa_args[0] = p.newExpr(E.This{}, loc); + p.recordUsage(ws_ref2); + sa_args[1] = p.newExpr(E.Identifier{ .ref = ws_ref2 }, loc); + const add_call2 = p.callRuntime(loc, "__privateAdd", sa_args); + const blk_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); + blk_stmts[0] = p.s(S.SExpr{ .value = add_call2 }, loc); + const sb = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); + sb.* = .{ .loc = loc, .stmts = bun.BabyList(Stmt).fromOwnedSlice(blk_stmts) }; + static_private_add_blocks.append(.{ + .kind = .class_static_block, + .class_static_block = sb, + }) catch unreachable; + } + } + // Don't add to new_properties (removed from class body) + continue; + } else { + // Non-decorated private field: lower to WeakMap + const wm_nm = std.fmt.allocPrint(p.allocator, "_{s}", .{npriv_orig[1..]}) catch unreachable; + const wm_ref2 = p.newSymbol(.other, wm_nm) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref2)); + private_lowered_map.put(p.allocator, npriv_inner, .{ + .storage_ref = wm_ref2, + }) catch unreachable; + + const wm_new2 = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const wm_d = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + wm_d[0] = .{ .binding = p.b(B.Identifier{ .ref = wm_ref2 }, loc), .value = wm_new2 }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_d) }, loc)) catch unreachable; + + // Add __privateAdd(this/ClassName, _wm, initializer) to constructor/suffix + const init_val2 = if (prop.initializer) |iv| iv else p.newExpr(E.Undefined{}, loc); + if (!prop.flags.contains(.is_static)) { + const pa_args2 = bun.handleOom(p.allocator.alloc(Expr, 3)); + pa_args2[0] = p.newExpr(E.This{}, loc); + p.recordUsage(wm_ref2); + pa_args2[1] = p.newExpr(E.Identifier{ .ref = wm_ref2 }, loc); + pa_args2[2] = init_val2; + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", pa_args2) }, loc), + ) catch unreachable; + } else { + p.recordUsage(wm_ref2); + const sa_args2 = bun.handleOom(p.allocator.alloc(Expr, 3)); + sa_args2[0] = p.newExpr(E.This{}, loc); + sa_args2[1] = p.newExpr(E.Identifier{ .ref = wm_ref2 }, loc); + sa_args2[2] = init_val2; + const add_call3 = p.callRuntime(loc, "__privateAdd", sa_args2); + const blk_stmts2 = bun.handleOom(p.allocator.alloc(Stmt, 1)); + blk_stmts2[0] = p.s(S.SExpr{ .value = add_call3 }, loc); + const sb2 = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); + sb2.* = .{ .loc = loc, .stmts = bun.BabyList(Stmt).fromOwnedSlice(blk_stmts2) }; + static_private_add_blocks.append(.{ + .kind = .class_static_block, + .class_static_block = sb2, + }) catch unreachable; + } + // Don't add to new_properties (removed from class body) + continue; + } + } + // Check for undecorated auto-accessor that needs lowering + if (prop.kind == .auto_accessor) { + // Lower undecorated auto-accessor to WeakMap storage + getter/setter + const accessor_name = brk: { + if (prop.key.?.data == .e_string) { + break :brk std.fmt.allocPrint(p.allocator, "_{s}", .{prop.key.?.data.e_string.data}) catch unreachable; + } + const name = std.fmt.allocPrint(p.allocator, "_accessor_storage{d}", .{accessor_storage_counter}) catch unreachable; + accessor_storage_counter += 1; + break :brk name; + }; + const wm_ref = p.newSymbol(.other, accessor_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); + + const wm_new = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + wm_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), + .value = wm_new, + }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; + + // Create getter: get foo() { return __privateGet(this, _foo); } + const get_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + get_args[0] = p.newExpr(E.This{}, loc); + p.recordUsage(wm_ref); + get_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); + const get_body_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); + get_body_stmts[0] = p.s(S.Return{ .value = p.callRuntime(loc, "__privateGet", get_args) }, loc); + const get_fn = bun.handleOom(p.allocator.create(G.Fn)); + get_fn.* = .{ .body = .{ .stmts = get_body_stmts, .loc = loc } }; + + // Create setter: set foo(v) { __privateSet(this, _foo, v); } + const setter_param_ref = p.newSymbol(.other, "v") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, setter_param_ref)); + const set_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + set_args[0] = p.newExpr(E.This{}, loc); + p.recordUsage(wm_ref); + set_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); + p.recordUsage(setter_param_ref); + set_args[2] = p.newExpr(E.Identifier{ .ref = setter_param_ref }, loc); + const set_body_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); + set_body_stmts[0] = p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateSet", set_args) }, loc); + const setter_fn_args = bun.handleOom(p.allocator.alloc(G.Arg, 1)); + setter_fn_args[0] = .{ .binding = p.b(B.Identifier{ .ref = setter_param_ref }, loc) }; + const set_fn = bun.handleOom(p.allocator.create(G.Fn)); + set_fn.* = .{ .args = setter_fn_args, .body = .{ .stmts = set_body_stmts, .loc = loc } }; + + // Add getter and setter properties + var getter_flags = prop.flags; + getter_flags.insert(.is_method); + new_properties.append(.{ + .key = prop.key, + .value = p.newExpr(E.Function{ .func = get_fn.* }, loc), + .kind = .get, + .flags = getter_flags, + }) catch unreachable; + new_properties.append(.{ + .key = prop.key, + .value = p.newExpr(E.Function{ .func = set_fn.* }, loc), + .kind = .set, + .flags = getter_flags, + }) catch unreachable; + + // Add initialization: __privateAdd(this/ClassName, _wm, initValue) + const init_val = if (prop.initializer) |iv| iv else p.newExpr(E.Undefined{}, loc); + if (!prop.flags.contains(.is_static)) { + const add_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + add_args[0] = p.newExpr(E.This{}, loc); + p.recordUsage(wm_ref); + add_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); + add_args[2] = init_val; + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", add_args) }, loc), + ) catch unreachable; + } else { + p.recordUsage(wm_ref); + p.recordUsage(class_name_ref); + const sa_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + sa_args[0] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + sa_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); + sa_args[2] = init_val; + suffix_exprs.append(p.callRuntime(loc, "__privateAdd", sa_args)) catch unreachable; + } + continue; + } + + // Extract static blocks from class body - they'll be emitted in the suffix + // interleaved with static field/accessor inits in source order + if (prop.kind == .class_static_block) { + if (prop.class_static_block) |sb| { + static_element_order.append(.{ .kind = .block, .index = extracted_static_blocks.items.len }) catch unreachable; + extracted_static_blocks.append(sb) catch unreachable; + } + continue; + } + + new_properties.append(prop.*) catch unreachable; + continue; + } + + // Compute kind flag + var flags: f64 = 0; + if (prop.flags.contains(.is_method)) { + switch (prop.kind) { + .get => flags = 2, + .set => flags = 3, + else => flags = 1, // method + } + } else { + switch (prop.kind) { + .auto_accessor => flags = 4, + else => flags = 5, // field + } + } + + // Add static flag + if (prop.flags.contains(.is_static)) { + flags += 8; + } + + const is_private = prop.key.?.data == .e_private_identifier; + + // Add private flag + if (is_private) { + flags += 16; + } + + // Use pre-evaluated decorator array reference (for source-order evaluation) + const decorator_array = if (prop_dec_refs.get(prop_idx)) |dec_ref| brk: { + p.recordUsage(dec_ref); + break :brk p.newExpr(E.Identifier{ .ref = dec_ref }, loc); + } else p.newExpr(E.Array{ + .items = prop.ts_decorators, + }, loc); + + const key_expr = prop.key.?; + const k = @as(u8, @intFromFloat(flags)) & 7; + + // For private members, we need WeakMap/WeakSet storage and string name + var dec_arg_count: usize = 5; + var private_storage_ref: ?Ref = null; + var private_extra_ref: ?Ref = null; + var private_method_fn_ref: ?Ref = null; + + if (is_private) { + if (k >= 1 and k <= 3) { + // Private method/getter/setter: use WeakSet for membership tracking + const private_orig = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; + const priv_inner = key_expr.data.e_private_identifier.ref.innerIndex(); + + // Check if a WeakSet already exists for this private name (getter+setter pair) + const existing = private_lowered_map.get(priv_inner); + const ws_ref = if (existing) |ex| ex.storage_ref else brk: { + const ws_name = std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig[1..]}) catch unreachable; + const ref = p.newSymbol(.other, ws_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, ref)); + break :brk ref; + }; + private_storage_ref = ws_ref; + + // Create function variable with kind-specific suffix: _fn, _get, _set + const fn_suffix = if (k == 2) "_get" else if (k == 3) "_set" else "_fn"; + const fn_name = std.fmt.allocPrint(p.allocator, "_{s}{s}", .{ private_orig[1..], fn_suffix }) catch unreachable; + const fn_ref = p.newSymbol(.other, fn_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, fn_ref)); + private_method_fn_ref = fn_ref; + + // Update or create map entry + var new_info = if (existing) |ex| ex else PrivateLoweredInfo{ .storage_ref = ws_ref }; + if (k == 1) { + new_info.method_fn_ref = fn_ref; + } else if (k == 2) { + new_info.getter_fn_ref = fn_ref; + } else { + new_info.setter_fn_ref = fn_ref; + } + private_lowered_map.put(p.allocator, priv_inner, new_info) catch unreachable; + + if (existing == null) { + // First time seeing this private name: create WeakSet declaration + const ws_global_ref = p.newSymbol(.unbound, "WeakSet") catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, ws_global_ref)); + const ws_new = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = ws_global_ref }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const ws_decls = bun.handleOom(p.allocator.alloc(G.Decl, 2)); + ws_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = ws_ref }, loc), + .value = ws_new, + }; + ws_decls[1] = .{ + .binding = p.b(B.Identifier{ .ref = fn_ref }, loc), + .value = null, + }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(ws_decls) }, loc)) catch unreachable; + } else { + // Getter+setter pair: WeakSet already declared, just add fn variable + const fn_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + fn_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = fn_ref }, loc), + .value = null, + }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(fn_decls) }, loc)) catch unreachable; + } + + // For private methods, the function itself is passed as extra (6th arg) + dec_arg_count = 6; + } else if (k == 5) { + // Private field: use WeakMap for storage + const private_orig2 = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; + const wm_name = std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig2[1..]}) catch unreachable; + const wm_ref = p.newSymbol(.other, wm_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); + private_storage_ref = wm_ref; + private_lowered_map.put(p.allocator, key_expr.data.e_private_identifier.ref.innerIndex(), .{ + .storage_ref = wm_ref, + }) catch unreachable; + + // var _name = new WeakMap() + const wm_new = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + wm_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), + .value = wm_new, + }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; + dec_arg_count = 5; + } else if (k == 4) { + // Private auto-accessor: need WeakMap for the accessor storage + const private_orig3 = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; + const wm_name3 = std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig3[1..]}) catch unreachable; + const wm_ref = p.newSymbol(.other, wm_name3) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); + private_storage_ref = wm_ref; + + // Create descriptor variable: var _foo_acc; + const acc_name = std.fmt.allocPrint(p.allocator, "_{s}_acc", .{private_orig3[1..]}) catch unreachable; + const acc_ref = p.newSymbol(.other, acc_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, acc_ref)); + private_method_fn_ref = acc_ref; // Reuse to capture __decorateElement return + + private_lowered_map.put(p.allocator, key_expr.data.e_private_identifier.ref.innerIndex(), .{ + .storage_ref = wm_ref, + .accessor_desc_ref = acc_ref, + }) catch unreachable; + + const wm_new = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 2)); + wm_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), + .value = wm_new, + }; + wm_decls[1] = .{ + .binding = p.b(B.Identifier{ .ref = acc_ref }, loc), + .value = null, + }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; + dec_arg_count = 6; + } + } else if (k == 4) { + // Public auto-accessor: need WeakMap for the accessor storage + const accessor_name = brk: { + if (key_expr.data == .e_string) { + break :brk std.fmt.allocPrint(p.allocator, "_{s}", .{key_expr.data.e_string.data}) catch unreachable; + } + const name = std.fmt.allocPrint(p.allocator, "_accessor_storage{d}", .{accessor_storage_counter}) catch unreachable; + accessor_storage_counter += 1; + break :brk name; + }; + const wm_ref = p.newSymbol(.other, accessor_name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); + private_extra_ref = wm_ref; + + const wm_new = p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), + .args = ExprNodeList.empty, + .close_parens_loc = loc, + }, loc); + const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + wm_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), + .value = wm_new, + }; + prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; + dec_arg_count = 6; + } + + // For expression mode, use the _class temp ref (expr_class_ref) as the target, + // since class_name_ref (Foo) isn't assigned until the class decorator runs. + const target_ref = if (is_expr and expr_class_ref != null) expr_class_ref.? else class_name_ref; + p.recordUsage(target_ref); + const class_ident = p.newExpr(E.Identifier{ .ref = target_ref }, class_name_loc); + + // Build __decorateElement(_init, flags, name, [decorators], target, extra?) + const dec_args = bun.handleOom(p.allocator.alloc(Expr, dec_arg_count)); + dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + dec_args[1] = p.newExpr(E.Number{ .value = flags }, loc); + if (is_private) { + // For private members, pass the name as a string "#name" + const orig_name = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; + dec_args[2] = p.newExpr(E.String{ .data = orig_name }, loc); + } else { + dec_args[2] = key_expr; + } + dec_args[3] = decorator_array; + + if (is_private and private_storage_ref != null) { + // For private members, target is the WeakMap/WeakSet ref + p.recordUsage(private_storage_ref.?); + dec_args[4] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); + if (dec_arg_count == 6) { + if (k >= 1 and k <= 3) { + // Private method/getter/setter: pass the function as extra + dec_args[5] = if (prop.value) |v| v else p.newExpr(E.Undefined{}, loc); + } else if (k == 4) { + // Private auto-accessor: extra = same WeakMap as target + // (used for value storage via __privateGet/Set in the generated descriptor) + p.recordUsage(private_storage_ref.?); + dec_args[5] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); + } else { + dec_args[5] = p.newExpr(E.Undefined{}, loc); + } + } + } else { + dec_args[4] = class_ident; + if (dec_arg_count == 6) { + if (private_extra_ref) |extra_ref| { + // Public auto-accessor: pass WeakMap as extra + p.recordUsage(extra_ref); + dec_args[5] = p.newExpr(E.Identifier{ .ref = extra_ref }, loc); + } else { + dec_args[5] = p.newExpr(E.Undefined{}, loc); + } + } + } + + const raw_element = p.callRuntime(loc, "__decorateElement", dec_args); + + // For private methods, capture the return value: _fn = __decorateElement(...) + const element = if (private_method_fn_ref) |fn_ref| brk: { + p.recordUsage(fn_ref); + break :brk Expr.assign(p.newExpr(E.Identifier{ .ref = fn_ref }, loc), raw_element); + } else raw_element; + + // Categorize the element + if (k >= 4) { + // field (k=5) or accessor (k=4) - remove from class body + var prop_copy = prop.*; + prop_copy.ts_decorators = .{}; + if (is_private) { + // Store the private storage ref in the property for later use + if (private_storage_ref) |ps_ref| { + prop_copy.key = p.newExpr(E.Identifier{ .ref = ps_ref }, loc); + } + } + if (private_extra_ref) |pe_ref| { + // For auto-accessors, we store the extra ref through the value field + prop_copy.value = p.newExpr(E.Identifier{ .ref = pe_ref }, loc); + } + + const is_accessor = (k == 4); + const init_entry = FieldInitEntry{ + .prop = prop_copy, + .is_private = is_private, + .is_accessor = is_accessor, + }; + + if (prop.flags.contains(.is_static)) { + // __decorateElement ordering: accessors go with non-fields, fields go separate + if (is_accessor) { + static_non_field_elements.append(element) catch unreachable; + static_accessor_count += 1; + } else { + static_field_decorate.append(element) catch unreachable; + } + // Track init entry and source order for interleaving with static blocks + static_element_order.append(.{ + .kind = .field_or_accessor, + .index = static_init_entries.items.len, + }) catch unreachable; + static_init_entries.append(init_entry) catch unreachable; + } else { + if (is_accessor) { + instance_non_field_elements.append(element) catch unreachable; + instance_accessor_count += 1; + } else { + instance_field_decorate.append(element) catch unreachable; + } + instance_init_entries.append(init_entry) catch unreachable; + } + } else if (is_private and private_storage_ref != null) { + // Private method/getter/setter - remove from class body + // The function is passed to __decorateElement as the 6th arg (extra) + // and access goes through WeakSet via __privateGet/__privateMethod + + // Add __privateAdd (only once per unique private name, for getter+setter pairs) + const priv_inner2 = key_expr.data.e_private_identifier.ref.innerIndex(); + if (!emitted_private_adds.contains(priv_inner2)) { + emitted_private_adds.put(p.allocator, priv_inner2, {}) catch unreachable; + if (!prop.flags.contains(.is_static)) { + const private_add_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + private_add_args[0] = p.newExpr(E.This{}, loc); + p.recordUsage(private_storage_ref.?); + private_add_args[1] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", private_add_args) }, loc), + ) catch unreachable; + } else { + // Static: inject as static block at beginning of class body + // (must run before other static blocks that may access private members) + const static_add_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + static_add_args[0] = p.newExpr(E.This{}, loc); // 'this' = class in static block + p.recordUsage(private_storage_ref.?); + static_add_args[1] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); + const add_call = p.callRuntime(loc, "__privateAdd", static_add_args); + + const block_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); + block_stmts[0] = p.s(S.SExpr{ .value = add_call }, loc); + const static_block = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); + static_block.* = .{ .loc = loc, .stmts = bun.BabyList(Stmt).fromOwnedSlice(block_stmts) }; + static_private_add_blocks.append(.{ + .kind = .class_static_block, + .class_static_block = static_block, + }) catch unreachable; + } + } + + // Private method __decorateElement goes in suffix (same ordering as public) + // Static blocks are extracted to suffix, so __decorateElement doesn't need to be in prefix + if (prop.flags.contains(.is_static)) { + static_non_field_elements.append(element) catch unreachable; + has_static_private_methods = true; + } else { + instance_non_field_elements.append(element) catch unreachable; + has_instance_private_methods = true; + } + } else { + // Public method, getter, setter - keep in class body (but clear decorators) + var new_prop = prop.*; + new_prop.ts_decorators = .{}; + new_properties.append(new_prop) catch unreachable; + + if (prop.flags.contains(.is_static)) { + static_non_field_elements.append(element) catch unreachable; + } else { + instance_non_field_elements.append(element) catch unreachable; + } + } + } + + // === Rewrite private member accesses === + // If any private members were lowered to WeakMap/WeakSet, rewrite all + // references in the remaining class body (methods, field initializers) + // AND in prefix statements and extracted static blocks + if (private_lowered_map.count() > 0) { + // Rewrite in remaining class body (method bodies etc.) + for (new_properties.items) |*nprop| { + if (nprop.value) |*v| { + p.rewritePrivateAccessesInExpr(v, &private_lowered_map); + } + if (nprop.class_static_block) |sb| { + p.rewritePrivateAccessesInStmts(sb.stmts.slice(), &private_lowered_map); + } + } + // Rewrite in field/accessor initializers + for (instance_init_entries.items) |*entry| { + if (entry.prop.initializer) |*ini| { + p.rewritePrivateAccessesInExpr(ini, &private_lowered_map); + } + } + for (static_init_entries.items) |*entry| { + if (entry.prop.initializer) |*ini| { + p.rewritePrivateAccessesInExpr(ini, &private_lowered_map); + } + } + // Rewrite in extracted static blocks + for (extracted_static_blocks.items) |sb| { + p.rewritePrivateAccessesInStmts(sb.stmts.slice(), &private_lowered_map); + } + // Rewrite in __decorateElement calls (suffix non-field elements + field decorate lists). + // These contain function bodies that may reference other private names + // (e.g., a decorated getter `get #foo() { return this.#bar }` where #bar is non-decorated). + for (static_non_field_elements.items) |*elem| { + p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); + } + for (instance_non_field_elements.items) |*elem| { + p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); + } + for (static_field_decorate.items) |*elem| { + p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); + } + for (instance_field_decorate.items) |*elem| { + p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); + } + // Rewrite in pre-evaluated decorator expressions (may contain private name references + // like `@(dec(() => new Foo().#foo))` that need rewriting when pre-evaluated outside the class) + p.rewritePrivateAccessesInStmts(pre_eval_stmts.items, &private_lowered_map); + // Rewrite in prefix statements (extracted functions that may reference other private members) + p.rewritePrivateAccessesInStmts(prefix_stmts.items, &private_lowered_map); + } + + // === Compute field_idx base indices === + // The _init array slots are allocated dynamically by __decorateElement for k>3 (field/accessor). + // The call order determines slot assignment. Our suffix order is: + // 1. static non-fields (includes accessors) 2. instance non-fields (includes accessors) + // 3. static fields 4. instance fields + // So slots are allocated: static accessors, instance accessors, static fields, instance fields. + const static_field_count = static_field_decorate.items.len; + const total_accessor_count = static_accessor_count + instance_accessor_count; + const static_field_base_idx = total_accessor_count; + const instance_accessor_base_idx = static_accessor_count; + const instance_field_base_idx = total_accessor_count + static_field_count; + + // === Emit suffix in correct order (per TC39 spec) === + + // 1. Static non-field __decorateElement calls (methods, getters, setters, accessors) + suffix_exprs.appendSlice(static_non_field_elements.items) catch unreachable; + + // 2. Instance non-field __decorateElement calls (methods, getters, setters, accessors) + suffix_exprs.appendSlice(instance_non_field_elements.items) catch unreachable; + + // 3. Static field __decorateElement calls (fields only, NOT accessors) + suffix_exprs.appendSlice(static_field_decorate.items) catch unreachable; + + // 4. Instance field __decorateElement calls (fields only, NOT accessors) + suffix_exprs.appendSlice(instance_field_decorate.items) catch unreachable; + + // 5. Class decorator: ClassName = __decorateElement(_init, 0, "ClassName", [decorators], target) + if (class_decorators.len > 0) { + p.recordUsage(class_name_ref); + + const class_name_str: []const u8 = if (original_class_name_for_decorator) |name| + name + else if (is_expr and expr_class_is_anonymous) + (name_from_context orelse "") + else + p.symbols.items[class_name_ref.inner_index].original_name; + const cls_dec_args = bun.handleOom(p.allocator.alloc(Expr, 5)); + cls_dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + cls_dec_args[1] = p.newExpr(E.Number{ .value = 0 }, loc); + cls_dec_args[2] = p.newExpr(E.String{ .data = class_name_str }, loc); + cls_dec_args[3] = if (class_dec_ref) |cdr| brk: { + p.recordUsage(cdr); + break :brk p.newExpr(E.Identifier{ .ref = cdr }, loc); + } else p.newExpr(E.Array{ .items = class_decorators }, loc); + if (is_expr) { + p.recordUsage(expr_class_ref.?); + cls_dec_args[4] = p.newExpr(E.Identifier{ .ref = expr_class_ref.? }, loc); + } else { + cls_dec_args[4] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + } + + const decorate_class = p.callRuntime(loc, "__decorateElement", cls_dec_args); + p.recordUsage(class_name_ref); + suffix_exprs.append( + Expr.assign(p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc), decorate_class), + ) catch unreachable; + } + + // 6. Static method extra initializers: __runInitializers(_init, 3, ClassName) + if (static_non_field_elements.items.len > 0 or has_static_private_methods) { + p.recordUsage(init_ref); + p.recordUsage(class_name_ref); + const run_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + run_args[1] = p.newExpr(E.Number{ .value = 3 }, loc); // array[1], call mode (1<<1|1=3) + run_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + suffix_exprs.append(p.callRuntime(loc, "__runInitializers", run_args)) catch unreachable; + } + + // 7. Static elements in source order: static blocks + static field/accessor init + extra-init + { + var s_accessor_idx: usize = 0; + var s_field_idx: usize = 0; + for (static_element_order.items) |elem| { + switch (elem.kind) { + .block => { + // Emit extracted static block statements. + // Replace `this` with the class name since we're moving + // the static block outside the class body. + const sb = extracted_static_blocks.items[elem.index]; + p.replaceThisInStmts(sb.stmts.slice(), class_name_ref, class_name_loc); + for (sb.stmts.slice()) |sb_stmt| { + if (sb_stmt.data == .s_expr) { + suffix_exprs.append(sb_stmt.data.s_expr.value) catch unreachable; + } + } + }, + .field_or_accessor => { + const entry = static_init_entries.items[elem.index]; + const field_idx: usize = if (entry.is_accessor) brk: { + const idx = s_accessor_idx; + s_accessor_idx += 1; + break :brk idx; // static accessor base = 0 + } else brk: { + const idx = static_field_base_idx + s_field_idx; + s_field_idx += 1; + break :brk idx; + }; + + const init_flags_val: f64 = @floatFromInt((4 + 2 * field_idx) << 1); + const extra_flags_val: f64 = @floatFromInt(((5 + 2 * field_idx) << 1) | 1); + + // Init: ClassName.x = __runInitializers(_init, initFlags, ClassName, initValue?) + p.recordUsage(init_ref); + p.recordUsage(class_name_ref); + const run_args_count: usize = if (entry.prop.initializer != null) 4 else 3; + const run_args = bun.handleOom(p.allocator.alloc(Expr, run_args_count)); + run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + run_args[1] = p.newExpr(E.Number{ .value = init_flags_val }, loc); + run_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + if (entry.prop.initializer) |init_val| { + run_args[3] = init_val; + } + const run_init_call = p.callRuntime(loc, "__runInitializers", run_args); + + if (entry.is_accessor or entry.is_private) { + const wm_ref_expr = if (entry.is_accessor and !entry.is_private) + entry.prop.value.? + else + entry.prop.key.?; + p.recordUsage(class_name_ref); + const add_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + add_args[0] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + add_args[1] = wm_ref_expr; + add_args[2] = run_init_call; + suffix_exprs.append(p.callRuntime(loc, "__privateAdd", add_args)) catch unreachable; + } else { + p.recordUsage(class_name_ref); + const class_target = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + const assign_target = p.buildDecoratedMemberTarget(class_target, entry.prop); + suffix_exprs.append(Expr.assign(assign_target, run_init_call)) catch unreachable; + } + + // Extra initializer: __runInitializers(_init, extraFlags, ClassName) + p.recordUsage(init_ref); + p.recordUsage(class_name_ref); + const extra_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + extra_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + extra_args[1] = p.newExpr(E.Number{ .value = extra_flags_val }, loc); + extra_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + suffix_exprs.append(p.callRuntime(loc, "__runInitializers", extra_args)) catch unreachable; + }, + } + } + } + + // 8. Class extra initializers (if class decorators exist) + if (class_decorators.len > 0) { + p.recordUsage(init_ref); + p.recordUsage(class_name_ref); + const run_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + run_args[1] = p.newExpr(E.Number{ .value = 1 }, loc); // array[0], call mode (0<<1|1=1) + run_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + suffix_exprs.append(p.callRuntime(loc, "__runInitializers", run_args)) catch unreachable; + } + + // 9. __decoratorMetadata(_init, ClassName) + { + p.recordUsage(init_ref); + p.recordUsage(class_name_ref); + const metadata_args = bun.handleOom(p.allocator.alloc(Expr, 2)); + metadata_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + metadata_args[1] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + suffix_exprs.append(p.callRuntime(loc, "__decoratorMetadata", metadata_args)) catch unreachable; + } + + // === Build constructor injection === + + // Instance method extra initializers: __runInitializers(_init, 5, this) + if (instance_non_field_elements.items.len > 0 or has_instance_private_methods) { + p.recordUsage(init_ref); + const run_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + run_args[1] = p.newExpr(E.Number{ .value = 5 }, loc); // array[2], call mode (2<<1|1=5) + run_args[2] = p.newExpr(E.This{}, loc); + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__runInitializers", run_args) }, loc), + ) catch unreachable; + } + + // Instance field/accessor init + extra-init in source order + { + var i_accessor_idx: usize = 0; + var i_field_idx: usize = 0; + for (instance_init_entries.items) |entry| { + const field_idx: usize = if (entry.is_accessor) brk: { + const idx = instance_accessor_base_idx + i_accessor_idx; + i_accessor_idx += 1; + break :brk idx; + } else brk: { + const idx = instance_field_base_idx + i_field_idx; + i_field_idx += 1; + break :brk idx; + }; + + const init_flags_val: f64 = @floatFromInt((4 + 2 * field_idx) << 1); + const extra_flags_val: f64 = @floatFromInt(((5 + 2 * field_idx) << 1) | 1); + + // this.x = __runInitializers(_init, initFlags, this, initValue?) + p.recordUsage(init_ref); + const run_args_count: usize = if (entry.prop.initializer != null) 4 else 3; + const run_args = bun.handleOom(p.allocator.alloc(Expr, run_args_count)); + run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + run_args[1] = p.newExpr(E.Number{ .value = init_flags_val }, loc); + run_args[2] = p.newExpr(E.This{}, loc); + if (entry.prop.initializer) |init_val| { + run_args[3] = init_val; + } + const run_init_call = p.callRuntime(loc, "__runInitializers", run_args); + + if (entry.is_accessor or entry.is_private) { + const wm_ref_expr = if (entry.is_accessor and !entry.is_private) + entry.prop.value.? + else + entry.prop.key.?; + const add_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + add_args[0] = p.newExpr(E.This{}, loc); + add_args[1] = wm_ref_expr; + add_args[2] = run_init_call; + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", add_args) }, loc), + ) catch unreachable; + } else { + const this_expr = p.newExpr(E.This{}, loc); + const assign_target = p.buildDecoratedMemberTarget(this_expr, entry.prop); + constructor_inject_stmts.append( + Stmt.assign(assign_target, run_init_call), + ) catch unreachable; + } + + // Extra initializer: __runInitializers(_init, extraFlags, this) + p.recordUsage(init_ref); + const extra_args = bun.handleOom(p.allocator.alloc(Expr, 3)); + extra_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + extra_args[1] = p.newExpr(E.Number{ .value = extra_flags_val }, loc); + extra_args[2] = p.newExpr(E.This{}, loc); + constructor_inject_stmts.append( + p.s(S.SExpr{ .value = p.callRuntime(loc, "__runInitializers", extra_args) }, loc), + ) catch unreachable; + } + } + + // === Inject into constructor if needed === + if (constructor_inject_stmts.items.len > 0) { + // Find existing constructor in new_properties + var found_constructor = false; + for (new_properties.items) |*prop| { + if (prop.flags.contains(.is_method) and prop.key != null and + prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor")) + { + const func = prop.value.?.data.e_function; + var body_stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, func.func.body.stmts); + + // Find super() call + var super_index: ?usize = null; + for (body_stmts.items, 0..) |item, index| { + if (item.data != .s_expr) continue; + if (item.data.s_expr.value.data != .e_call) continue; + if (item.data.s_expr.value.data.e_call.target.data != .e_super) continue; + super_index = index; + break; + } + + const insert_at = if (super_index) |j| j + 1 else 0; + body_stmts.insertSlice(insert_at, constructor_inject_stmts.items) catch unreachable; + func.func.body.stmts = body_stmts.items; + found_constructor = true; + break; + } + } + + if (!found_constructor) { + var ctor_stmts = ListManaged(Stmt).init(p.allocator); + if (class.extends != null) { + const target = p.newExpr(E.Super{}, loc); + const args_ref = p.newSymbol(.unbound, arguments_str) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, args_ref)); + const spread = p.newExpr(E.Spread{ .value = p.newExpr(E.Identifier{ .ref = args_ref }, loc) }, loc); + const call_args = bun.handleOom(ExprNodeList.initOne(p.allocator, spread)); + ctor_stmts.append( + p.s(S.SExpr{ .value = p.newExpr(E.Call{ .target = target, .args = call_args }, loc) }, loc), + ) catch unreachable; + } + ctor_stmts.appendSlice(constructor_inject_stmts.items) catch unreachable; + + new_properties.insert(0, G.Property{ + .flags = Flags.Property.init(.{ .is_method = true }), + .key = p.newExpr(E.String{ .data = "constructor" }, loc), + .value = p.newExpr(E.Function{ .func = G.Fn{ + .name = null, + .open_parens_loc = logger.Loc.Empty, + .args = &[_]Arg{}, + .body = .{ .loc = loc, .stmts = ctor_stmts.items }, + .flags = Flags.Function.init(.{}), + } }, loc), + }) catch unreachable; + } + } + + // Insert static private __privateAdd blocks at the beginning of class body + if (static_private_add_blocks.items.len > 0) { + new_properties.insertSlice(0, static_private_add_blocks.items) catch unreachable; + } + + // Update class properties (decorated fields/static blocks removed) + class.properties = new_properties.items; + + // Clear class-level decorator flags + class.has_decorators = false; + class.should_lower_standard_decorators = false; + + // === Build result === + if (is_expr) { + // For expressions, build a comma expression: + // (_dec = [class_decs], _base = extends, pre_evals..., prefix..., _init = __decoratorStart(_base), _class = class { }, suffix..., result) + var comma_parts = ListManaged(Expr).init(p.allocator); + + // 1. Class decorator pre-evaluation (before extends) + if (class_dec_assign_expr) |cda| { + comma_parts.append(cda) catch unreachable; + } + + // 2. _base = extends_expr (if extends exists) + if (base_assign_expr) |base_assign| { + comma_parts.append(base_assign) catch unreachable; + } + + // Helper to convert S.Local decls to comma assignments + const appendDeclsAsAssigns = struct { + fn f(parts: *ListManaged(Expr), var_decls: *ListManaged(G.Decl), stmts_list: []const Stmt, parser: *P, l: logger.Loc) void { + for (stmts_list) |pstmt| { + if (pstmt.data == .s_local) { + for (pstmt.data.s_local.decls.slice()) |decl| { + const ref = decl.binding.data.b_identifier.ref; + var_decls.append(.{ .binding = parser.b(B.Identifier{ .ref = ref }, l) }) catch unreachable; + if (decl.value) |val| { + parser.recordUsage(ref); + parts.append( + Expr.assign(parser.newExpr(E.Identifier{ .ref = ref }, l), val), + ) catch unreachable; + } + } + } + } + } + }.f; + + // 3. Property decorator + computed key pre-evaluations (source order) + appendDeclsAsAssigns(&comma_parts, &expr_var_decls, pre_eval_stmts.items, p, loc); + + // 4. Prefix statements as comma expressions (WeakMap/WeakSet declarations) + appendDeclsAsAssigns(&comma_parts, &expr_var_decls, prefix_stmts.items, p, loc); + + // 5. _init = __decoratorStart(_base) + p.recordUsage(init_ref); + comma_parts.append( + Expr.assign(p.newExpr(E.Identifier{ .ref = init_ref }, loc), init_start_expr), + ) catch unreachable; + + // _class = class { ... } + p.recordUsage(expr_class_ref.?); + comma_parts.append( + Expr.assign( + p.newExpr(E.Identifier{ .ref = expr_class_ref.? }, loc), + p.newExpr(class.*, loc), + ), + ) catch unreachable; + + // suffix expressions + comma_parts.appendSlice(suffix_exprs.items) catch unreachable; + + // Final value: class_name_ref if decorated (has the decorated class), else _class + const final_ref = if (class_decorators.len > 0) class_name_ref else expr_class_ref.?; + p.recordUsage(final_ref); + comma_parts.append(p.newExpr(E.Identifier{ .ref = final_ref }, loc)) catch unreachable; + + // Build comma expression chain + var result = comma_parts.items[0]; + for (comma_parts.items[1..]) |part| { + result = p.newExpr(E.Binary{ + .op = .bin_comma, + .left = result, + .right = part, + }, loc); + } + + // Emit var declarations for temp variables via nearest_stmt_list + if (expr_var_decls.items.len > 0) { + const var_decl_stmt = p.s(S.Local{ + .decls = Decl.List.fromOwnedSlice(expr_var_decls.items), + }, loc); + if (p.nearest_stmt_list) |stmt_list| { + stmt_list.append(var_decl_stmt) catch unreachable; + } + } + + // Return as a single statement wrapping the comma expression + var stmts = ListManaged(Stmt).initCapacity(p.allocator, 1) catch unreachable; + stmts.appendAssumeCapacity(p.s(S.SExpr{ .value = result }, loc)); + return stmts.items; + } + + // Statement mode: [var _dec = [...], var _base = ..., pre_eval_stmts..., prefix_stmts..., var _init = ..., class Foo { ... }, suffix...] + // Order ensures decorator expressions and computed keys are evaluated in source order before the class. + var stmts = ListManaged(Stmt).initCapacity(p.allocator, prefix_stmts.items.len + pre_eval_stmts.items.len + 5 + suffix_exprs.items.len) catch unreachable; + // 1. Class decorator pre-evaluation (before extends) + if (class_dec_stmt.data != .s_empty) { + stmts.append(class_dec_stmt) catch unreachable; + } + // 2. Extends expression extraction + if (base_decl_stmt.data != .s_empty) { + stmts.append(base_decl_stmt) catch unreachable; + } + // 3. Property decorator + computed key pre-evaluations (source order) + stmts.appendSlice(pre_eval_stmts.items) catch unreachable; + // 4. WeakMap/WeakSet declarations + stmts.appendSlice(prefix_stmts.items) catch unreachable; + // 5. __decoratorStart + stmts.appendAssumeCapacity(init_decl_stmt); + // 6. Class declaration + stmts.appendAssumeCapacity(stmt); + for (suffix_exprs.items) |expr| { + stmts.append(p.s(S.SExpr{ .value = expr }, expr.loc)) catch unreachable; + } + // 7. Inner class binding: let _Foo = Foo + // Element decorator closures capture inner_class_ref (which is set via `let` + // with TDZ), so mutations to the outer class_name_ref don't affect them. + if (!inner_class_ref.eql(class_name_ref)) { + p.recordUsage(class_name_ref); + const inner_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + inner_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = inner_class_ref }, loc), + .value = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc), + }; + stmts.append(p.s(S.Local{ + .kind = .k_let, + .decls = Decl.List.fromOwnedSlice(inner_decls), + }, loc)) catch unreachable; + } + + return stmts.items; + } + + /// Build a property access expression for decorated member targets. + /// For string keys: target.name + /// For computed/number keys: target[key] + fn buildDecoratedMemberTarget(p: *P, target_expr: Expr, prop: Property) Expr { + const key_expr = prop.key.?; + if (prop.flags.contains(.is_computed) or key_expr.data == .e_number) { + return p.newExpr(E.Index{ + .target = target_expr, + .index = key_expr, + }, key_expr.loc); + } else if (key_expr.data == .e_string) { + return p.newExpr(E.Dot{ + .target = target_expr, + .name = key_expr.data.e_string.data, + .name_loc = key_expr.loc, + }, key_expr.loc); + } else { + // For private or other key types, use index + return p.newExpr(E.Index{ + .target = target_expr, + .index = key_expr, + }, key_expr.loc); + } + } + + /// Replace all E.This nodes in an expression tree with a given replacement expression. + /// Does NOT descend into regular functions or classes (they have their own `this`), + /// but DOES descend into arrow functions (they inherit `this`). + fn replaceThisInExpr(p: *P, expr: *Expr, replacement_ref: Ref, replacement_loc: logger.Loc) void { + switch (expr.data) { + .e_this => { + p.recordUsage(replacement_ref); + expr.* = p.newExpr(E.Identifier{ .ref = replacement_ref }, replacement_loc); + }, + .e_binary => |bin| { + p.replaceThisInExpr(&bin.left, replacement_ref, replacement_loc); + p.replaceThisInExpr(&bin.right, replacement_ref, replacement_loc); + }, + .e_call => |call| { + p.replaceThisInExpr(&call.target, replacement_ref, replacement_loc); + for (call.args.slice()) |*arg| { + p.replaceThisInExpr(arg, replacement_ref, replacement_loc); + } + }, + .e_new => |new_expr| { + p.replaceThisInExpr(&new_expr.target, replacement_ref, replacement_loc); + for (new_expr.args.slice()) |*arg| { + p.replaceThisInExpr(arg, replacement_ref, replacement_loc); + } + }, + .e_index => |idx| { + p.replaceThisInExpr(&idx.target, replacement_ref, replacement_loc); + p.replaceThisInExpr(&idx.index, replacement_ref, replacement_loc); + }, + .e_dot => |d| { + p.replaceThisInExpr(&d.target, replacement_ref, replacement_loc); + }, + .e_spread => |spr| { + p.replaceThisInExpr(&spr.value, replacement_ref, replacement_loc); + }, + .e_unary => |u| { + p.replaceThisInExpr(&u.value, replacement_ref, replacement_loc); + }, + .e_if => |i| { + p.replaceThisInExpr(&i.yes, replacement_ref, replacement_loc); + p.replaceThisInExpr(&i.no, replacement_ref, replacement_loc); + p.replaceThisInExpr(&i.test_, replacement_ref, replacement_loc); + }, + .e_array => |a| { + for (a.items.slice()) |*item| { + p.replaceThisInExpr(item, replacement_ref, replacement_loc); + } + }, + .e_object => |o| { + for (o.properties.slice()) |*property| { + if (property.value) |*v| { + p.replaceThisInExpr(v, replacement_ref, replacement_loc); + } + if (property.initializer) |*prop_init| { + p.replaceThisInExpr(prop_init, replacement_ref, replacement_loc); + } + } + }, + .e_template => |t| { + if (t.tag) |*tag| { + p.replaceThisInExpr(tag, replacement_ref, replacement_loc); + } + }, + .e_arrow => |arrow| { + // Arrow functions inherit `this`, so descend into their bodies + p.replaceThisInStmts(arrow.body.stmts, replacement_ref, replacement_loc); + }, + // Don't descend into regular functions or classes (they have their own `this`) + .e_function, .e_class => {}, + // Leaf nodes - nothing to replace + else => {}, + } + } + + /// Replace all E.Identifier nodes matching old_ref with new_ref in an expression tree. + /// Unlike replaceThisInExpr, this walks into all nested scopes since ref identity + /// is unique and won't clash with same-named bindings in nested scopes. + fn replaceRefInExpr(p: *P, expr: *Expr, old_ref: Ref, new_ref: Ref) void { + switch (expr.data) { + .e_identifier => |id| { + if (id.ref.eql(old_ref)) { + p.recordUsage(new_ref); + expr.data = .{ .e_identifier = .{ .ref = new_ref } }; + } + }, + .e_binary => |bin| { + p.replaceRefInExpr(&bin.left, old_ref, new_ref); + p.replaceRefInExpr(&bin.right, old_ref, new_ref); + }, + .e_call => |call| { + p.replaceRefInExpr(&call.target, old_ref, new_ref); + for (call.args.slice()) |*arg| { + p.replaceRefInExpr(arg, old_ref, new_ref); + } + }, + .e_new => |new_expr| { + p.replaceRefInExpr(&new_expr.target, old_ref, new_ref); + for (new_expr.args.slice()) |*arg| { + p.replaceRefInExpr(arg, old_ref, new_ref); + } + }, + .e_index => |idx| { + p.replaceRefInExpr(&idx.target, old_ref, new_ref); + p.replaceRefInExpr(&idx.index, old_ref, new_ref); + }, + .e_dot => |d| { + p.replaceRefInExpr(&d.target, old_ref, new_ref); + }, + .e_spread => |spr| { + p.replaceRefInExpr(&spr.value, old_ref, new_ref); + }, + .e_unary => |u| { + p.replaceRefInExpr(&u.value, old_ref, new_ref); + }, + .e_if => |i| { + p.replaceRefInExpr(&i.yes, old_ref, new_ref); + p.replaceRefInExpr(&i.no, old_ref, new_ref); + p.replaceRefInExpr(&i.test_, old_ref, new_ref); + }, + .e_array => |a| { + for (a.items.slice()) |*item| { + p.replaceRefInExpr(item, old_ref, new_ref); + } + }, + .e_object => |o| { + for (o.properties.slice()) |*property| { + if (property.value) |*v| { + p.replaceRefInExpr(v, old_ref, new_ref); + } + if (property.initializer) |*prop_init| { + p.replaceRefInExpr(prop_init, old_ref, new_ref); + } + } + }, + .e_template => |t| { + if (t.tag) |*tag| { + p.replaceRefInExpr(tag, old_ref, new_ref); + } + }, + .e_arrow => |arrow| { + p.replaceRefInStmts(arrow.body.stmts, old_ref, new_ref); + }, + .e_function => |func| { + if (func.func.body.stmts.len > 0) { + p.replaceRefInStmts(func.func.body.stmts, old_ref, new_ref); + } + }, + // Don't descend into nested classes (they may rebind the class name) + .e_class => {}, + else => {}, + } + } + + /// Replace all E.Identifier nodes matching old_ref with new_ref in statements. + fn replaceRefInStmts(p: *P, stmts: []Stmt, old_ref: Ref, new_ref: Ref) void { + for (stmts) |*cur_stmt| { + switch (cur_stmt.data) { + .s_expr => |sexpr| { + var val = sexpr.value; + p.replaceRefInExpr(&val, old_ref, new_ref); + cur_stmt.* = p.s(S.SExpr{ .value = val, .does_not_affect_tree_shaking = sexpr.does_not_affect_tree_shaking }, cur_stmt.loc); + }, + .s_local => |local| { + for (local.decls.slice()) |*decl| { + if (decl.value) |*v| { + p.replaceRefInExpr(v, old_ref, new_ref); + } + } + }, + .s_return => |ret| { + if (ret.value) |*v| { + p.replaceRefInExpr(v, old_ref, new_ref); + } + }, + else => {}, + } + } + } + + /// Replace E.This in a slice of statements. + fn replaceThisInStmts(p: *P, stmts: []Stmt, replacement_ref: Ref, replacement_loc: logger.Loc) void { + for (stmts) |*cur_stmt| { + switch (cur_stmt.data) { + .s_expr => |sexpr| { + var val = sexpr.value; + p.replaceThisInExpr(&val, replacement_ref, replacement_loc); + cur_stmt.* = p.s(S.SExpr{ .value = val, .does_not_affect_tree_shaking = sexpr.does_not_affect_tree_shaking }, cur_stmt.loc); + }, + .s_local => |local| { + for (local.decls.slice()) |*decl| { + if (decl.value) |*v| { + p.replaceThisInExpr(v, replacement_ref, replacement_loc); + } + } + }, + .s_return => |ret| { + if (ret.value) |*v| { + p.replaceThisInExpr(v, replacement_ref, replacement_loc); + } + }, + else => {}, + } + } + } + pub fn lowerClass( noalias p: *P, stmtorexpr: js_ast.StmtOrExpr, ) []Stmt { switch (stmtorexpr) { .stmt => |stmt| { + // Standard decorator lowering path (for both JS and TS files) + if (stmt.data.s_class.class.should_lower_standard_decorators) { + return p.lowerStandardDecoratorsStmt(stmt); + } + if (comptime !is_typescript_enabled) { if (!stmt.data.s_class.class.has_decorators) { var stmts = p.allocator.alloc(Stmt, 1) catch unreachable; @@ -5007,7 +7045,7 @@ pub fn NewParser_( } } }, - .spread, .declare => {}, // not allowed in a class + .spread, .declare, .auto_accessor => {}, // not allowed in a class (auto_accessor is standard decorators only) .class_static_block => {}, // not allowed to decorate this } } diff --git a/src/ast/parse.zig b/src/ast/parse.zig index a677d68a63ec..2be1cd78f073 100644 --- a/src/ast/parse.zig +++ b/src/ast/parse.zig @@ -22,6 +22,7 @@ pub fn Parse( pub const parseImportClause = @import("./parseImportExport.zig").ParseImportExport(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseImportClause; pub const parseExportClause = @import("./parseImportExport.zig").ParseImportExport(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseExportClause; pub const parseTypeScriptDecorators = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypeScriptDecorators; + pub const parseStandardDecorator = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseStandardDecorator; pub const parseTypeScriptNamespaceStmt = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypeScriptNamespaceStmt; pub const parseTypeScriptImportEqualsStmt = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypeScriptImportEqualsStmt; pub const parseTypescriptEnumStmt = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypescriptEnumStmt; @@ -102,6 +103,7 @@ pub fn Parse( pub fn parseClass(p: *P, class_keyword: logger.Range, name: ?js_ast.LocRef, class_opts: ParseClassOptions) !G.Class { var extends: ?Expr = null; var has_decorators: bool = false; + var has_auto_accessor: bool = false; if (p.lexer.token == .t_extends) { try p.lexer.next(); @@ -168,6 +170,7 @@ pub fn Parse( // This property may turn out to be a type in TypeScript, which should be ignored if (try p.parseProperty(.normal, &opts, null)) |property| { properties.append(property) catch unreachable; + has_auto_accessor = has_auto_accessor or property.kind == .auto_accessor; // Forbid decorators on class constructors if (opts.ts_decorators.len > 0) { @@ -196,6 +199,7 @@ pub fn Parse( const close_brace_loc = p.lexer.loc(); try p.lexer.expect(.t_close_brace); + const has_any_decorators = has_decorators or class_opts.ts_decorators.len > 0; return G.Class{ .class_name = name, .extends = extends, @@ -204,7 +208,8 @@ pub fn Parse( .class_keyword = class_keyword, .body_loc = body_loc, .properties = properties.items, - .has_decorators = has_decorators or class_opts.ts_decorators.len > 0, + .has_decorators = has_any_decorators, + .should_lower_standard_decorators = p.options.features.standard_decorators and (has_any_decorators or has_auto_accessor), }; } diff --git a/src/ast/parsePrefix.zig b/src/ast/parsePrefix.zig index 5462bfd89d3e..7eed868dbc75 100644 --- a/src/ast/parsePrefix.zig +++ b/src/ast/parsePrefix.zig @@ -390,7 +390,58 @@ pub fn ParsePrefix( _ = try p.skipTypeScriptTypeParameters(.{ .allow_in_out_variance_annotations = true, .allow_const_modifier = true }); } - const class = try p.parseClass(classKeyword, name, ParseClassOptions{}); + const class = try p.parseClass(classKeyword, name, ParseClassOptions{ + .allow_ts_decorators = is_typescript_enabled or p.options.features.standard_decorators, + }); + p.popScope(); + + return p.newExpr(class, loc); + } + fn t_at(noalias p: *P) anyerror!Expr { + // Parse decorators before a class expression: @dec class { ... } + const ts_decorators = try p.parseTypeScriptDecorators(); + + // Expect class keyword after decorators + if (p.lexer.token != .t_class) { + try p.lexer.expected(.t_class); + return error.SyntaxError; + } + + const loc = p.lexer.loc(); + const classKeyword = p.lexer.range(); + try p.lexer.next(); + var name: ?js_ast.LocRef = null; + + _ = p.pushScopeForParsePass(.class_name, loc) catch unreachable; + + // Parse an optional class name + if (p.lexer.token == .t_identifier) { + const name_text = p.lexer.identifier; + if (!is_typescript_enabled or !strings.eqlComptime(name_text, "implements")) { + if (p.fn_or_arrow_data_parse.allow_await != .allow_ident and strings.eqlComptime(name_text, "await")) { + p.log.addRangeError(p.source, p.lexer.range(), "Cannot use \"await\" as an identifier here") catch unreachable; + } + + name = js_ast.LocRef{ + .loc = p.lexer.loc(), + .ref = p.newSymbol( + .other, + name_text, + ) catch unreachable, + }; + try p.lexer.next(); + } + } + + // Even anonymous classes can have TypeScript type parameters + if (is_typescript_enabled) { + _ = try p.skipTypeScriptTypeParameters(.{ .allow_in_out_variance_annotations = true, .allow_const_modifier = true }); + } + + const class = try p.parseClass(classKeyword, name, ParseClassOptions{ + .ts_decorators = ts_decorators, + .allow_ts_decorators = true, + }); p.popScope(); return p.newExpr(class, loc); @@ -727,6 +778,7 @@ pub fn ParsePrefix( .t_plus_plus => t_plus_plus(p), .t_function => t_function(p), .t_class => t_class(p), + .t_at => t_at(p), .t_new => t_new(p, flags), .t_super => t_super(p, level), else => { diff --git a/src/ast/parseProperty.zig b/src/ast/parseProperty.zig index aa7bf0e665c0..24e81306f2da 100644 --- a/src/ast/parseProperty.zig +++ b/src/ast/parseProperty.zig @@ -158,7 +158,7 @@ pub fn ParseProperty( try p.lexer.next(); }, .t_private_identifier => { - if (!opts.is_class or opts.ts_decorators.len > 0) { + if (!opts.is_class or (opts.ts_decorators.len > 0 and !p.options.features.standard_decorators)) { try p.lexer.expected(.t_identifier); } @@ -299,6 +299,16 @@ pub fn ParseProperty( return null; } }, + .p_accessor => { + // "accessor" keyword for auto-accessor fields (TC39 standard decorators) + if (opts.is_class and p.options.features.standard_decorators and + (js_lexer.PropertyModifierKeyword.List.get(raw) orelse .p_static) == .p_accessor) + { + kind = .auto_accessor; + errors = null; + continue :restart; + } + }, .p_private, .p_protected, .p_public, .p_readonly, .p_override => { // Skip over TypeScript keywords if (opts.is_class and is_typescript_enabled and (js_lexer.PropertyModifierKeyword.List.get(raw) orelse .p_static) == keyword) { @@ -430,7 +440,7 @@ pub fn ParseProperty( // Parse a class field with an optional initial value if (opts.is_class and - kind == .normal and !opts.is_async and + (kind == .normal or kind == .auto_accessor) and !opts.is_async and !opts.is_generator and p.lexer.token != .t_open_paren and !has_type_parameters and diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig index 785f483efd5d..d6f4e85d1a1c 100644 --- a/src/ast/parseStmt.zig +++ b/src/ast/parseStmt.zig @@ -445,7 +445,7 @@ pub fn ParseStmt( } fn t_at(p: *P, opts: *ParseStatementOptions) anyerror!Stmt { // Parse decorators before class statements, which are potentially exported - if (is_typescript_enabled) { + if (is_typescript_enabled or p.options.features.standard_decorators) { const scope_index = p.scopes_in_order.items.len; const ts_decorators = try p.parseTypeScriptDecorators(); @@ -473,7 +473,10 @@ pub fn ParseStmt( // "@decorator export declare abstract class Foo {}" // "@decorator export default class Foo {}" // "@decorator export default abstract class Foo {}" - if (p.lexer.token != .t_class and p.lexer.token != .t_export and !p.lexer.isContextualKeyword("abstract") and !p.lexer.isContextualKeyword("declare")) { + if (p.lexer.token != .t_class and p.lexer.token != .t_export and + !(is_typescript_enabled and p.lexer.isContextualKeyword("abstract")) and + !(is_typescript_enabled and p.lexer.isContextualKeyword("declare"))) + { try p.lexer.expected(.t_class); } diff --git a/src/ast/parseTypescript.zig b/src/ast/parseTypescript.zig index bdb82b2735ba..7c24abad9522 100644 --- a/src/ast/parseTypescript.zig +++ b/src/ast/parseTypescript.zig @@ -8,7 +8,7 @@ pub fn ParseTypescript( const is_typescript_enabled = P.is_typescript_enabled; pub fn parseTypeScriptDecorators(p: *P) ![]ExprNodeIndex { - if (!is_typescript_enabled) { + if (!is_typescript_enabled and !p.options.features.standard_decorators) { return &([_]ExprNodeIndex{}); } @@ -16,22 +16,106 @@ pub fn ParseTypescript( while (p.lexer.token == T.t_at) { try p.lexer.next(); - // Parse a new/call expression with "exprFlagTSDecorator" so we ignore - // EIndex expressions, since they may be part of a computed property: - // - // class Foo { - // @foo ['computed']() {} - // } - // - // This matches the behavior of the TypeScript compiler. - try decorators.ensureUnusedCapacity(1); - try p.parseExprWithFlags(.new, Expr.EFlags.ts_decorator, &decorators.unusedCapacitySlice()[0]); - decorators.items.len += 1; + if (p.options.features.standard_decorators) { + // TC39 standard decorator grammar: + // @Identifier + // @Identifier.member + // @Identifier.member(args) + // @(Expression) + try decorators.ensureUnusedCapacity(1); + decorators.unusedCapacitySlice()[0] = try p.parseStandardDecorator(); + decorators.items.len += 1; + } else { + // Parse a new/call expression with "exprFlagTSDecorator" so we ignore + // EIndex expressions, since they may be part of a computed property: + // + // class Foo { + // @foo ['computed']() {} + // } + // + // This matches the behavior of the TypeScript compiler. + try decorators.ensureUnusedCapacity(1); + try p.parseExprWithFlags(.new, Expr.EFlags.ts_decorator, &decorators.unusedCapacitySlice()[0]); + decorators.items.len += 1; + } } return decorators.items; } + /// Parse a standard (TC39) decorator expression following the `@` token. + /// + /// DecoratorExpression: + /// @ IdentifierReference + /// @ DecoratorMemberExpression + /// @ DecoratorCallExpression + /// @ DecoratorParenthesizedExpression + pub fn parseStandardDecorator(p: *P) !ExprNodeIndex { + const loc = p.lexer.loc(); + + // @(Expression) — parenthesized, any expression allowed + if (p.lexer.token == .t_open_paren) { + try p.lexer.next(); + const expr = try p.parseExpr(.lowest); + try p.lexer.expect(.t_close_paren); + return expr; + } + + // Must start with an identifier + if (p.lexer.token != .t_identifier) { + try p.lexer.expect(.t_identifier); + return error.SyntaxError; + } + + var expr = p.newExpr(E.Identifier{ .ref = try p.storeNameInRef(p.lexer.identifier) }, loc); + try p.lexer.next(); + + // Skip TypeScript type arguments after the identifier (e.g., @foo) + if (is_typescript_enabled) { + _ = try p.skipTypeScriptTypeArguments(false); + } + + // DecoratorMemberExpression: Identifier (.Identifier)* + while (p.lexer.token == .t_dot or p.lexer.token == .t_question_dot) { + // Forbid optional chaining in decorators + if (p.lexer.token == .t_question_dot) { + try p.log.addError(p.source, p.lexer.loc(), "Optional chaining is not allowed in decorator expressions"); + return error.SyntaxError; + } + + try p.lexer.next(); + + if (!p.lexer.isIdentifierOrKeyword()) { + try p.lexer.expect(.t_identifier); + return error.SyntaxError; + } + + const name = p.lexer.identifier; + const name_loc = p.lexer.loc(); + try p.lexer.next(); + + expr = p.newExpr(E.Dot{ .target = expr, .name = name, .name_loc = name_loc }, loc); + + // Skip TypeScript type arguments after member access (e.g., @foo.bar) + if (is_typescript_enabled) { + _ = try p.skipTypeScriptTypeArguments(false); + } + } + + // DecoratorCallExpression: DecoratorMemberExpression Arguments + // Only a single call is allowed, no chaining after the call + if (p.lexer.token == .t_open_paren) { + const args = try p.parseCallArgs(); + expr = p.newExpr(E.Call{ + .target = expr, + .args = args.list, + .close_paren_loc = args.loc, + }, loc); + } + + return expr; + } + pub fn parseTypeScriptNamespaceStmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions) anyerror!Stmt { // "namespace foo {}"; const name_loc = p.lexer.loc(); diff --git a/src/ast/visit.zig b/src/ast/visit.zig index e3e79a0b9c90..79136c77b3fa 100644 --- a/src/ast/visit.zig +++ b/src/ast/visit.zig @@ -186,9 +186,14 @@ pub fn Visit( if (only_scan_imports_and_do_not_visit) { @compileError("only_scan_imports_and_do_not_visit must not run this."); } + // Propagate name from binding to anonymous decorated class expressions + if (was_anonymous_named_expr and val.data == .e_class and val.data.e_class.should_lower_standard_decorators and decl.binding.data == .b_identifier) { + p.decorator_class_name = p.loadNameFromRef(decl.binding.data.b_identifier.ref); + } decl.value = p.visitExprInOut(val, .{ .is_immediately_assigned_to_decl = true, }); + p.decorator_class_name = null; if (p.options.features.react_fast_refresh) { // When hooks are immediately assigned to something, we need to hash the binding. @@ -407,7 +412,14 @@ pub fn Visit( p.visitBinding(item.binding, duplicate_arg_check); if (item.default_value) |default_value| { const was_anonymous_named_expr = default_value.isAnonymousNamed(); + if (was_anonymous_named_expr and default_value.data == .e_class and + default_value.data.e_class.should_lower_standard_decorators and + item.binding.data == .b_identifier) + { + p.decorator_class_name = p.loadNameFromRef(item.binding.data.b_identifier.ref); + } item.default_value = p.visitExpr(default_value); + p.decorator_class_name = null; switch (item.binding.data) { .b_identifier => |bind_| { @@ -431,7 +443,14 @@ pub fn Visit( p.visitBinding(property.value, duplicate_arg_check); if (property.default_value) |default_value| { const was_anonymous_named_expr = default_value.isAnonymousNamed(); + if (was_anonymous_named_expr and default_value.data == .e_class and + default_value.data.e_class.should_lower_standard_decorators and + property.value.data == .b_identifier) + { + p.decorator_class_name = p.loadNameFromRef(property.value.data.b_identifier.ref); + } property.default_value = p.visitExpr(default_value); + p.decorator_class_name = null; switch (property.value.data) { .b_identifier => |bind_| { @@ -627,7 +646,11 @@ pub fn Visit( if (property.value) |val| { if (name_to_keep) |name| { const was_anon = val.isAnonymousNamed(); + if (val.data == .e_class and val.data.e_class.class_name == null and val.data.e_class.should_lower_standard_decorators) { + p.decorator_class_name = name; + } property.value = p.maybeKeepExprSymbolName(p.visitExpr(val), name, was_anon); + p.decorator_class_name = null; } else { property.value = p.visitExpr(val); } @@ -643,7 +666,11 @@ pub fn Visit( // if (property.flags.is_static and ) if (name_to_keep) |name| { const was_anon = val.isAnonymousNamed(); + if (val.data == .e_class and val.data.e_class.class_name == null and val.data.e_class.should_lower_standard_decorators) { + p.decorator_class_name = name; + } property.initializer = p.maybeKeepExprSymbolName(p.visitExpr(val), name, was_anon); + p.decorator_class_name = null; } else { property.initializer = p.visitExpr(val); } diff --git a/src/ast/visitBinaryExpression.zig b/src/ast/visitBinaryExpression.zig index dc03bdf189e7..69143cc2018f 100644 --- a/src/ast/visitBinaryExpression.zig +++ b/src/ast/visitBinaryExpression.zig @@ -70,6 +70,14 @@ pub fn CreateBinaryExpressionVisitor( // const is_stmt_expr = @as(Expr.Tag, p.stmt_expr_value) == .e_binary and expr.data.e_binary == p.stmt_expr_value.e_binary; const was_anonymous_named_expr = e_.right.isAnonymousNamed(); + // Propagate name for anonymous decorated class expressions in assignments + if (e_.op == .bin_assign and was_anonymous_named_expr and + e_.right.data == .e_class and e_.right.data.e_class.should_lower_standard_decorators and + e_.left.data == .e_identifier) + { + p.decorator_class_name = p.loadNameFromRef(e_.left.data.e_identifier.ref); + } + // Mark the control flow as dead if the branch is never taken switch (e_.op) { .bin_logical_or => { @@ -112,6 +120,7 @@ pub fn CreateBinaryExpressionVisitor( e_.right = p.visitExpr(e_.right); }, } + p.decorator_class_name = null; // Always put constants on the right for equality comparisons to help // reduce the number of cases we have to check during pattern matching. We diff --git a/src/ast/visitExpr.zig b/src/ast/visitExpr.zig index cc8bcb1b49f6..724e1f0a01b5 100644 --- a/src/ast/visitExpr.zig +++ b/src/ast/visitExpr.zig @@ -1015,8 +1015,16 @@ pub fn VisitExpr( .e_binary => |e2| { if (in.assign_target != .none and e2.op == .bin_assign) { const was_anonymous_named_expr = e2.right.isAnonymousNamed(); + // Propagate name for anonymous decorated class expressions + if (was_anonymous_named_expr and e2.right.data == .e_class and + e2.right.data.e_class.should_lower_standard_decorators and + @as(Expr.Tag, e2.left.data) == .e_identifier) + { + p.decorator_class_name = p.loadNameFromRef(e2.left.data.e_identifier.ref); + } e2.left = p.visitExprInOut(e2.left, ExprIn{ .assign_target = .replace }); e2.right = p.visitExpr(e2.right); + p.decorator_class_name = null; if (@as(Expr.Tag, e2.left.data) == .e_identifier) { e2.right = p.maybeKeepExprSymbolName( @@ -1089,12 +1097,34 @@ pub fn VisitExpr( } if (property.value != null) { + // Propagate name from property key for decorated anonymous class expressions + // e.g., { Foo: @dec class {} } should give the class .name = "Foo" + if (in.assign_target == .none and + property.value.?.data == .e_class and + property.value.?.data.e_class.should_lower_standard_decorators and + property.value.?.data.e_class.class_name == null and + property.key != null and + property.key.?.data == .e_string) + { + p.decorator_class_name = property.key.?.data.e_string.string(p.allocator) catch null; + } property.value = p.visitExprInOut(property.value.?, ExprIn{ .assign_target = in.assign_target }); + p.decorator_class_name = null; } if (property.initializer != null) { const was_anonymous_named_expr = property.initializer.?.isAnonymousNamed(); + if (was_anonymous_named_expr and property.initializer.?.data == .e_class and + property.initializer.?.data.e_class.should_lower_standard_decorators) + { + if (property.value) |val| { + if (@as(Expr.Tag, val.data) == .e_identifier) { + p.decorator_class_name = p.loadNameFromRef(val.data.e_identifier.ref); + } + } + } property.initializer = p.visitExpr(property.initializer.?); + p.decorator_class_name = null; if (property.value) |val| { if (@as(Expr.Tag, val.data) == .e_identifier) { @@ -1627,8 +1657,17 @@ pub fn VisitExpr( return expr; } + // Save name from assignment context before visiting (nested visits may overwrite it) + const decorator_name_from_context = p.decorator_class_name; + p.decorator_class_name = null; + _ = p.visitClass(expr.loc, e_, Ref.None); + // Lower standard decorators for class expressions + if (e_.should_lower_standard_decorators) { + return p.lowerStandardDecoratorsExpr(e_, expr.loc, decorator_name_from_context); + } + // Remove unused class names when minifying (only when bundling is enabled) // unless --keep-names is specified if (p.options.features.minify_syntax and p.options.bundle and diff --git a/src/ast/visitStmt.zig b/src/ast/visitStmt.zig index 44b3642711ac..e35d90c2c983 100644 --- a/src/ast/visitStmt.zig +++ b/src/ast/visitStmt.zig @@ -215,8 +215,11 @@ pub fn VisitStmt( switch (data.value) { .expr => |expr| { const was_anonymous_named_expr = expr.isAnonymousNamed(); - + if (was_anonymous_named_expr and expr.data == .e_class and expr.data.e_class.should_lower_standard_decorators) { + p.decorator_class_name = js_ast.ClauseItem.default_alias; + } data.value.expr = p.visitExpr(expr); + p.decorator_class_name = null; if (p.is_control_flow_dead) { return; diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index 16eb26e87423..2af3d64fbd85 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -232,6 +232,7 @@ pub fn transpileSourceCode( .macro_remappings = macro_remappings, .jsx = jsc_vm.transpiler.options.jsx, .emit_decorator_metadata = jsc_vm.transpiler.options.emit_decorator_metadata, + .experimental_decorators = jsc_vm.transpiler.options.experimental_decorators, .virtual_source = virtual_source, .dont_bundle_twice = true, .allow_commonjs = true, diff --git a/src/bun.js/RuntimeTranspilerStore.zig b/src/bun.js/RuntimeTranspilerStore.zig index b43de4516ad8..54cd1c5d3aff 100644 --- a/src/bun.js/RuntimeTranspilerStore.zig +++ b/src/bun.js/RuntimeTranspilerStore.zig @@ -385,6 +385,7 @@ pub const RuntimeTranspilerStore = struct { .macro_remappings = macro_remappings, .jsx = transpiler.options.jsx, .emit_decorator_metadata = transpiler.options.emit_decorator_metadata, + .experimental_decorators = transpiler.options.experimental_decorators, .virtual_source = null, .dont_bundle_twice = true, .allow_commonjs = true, diff --git a/src/bundler/ParseTask.zig b/src/bundler/ParseTask.zig index 81f96cc9f746..4c6125897d3c 100644 --- a/src/bundler/ParseTask.zig +++ b/src/bundler/ParseTask.zig @@ -31,6 +31,7 @@ tree_shaking: bool = false, known_target: options.Target, module_type: options.ModuleType = .unknown, emit_decorator_metadata: bool = false, +experimental_decorators: bool = false, ctx: *BundleV2, package_version: string = "", is_entry_point: bool = false, @@ -118,6 +119,7 @@ pub fn init(resolve_result: *const _resolver.Result, source_index: Index, ctx: * .source_index = source_index, .module_type = resolve_result.module_type, .emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata, + .experimental_decorators = resolve_result.flags.experimental_decorators, .package_version = if (resolve_result.package_json) |package_json| package_json.version else "", .known_target = ctx.transpiler.options.target, }; @@ -1196,6 +1198,7 @@ fn runWithSourceCode( opts.features.minify_keep_names = transpiler.options.keep_names; opts.features.minify_whitespace = transpiler.options.minify_whitespace; opts.features.emit_decorator_metadata = transpiler.options.emit_decorator_metadata; + opts.features.standard_decorators = !loader.isTypeScript() or !transpiler.options.experimental_decorators; opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages; opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags; opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime(); diff --git a/src/js_lexer_tables.zig b/src/js_lexer_tables.zig index e1d4cbc99716..1d72f02113e5 100644 --- a/src/js_lexer_tables.zig +++ b/src/js_lexer_tables.zig @@ -214,6 +214,7 @@ pub const StrictModeReservedWordsRemap = ComptimeStringMap(string, .{ pub const PropertyModifierKeyword = enum { p_abstract, + p_accessor, p_async, p_declare, p_get, @@ -227,6 +228,7 @@ pub const PropertyModifierKeyword = enum { pub const List = ComptimeStringMap(PropertyModifierKeyword, .{ .{ "abstract", .p_abstract }, + .{ "accessor", .p_accessor }, .{ "async", .p_async }, .{ "declare", .p_declare }, .{ "get", .p_get }, diff --git a/src/js_printer.zig b/src/js_printer.zig index 9d6f48f8c1b5..946cea4323eb 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -3292,6 +3292,11 @@ fn NewPrinter( p.print("set"); p.printSpace(); }, + .auto_accessor => { + p.printSpaceBeforeIdentifier(); + p.print("accessor"); + p.printSpace(); + }, else => {}, } @@ -3478,7 +3483,7 @@ fn NewPrinter( }, } - if (item.kind != .normal) { + if (item.kind != .normal and item.kind != .auto_accessor) { if (comptime is_json) { bun.unreachablePanic("item.kind must be normal in json", .{}); } diff --git a/src/options.zig b/src/options.zig index c770a92dfa52..783640f4eb5c 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1732,6 +1732,7 @@ pub const BundleOptions = struct { resolve_dir: string = "/", jsx: JSX.Pragma = JSX.Pragma{}, emit_decorator_metadata: bool = false, + experimental_decorators: bool = false, auto_import_jsx: bool = true, allow_runtime: bool = true, diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index ec0ae1dc10d9..7fcc47afa784 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -171,7 +171,8 @@ pub const Result = struct { // value is not "remove". preserve_unused_imports_ts: bool = false, emit_decorator_metadata: bool = false, - _padding: u2 = 0, + experimental_decorators: bool = false, + _padding: u1 = 0, }; pub const Union = union(enum) { @@ -1000,6 +1001,7 @@ pub const Resolver = struct { if (dir.enclosing_tsconfig_json) |tsconfig| { result.jsx = tsconfig.mergeJSX(result.jsx); result.flags.emit_decorator_metadata = result.flags.emit_decorator_metadata or tsconfig.emit_decorator_metadata; + result.flags.experimental_decorators = result.flags.experimental_decorators or tsconfig.experimental_decorators; } // If you use mjs or mts, then you're using esm diff --git a/src/resolver/tsconfig_json.zig b/src/resolver/tsconfig_json.zig index d7510c536368..0ea46348aad7 100644 --- a/src/resolver/tsconfig_json.zig +++ b/src/resolver/tsconfig_json.zig @@ -41,6 +41,7 @@ pub const TSConfigJSON = struct { preserve_imports_not_used_as_values: ?bool = false, emit_decorator_metadata: bool = false, + experimental_decorators: bool = false, pub fn hasBaseURL(tsconfig: *const TSConfigJSON) bool { return tsconfig.base_url.len > 0; @@ -177,6 +178,13 @@ pub const TSConfigJSON = struct { } } + // Parse "experimentalDecorators" + if (compiler_opts.expr.asProperty("experimentalDecorators")) |experimental_decorators_prop| { + if (experimental_decorators_prop.expr.asBool()) |val| { + result.experimental_decorators = val; + } + } + // Parse "jsxFactory" if (compiler_opts.expr.asProperty("jsxFactory")) |jsx_prop| { if (jsx_prop.expr.asString(allocator)) |str| { diff --git a/src/runtime.js b/src/runtime.js index 76abb726f0d1..e4d440f48419 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -169,6 +169,123 @@ export var __legacyMetadataTS = (k, v) => { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; +// Internal helpers for ES decorators +var __knownSymbol = (name, symbol) => ((symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name)); +var __typeError = msg => { + throw TypeError(msg); +}; +var __defNormalProp = (obj, key, value) => + key in obj + ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) + : (obj[key] = value); + +// ES decorator helpers +export var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); +export var __privateIn = (member, obj) => + Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj); +export var __privateGet = (obj, member, getter) => ( + __accessCheck(obj, member, "read from private field"), + getter ? getter.call(obj) : member.get(obj) +); +export var __privateAdd = (obj, member, value) => + member.has(obj) + ? __typeError("Cannot add the same private member more than once") + : member instanceof WeakSet + ? member.add(obj) + : member.set(obj, value); +export var __privateSet = (obj, member, value, setter) => ( + __accessCheck(obj, member, "write to private field"), + setter ? setter.call(obj, value) : member.set(obj, value), + value +); +export var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); + +export var __decoratorStart = base => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)]; +var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"]; +var __expectFn = fn => (fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn); +var __decoratorContext = (kind, name, done, metadata, fns) => ({ + kind: __decoratorStrings[kind], + name, + metadata, + addInitializer: fn => (done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))), +}); +export var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]); +export var __runInitializers = (array, flags, self, value) => { + for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) + flags & 1 ? fns[i].call(self) : (value = fns[i].call(self, value)); + return value; +}; +export var __decorateElement = (array, flags, name, decorators, target, extra) => { + var fn, + it, + done, + ctx, + access, + k = flags & 7, + s = !!(flags & 8), + p = !!(flags & 16); + var j = k > 3 ? array.length + 1 : k ? (s ? 1 : 2) : 0, + key = __decoratorStrings[k + 5]; + var initializers = k > 3 && (array[j - 1] = []), + extraInitializers = array[j] || (array[j] = []); + var desc = + k && + (!p && !s && (target = target.prototype), + k < 5 && + (k > 3 || !p) && + __getOwnPropDesc( + k < 4 + ? target + : { + get [name]() { + return __privateGet(this, extra); + }, + set [name](x) { + return __privateSet(this, extra, x); + }, + }, + name, + )); + k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name); + + for (var i = decorators.length - 1; i >= 0; i--) { + ctx = __decoratorContext(k, name, (done = {}), array[3], extraInitializers); + + if (k) { + ((ctx.static = s), + (ctx.private = p), + (access = ctx.access = { has: p ? x => __privateIn(target, x) : x => name in x })); + if (k ^ 3) + access.get = p + ? x => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) + : x => x[name]; + if (k > 2) + access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => (x[name] = y); + } + + it = (0, decorators[i])( + k ? (k < 4 ? (p ? extra : desc[key]) : k > 4 ? void 0 : { get: desc.get, set: desc.set }) : target, + ctx, + ); + done._ = 1; + + if (k ^ 4 || it === void 0) + __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? (p ? (extra = it) : (desc[key] = it)) : (target = it)); + else if (typeof it !== "object" || it === null) __typeError("Object expected"); + else + (__expectFn((fn = it.get)) && (desc.get = fn), + __expectFn((fn = it.set)) && (desc.set = fn), + __expectFn((fn = it.init)) && initializers.unshift(fn)); + } + + return ( + k || __decoratorMetadata(array, target), + desc && __defProp(target, name, desc), + p ? (k ^ 4 ? extra : desc) : target + ); +}; + export var __esm = (fn, res) => () => (fn && (res = fn((fn = 0))), res); // This is used for JSX inlining with React. diff --git a/src/runtime.zig b/src/runtime.zig index f463d21e85a5..59016dc445f5 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -201,6 +201,7 @@ pub const Runtime = struct { unwrap_commonjs_to_esm: bool = false, emit_decorator_metadata: bool = false, + standard_decorators: bool = false, /// If true and if the source is transpiled as cjs, don't wrap the module. /// This is used for `--print` entry points so we can get the result. @@ -254,6 +255,7 @@ pub const Runtime = struct { .dont_bundle_twice, .commonjs_at_runtime, .emit_decorator_metadata, + .standard_decorators, .lower_using, // note that we do not include .inject_jest_globals, as we bail out of the cache entirely if this is true @@ -341,6 +343,16 @@ pub const Runtime = struct { __legacyDecorateClassTS: ?Ref = null, __legacyDecorateParamTS: ?Ref = null, __legacyMetadataTS: ?Ref = null, + __publicField: ?Ref = null, + __privateIn: ?Ref = null, + __privateGet: ?Ref = null, + __privateAdd: ?Ref = null, + __privateSet: ?Ref = null, + __privateMethod: ?Ref = null, + __decoratorStart: ?Ref = null, + __decoratorMetadata: ?Ref = null, + __runInitializers: ?Ref = null, + __decorateElement: ?Ref = null, @"$$typeof": ?Ref = null, __using: ?Ref = null, __callDispose: ?Ref = null, @@ -358,6 +370,16 @@ pub const Runtime = struct { "__legacyDecorateClassTS", "__legacyDecorateParamTS", "__legacyMetadataTS", + "__publicField", + "__privateIn", + "__privateGet", + "__privateAdd", + "__privateSet", + "__privateMethod", + "__decoratorStart", + "__decoratorMetadata", + "__runInitializers", + "__decorateElement", "$$typeof", "__using", "__callDispose", @@ -379,6 +401,7 @@ pub const Runtime = struct { /// When generating the list of runtime imports, we sort it for determinism. /// This is a lookup table so we don't need to resort the strings each time pub const all_sorted_index = brk: { + @setEvalBranchQuota(1000000); var out: [all.len]usize = undefined; for (all, 0..) |name, i| { for (all_sorted, 0..) |cmp, j| { diff --git a/src/transpiler.zig b/src/transpiler.zig index 3315f6124547..07d9b57b4100 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -471,6 +471,7 @@ pub const Transpiler = struct { transpiler.options.jsx = tsconfig.jsx; } transpiler.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata; + transpiler.options.experimental_decorators = tsconfig.experimental_decorators; } } } @@ -638,6 +639,7 @@ pub const Transpiler = struct { .macro_remappings = transpiler.options.macro_remap, .jsx = resolve_result.jsx, .emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata, + .experimental_decorators = resolve_result.flags.experimental_decorators, }, client_entry_point_, ) orelse { @@ -954,6 +956,7 @@ pub const Transpiler = struct { inject_jest_globals: bool = false, set_breakpoint_on_first_line: bool = false, emit_decorator_metadata: bool = false, + experimental_decorators: bool = false, remove_cjs_module_wrapper: bool = false, dont_bundle_twice: bool = false, @@ -1094,6 +1097,7 @@ pub const Transpiler = struct { var opts = js_parser.Parser.Options.init(jsx, loader); opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata; + opts.features.standard_decorators = !loader.isTypeScript() or !this_parse.experimental_decorators; opts.features.allow_runtime = transpiler.options.allow_runtime; opts.features.set_breakpoint_on_first_line = this_parse.set_breakpoint_on_first_line; opts.features.trim_unused_imports = transpiler.options.trim_unused_imports orelse loader.isTypeScript(); diff --git a/test/bundler/transpiler/es-decorators-esbuild.test.ts b/test/bundler/transpiler/es-decorators-esbuild.test.ts new file mode 100644 index 000000000000..490f9143b3e9 --- /dev/null +++ b/test/bundler/transpiler/es-decorators-esbuild.test.ts @@ -0,0 +1,199 @@ +import { describe, test } from "bun:test"; +import { readFileSync } from "fs"; +import { bunEnv, bunExe, tempDir } from "harness"; +import { join } from "path"; + +// This test file programmatically runs esbuild's decorator test suite +// (vendor/esbuild/scripts/decorator-tests.ts) against Bun's transpiler. +// Each test is run as a standalone .ts file in a temp directory with +// a tsconfig that does NOT have experimentalDecorators, so standard +// decorators are used. + +const testBoilerplate = ` +// Polyfill Symbol.metadata (not natively available in JSC) +const metaKey = Symbol.metadata || Symbol.for("Symbol.metadata"); +if (!(metaKey in Function.prototype)) { + Object.defineProperty(Function.prototype, metaKey, { value: null }); +} +if (!Symbol.metadata) Symbol.metadata = metaKey; + +function prettyPrint(x) { + if (x && x.prototype && x.prototype.constructor === x) return 'class'; + if (typeof x === 'string') return JSON.stringify(x); + try { return x + ''; } catch { return 'typeof ' + typeof x; } +} + +let _testName = ''; +let _failures = 0; + +function assertEq(callback, expected) { + let x; + try { x = callback(); } catch (e) { + const code = callback.toString().replace(/^\\(\\) => /, '').replace(/\\s+/g, ' '); + throw new Error('assertEq threw: ' + e + '\\nCode: ' + code); + } + if (x !== expected) { + const code = callback.toString().replace(/^\\(\\) => /, '').replace(/\\s+/g, ' '); + throw new Error('Expected ' + prettyPrint(expected) + ' but got ' + prettyPrint(x) + '\\nCode: ' + code); + } + return true; +} + +function assertThrows(callback, expected) { + try { + callback(); + } catch (e) { + if (e instanceof expected) return true; + throw new Error('Expected ' + expected.name + ' but threw: ' + e); + } + throw new Error('Expected ' + expected.name + ' to be thrown but nothing was thrown'); +} +`; + +function filterStderr(stderr: string) { + return stderr + .split("\n") + .filter(line => !line.startsWith("WARNING: ASAN")) + .join("\n") + .trim(); +} + +async function runDecoratorTest(code: string) { + using dir = tempDir("es-dec-esbuild", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "test.ts": testBoilerplate + "\n" + code, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr: filterStderr(rawStderr), exitCode }; +} + +// Read the esbuild decorator test source and extract individual tests +const esbuildTestPath = join(import.meta.dir, "../../../vendor/esbuild/scripts/decorator-tests.ts"); +const esbuildSource = readFileSync(esbuildTestPath, "utf8"); + +interface TestEntry { + name: string; + body: string; + isAsync: boolean; +} + +function extractTests(source: string): TestEntry[] { + const results: TestEntry[] = []; + + // Find test entry start positions using the pattern: + // 'Test Name': () => { + // 'Test Name': async () => { + const testStartRegex = /^ '([^']+)':\s*(async\s*)?\(\)\s*=>\s*\{/gm; + const starts: { name: string; isAsync: boolean; index: number; matchEnd: number }[] = []; + + let m; + while ((m = testStartRegex.exec(source)) !== null) { + starts.push({ + name: m[1], + isAsync: !!m[2], + index: m.index, + matchEnd: m.index + m[0].length, + }); + } + + // For each test, find the function body by brace counting from the opening { + for (let i = 0; i < starts.length; i++) { + const { name, isAsync, matchEnd } = starts[i]; + let depth = 1; + let pos = matchEnd; + let inString: string | null = null; + let escaped = false; + + while (pos < source.length && depth > 0) { + const ch = source[pos]; + + if (escaped) { + escaped = false; + pos++; + continue; + } + + if (ch === "\\") { + escaped = true; + pos++; + continue; + } + + if (inString) { + if (ch === inString) { + inString = null; + } + pos++; + continue; + } + + // Skip line comments (handles apostrophes in comments like "context's") + if (ch === "/" && pos + 1 < source.length && source[pos + 1] === "/") { + const nl = source.indexOf("\n", pos); + pos = nl === -1 ? source.length : nl + 1; + continue; + } + + if (ch === "'" || ch === '"' || ch === "`") { + inString = ch; + pos++; + continue; + } + + if (ch === "{") depth++; + if (ch === "}") depth--; + + pos++; + } + + // pos is now right after the closing brace + const functionBody = source.substring(matchEnd, pos - 1); // exclude closing } + results.push({ name, body: functionBody, isAsync }); + } + + return results; +} + +const allTests = extractTests(esbuildSource); + +// Known categories of tests that use unimplemented features +const todoPatterns: RegExp[] = []; + +// Additional specific tests that use features not yet working +const todoTests = new Set([]); + +function shouldTodo(name: string): boolean { + if (todoTests.has(name)) return true; + return todoPatterns.some(p => p.test(name)); +} + +describe("ES Decorators (esbuild test suite)", () => { + for (const entry of allTests) { + const { name, body, isAsync } = entry; + + const testCode = isAsync + ? `(async () => {${body}})().catch(e => { console.error(e); process.exit(1); });` + : `(() => {${body}})();`; + + if (shouldTodo(name)) { + test.todo(name); + } else { + test(name, async () => { + const { stdout, stderr, exitCode } = await runDecoratorTest(testCode); + if (exitCode !== 0) { + throw new Error( + `Test "${name}" failed with exit code ${exitCode}\n` + `stdout: ${stdout}\n` + `stderr: ${stderr}`, + ); + } + }); + } + } +}); diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts new file mode 100644 index 000000000000..a22d9110547c --- /dev/null +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -0,0 +1,475 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// ES standard decorators are used for .js files (always) and for .ts files +// when experimentalDecorators is NOT set in tsconfig. +// We test using .js files in temp directories to avoid inheriting +// the root tsconfig's experimentalDecorators: true setting. + +function filterStderr(stderr: string) { + // Filter out ASAN warnings that only appear in debug builds + return stderr + .split("\n") + .filter(line => !line.startsWith("WARNING: ASAN")) + .join("\n") + .trim(); +} + +async function runDecorator(code: string) { + using dir = tempDir("es-dec", { + "test.js": code, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr: filterStderr(rawStderr), exitCode }; +} + +describe("ES Decorators", () => { + describe("class decorators", () => { + test("basic class decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(cls, ctx) { + ctx.addInitializer(function() { + this.initialized = true; + }); + return cls; + } + @dec class Foo {} + const f = new Foo(); + console.log(Foo.initialized); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true\n"); + expect(exitCode).toBe(0); + }); + + test("class decorator receives correct context", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(cls, ctx) { + console.log(ctx.kind); + console.log(ctx.name); + console.log(typeof ctx.addInitializer); + return cls; + } + @dec class MyClass {} + `); + expect(stderr).toBe(""); + expect(stdout).toBe("class\nMyClass\nfunction\n"); + expect(exitCode).toBe(0); + }); + + test("class decorator can replace class", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(cls, ctx) { + return class extends cls { + extra = true; + }; + } + @dec class Foo { + original = true; + } + const f = new Foo(); + console.log(f.original, f.extra); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true true\n"); + expect(exitCode).toBe(0); + }); + + test("multiple class decorators apply in reverse order", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const order = []; + function dec1(cls, ctx) { order.push("dec1"); return cls; } + function dec2(cls, ctx) { order.push("dec2"); return cls; } + function dec3(cls, ctx) { order.push("dec3"); return cls; } + @dec1 @dec2 @dec3 class Foo {} + console.log(order.join(",")); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("dec3,dec2,dec1\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("method decorators", () => { + test("instance method decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function log(fn, ctx) { + return function(...args) { + console.log("before", ctx.name); + const result = fn.call(this, ...args); + console.log("after", ctx.name); + return result; + }; + } + class Foo { + @log greet() { console.log("hello"); } + } + new Foo().greet(); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("before greet\nhello\nafter greet\n"); + expect(exitCode).toBe(0); + }); + + test("static method decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(fn, ctx) { + console.log(ctx.kind, ctx.name, ctx.static); + return fn; + } + class Foo { + @dec static bar() { return 42; } + } + console.log(Foo.bar()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("method bar true\n42\n"); + expect(exitCode).toBe(0); + }); + + test("method decorator context has correct access", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + let savedAccess; + function dec(fn, ctx) { + savedAccess = ctx.access; + return fn; + } + class Foo { + @dec bar() { return 42; } + } + const f = new Foo(); + console.log(savedAccess.has(f)); + console.log(savedAccess.get(f)()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true\n42\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("getter decorators", () => { + test("getter decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(fn, ctx) { + console.log(ctx.kind, ctx.name); + return fn; + } + class Foo { + @dec get x() { return 42; } + } + console.log(new Foo().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("getter x\n42\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("setter decorators", () => { + test("setter decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(fn, ctx) { + console.log(ctx.kind, ctx.name); + return fn; + } + class Foo { + _val = 0; + @dec set x(v) { this._val = v; } + } + const f = new Foo(); + f.x = 99; + console.log(f._val); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("setter x\n99\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("field decorators", () => { + test("field decorator receives undefined value", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { + console.log(ctx.kind, ctx.name, value); + return undefined; + } + class Foo { + @dec x = 42; + } + console.log(new Foo().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("field x undefined\n42\n"); + expect(exitCode).toBe(0); + }); + + test("multiple field decorators", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const order = []; + function dec1(value, ctx) { order.push("dec1:" + ctx.name); } + function dec2(value, ctx) { order.push("dec2:" + ctx.name); } + class Foo { + @dec1 @dec2 x = 1; + @dec1 y = 2; + } + console.log(order.join(",")); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("dec2:x,dec1:x,dec1:y\n"); + expect(exitCode).toBe(0); + }); + + test("static field decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { + console.log(ctx.kind, ctx.name, ctx.static); + return undefined; + } + class Foo { + @dec static x = 10; + } + console.log(Foo.x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("field x true\n10\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("decorator ordering", () => { + test("decorators on different elements evaluate in source order", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const order = []; + function track(name) { + return function(value, ctx) { + order.push(name + ":" + ctx.kind + ":" + ctx.name); + return value; + }; + } + @track("cls") + class Foo { + @track("method") foo() {} + @track("field") x = 1; + @track("getter") get y() { return 2; } + @track("setter") set y(v) {} + } + console.log(order.join("\\n")); + `); + expect(stderr).toBe(""); + expect(stdout).toContain("method:method:foo"); + expect(stdout).toContain("field:field:x"); + expect(stdout).toContain("getter:getter:y"); + expect(stdout).toContain("setter:setter:y"); + expect(stdout).toContain("cls:class:Foo"); + expect(exitCode).toBe(0); + }); + }); + + describe("decorator expressions", () => { + test("member expression decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const decorators = { + log: function(fn, ctx) { + console.log("decorated", ctx.name); + return fn; + } + }; + class Foo { + @decorators.log bar() {} + } + console.log("done"); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("decorated bar\ndone\n"); + expect(exitCode).toBe(0); + }); + + test("call expression decorator", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function withTag(tag) { + return function(fn, ctx) { + console.log(tag, ctx.name); + return fn; + }; + } + class Foo { + @withTag("hello") bar() {} + } + console.log("done"); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("hello bar\ndone\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("metadata", () => { + test("Symbol.metadata is set on decorated class", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + // Symbol.metadata may not exist natively, use the same fallback as the runtime + const metadataKey = Symbol.metadata || Symbol.for("Symbol.metadata"); + function dec(cls, ctx) { return cls; } + @dec class Foo {} + console.log(typeof Foo[metadataKey]); + console.log(Foo[metadataKey] !== null); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("object\ntrue\n"); + expect(exitCode).toBe(0); + }); + + test("metadata inherits from parent class", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const metadataKey = Symbol.metadata || Symbol.for("Symbol.metadata"); + function dec(cls, ctx) { + ctx.metadata.decorated = true; + return cls; + } + @dec class Base {} + @dec class Child extends Base {} + console.log(Base[metadataKey].decorated); + console.log(Child[metadataKey].decorated); + console.log(Object.getPrototypeOf(Child[metadataKey]) === Base[metadataKey]); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true\ntrue\ntrue\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("addInitializer", () => { + test("class addInitializer runs after class is created", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const order = []; + function dec(cls, ctx) { + ctx.addInitializer(function() { + order.push("initializer"); + }); + return cls; + } + order.push("before"); + @dec class Foo {} + order.push("after"); + console.log(order.join(",")); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("before,initializer,after\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("standard vs experimental mode switching", () => { + test("JS files use standard decorators by default", async () => { + // JS files always use standard decorators, even when + // experimentalDecorators is set in tsconfig + using dir = tempDir("es-dec-js", { + "tsconfig.json": JSON.stringify({ + compilerOptions: { experimentalDecorators: true }, + }), + "test.js": ` + function dec(cls, ctx) { + console.log(ctx.kind); + return cls; + } + @dec class Foo {} + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("class\n"); + expect(exitCode).toBe(0); + }); + + test("TS files use experimental decorators when experimentalDecorators is set", async () => { + using dir = tempDir("es-dec-ts-exp", { + "tsconfig.json": JSON.stringify({ + compilerOptions: { experimentalDecorators: true }, + }), + "test.ts": ` + function dec(target: any) { + target.decorated = true; + } + @dec class Foo {} + console.log((Foo as any).decorated); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("true\n"); + expect(exitCode).toBe(0); + }); + + test("TS files use standard decorators when experimentalDecorators is not set", async () => { + using dir = tempDir("es-dec-ts-std", { + "tsconfig.json": JSON.stringify({ + compilerOptions: {}, + }), + "test.ts": ` + function dec(cls: any, ctx: any) { + console.log(ctx.kind, ctx.name); + return cls; + } + @dec class Foo {} + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("class Foo\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("extends clause", () => { + test("decorator on class with extends", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(cls, ctx) { + console.log(ctx.kind, ctx.name); + return cls; + } + class Base { + base = true; + } + @dec class Child extends Base { + child = true; + } + const c = new Child(); + console.log(c.base, c.child); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("class Child\ntrue true\n"); + expect(exitCode).toBe(0); + }); + }); +}); From e0c5e03d63ab8841a64a84cf09d6940b133675e0 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 08:48:03 +0100 Subject: [PATCH 02/12] refactor(transpiler): extract decorator lowering into lowerDecorators.zig Move ~2027 lines of ES decorator lowering logic from P.zig into a dedicated lowerDecorators.zig module. Key improvements: - Helper functions (useRef, callRt, newSym) eliminate duplicated allocation and recordUsage patterns (~80 sites consolidated) - Unified tree rewriter replaces 4 separate replace functions - Private access rewriting uses one-liner helpers instead of manual arg-array construction - Net reduction of ~600 lines (2027 removed, 1426 added) All 191 decorator tests pass (es-decorators-esbuild: 147, es-decorators: 22, decorators: 22). Co-Authored-By: Claude --- src/ast/P.zig | 2031 +---------------------------------- src/ast/lowerDecorators.zig | 1426 ++++++++++++++++++++++++ 2 files changed, 1430 insertions(+), 2027 deletions(-) create mode 100644 src/ast/lowerDecorators.zig diff --git a/src/ast/P.zig b/src/ast/P.zig index 188dc2897a4e..81b696f991d7 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -137,6 +137,10 @@ pub fn NewParser_( pub const findSymbol = symbols_zig.findSymbol; pub const findSymbolWithRecordUsage = symbols_zig.findSymbolWithRecordUsage; + const lowerDecorators_zig = @import("./lowerDecorators.zig").LowerDecorators(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only); + pub const lowerStandardDecoratorsStmt = lowerDecorators_zig.lowerStandardDecoratorsStmt; + pub const lowerStandardDecoratorsExpr = lowerDecorators_zig.lowerStandardDecoratorsExpr; + macro: MacroState = undefined, allocator: Allocator, options: Parser.Options, @@ -4856,2033 +4860,6 @@ pub fn NewParser_( stmts.append(closure) catch unreachable; } - /// Info about a lowered private member. - const PrivateLoweredInfo = struct { - storage_ref: Ref, // WeakMap (fields) or WeakSet (methods/getters/setters) ref - method_fn_ref: ?Ref = null, // For k=1 (method): ref to decorated function variable - getter_fn_ref: ?Ref = null, // For k=2 (getter): ref to decorated getter function variable - setter_fn_ref: ?Ref = null, // For k=3 (setter): ref to decorated setter function variable - accessor_desc_ref: ?Ref = null, // For k=4 private accessor: ref to captured descriptor - }; - - /// Map type for tracking private members that need lowering to WeakMap/WeakSet. - /// Maps private identifier inner_index -> storage info. - const PrivateLoweredMap = std.AutoHashMapUnmanaged(u32, PrivateLoweredInfo); - - /// Recursively walk an expression tree and rewrite private member accesses - /// that have been lowered to WeakMap/WeakSet storage. - fn rewritePrivateAccessesInExpr(p: *P, expr: *Expr, map: *const PrivateLoweredMap) void { - switch (expr.data) { - .e_index => |e| { - // Recurse into target first - p.rewritePrivateAccessesInExpr(&e.target, map); - // Check if index is a private identifier that needs lowering - if (e.index.data == .e_private_identifier) { - if (map.get(e.index.data.e_private_identifier.ref.innerIndex())) |info| { - if (info.accessor_desc_ref) |desc_ref| { - // Private auto-accessor: obj.#prop → __privateGet(obj, _wm, _acc.get) - const args = bun.handleOom(p.allocator.alloc(Expr, 3)); - args[0] = e.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - p.recordUsage(desc_ref); - args[2] = p.newExpr(E.Dot{ - .target = p.newExpr(E.Identifier{ .ref = desc_ref }, expr.loc), - .name = "get", - .name_loc = expr.loc, - }, expr.loc); - expr.* = p.callRuntime(expr.loc, "__privateGet", args); - } else if (info.getter_fn_ref) |fn_ref| { - // Private getter: obj.#prop → __privateGet(obj, _ws, _getter_fn) - const args = bun.handleOom(p.allocator.alloc(Expr, 3)); - args[0] = e.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - p.recordUsage(fn_ref); - args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); - expr.* = p.callRuntime(expr.loc, "__privateGet", args); - } else if (info.method_fn_ref) |fn_ref| { - // Private method: obj.#method → __privateMethod(obj, _ws, _fn) - const args = bun.handleOom(p.allocator.alloc(Expr, 3)); - args[0] = e.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - p.recordUsage(fn_ref); - args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); - expr.* = p.callRuntime(expr.loc, "__privateMethod", args); - } else { - // Private field/accessor: obj.#name → __privateGet(obj, _wm) - const args = bun.handleOom(p.allocator.alloc(Expr, 2)); - args[0] = e.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - expr.* = p.callRuntime(expr.loc, "__privateGet", args); - } - return; - } - } - p.rewritePrivateAccessesInExpr(&e.index, map); - }, - .e_binary => |e| { - // Check for assignment to private member: obj.#name = val - if (e.op == .bin_assign and e.left.data == .e_index) { - if (e.left.data.e_index.index.data == .e_private_identifier) { - if (map.get(e.left.data.e_index.index.data.e_private_identifier.ref.innerIndex())) |info| { - p.rewritePrivateAccessesInExpr(&e.left.data.e_index.target, map); - p.rewritePrivateAccessesInExpr(&e.right, map); - if (info.accessor_desc_ref) |desc_ref| { - // Private auto-accessor: obj.#prop = val → __privateSet(obj, _wm, val, _acc.set) - const args = bun.handleOom(p.allocator.alloc(Expr, 4)); - args[0] = e.left.data.e_index.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - args[2] = e.right; - p.recordUsage(desc_ref); - args[3] = p.newExpr(E.Dot{ - .target = p.newExpr(E.Identifier{ .ref = desc_ref }, expr.loc), - .name = "set", - .name_loc = expr.loc, - }, expr.loc); - expr.* = p.callRuntime(expr.loc, "__privateSet", args); - } else if (info.setter_fn_ref) |fn_ref| { - // Private setter: obj.#prop = val → __privateSet(obj, _ws, val, _setter_fn) - const args = bun.handleOom(p.allocator.alloc(Expr, 4)); - args[0] = e.left.data.e_index.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - args[2] = e.right; - p.recordUsage(fn_ref); - args[3] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); - expr.* = p.callRuntime(expr.loc, "__privateSet", args); - } else { - // Private field: obj.#name = val → __privateSet(obj, _wm, val) - const args = bun.handleOom(p.allocator.alloc(Expr, 3)); - args[0] = e.left.data.e_index.target; - p.recordUsage(info.storage_ref); - args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - args[2] = e.right; - expr.* = p.callRuntime(expr.loc, "__privateSet", args); - } - return; - } - } - } - // Check for #name in obj - if (e.op == .bin_in and e.left.data == .e_private_identifier) { - if (map.get(e.left.data.e_private_identifier.ref.innerIndex())) |info| { - p.rewritePrivateAccessesInExpr(&e.right, map); - const args = bun.handleOom(p.allocator.alloc(Expr, 2)); - p.recordUsage(info.storage_ref); - args[0] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - args[1] = e.right; - expr.* = p.callRuntime(expr.loc, "__privateIn", args); - return; - } - } - p.rewritePrivateAccessesInExpr(&e.left, map); - p.rewritePrivateAccessesInExpr(&e.right, map); - }, - .e_unary => |e| { - p.rewritePrivateAccessesInExpr(&e.value, map); - }, - .e_call => |e| { - // Check for obj.#method(args) → __privateMethod(obj, _ws, _fn).call(obj, args) - // or obj.#field(args) → __privateGet(obj, _wm).call(obj, args) - if (e.target.data == .e_index) { - if (e.target.data.e_index.index.data == .e_private_identifier) { - if (map.get(e.target.data.e_index.index.data.e_private_identifier.ref.innerIndex())) |info| { - p.rewritePrivateAccessesInExpr(&e.target.data.e_index.target, map); - const obj_expr = e.target.data.e_index.target; - // Build the accessor call - const private_access = brk: { - if (info.getter_fn_ref) |fn_ref| { - // Private getter called as function: __privateGet(obj, _ws, _getter_fn) - const get_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - get_args[0] = obj_expr; - p.recordUsage(info.storage_ref); - get_args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - p.recordUsage(fn_ref); - get_args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); - break :brk p.callRuntime(expr.loc, "__privateGet", get_args); - } else if (info.method_fn_ref) |fn_ref| { - // Private method: __privateMethod(obj, _ws, _fn) - const method_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - method_args[0] = obj_expr; - p.recordUsage(info.storage_ref); - method_args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - p.recordUsage(fn_ref); - method_args[2] = p.newExpr(E.Identifier{ .ref = fn_ref }, expr.loc); - break :brk p.callRuntime(expr.loc, "__privateMethod", method_args); - } else { - // Private field/accessor: __privateGet(obj, _wm) - const get_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - get_args[0] = obj_expr; - p.recordUsage(info.storage_ref); - get_args[1] = p.newExpr(E.Identifier{ .ref = info.storage_ref }, expr.loc); - break :brk p.callRuntime(expr.loc, "__privateGet", get_args); - } - }; - // Build .call(obj, ...original_args) - const call_target = p.newExpr(E.Dot{ - .target = private_access, - .name = "call", - .name_loc = expr.loc, - }, expr.loc); - const orig_args = e.args.slice(); - const new_args = bun.handleOom(p.allocator.alloc(Expr, 1 + orig_args.len)); - new_args[0] = obj_expr; // 'this' for .call - for (orig_args, 0..) |*arg, ai| { - p.rewritePrivateAccessesInExpr(arg, map); - new_args[1 + ai] = arg.*; - } - e.target = call_target; - e.args = ExprNodeList.fromOwnedSlice(new_args); - return; - } - } - } - p.rewritePrivateAccessesInExpr(&e.target, map); - for (e.args.slice()) |*arg| { - p.rewritePrivateAccessesInExpr(arg, map); - } - }, - .e_dot => |e| { - p.rewritePrivateAccessesInExpr(&e.target, map); - }, - .e_spread => |e| { - p.rewritePrivateAccessesInExpr(&e.value, map); - }, - .e_if => |e| { - p.rewritePrivateAccessesInExpr(&e.test_, map); - p.rewritePrivateAccessesInExpr(&e.yes, map); - p.rewritePrivateAccessesInExpr(&e.no, map); - }, - .e_await => |e| { - p.rewritePrivateAccessesInExpr(&e.value, map); - }, - .e_yield => |e| { - if (e.value) |*v| p.rewritePrivateAccessesInExpr(v, map); - }, - .e_new => |e| { - p.rewritePrivateAccessesInExpr(&e.target, map); - for (e.args.slice()) |*arg| { - p.rewritePrivateAccessesInExpr(arg, map); - } - }, - .e_array => |e| { - for (e.items.slice()) |*item| { - p.rewritePrivateAccessesInExpr(item, map); - } - }, - .e_object => |e| { - for (e.properties.slice()) |*prop| { - if (prop.value) |*v| p.rewritePrivateAccessesInExpr(v, map); - if (prop.initializer) |*ini| p.rewritePrivateAccessesInExpr(ini, map); - } - }, - .e_template => |e| { - if (e.tag) |*t| p.rewritePrivateAccessesInExpr(t, map); - for (e.parts) |*part| { - p.rewritePrivateAccessesInExpr(&part.value, map); - } - }, - .e_function => |e| { - p.rewritePrivateAccessesInStmts(e.func.body.stmts, map); - }, - .e_arrow => |e| { - p.rewritePrivateAccessesInStmts(e.body.stmts, map); - }, - else => {}, - } - } - - /// Recursively walk a statement list and rewrite private member accesses. - fn rewritePrivateAccessesInStmts(p: *P, stmts: []Stmt, map: *const PrivateLoweredMap) void { - for (stmts) |*stmt_item| { - switch (stmt_item.data) { - .s_expr => |data| p.rewritePrivateAccessesInExpr(&data.value, map), - .s_return => |data| { - if (data.value) |*v| p.rewritePrivateAccessesInExpr(v, map); - }, - .s_throw => |data| p.rewritePrivateAccessesInExpr(&data.value, map), - .s_local => |data| { - for (data.decls.slice()) |*decl| { - if (decl.value) |*v| p.rewritePrivateAccessesInExpr(v, map); - } - }, - .s_if => |data| { - p.rewritePrivateAccessesInExpr(&data.test_, map); - p.rewritePrivateAccessesInStmts((&data.yes)[0..1], map); - if (data.no) |*no| p.rewritePrivateAccessesInStmts(no[0..1], map); - }, - .s_block => |data| p.rewritePrivateAccessesInStmts(data.stmts, map), - .s_for => |data| { - if (data.init) |*for_init| p.rewritePrivateAccessesInStmts(for_init[0..1], map); - if (data.test_) |*t| p.rewritePrivateAccessesInExpr(t, map); - if (data.update) |*u| p.rewritePrivateAccessesInExpr(u, map); - p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); - }, - .s_for_in => |data| { - p.rewritePrivateAccessesInExpr(&data.value, map); - p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); - }, - .s_for_of => |data| { - p.rewritePrivateAccessesInExpr(&data.value, map); - p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); - }, - .s_while => |data| { - p.rewritePrivateAccessesInExpr(&data.test_, map); - p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); - }, - .s_do_while => |data| { - p.rewritePrivateAccessesInExpr(&data.test_, map); - p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); - }, - .s_switch => |data| { - p.rewritePrivateAccessesInExpr(&data.test_, map); - for (data.cases) |*case| { - if (case.value) |*v| p.rewritePrivateAccessesInExpr(v, map); - p.rewritePrivateAccessesInStmts(case.body, map); - } - }, - .s_try => |data| { - p.rewritePrivateAccessesInStmts(data.body, map); - if (data.catch_) |c| p.rewritePrivateAccessesInStmts(c.body, map); - if (data.finally) |f| p.rewritePrivateAccessesInStmts(f.stmts, map); - }, - .s_label => |data| p.rewritePrivateAccessesInStmts((&data.stmt)[0..1], map), - .s_with => |data| { - p.rewritePrivateAccessesInExpr(&data.value, map); - p.rewritePrivateAccessesInStmts((&data.body)[0..1], map); - }, - else => {}, - } - } - } - - /// Lower standard (TC39) decorators on a class statement. - /// Emits: var _init = __decoratorStart(base); class Foo { ... } __decorateElement(...); ... - fn lowerStandardDecoratorsStmt(p: *P, stmt: Stmt) []Stmt { - return p.lowerStandardDecoratorsImpl(stmt, .stmt); - } - - pub fn lowerStandardDecoratorsExpr(p: *P, class: *G.Class, loc: logger.Loc, name_from_context: ?[]const u8) Expr { - const result = p.lowerStandardDecoratorsImpl(Stmt.empty(), .{ .expr = .{ .class = class, .loc = loc, .name_from_context = name_from_context } }); - if (result.len == 0) return p.newExpr(E.Missing{}, loc); - // For expressions, result[0] contains the comma expression - return result[0].data.s_expr.value; - } - - const StdDecMode = union(enum) { - stmt, - expr: struct { - class: *G.Class, - loc: logger.Loc, - name_from_context: ?[]const u8 = null, - }, - }; - - /// Core lowering logic for standard (TC39) decorators. - /// Handles both statement and expression forms. - fn lowerStandardDecoratorsImpl(p: *P, stmt: Stmt, mode: StdDecMode) []Stmt { - const is_expr = mode == .expr; - var class = switch (mode) { - .stmt => &stmt.data.s_class.class, - .expr => |e| e.class, - }; - const loc = switch (mode) { - .stmt => stmt.loc, - .expr => |e| e.loc, - }; - const name_from_context: ?[]const u8 = switch (mode) { - .expr => |e| e.name_from_context, - .stmt => null, - }; - - // For expressions, create a temp variable to capture the class - var class_name_ref: Ref = undefined; - var class_name_loc: logger.Loc = undefined; - var expr_class_ref: ?Ref = null; - var expr_class_is_anonymous = false; - - // For expressions, collect refs that need var declarations - var expr_var_decls = ListManaged(G.Decl).init(p.allocator); - - if (is_expr) { - // Create a temp ref for the class expression value - expr_class_ref = p.newSymbol(.other, "_class") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, expr_class_ref.?)); - expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = expr_class_ref.? }, loc) }) catch unreachable; - - // For class expressions, create or use the class name binding - if (class.class_name) |cn| { - class_name_ref = cn.ref.?; - class_name_loc = cn.loc; - // Don't declare class_name_ref (e.g., Foo) as var here. - // For class binding semantics, Foo must remain undeclared in the - // outer scope so that class decorator closures over Foo get - // ReferenceError. We'll swap class_name_ref to expr_class_ref - // after pre-eval replacement for all suffix operations. - } else { - // Anonymous class: use _class ref for both class value and name target - class_name_ref = expr_class_ref.?; - class_name_loc = loc; - expr_class_is_anonymous = true; - - // If we have a name from assignment context (e.g., { Foo: @dec class {} }), - // set the class name so the class expression preserves .name property - if (name_from_context) |name| { - const name_ref = p.newSymbol(.other, name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, name_ref)); - class.class_name = .{ .ref = name_ref, .loc = loc }; - } - } - } else { - class_name_ref = class.class_name.?.ref.?; - class_name_loc = class.class_name.?.loc; - } - - // For proper class binding semantics (TC39 15.7.14): element decorators - // should capture an inner class name binding that survives mutation of the - // outer class name binding. In statement mode, we create an inner `let _Foo = Foo` - // after the suffix. In expression mode, element decorators use expr_class_ref. - // Element decorator pre-eval expressions will have class_name_ref replaced - // with inner_class_ref so their closures capture the inner binding. - var inner_class_ref: Ref = class_name_ref; - if (!is_expr) { - const class_name_str = p.symbols.items[class_name_ref.innerIndex()].original_name; - const inner_name = std.fmt.allocPrint(p.allocator, "_{s}", .{class_name_str}) catch unreachable; - inner_class_ref = p.newSymbol(.other, inner_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, inner_class_ref)); - } - - // Save and clear class-level decorators - const class_decorators = class.ts_decorators; - class.ts_decorators = .{}; - - // Create the _init symbol for the decorator context - const init_ref = p.newSymbol(.other, "_init") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, init_ref)); - if (is_expr) { - expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = init_ref }, loc) }) catch unreachable; - } - - // If class extends a base, extract the extends expression into a temp variable - // to avoid double evaluation (once for __decoratorStart, once for class extends). - // Input: class Foo extends (expr) { ... } - // Output: var _base = expr; var _init = __decoratorStart(_base); class Foo extends _base { ... } - var base_ref: ?Ref = null; - if (class.extends != null) { - base_ref = p.newSymbol(.other, "_base") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, base_ref.?)); - if (is_expr) { - expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = base_ref.? }, loc) }) catch unreachable; - } - } - - // === Pre-evaluate decorator expressions in source order === - // TC39 spec requires all decorator expressions and computed property keys - // to be evaluated in source order before the class body. We extract them - // into temp variables in the prefix. - var dec_counter: usize = 0; - - // Pre-evaluate class decorator expressions (evaluated before extends) - var class_dec_ref: ?Ref = null; - var class_dec_stmt: Stmt = Stmt.empty(); - var class_dec_assign_expr: ?Expr = null; - if (class_decorators.len > 0) { - dec_counter += 1; - class_dec_ref = p.newSymbol(.other, "_dec") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, class_dec_ref.?)); - const class_dec_array = p.newExpr(E.Array{ .items = class_decorators }, loc); - if (is_expr) { - expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = class_dec_ref.? }, loc) }) catch unreachable; - p.recordUsage(class_dec_ref.?); - class_dec_assign_expr = Expr.assign( - p.newExpr(E.Identifier{ .ref = class_dec_ref.? }, loc), - class_dec_array, - ); - } else { - const decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = class_dec_ref.? }, loc), - .value = class_dec_array, - }; - class_dec_stmt = p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(decls) }, loc); - } - } - - // Pre-evaluate property decorator expressions and computed keys - // Maps property index (in class.properties) to its pre-evaluated decorator ref - var prop_dec_refs = std.AutoHashMapUnmanaged(usize, Ref){}; - var computed_key_refs = std.AutoHashMapUnmanaged(usize, Ref){}; - // Collect all pre-evaluation declarations in source order - var pre_eval_stmts = ListManaged(Stmt).init(p.allocator); - var computed_key_counter: usize = 0; - for (class.properties, 0..) |*prop, prop_idx| { - if (prop.kind == .class_static_block) continue; - - // Pre-evaluate decorator expressions for this property - if (prop.ts_decorators.len > 0) { - dec_counter += 1; - const dec_name = if (dec_counter == 1) - "_dec" - else - std.fmt.allocPrint(p.allocator, "_dec{d}", .{dec_counter}) catch unreachable; - const dec_ref = p.newSymbol(.other, dec_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, dec_ref)); - prop_dec_refs.put(p.allocator, prop_idx, dec_ref) catch unreachable; - - const dec_array = p.newExpr(E.Array{ .items = prop.ts_decorators }, loc); - if (is_expr) { - expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = dec_ref }, loc) }) catch unreachable; - } - const dec_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - dec_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = dec_ref }, loc), - .value = dec_array, - }; - pre_eval_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(dec_decls) }, loc)) catch unreachable; - } - - // Pre-evaluate computed property keys (for decorated properties or properties - // in decorated classes where source-order evaluation matters) - if (prop.flags.contains(.is_computed) and prop.key != null and prop.ts_decorators.len > 0) { - computed_key_counter += 1; - const key_name = if (computed_key_counter == 1) - "_computedKey" - else - std.fmt.allocPrint(p.allocator, "_computedKey{d}", .{computed_key_counter}) catch unreachable; - const key_ref = p.newSymbol(.other, key_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, key_ref)); - computed_key_refs.put(p.allocator, prop_idx, key_ref) catch unreachable; - - if (is_expr) { - expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = key_ref }, loc) }) catch unreachable; - } - const key_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - key_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = key_ref }, loc), - .value = prop.key.?, - }; - pre_eval_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(key_decls) }, loc)) catch unreachable; - - // Replace the property key with the temp variable reference - p.recordUsage(key_ref); - prop.key = p.newExpr(E.Identifier{ .ref = key_ref }, prop.key.?.loc); - // Computed keys referenced via temp var are no longer "computed" in the - // traditional sense (they're simple identifier lookups), but we keep the - // flag so downstream code still treats them as dynamic keys. - } - } - - // Replace class name refs in element decorator pre-eval expressions. - // Element decorators should capture the inner class binding, not the outer one. - { - const replacement_ref = if (is_expr) (expr_class_ref orelse class_name_ref) else inner_class_ref; - if (!replacement_ref.eql(class_name_ref)) { - for (pre_eval_stmts.items) |*pre_stmt| { - switch (pre_stmt.data) { - .s_local => |local| { - for (local.decls.slice()) |*decl| { - if (decl.value) |*v| { - p.replaceRefInExpr(v, class_name_ref, replacement_ref); - } - } - }, - else => {}, - } - } - } - } - - // For named class expressions: swap class_name_ref to expr_class_ref so that - // all suffix operations (class decorator assignment, __runInitializers, metadata) - // use expr_class_ref (_class) instead of the original class name (Foo). - // This keeps Foo undeclared in the outer scope, so class decorator closures - // over Foo correctly throw ReferenceError. - var original_class_name_for_decorator: ?[]const u8 = null; - if (is_expr and !expr_class_is_anonymous and expr_class_ref != null) { - original_class_name_for_decorator = p.symbols.items[class_name_ref.inner_index].original_name; - class_name_ref = expr_class_ref.?; - class_name_loc = loc; - } - - // Build: var _init = __decoratorStart(base) (for statements) - // or: _init = __decoratorStart(base) (for expressions, part of comma chain) - const init_start_expr: Expr = brk: { - const base_expr = if (base_ref) |br| - p.newExpr(E.Identifier{ .ref = br }, loc) - else - p.newExpr(E.Undefined{}, loc); - - const start_args = bun.handleOom(p.allocator.alloc(Expr, 1)); - start_args[0] = base_expr; - break :brk p.callRuntime(loc, "__decoratorStart", start_args); - }; - - // For statement mode: base decl goes before init decl - var base_decl_stmt: Stmt = Stmt.empty(); - if (!is_expr) { - if (base_ref) |br| { - const base_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - base_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = br }, loc), - .value = class.extends, - }; - base_decl_stmt = p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(base_decls) }, loc); - } - } - - // For expressions: store base assignment to be added to comma chain later - const base_assign_expr: ?Expr = if (is_expr and base_ref != null) brk: { - p.recordUsage(base_ref.?); - break :brk Expr.assign( - p.newExpr(E.Identifier{ .ref = base_ref.? }, loc), - class.extends.?, - ); - } else null; - - // Update class.extends to use the temp variable - if (base_ref) |br| { - p.recordUsage(br); - class.extends = p.newExpr(E.Identifier{ .ref = br }, loc); - } - - const init_decl_stmt: Stmt = if (!is_expr) brk: { - const decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = init_ref }, loc), - .value = init_start_expr, - }; - break :brk p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(decls) }, loc); - } else Stmt.empty(); - - // Collect suffix expressions to be emitted as statements after the class - var suffix_exprs = ListManaged(Expr).init(p.allocator); - - // Collect statements to inject into the constructor (for instance fields/methods) - var constructor_inject_stmts = ListManaged(Stmt).init(p.allocator); - - // New class properties (decorated fields removed from class body) - var new_properties = ListManaged(Property).init(p.allocator); - - // Categorize decorated elements for __decorateElement ordering - // Per TC39 spec, the order is: static non-fields → instance non-fields → static fields → instance fields - // Accessors (k=4) go in the NON-FIELD group for __decorateElement ordering, - // but need field-like init/extra-init tracking. - var static_non_field_elements = ListManaged(Expr).init(p.allocator); - var instance_non_field_elements = ListManaged(Expr).init(p.allocator); - // Track private methods separately (they go in prefix, not suffix) - var has_static_private_methods = false; - var has_instance_private_methods = false; - - // Separate field-only __decorateElement lists (k=5, NOT accessors) - var static_field_decorate = ListManaged(Expr).init(p.allocator); - var instance_field_decorate = ListManaged(Expr).init(p.allocator); - - // Accessor counts for field_idx computation - var static_accessor_count: usize = 0; - var instance_accessor_count: usize = 0; - - // Combined field/accessor init entries in source order (for init/extra-init) - const FieldInitEntry = struct { - prop: Property, - is_private: bool, - is_accessor: bool, - }; - var static_init_entries = ListManaged(FieldInitEntry).init(p.allocator); - var instance_init_entries = ListManaged(FieldInitEntry).init(p.allocator); - - // Track source order of static elements (blocks + field/accessor inits) - const StaticElementKind = enum { block, field_or_accessor }; - const StaticElement = struct { - kind: StaticElementKind, - index: usize, // index into static_init_entries (for field_or_accessor) or extracted_static_blocks - }; - var static_element_order = ListManaged(StaticElement).init(p.allocator); - var extracted_static_blocks = ListManaged(*G.ClassStaticBlock).init(p.allocator); - - // For private members, track WeakMap/WeakSet refs and prefix statements - var prefix_stmts = ListManaged(Stmt).init(p.allocator); - - // Map private identifier refs to their WeakMap/WeakSet storage refs - // Used to rewrite this.#x → __privateGet(this, _x) in the class body - var private_lowered_map = PrivateLoweredMap{}; - - // Counter for unique accessor storage names (used for computed keys) - var accessor_storage_counter: usize = 0; - - // Track which private names already had __privateAdd emitted (for getter+setter pairs) - var emitted_private_adds = std.AutoHashMapUnmanaged(u32, void){}; - // Static private __privateAdd calls need to be static blocks at the START of the class body - var static_private_add_blocks = ListManaged(Property).init(p.allocator); - - // Pre-scan: if any private member is decorated OR if the class has both - // private members and decorated members, ALL private members need lowering. - // This is because: - // 1. Extracted decorator functions may reference other private members - // 2. Decorator expressions pre-evaluated outside the class may reference - // private names (e.g., `@(dec(() => new Foo().#priv)) method() {}`) - var lower_all_private = false; - { - var has_any_private = false; - var has_any_decorated = false; - for (class.properties) |cprop| { - if (cprop.kind == .class_static_block) continue; - if (cprop.ts_decorators.len > 0) { - has_any_decorated = true; - if (cprop.key != null and cprop.key.?.data == .e_private_identifier) { - // If a private member itself is decorated, always lower - lower_all_private = true; - break; - } - } - if (cprop.key != null and cprop.key.?.data == .e_private_identifier) { - has_any_private = true; - } - } - // Also lower if the class has both private members and decorated members, - // since decorator expressions may reference private names - if (!lower_all_private and has_any_private and has_any_decorated) { - lower_all_private = true; - } - } - - // Process each property - for (class.properties, 0..) |*prop, prop_idx| { - if (prop.ts_decorators.len == 0) { - // Non-decorated property - if (lower_all_private and prop.key != null and prop.key.?.data == .e_private_identifier and prop.kind != .class_static_block) { - // Lower this non-decorated private member to WeakMap/WeakSet - // so that references from extracted decorated functions can be rewritten - const nk_expr = prop.key.?; - const npriv_orig = p.symbols.items[nk_expr.data.e_private_identifier.ref.inner_index].original_name; - const npriv_inner = nk_expr.data.e_private_identifier.ref.innerIndex(); - - if (prop.flags.contains(.is_method)) { - // Non-decorated private method/getter/setter: lower to WeakSet - const nk: u8 = switch (prop.kind) { - .get => 2, - .set => 3, - else => 1, - }; - const existing2 = private_lowered_map.get(npriv_inner); - const ws_ref2 = if (existing2) |ex| ex.storage_ref else brk2: { - const ws_nm = std.fmt.allocPrint(p.allocator, "_{s}", .{npriv_orig[1..]}) catch unreachable; - const ref2 = p.newSymbol(.other, ws_nm) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, ref2)); - break :brk2 ref2; - }; - const fn_sfx = if (nk == 2) "_get" else if (nk == 3) "_set" else "_fn"; - const fn_nm = std.fmt.allocPrint(p.allocator, "_{s}{s}", .{ npriv_orig[1..], fn_sfx }) catch unreachable; - const fn_ref2 = p.newSymbol(.other, fn_nm) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, fn_ref2)); - - var new_info2 = if (existing2) |ex| ex else PrivateLoweredInfo{ .storage_ref = ws_ref2 }; - if (nk == 1) { - new_info2.method_fn_ref = fn_ref2; - } else if (nk == 2) { - new_info2.getter_fn_ref = fn_ref2; - } else { - new_info2.setter_fn_ref = fn_ref2; - } - private_lowered_map.put(p.allocator, npriv_inner, new_info2) catch unreachable; - - if (existing2 == null) { - // Create WeakSet declaration - const ws_gr = p.newSymbol(.unbound, "WeakSet") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, ws_gr)); - const ws_new2 = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = ws_gr }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const ws_d = bun.handleOom(p.allocator.alloc(G.Decl, 2)); - ws_d[0] = .{ .binding = p.b(B.Identifier{ .ref = ws_ref2 }, loc), .value = ws_new2 }; - ws_d[1] = .{ .binding = p.b(B.Identifier{ .ref = fn_ref2 }, loc), .value = null }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(ws_d) }, loc)) catch unreachable; - } else { - const fn_d = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - fn_d[0] = .{ .binding = p.b(B.Identifier{ .ref = fn_ref2 }, loc), .value = null }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(fn_d) }, loc)) catch unreachable; - } - - // Assign the function: _fn = function() { ... } - p.recordUsage(fn_ref2); - const fn_assign = Expr.assign( - p.newExpr(E.Identifier{ .ref = fn_ref2 }, loc), - if (prop.value) |v| v else p.newExpr(E.Undefined{}, loc), - ); - prefix_stmts.append(p.s(S.SExpr{ .value = fn_assign }, loc)) catch unreachable; - - // Add __privateAdd (once per name) - if (!emitted_private_adds.contains(npriv_inner)) { - emitted_private_adds.put(p.allocator, npriv_inner, {}) catch unreachable; - if (!prop.flags.contains(.is_static)) { - const pa_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - pa_args[0] = p.newExpr(E.This{}, loc); - p.recordUsage(ws_ref2); - pa_args[1] = p.newExpr(E.Identifier{ .ref = ws_ref2 }, loc); - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", pa_args) }, loc), - ) catch unreachable; - } else { - const sa_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - sa_args[0] = p.newExpr(E.This{}, loc); - p.recordUsage(ws_ref2); - sa_args[1] = p.newExpr(E.Identifier{ .ref = ws_ref2 }, loc); - const add_call2 = p.callRuntime(loc, "__privateAdd", sa_args); - const blk_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); - blk_stmts[0] = p.s(S.SExpr{ .value = add_call2 }, loc); - const sb = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); - sb.* = .{ .loc = loc, .stmts = bun.BabyList(Stmt).fromOwnedSlice(blk_stmts) }; - static_private_add_blocks.append(.{ - .kind = .class_static_block, - .class_static_block = sb, - }) catch unreachable; - } - } - // Don't add to new_properties (removed from class body) - continue; - } else { - // Non-decorated private field: lower to WeakMap - const wm_nm = std.fmt.allocPrint(p.allocator, "_{s}", .{npriv_orig[1..]}) catch unreachable; - const wm_ref2 = p.newSymbol(.other, wm_nm) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref2)); - private_lowered_map.put(p.allocator, npriv_inner, .{ - .storage_ref = wm_ref2, - }) catch unreachable; - - const wm_new2 = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const wm_d = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - wm_d[0] = .{ .binding = p.b(B.Identifier{ .ref = wm_ref2 }, loc), .value = wm_new2 }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_d) }, loc)) catch unreachable; - - // Add __privateAdd(this/ClassName, _wm, initializer) to constructor/suffix - const init_val2 = if (prop.initializer) |iv| iv else p.newExpr(E.Undefined{}, loc); - if (!prop.flags.contains(.is_static)) { - const pa_args2 = bun.handleOom(p.allocator.alloc(Expr, 3)); - pa_args2[0] = p.newExpr(E.This{}, loc); - p.recordUsage(wm_ref2); - pa_args2[1] = p.newExpr(E.Identifier{ .ref = wm_ref2 }, loc); - pa_args2[2] = init_val2; - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", pa_args2) }, loc), - ) catch unreachable; - } else { - p.recordUsage(wm_ref2); - const sa_args2 = bun.handleOom(p.allocator.alloc(Expr, 3)); - sa_args2[0] = p.newExpr(E.This{}, loc); - sa_args2[1] = p.newExpr(E.Identifier{ .ref = wm_ref2 }, loc); - sa_args2[2] = init_val2; - const add_call3 = p.callRuntime(loc, "__privateAdd", sa_args2); - const blk_stmts2 = bun.handleOom(p.allocator.alloc(Stmt, 1)); - blk_stmts2[0] = p.s(S.SExpr{ .value = add_call3 }, loc); - const sb2 = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); - sb2.* = .{ .loc = loc, .stmts = bun.BabyList(Stmt).fromOwnedSlice(blk_stmts2) }; - static_private_add_blocks.append(.{ - .kind = .class_static_block, - .class_static_block = sb2, - }) catch unreachable; - } - // Don't add to new_properties (removed from class body) - continue; - } - } - // Check for undecorated auto-accessor that needs lowering - if (prop.kind == .auto_accessor) { - // Lower undecorated auto-accessor to WeakMap storage + getter/setter - const accessor_name = brk: { - if (prop.key.?.data == .e_string) { - break :brk std.fmt.allocPrint(p.allocator, "_{s}", .{prop.key.?.data.e_string.data}) catch unreachable; - } - const name = std.fmt.allocPrint(p.allocator, "_accessor_storage{d}", .{accessor_storage_counter}) catch unreachable; - accessor_storage_counter += 1; - break :brk name; - }; - const wm_ref = p.newSymbol(.other, accessor_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); - - const wm_new = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - wm_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), - .value = wm_new, - }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; - - // Create getter: get foo() { return __privateGet(this, _foo); } - const get_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - get_args[0] = p.newExpr(E.This{}, loc); - p.recordUsage(wm_ref); - get_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); - const get_body_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); - get_body_stmts[0] = p.s(S.Return{ .value = p.callRuntime(loc, "__privateGet", get_args) }, loc); - const get_fn = bun.handleOom(p.allocator.create(G.Fn)); - get_fn.* = .{ .body = .{ .stmts = get_body_stmts, .loc = loc } }; - - // Create setter: set foo(v) { __privateSet(this, _foo, v); } - const setter_param_ref = p.newSymbol(.other, "v") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, setter_param_ref)); - const set_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - set_args[0] = p.newExpr(E.This{}, loc); - p.recordUsage(wm_ref); - set_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); - p.recordUsage(setter_param_ref); - set_args[2] = p.newExpr(E.Identifier{ .ref = setter_param_ref }, loc); - const set_body_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); - set_body_stmts[0] = p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateSet", set_args) }, loc); - const setter_fn_args = bun.handleOom(p.allocator.alloc(G.Arg, 1)); - setter_fn_args[0] = .{ .binding = p.b(B.Identifier{ .ref = setter_param_ref }, loc) }; - const set_fn = bun.handleOom(p.allocator.create(G.Fn)); - set_fn.* = .{ .args = setter_fn_args, .body = .{ .stmts = set_body_stmts, .loc = loc } }; - - // Add getter and setter properties - var getter_flags = prop.flags; - getter_flags.insert(.is_method); - new_properties.append(.{ - .key = prop.key, - .value = p.newExpr(E.Function{ .func = get_fn.* }, loc), - .kind = .get, - .flags = getter_flags, - }) catch unreachable; - new_properties.append(.{ - .key = prop.key, - .value = p.newExpr(E.Function{ .func = set_fn.* }, loc), - .kind = .set, - .flags = getter_flags, - }) catch unreachable; - - // Add initialization: __privateAdd(this/ClassName, _wm, initValue) - const init_val = if (prop.initializer) |iv| iv else p.newExpr(E.Undefined{}, loc); - if (!prop.flags.contains(.is_static)) { - const add_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - add_args[0] = p.newExpr(E.This{}, loc); - p.recordUsage(wm_ref); - add_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); - add_args[2] = init_val; - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", add_args) }, loc), - ) catch unreachable; - } else { - p.recordUsage(wm_ref); - p.recordUsage(class_name_ref); - const sa_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - sa_args[0] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - sa_args[1] = p.newExpr(E.Identifier{ .ref = wm_ref }, loc); - sa_args[2] = init_val; - suffix_exprs.append(p.callRuntime(loc, "__privateAdd", sa_args)) catch unreachable; - } - continue; - } - - // Extract static blocks from class body - they'll be emitted in the suffix - // interleaved with static field/accessor inits in source order - if (prop.kind == .class_static_block) { - if (prop.class_static_block) |sb| { - static_element_order.append(.{ .kind = .block, .index = extracted_static_blocks.items.len }) catch unreachable; - extracted_static_blocks.append(sb) catch unreachable; - } - continue; - } - - new_properties.append(prop.*) catch unreachable; - continue; - } - - // Compute kind flag - var flags: f64 = 0; - if (prop.flags.contains(.is_method)) { - switch (prop.kind) { - .get => flags = 2, - .set => flags = 3, - else => flags = 1, // method - } - } else { - switch (prop.kind) { - .auto_accessor => flags = 4, - else => flags = 5, // field - } - } - - // Add static flag - if (prop.flags.contains(.is_static)) { - flags += 8; - } - - const is_private = prop.key.?.data == .e_private_identifier; - - // Add private flag - if (is_private) { - flags += 16; - } - - // Use pre-evaluated decorator array reference (for source-order evaluation) - const decorator_array = if (prop_dec_refs.get(prop_idx)) |dec_ref| brk: { - p.recordUsage(dec_ref); - break :brk p.newExpr(E.Identifier{ .ref = dec_ref }, loc); - } else p.newExpr(E.Array{ - .items = prop.ts_decorators, - }, loc); - - const key_expr = prop.key.?; - const k = @as(u8, @intFromFloat(flags)) & 7; - - // For private members, we need WeakMap/WeakSet storage and string name - var dec_arg_count: usize = 5; - var private_storage_ref: ?Ref = null; - var private_extra_ref: ?Ref = null; - var private_method_fn_ref: ?Ref = null; - - if (is_private) { - if (k >= 1 and k <= 3) { - // Private method/getter/setter: use WeakSet for membership tracking - const private_orig = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; - const priv_inner = key_expr.data.e_private_identifier.ref.innerIndex(); - - // Check if a WeakSet already exists for this private name (getter+setter pair) - const existing = private_lowered_map.get(priv_inner); - const ws_ref = if (existing) |ex| ex.storage_ref else brk: { - const ws_name = std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig[1..]}) catch unreachable; - const ref = p.newSymbol(.other, ws_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, ref)); - break :brk ref; - }; - private_storage_ref = ws_ref; - - // Create function variable with kind-specific suffix: _fn, _get, _set - const fn_suffix = if (k == 2) "_get" else if (k == 3) "_set" else "_fn"; - const fn_name = std.fmt.allocPrint(p.allocator, "_{s}{s}", .{ private_orig[1..], fn_suffix }) catch unreachable; - const fn_ref = p.newSymbol(.other, fn_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, fn_ref)); - private_method_fn_ref = fn_ref; - - // Update or create map entry - var new_info = if (existing) |ex| ex else PrivateLoweredInfo{ .storage_ref = ws_ref }; - if (k == 1) { - new_info.method_fn_ref = fn_ref; - } else if (k == 2) { - new_info.getter_fn_ref = fn_ref; - } else { - new_info.setter_fn_ref = fn_ref; - } - private_lowered_map.put(p.allocator, priv_inner, new_info) catch unreachable; - - if (existing == null) { - // First time seeing this private name: create WeakSet declaration - const ws_global_ref = p.newSymbol(.unbound, "WeakSet") catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, ws_global_ref)); - const ws_new = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = ws_global_ref }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const ws_decls = bun.handleOom(p.allocator.alloc(G.Decl, 2)); - ws_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = ws_ref }, loc), - .value = ws_new, - }; - ws_decls[1] = .{ - .binding = p.b(B.Identifier{ .ref = fn_ref }, loc), - .value = null, - }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(ws_decls) }, loc)) catch unreachable; - } else { - // Getter+setter pair: WeakSet already declared, just add fn variable - const fn_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - fn_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = fn_ref }, loc), - .value = null, - }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(fn_decls) }, loc)) catch unreachable; - } - - // For private methods, the function itself is passed as extra (6th arg) - dec_arg_count = 6; - } else if (k == 5) { - // Private field: use WeakMap for storage - const private_orig2 = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; - const wm_name = std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig2[1..]}) catch unreachable; - const wm_ref = p.newSymbol(.other, wm_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); - private_storage_ref = wm_ref; - private_lowered_map.put(p.allocator, key_expr.data.e_private_identifier.ref.innerIndex(), .{ - .storage_ref = wm_ref, - }) catch unreachable; - - // var _name = new WeakMap() - const wm_new = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - wm_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), - .value = wm_new, - }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; - dec_arg_count = 5; - } else if (k == 4) { - // Private auto-accessor: need WeakMap for the accessor storage - const private_orig3 = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; - const wm_name3 = std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig3[1..]}) catch unreachable; - const wm_ref = p.newSymbol(.other, wm_name3) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); - private_storage_ref = wm_ref; - - // Create descriptor variable: var _foo_acc; - const acc_name = std.fmt.allocPrint(p.allocator, "_{s}_acc", .{private_orig3[1..]}) catch unreachable; - const acc_ref = p.newSymbol(.other, acc_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, acc_ref)); - private_method_fn_ref = acc_ref; // Reuse to capture __decorateElement return - - private_lowered_map.put(p.allocator, key_expr.data.e_private_identifier.ref.innerIndex(), .{ - .storage_ref = wm_ref, - .accessor_desc_ref = acc_ref, - }) catch unreachable; - - const wm_new = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 2)); - wm_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), - .value = wm_new, - }; - wm_decls[1] = .{ - .binding = p.b(B.Identifier{ .ref = acc_ref }, loc), - .value = null, - }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; - dec_arg_count = 6; - } - } else if (k == 4) { - // Public auto-accessor: need WeakMap for the accessor storage - const accessor_name = brk: { - if (key_expr.data == .e_string) { - break :brk std.fmt.allocPrint(p.allocator, "_{s}", .{key_expr.data.e_string.data}) catch unreachable; - } - const name = std.fmt.allocPrint(p.allocator, "_accessor_storage{d}", .{accessor_storage_counter}) catch unreachable; - accessor_storage_counter += 1; - break :brk name; - }; - const wm_ref = p.newSymbol(.other, accessor_name) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, wm_ref)); - private_extra_ref = wm_ref; - - const wm_new = p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(loc, "WeakMap") catch unreachable).ref }, loc), - .args = ExprNodeList.empty, - .close_parens_loc = loc, - }, loc); - const wm_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - wm_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = wm_ref }, loc), - .value = wm_new, - }; - prefix_stmts.append(p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(wm_decls) }, loc)) catch unreachable; - dec_arg_count = 6; - } - - // For expression mode, use the _class temp ref (expr_class_ref) as the target, - // since class_name_ref (Foo) isn't assigned until the class decorator runs. - const target_ref = if (is_expr and expr_class_ref != null) expr_class_ref.? else class_name_ref; - p.recordUsage(target_ref); - const class_ident = p.newExpr(E.Identifier{ .ref = target_ref }, class_name_loc); - - // Build __decorateElement(_init, flags, name, [decorators], target, extra?) - const dec_args = bun.handleOom(p.allocator.alloc(Expr, dec_arg_count)); - dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - dec_args[1] = p.newExpr(E.Number{ .value = flags }, loc); - if (is_private) { - // For private members, pass the name as a string "#name" - const orig_name = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; - dec_args[2] = p.newExpr(E.String{ .data = orig_name }, loc); - } else { - dec_args[2] = key_expr; - } - dec_args[3] = decorator_array; - - if (is_private and private_storage_ref != null) { - // For private members, target is the WeakMap/WeakSet ref - p.recordUsage(private_storage_ref.?); - dec_args[4] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); - if (dec_arg_count == 6) { - if (k >= 1 and k <= 3) { - // Private method/getter/setter: pass the function as extra - dec_args[5] = if (prop.value) |v| v else p.newExpr(E.Undefined{}, loc); - } else if (k == 4) { - // Private auto-accessor: extra = same WeakMap as target - // (used for value storage via __privateGet/Set in the generated descriptor) - p.recordUsage(private_storage_ref.?); - dec_args[5] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); - } else { - dec_args[5] = p.newExpr(E.Undefined{}, loc); - } - } - } else { - dec_args[4] = class_ident; - if (dec_arg_count == 6) { - if (private_extra_ref) |extra_ref| { - // Public auto-accessor: pass WeakMap as extra - p.recordUsage(extra_ref); - dec_args[5] = p.newExpr(E.Identifier{ .ref = extra_ref }, loc); - } else { - dec_args[5] = p.newExpr(E.Undefined{}, loc); - } - } - } - - const raw_element = p.callRuntime(loc, "__decorateElement", dec_args); - - // For private methods, capture the return value: _fn = __decorateElement(...) - const element = if (private_method_fn_ref) |fn_ref| brk: { - p.recordUsage(fn_ref); - break :brk Expr.assign(p.newExpr(E.Identifier{ .ref = fn_ref }, loc), raw_element); - } else raw_element; - - // Categorize the element - if (k >= 4) { - // field (k=5) or accessor (k=4) - remove from class body - var prop_copy = prop.*; - prop_copy.ts_decorators = .{}; - if (is_private) { - // Store the private storage ref in the property for later use - if (private_storage_ref) |ps_ref| { - prop_copy.key = p.newExpr(E.Identifier{ .ref = ps_ref }, loc); - } - } - if (private_extra_ref) |pe_ref| { - // For auto-accessors, we store the extra ref through the value field - prop_copy.value = p.newExpr(E.Identifier{ .ref = pe_ref }, loc); - } - - const is_accessor = (k == 4); - const init_entry = FieldInitEntry{ - .prop = prop_copy, - .is_private = is_private, - .is_accessor = is_accessor, - }; - - if (prop.flags.contains(.is_static)) { - // __decorateElement ordering: accessors go with non-fields, fields go separate - if (is_accessor) { - static_non_field_elements.append(element) catch unreachable; - static_accessor_count += 1; - } else { - static_field_decorate.append(element) catch unreachable; - } - // Track init entry and source order for interleaving with static blocks - static_element_order.append(.{ - .kind = .field_or_accessor, - .index = static_init_entries.items.len, - }) catch unreachable; - static_init_entries.append(init_entry) catch unreachable; - } else { - if (is_accessor) { - instance_non_field_elements.append(element) catch unreachable; - instance_accessor_count += 1; - } else { - instance_field_decorate.append(element) catch unreachable; - } - instance_init_entries.append(init_entry) catch unreachable; - } - } else if (is_private and private_storage_ref != null) { - // Private method/getter/setter - remove from class body - // The function is passed to __decorateElement as the 6th arg (extra) - // and access goes through WeakSet via __privateGet/__privateMethod - - // Add __privateAdd (only once per unique private name, for getter+setter pairs) - const priv_inner2 = key_expr.data.e_private_identifier.ref.innerIndex(); - if (!emitted_private_adds.contains(priv_inner2)) { - emitted_private_adds.put(p.allocator, priv_inner2, {}) catch unreachable; - if (!prop.flags.contains(.is_static)) { - const private_add_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - private_add_args[0] = p.newExpr(E.This{}, loc); - p.recordUsage(private_storage_ref.?); - private_add_args[1] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", private_add_args) }, loc), - ) catch unreachable; - } else { - // Static: inject as static block at beginning of class body - // (must run before other static blocks that may access private members) - const static_add_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - static_add_args[0] = p.newExpr(E.This{}, loc); // 'this' = class in static block - p.recordUsage(private_storage_ref.?); - static_add_args[1] = p.newExpr(E.Identifier{ .ref = private_storage_ref.? }, loc); - const add_call = p.callRuntime(loc, "__privateAdd", static_add_args); - - const block_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); - block_stmts[0] = p.s(S.SExpr{ .value = add_call }, loc); - const static_block = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); - static_block.* = .{ .loc = loc, .stmts = bun.BabyList(Stmt).fromOwnedSlice(block_stmts) }; - static_private_add_blocks.append(.{ - .kind = .class_static_block, - .class_static_block = static_block, - }) catch unreachable; - } - } - - // Private method __decorateElement goes in suffix (same ordering as public) - // Static blocks are extracted to suffix, so __decorateElement doesn't need to be in prefix - if (prop.flags.contains(.is_static)) { - static_non_field_elements.append(element) catch unreachable; - has_static_private_methods = true; - } else { - instance_non_field_elements.append(element) catch unreachable; - has_instance_private_methods = true; - } - } else { - // Public method, getter, setter - keep in class body (but clear decorators) - var new_prop = prop.*; - new_prop.ts_decorators = .{}; - new_properties.append(new_prop) catch unreachable; - - if (prop.flags.contains(.is_static)) { - static_non_field_elements.append(element) catch unreachable; - } else { - instance_non_field_elements.append(element) catch unreachable; - } - } - } - - // === Rewrite private member accesses === - // If any private members were lowered to WeakMap/WeakSet, rewrite all - // references in the remaining class body (methods, field initializers) - // AND in prefix statements and extracted static blocks - if (private_lowered_map.count() > 0) { - // Rewrite in remaining class body (method bodies etc.) - for (new_properties.items) |*nprop| { - if (nprop.value) |*v| { - p.rewritePrivateAccessesInExpr(v, &private_lowered_map); - } - if (nprop.class_static_block) |sb| { - p.rewritePrivateAccessesInStmts(sb.stmts.slice(), &private_lowered_map); - } - } - // Rewrite in field/accessor initializers - for (instance_init_entries.items) |*entry| { - if (entry.prop.initializer) |*ini| { - p.rewritePrivateAccessesInExpr(ini, &private_lowered_map); - } - } - for (static_init_entries.items) |*entry| { - if (entry.prop.initializer) |*ini| { - p.rewritePrivateAccessesInExpr(ini, &private_lowered_map); - } - } - // Rewrite in extracted static blocks - for (extracted_static_blocks.items) |sb| { - p.rewritePrivateAccessesInStmts(sb.stmts.slice(), &private_lowered_map); - } - // Rewrite in __decorateElement calls (suffix non-field elements + field decorate lists). - // These contain function bodies that may reference other private names - // (e.g., a decorated getter `get #foo() { return this.#bar }` where #bar is non-decorated). - for (static_non_field_elements.items) |*elem| { - p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); - } - for (instance_non_field_elements.items) |*elem| { - p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); - } - for (static_field_decorate.items) |*elem| { - p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); - } - for (instance_field_decorate.items) |*elem| { - p.rewritePrivateAccessesInExpr(elem, &private_lowered_map); - } - // Rewrite in pre-evaluated decorator expressions (may contain private name references - // like `@(dec(() => new Foo().#foo))` that need rewriting when pre-evaluated outside the class) - p.rewritePrivateAccessesInStmts(pre_eval_stmts.items, &private_lowered_map); - // Rewrite in prefix statements (extracted functions that may reference other private members) - p.rewritePrivateAccessesInStmts(prefix_stmts.items, &private_lowered_map); - } - - // === Compute field_idx base indices === - // The _init array slots are allocated dynamically by __decorateElement for k>3 (field/accessor). - // The call order determines slot assignment. Our suffix order is: - // 1. static non-fields (includes accessors) 2. instance non-fields (includes accessors) - // 3. static fields 4. instance fields - // So slots are allocated: static accessors, instance accessors, static fields, instance fields. - const static_field_count = static_field_decorate.items.len; - const total_accessor_count = static_accessor_count + instance_accessor_count; - const static_field_base_idx = total_accessor_count; - const instance_accessor_base_idx = static_accessor_count; - const instance_field_base_idx = total_accessor_count + static_field_count; - - // === Emit suffix in correct order (per TC39 spec) === - - // 1. Static non-field __decorateElement calls (methods, getters, setters, accessors) - suffix_exprs.appendSlice(static_non_field_elements.items) catch unreachable; - - // 2. Instance non-field __decorateElement calls (methods, getters, setters, accessors) - suffix_exprs.appendSlice(instance_non_field_elements.items) catch unreachable; - - // 3. Static field __decorateElement calls (fields only, NOT accessors) - suffix_exprs.appendSlice(static_field_decorate.items) catch unreachable; - - // 4. Instance field __decorateElement calls (fields only, NOT accessors) - suffix_exprs.appendSlice(instance_field_decorate.items) catch unreachable; - - // 5. Class decorator: ClassName = __decorateElement(_init, 0, "ClassName", [decorators], target) - if (class_decorators.len > 0) { - p.recordUsage(class_name_ref); - - const class_name_str: []const u8 = if (original_class_name_for_decorator) |name| - name - else if (is_expr and expr_class_is_anonymous) - (name_from_context orelse "") - else - p.symbols.items[class_name_ref.inner_index].original_name; - const cls_dec_args = bun.handleOom(p.allocator.alloc(Expr, 5)); - cls_dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - cls_dec_args[1] = p.newExpr(E.Number{ .value = 0 }, loc); - cls_dec_args[2] = p.newExpr(E.String{ .data = class_name_str }, loc); - cls_dec_args[3] = if (class_dec_ref) |cdr| brk: { - p.recordUsage(cdr); - break :brk p.newExpr(E.Identifier{ .ref = cdr }, loc); - } else p.newExpr(E.Array{ .items = class_decorators }, loc); - if (is_expr) { - p.recordUsage(expr_class_ref.?); - cls_dec_args[4] = p.newExpr(E.Identifier{ .ref = expr_class_ref.? }, loc); - } else { - cls_dec_args[4] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - } - - const decorate_class = p.callRuntime(loc, "__decorateElement", cls_dec_args); - p.recordUsage(class_name_ref); - suffix_exprs.append( - Expr.assign(p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc), decorate_class), - ) catch unreachable; - } - - // 6. Static method extra initializers: __runInitializers(_init, 3, ClassName) - if (static_non_field_elements.items.len > 0 or has_static_private_methods) { - p.recordUsage(init_ref); - p.recordUsage(class_name_ref); - const run_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - run_args[1] = p.newExpr(E.Number{ .value = 3 }, loc); // array[1], call mode (1<<1|1=3) - run_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - suffix_exprs.append(p.callRuntime(loc, "__runInitializers", run_args)) catch unreachable; - } - - // 7. Static elements in source order: static blocks + static field/accessor init + extra-init - { - var s_accessor_idx: usize = 0; - var s_field_idx: usize = 0; - for (static_element_order.items) |elem| { - switch (elem.kind) { - .block => { - // Emit extracted static block statements. - // Replace `this` with the class name since we're moving - // the static block outside the class body. - const sb = extracted_static_blocks.items[elem.index]; - p.replaceThisInStmts(sb.stmts.slice(), class_name_ref, class_name_loc); - for (sb.stmts.slice()) |sb_stmt| { - if (sb_stmt.data == .s_expr) { - suffix_exprs.append(sb_stmt.data.s_expr.value) catch unreachable; - } - } - }, - .field_or_accessor => { - const entry = static_init_entries.items[elem.index]; - const field_idx: usize = if (entry.is_accessor) brk: { - const idx = s_accessor_idx; - s_accessor_idx += 1; - break :brk idx; // static accessor base = 0 - } else brk: { - const idx = static_field_base_idx + s_field_idx; - s_field_idx += 1; - break :brk idx; - }; - - const init_flags_val: f64 = @floatFromInt((4 + 2 * field_idx) << 1); - const extra_flags_val: f64 = @floatFromInt(((5 + 2 * field_idx) << 1) | 1); - - // Init: ClassName.x = __runInitializers(_init, initFlags, ClassName, initValue?) - p.recordUsage(init_ref); - p.recordUsage(class_name_ref); - const run_args_count: usize = if (entry.prop.initializer != null) 4 else 3; - const run_args = bun.handleOom(p.allocator.alloc(Expr, run_args_count)); - run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - run_args[1] = p.newExpr(E.Number{ .value = init_flags_val }, loc); - run_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - if (entry.prop.initializer) |init_val| { - run_args[3] = init_val; - } - const run_init_call = p.callRuntime(loc, "__runInitializers", run_args); - - if (entry.is_accessor or entry.is_private) { - const wm_ref_expr = if (entry.is_accessor and !entry.is_private) - entry.prop.value.? - else - entry.prop.key.?; - p.recordUsage(class_name_ref); - const add_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - add_args[0] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - add_args[1] = wm_ref_expr; - add_args[2] = run_init_call; - suffix_exprs.append(p.callRuntime(loc, "__privateAdd", add_args)) catch unreachable; - } else { - p.recordUsage(class_name_ref); - const class_target = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - const assign_target = p.buildDecoratedMemberTarget(class_target, entry.prop); - suffix_exprs.append(Expr.assign(assign_target, run_init_call)) catch unreachable; - } - - // Extra initializer: __runInitializers(_init, extraFlags, ClassName) - p.recordUsage(init_ref); - p.recordUsage(class_name_ref); - const extra_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - extra_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - extra_args[1] = p.newExpr(E.Number{ .value = extra_flags_val }, loc); - extra_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - suffix_exprs.append(p.callRuntime(loc, "__runInitializers", extra_args)) catch unreachable; - }, - } - } - } - - // 8. Class extra initializers (if class decorators exist) - if (class_decorators.len > 0) { - p.recordUsage(init_ref); - p.recordUsage(class_name_ref); - const run_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - run_args[1] = p.newExpr(E.Number{ .value = 1 }, loc); // array[0], call mode (0<<1|1=1) - run_args[2] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - suffix_exprs.append(p.callRuntime(loc, "__runInitializers", run_args)) catch unreachable; - } - - // 9. __decoratorMetadata(_init, ClassName) - { - p.recordUsage(init_ref); - p.recordUsage(class_name_ref); - const metadata_args = bun.handleOom(p.allocator.alloc(Expr, 2)); - metadata_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - metadata_args[1] = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); - suffix_exprs.append(p.callRuntime(loc, "__decoratorMetadata", metadata_args)) catch unreachable; - } - - // === Build constructor injection === - - // Instance method extra initializers: __runInitializers(_init, 5, this) - if (instance_non_field_elements.items.len > 0 or has_instance_private_methods) { - p.recordUsage(init_ref); - const run_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - run_args[1] = p.newExpr(E.Number{ .value = 5 }, loc); // array[2], call mode (2<<1|1=5) - run_args[2] = p.newExpr(E.This{}, loc); - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__runInitializers", run_args) }, loc), - ) catch unreachable; - } - - // Instance field/accessor init + extra-init in source order - { - var i_accessor_idx: usize = 0; - var i_field_idx: usize = 0; - for (instance_init_entries.items) |entry| { - const field_idx: usize = if (entry.is_accessor) brk: { - const idx = instance_accessor_base_idx + i_accessor_idx; - i_accessor_idx += 1; - break :brk idx; - } else brk: { - const idx = instance_field_base_idx + i_field_idx; - i_field_idx += 1; - break :brk idx; - }; - - const init_flags_val: f64 = @floatFromInt((4 + 2 * field_idx) << 1); - const extra_flags_val: f64 = @floatFromInt(((5 + 2 * field_idx) << 1) | 1); - - // this.x = __runInitializers(_init, initFlags, this, initValue?) - p.recordUsage(init_ref); - const run_args_count: usize = if (entry.prop.initializer != null) 4 else 3; - const run_args = bun.handleOom(p.allocator.alloc(Expr, run_args_count)); - run_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - run_args[1] = p.newExpr(E.Number{ .value = init_flags_val }, loc); - run_args[2] = p.newExpr(E.This{}, loc); - if (entry.prop.initializer) |init_val| { - run_args[3] = init_val; - } - const run_init_call = p.callRuntime(loc, "__runInitializers", run_args); - - if (entry.is_accessor or entry.is_private) { - const wm_ref_expr = if (entry.is_accessor and !entry.is_private) - entry.prop.value.? - else - entry.prop.key.?; - const add_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - add_args[0] = p.newExpr(E.This{}, loc); - add_args[1] = wm_ref_expr; - add_args[2] = run_init_call; - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__privateAdd", add_args) }, loc), - ) catch unreachable; - } else { - const this_expr = p.newExpr(E.This{}, loc); - const assign_target = p.buildDecoratedMemberTarget(this_expr, entry.prop); - constructor_inject_stmts.append( - Stmt.assign(assign_target, run_init_call), - ) catch unreachable; - } - - // Extra initializer: __runInitializers(_init, extraFlags, this) - p.recordUsage(init_ref); - const extra_args = bun.handleOom(p.allocator.alloc(Expr, 3)); - extra_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); - extra_args[1] = p.newExpr(E.Number{ .value = extra_flags_val }, loc); - extra_args[2] = p.newExpr(E.This{}, loc); - constructor_inject_stmts.append( - p.s(S.SExpr{ .value = p.callRuntime(loc, "__runInitializers", extra_args) }, loc), - ) catch unreachable; - } - } - - // === Inject into constructor if needed === - if (constructor_inject_stmts.items.len > 0) { - // Find existing constructor in new_properties - var found_constructor = false; - for (new_properties.items) |*prop| { - if (prop.flags.contains(.is_method) and prop.key != null and - prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor")) - { - const func = prop.value.?.data.e_function; - var body_stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, func.func.body.stmts); - - // Find super() call - var super_index: ?usize = null; - for (body_stmts.items, 0..) |item, index| { - if (item.data != .s_expr) continue; - if (item.data.s_expr.value.data != .e_call) continue; - if (item.data.s_expr.value.data.e_call.target.data != .e_super) continue; - super_index = index; - break; - } - - const insert_at = if (super_index) |j| j + 1 else 0; - body_stmts.insertSlice(insert_at, constructor_inject_stmts.items) catch unreachable; - func.func.body.stmts = body_stmts.items; - found_constructor = true; - break; - } - } - - if (!found_constructor) { - var ctor_stmts = ListManaged(Stmt).init(p.allocator); - if (class.extends != null) { - const target = p.newExpr(E.Super{}, loc); - const args_ref = p.newSymbol(.unbound, arguments_str) catch unreachable; - bun.handleOom(p.current_scope.generated.append(p.allocator, args_ref)); - const spread = p.newExpr(E.Spread{ .value = p.newExpr(E.Identifier{ .ref = args_ref }, loc) }, loc); - const call_args = bun.handleOom(ExprNodeList.initOne(p.allocator, spread)); - ctor_stmts.append( - p.s(S.SExpr{ .value = p.newExpr(E.Call{ .target = target, .args = call_args }, loc) }, loc), - ) catch unreachable; - } - ctor_stmts.appendSlice(constructor_inject_stmts.items) catch unreachable; - - new_properties.insert(0, G.Property{ - .flags = Flags.Property.init(.{ .is_method = true }), - .key = p.newExpr(E.String{ .data = "constructor" }, loc), - .value = p.newExpr(E.Function{ .func = G.Fn{ - .name = null, - .open_parens_loc = logger.Loc.Empty, - .args = &[_]Arg{}, - .body = .{ .loc = loc, .stmts = ctor_stmts.items }, - .flags = Flags.Function.init(.{}), - } }, loc), - }) catch unreachable; - } - } - - // Insert static private __privateAdd blocks at the beginning of class body - if (static_private_add_blocks.items.len > 0) { - new_properties.insertSlice(0, static_private_add_blocks.items) catch unreachable; - } - - // Update class properties (decorated fields/static blocks removed) - class.properties = new_properties.items; - - // Clear class-level decorator flags - class.has_decorators = false; - class.should_lower_standard_decorators = false; - - // === Build result === - if (is_expr) { - // For expressions, build a comma expression: - // (_dec = [class_decs], _base = extends, pre_evals..., prefix..., _init = __decoratorStart(_base), _class = class { }, suffix..., result) - var comma_parts = ListManaged(Expr).init(p.allocator); - - // 1. Class decorator pre-evaluation (before extends) - if (class_dec_assign_expr) |cda| { - comma_parts.append(cda) catch unreachable; - } - - // 2. _base = extends_expr (if extends exists) - if (base_assign_expr) |base_assign| { - comma_parts.append(base_assign) catch unreachable; - } - - // Helper to convert S.Local decls to comma assignments - const appendDeclsAsAssigns = struct { - fn f(parts: *ListManaged(Expr), var_decls: *ListManaged(G.Decl), stmts_list: []const Stmt, parser: *P, l: logger.Loc) void { - for (stmts_list) |pstmt| { - if (pstmt.data == .s_local) { - for (pstmt.data.s_local.decls.slice()) |decl| { - const ref = decl.binding.data.b_identifier.ref; - var_decls.append(.{ .binding = parser.b(B.Identifier{ .ref = ref }, l) }) catch unreachable; - if (decl.value) |val| { - parser.recordUsage(ref); - parts.append( - Expr.assign(parser.newExpr(E.Identifier{ .ref = ref }, l), val), - ) catch unreachable; - } - } - } - } - } - }.f; - - // 3. Property decorator + computed key pre-evaluations (source order) - appendDeclsAsAssigns(&comma_parts, &expr_var_decls, pre_eval_stmts.items, p, loc); - - // 4. Prefix statements as comma expressions (WeakMap/WeakSet declarations) - appendDeclsAsAssigns(&comma_parts, &expr_var_decls, prefix_stmts.items, p, loc); - - // 5. _init = __decoratorStart(_base) - p.recordUsage(init_ref); - comma_parts.append( - Expr.assign(p.newExpr(E.Identifier{ .ref = init_ref }, loc), init_start_expr), - ) catch unreachable; - - // _class = class { ... } - p.recordUsage(expr_class_ref.?); - comma_parts.append( - Expr.assign( - p.newExpr(E.Identifier{ .ref = expr_class_ref.? }, loc), - p.newExpr(class.*, loc), - ), - ) catch unreachable; - - // suffix expressions - comma_parts.appendSlice(suffix_exprs.items) catch unreachable; - - // Final value: class_name_ref if decorated (has the decorated class), else _class - const final_ref = if (class_decorators.len > 0) class_name_ref else expr_class_ref.?; - p.recordUsage(final_ref); - comma_parts.append(p.newExpr(E.Identifier{ .ref = final_ref }, loc)) catch unreachable; - - // Build comma expression chain - var result = comma_parts.items[0]; - for (comma_parts.items[1..]) |part| { - result = p.newExpr(E.Binary{ - .op = .bin_comma, - .left = result, - .right = part, - }, loc); - } - - // Emit var declarations for temp variables via nearest_stmt_list - if (expr_var_decls.items.len > 0) { - const var_decl_stmt = p.s(S.Local{ - .decls = Decl.List.fromOwnedSlice(expr_var_decls.items), - }, loc); - if (p.nearest_stmt_list) |stmt_list| { - stmt_list.append(var_decl_stmt) catch unreachable; - } - } - - // Return as a single statement wrapping the comma expression - var stmts = ListManaged(Stmt).initCapacity(p.allocator, 1) catch unreachable; - stmts.appendAssumeCapacity(p.s(S.SExpr{ .value = result }, loc)); - return stmts.items; - } - - // Statement mode: [var _dec = [...], var _base = ..., pre_eval_stmts..., prefix_stmts..., var _init = ..., class Foo { ... }, suffix...] - // Order ensures decorator expressions and computed keys are evaluated in source order before the class. - var stmts = ListManaged(Stmt).initCapacity(p.allocator, prefix_stmts.items.len + pre_eval_stmts.items.len + 5 + suffix_exprs.items.len) catch unreachable; - // 1. Class decorator pre-evaluation (before extends) - if (class_dec_stmt.data != .s_empty) { - stmts.append(class_dec_stmt) catch unreachable; - } - // 2. Extends expression extraction - if (base_decl_stmt.data != .s_empty) { - stmts.append(base_decl_stmt) catch unreachable; - } - // 3. Property decorator + computed key pre-evaluations (source order) - stmts.appendSlice(pre_eval_stmts.items) catch unreachable; - // 4. WeakMap/WeakSet declarations - stmts.appendSlice(prefix_stmts.items) catch unreachable; - // 5. __decoratorStart - stmts.appendAssumeCapacity(init_decl_stmt); - // 6. Class declaration - stmts.appendAssumeCapacity(stmt); - for (suffix_exprs.items) |expr| { - stmts.append(p.s(S.SExpr{ .value = expr }, expr.loc)) catch unreachable; - } - // 7. Inner class binding: let _Foo = Foo - // Element decorator closures capture inner_class_ref (which is set via `let` - // with TDZ), so mutations to the outer class_name_ref don't affect them. - if (!inner_class_ref.eql(class_name_ref)) { - p.recordUsage(class_name_ref); - const inner_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); - inner_decls[0] = .{ - .binding = p.b(B.Identifier{ .ref = inner_class_ref }, loc), - .value = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc), - }; - stmts.append(p.s(S.Local{ - .kind = .k_let, - .decls = Decl.List.fromOwnedSlice(inner_decls), - }, loc)) catch unreachable; - } - - return stmts.items; - } - - /// Build a property access expression for decorated member targets. - /// For string keys: target.name - /// For computed/number keys: target[key] - fn buildDecoratedMemberTarget(p: *P, target_expr: Expr, prop: Property) Expr { - const key_expr = prop.key.?; - if (prop.flags.contains(.is_computed) or key_expr.data == .e_number) { - return p.newExpr(E.Index{ - .target = target_expr, - .index = key_expr, - }, key_expr.loc); - } else if (key_expr.data == .e_string) { - return p.newExpr(E.Dot{ - .target = target_expr, - .name = key_expr.data.e_string.data, - .name_loc = key_expr.loc, - }, key_expr.loc); - } else { - // For private or other key types, use index - return p.newExpr(E.Index{ - .target = target_expr, - .index = key_expr, - }, key_expr.loc); - } - } - - /// Replace all E.This nodes in an expression tree with a given replacement expression. - /// Does NOT descend into regular functions or classes (they have their own `this`), - /// but DOES descend into arrow functions (they inherit `this`). - fn replaceThisInExpr(p: *P, expr: *Expr, replacement_ref: Ref, replacement_loc: logger.Loc) void { - switch (expr.data) { - .e_this => { - p.recordUsage(replacement_ref); - expr.* = p.newExpr(E.Identifier{ .ref = replacement_ref }, replacement_loc); - }, - .e_binary => |bin| { - p.replaceThisInExpr(&bin.left, replacement_ref, replacement_loc); - p.replaceThisInExpr(&bin.right, replacement_ref, replacement_loc); - }, - .e_call => |call| { - p.replaceThisInExpr(&call.target, replacement_ref, replacement_loc); - for (call.args.slice()) |*arg| { - p.replaceThisInExpr(arg, replacement_ref, replacement_loc); - } - }, - .e_new => |new_expr| { - p.replaceThisInExpr(&new_expr.target, replacement_ref, replacement_loc); - for (new_expr.args.slice()) |*arg| { - p.replaceThisInExpr(arg, replacement_ref, replacement_loc); - } - }, - .e_index => |idx| { - p.replaceThisInExpr(&idx.target, replacement_ref, replacement_loc); - p.replaceThisInExpr(&idx.index, replacement_ref, replacement_loc); - }, - .e_dot => |d| { - p.replaceThisInExpr(&d.target, replacement_ref, replacement_loc); - }, - .e_spread => |spr| { - p.replaceThisInExpr(&spr.value, replacement_ref, replacement_loc); - }, - .e_unary => |u| { - p.replaceThisInExpr(&u.value, replacement_ref, replacement_loc); - }, - .e_if => |i| { - p.replaceThisInExpr(&i.yes, replacement_ref, replacement_loc); - p.replaceThisInExpr(&i.no, replacement_ref, replacement_loc); - p.replaceThisInExpr(&i.test_, replacement_ref, replacement_loc); - }, - .e_array => |a| { - for (a.items.slice()) |*item| { - p.replaceThisInExpr(item, replacement_ref, replacement_loc); - } - }, - .e_object => |o| { - for (o.properties.slice()) |*property| { - if (property.value) |*v| { - p.replaceThisInExpr(v, replacement_ref, replacement_loc); - } - if (property.initializer) |*prop_init| { - p.replaceThisInExpr(prop_init, replacement_ref, replacement_loc); - } - } - }, - .e_template => |t| { - if (t.tag) |*tag| { - p.replaceThisInExpr(tag, replacement_ref, replacement_loc); - } - }, - .e_arrow => |arrow| { - // Arrow functions inherit `this`, so descend into their bodies - p.replaceThisInStmts(arrow.body.stmts, replacement_ref, replacement_loc); - }, - // Don't descend into regular functions or classes (they have their own `this`) - .e_function, .e_class => {}, - // Leaf nodes - nothing to replace - else => {}, - } - } - - /// Replace all E.Identifier nodes matching old_ref with new_ref in an expression tree. - /// Unlike replaceThisInExpr, this walks into all nested scopes since ref identity - /// is unique and won't clash with same-named bindings in nested scopes. - fn replaceRefInExpr(p: *P, expr: *Expr, old_ref: Ref, new_ref: Ref) void { - switch (expr.data) { - .e_identifier => |id| { - if (id.ref.eql(old_ref)) { - p.recordUsage(new_ref); - expr.data = .{ .e_identifier = .{ .ref = new_ref } }; - } - }, - .e_binary => |bin| { - p.replaceRefInExpr(&bin.left, old_ref, new_ref); - p.replaceRefInExpr(&bin.right, old_ref, new_ref); - }, - .e_call => |call| { - p.replaceRefInExpr(&call.target, old_ref, new_ref); - for (call.args.slice()) |*arg| { - p.replaceRefInExpr(arg, old_ref, new_ref); - } - }, - .e_new => |new_expr| { - p.replaceRefInExpr(&new_expr.target, old_ref, new_ref); - for (new_expr.args.slice()) |*arg| { - p.replaceRefInExpr(arg, old_ref, new_ref); - } - }, - .e_index => |idx| { - p.replaceRefInExpr(&idx.target, old_ref, new_ref); - p.replaceRefInExpr(&idx.index, old_ref, new_ref); - }, - .e_dot => |d| { - p.replaceRefInExpr(&d.target, old_ref, new_ref); - }, - .e_spread => |spr| { - p.replaceRefInExpr(&spr.value, old_ref, new_ref); - }, - .e_unary => |u| { - p.replaceRefInExpr(&u.value, old_ref, new_ref); - }, - .e_if => |i| { - p.replaceRefInExpr(&i.yes, old_ref, new_ref); - p.replaceRefInExpr(&i.no, old_ref, new_ref); - p.replaceRefInExpr(&i.test_, old_ref, new_ref); - }, - .e_array => |a| { - for (a.items.slice()) |*item| { - p.replaceRefInExpr(item, old_ref, new_ref); - } - }, - .e_object => |o| { - for (o.properties.slice()) |*property| { - if (property.value) |*v| { - p.replaceRefInExpr(v, old_ref, new_ref); - } - if (property.initializer) |*prop_init| { - p.replaceRefInExpr(prop_init, old_ref, new_ref); - } - } - }, - .e_template => |t| { - if (t.tag) |*tag| { - p.replaceRefInExpr(tag, old_ref, new_ref); - } - }, - .e_arrow => |arrow| { - p.replaceRefInStmts(arrow.body.stmts, old_ref, new_ref); - }, - .e_function => |func| { - if (func.func.body.stmts.len > 0) { - p.replaceRefInStmts(func.func.body.stmts, old_ref, new_ref); - } - }, - // Don't descend into nested classes (they may rebind the class name) - .e_class => {}, - else => {}, - } - } - - /// Replace all E.Identifier nodes matching old_ref with new_ref in statements. - fn replaceRefInStmts(p: *P, stmts: []Stmt, old_ref: Ref, new_ref: Ref) void { - for (stmts) |*cur_stmt| { - switch (cur_stmt.data) { - .s_expr => |sexpr| { - var val = sexpr.value; - p.replaceRefInExpr(&val, old_ref, new_ref); - cur_stmt.* = p.s(S.SExpr{ .value = val, .does_not_affect_tree_shaking = sexpr.does_not_affect_tree_shaking }, cur_stmt.loc); - }, - .s_local => |local| { - for (local.decls.slice()) |*decl| { - if (decl.value) |*v| { - p.replaceRefInExpr(v, old_ref, new_ref); - } - } - }, - .s_return => |ret| { - if (ret.value) |*v| { - p.replaceRefInExpr(v, old_ref, new_ref); - } - }, - else => {}, - } - } - } - - /// Replace E.This in a slice of statements. - fn replaceThisInStmts(p: *P, stmts: []Stmt, replacement_ref: Ref, replacement_loc: logger.Loc) void { - for (stmts) |*cur_stmt| { - switch (cur_stmt.data) { - .s_expr => |sexpr| { - var val = sexpr.value; - p.replaceThisInExpr(&val, replacement_ref, replacement_loc); - cur_stmt.* = p.s(S.SExpr{ .value = val, .does_not_affect_tree_shaking = sexpr.does_not_affect_tree_shaking }, cur_stmt.loc); - }, - .s_local => |local| { - for (local.decls.slice()) |*decl| { - if (decl.value) |*v| { - p.replaceThisInExpr(v, replacement_ref, replacement_loc); - } - } - }, - .s_return => |ret| { - if (ret.value) |*v| { - p.replaceThisInExpr(v, replacement_ref, replacement_loc); - } - }, - else => {}, - } - } - } pub fn lowerClass( noalias p: *P, diff --git a/src/ast/lowerDecorators.zig b/src/ast/lowerDecorators.zig new file mode 100644 index 000000000000..7c46a82c0a3d --- /dev/null +++ b/src/ast/lowerDecorators.zig @@ -0,0 +1,1426 @@ +/// Lowering for TC39 standard ES decorators. +/// Extracted from P.zig to reduce duplication via shared helpers. +pub fn LowerDecorators( + comptime parser_feature__typescript: bool, + comptime parser_feature__jsx: JSXTransformType, + comptime parser_feature__scan_only: bool, +) type { + return struct { + const P = js_parser.NewParser_(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only); + + // ── Types ──────────────────────────────────────────── + + const PrivateLoweredInfo = struct { + storage_ref: Ref, + method_fn_ref: ?Ref = null, + getter_fn_ref: ?Ref = null, + setter_fn_ref: ?Ref = null, + accessor_desc_ref: ?Ref = null, + }; + + const PrivateLoweredMap = std.AutoHashMapUnmanaged(u32, PrivateLoweredInfo); + + const StdDecMode = union(enum) { + stmt, + expr: struct { + class: *G.Class, + loc: logger.Loc, + name_from_context: ?[]const u8 = null, + }, + }; + + const FieldInitEntry = struct { + prop: Property, + is_private: bool, + is_accessor: bool, + }; + + const StaticElement = struct { + kind: enum { block, field_or_accessor }, + index: usize, + }; + + // ── Expression builder helpers ─────────────────────── + + /// recordUsage + E.Identifier in one call. + inline fn useRef(p: *P, ref: Ref, l: logger.Loc) Expr { + p.recordUsage(ref); + return p.newExpr(E.Identifier{ .ref = ref }, l); + } + + /// Allocate args + callRuntime in one call. + fn callRt(p: *P, l: logger.Loc, comptime name: []const u8, args: []const Expr) Expr { + const a = bun.handleOom(p.allocator.alloc(Expr, args.len)); + @memcpy(a, args); + return p.callRuntime(l, name, a); + } + + /// newSymbol + scope.generated.append in one call. + fn newSym(p: *P, kind: Symbol.Kind, name: []const u8) Ref { + const ref = p.newSymbol(kind, name) catch unreachable; + bun.handleOom(p.current_scope.generated.append(p.allocator, ref)); + return ref; + } + + /// Single var declaration statement. + fn varDecl(p: *P, ref: Ref, value: ?Expr, l: logger.Loc) Stmt { + const decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + decls[0] = .{ .binding = p.b(B.Identifier{ .ref = ref }, l), .value = value }; + return p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(decls) }, l); + } + + /// Two-variable declaration statement. + fn varDecl2(p: *P, r1: Ref, v1: ?Expr, r2: Ref, v2: ?Expr, l: logger.Loc) Stmt { + const decls = bun.handleOom(p.allocator.alloc(G.Decl, 2)); + decls[0] = .{ .binding = p.b(B.Identifier{ .ref = r1 }, l), .value = v1 }; + decls[1] = .{ .binding = p.b(B.Identifier{ .ref = r2 }, l), .value = v2 }; + return p.s(S.Local{ .decls = Decl.List.fromOwnedSlice(decls) }, l); + } + + /// recordUsage + Expr.assign. + fn assignTo(p: *P, ref: Ref, value: Expr, l: logger.Loc) Expr { + p.recordUsage(ref); + return Expr.assign(p.newExpr(E.Identifier{ .ref = ref }, l), value); + } + + /// new WeakMap() expression. + fn newWeakMapExpr(p: *P, l: logger.Loc) Expr { + return p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(l, "WeakMap") catch unreachable).ref }, l), + .args = ExprNodeList.empty, + .close_parens_loc = l, + }, l); + } + + /// new WeakSet() expression. + fn newWeakSetExpr(p: *P, l: logger.Loc) Expr { + const ref = newSym(p, .unbound, "WeakSet"); + return p.newExpr(E.New{ + .target = p.newExpr(E.Identifier{ .ref = ref }, l), + .args = ExprNodeList.empty, + .close_parens_loc = l, + }, l); + } + + /// Create a static block property from a single expression. + fn makeStaticBlock(p: *P, expr: Expr, l: logger.Loc) Property { + const stmts = bun.handleOom(p.allocator.alloc(Stmt, 1)); + stmts[0] = p.s(S.SExpr{ .value = expr }, l); + const sb = bun.handleOom(p.allocator.create(G.ClassStaticBlock)); + sb.* = .{ .loc = l, .stmts = bun.BabyList(Stmt).fromOwnedSlice(stmts) }; + return .{ .kind = .class_static_block, .class_static_block = sb }; + } + + /// Build property access: target.name or target[key]. + fn memberTarget(p: *P, target_expr: Expr, prop: Property) Expr { + const key_expr = prop.key.?; + if (prop.flags.contains(.is_computed) or key_expr.data == .e_number) { + return p.newExpr(E.Index{ .target = target_expr, .index = key_expr }, key_expr.loc); + } else if (key_expr.data == .e_string) { + return p.newExpr(E.Dot{ + .target = target_expr, + .name = key_expr.data.e_string.data, + .name_loc = key_expr.loc, + }, key_expr.loc); + } else { + return p.newExpr(E.Index{ .target = target_expr, .index = key_expr }, key_expr.loc); + } + } + + fn initFlag(idx: usize) f64 { + return @floatFromInt((4 + 2 * idx) << 1); + } + + fn extraInitFlag(idx: usize) f64 { + return @floatFromInt(((5 + 2 * idx) << 1) | 1); + } + + /// Emit __privateAdd for a given storage ref. Appends to constructor or static blocks. + fn emitPrivateAdd( + p: *P, + is_static: bool, + storage_ref: Ref, + value: ?Expr, + loc: logger.Loc, + constructor_inject: *ListManaged(Stmt), + static_blocks: *ListManaged(Property), + ) void { + const target = p.newExpr(E.This{}, loc); + if (value) |v| { + const call = callRt(p, loc, "__privateAdd", &[_]Expr{ target, useRef(p, storage_ref, loc), v }); + if (is_static) { + static_blocks.append(makeStaticBlock(p, call, loc)) catch unreachable; + } else { + constructor_inject.append(p.s(S.SExpr{ .value = call }, loc)) catch unreachable; + } + } else { + const call = callRt(p, loc, "__privateAdd", &[_]Expr{ target, useRef(p, storage_ref, loc) }); + if (is_static) { + static_blocks.append(makeStaticBlock(p, call, loc)) catch unreachable; + } else { + constructor_inject.append(p.s(S.SExpr{ .value = call }, loc)) catch unreachable; + } + } + } + + /// Get the method kind code (1=method, 2=getter, 3=setter). + fn methodKind(prop: *const Property) u8 { + return switch (prop.kind) { + .get => 2, + .set => 3, + else => 1, + }; + } + + /// Get fn variable suffix for a given kind code. + fn fnSuffix(k: u8) []const u8 { + return if (k == 2) "_get" else if (k == 3) "_set" else "_fn"; + } + + // ── Generic tree rewriter ──────────────────────────── + + const RewriteKind = union(enum) { + replace_ref: struct { old: Ref, new: Ref }, + replace_this: struct { ref: Ref, loc: logger.Loc }, + }; + + fn rewriteExpr(p: *P, expr: *Expr, kind: RewriteKind) void { + switch (kind) { + .replace_ref => |r| { + if (expr.data == .e_identifier and expr.data.e_identifier.ref.eql(r.old)) { + p.recordUsage(r.new); + expr.data = .{ .e_identifier = .{ .ref = r.new } }; + return; + } + }, + .replace_this => |r| { + if (expr.data == .e_this) { + expr.* = useRef(p, r.ref, r.loc); + return; + } + }, + } + switch (expr.data) { + .e_binary => |e| { + rewriteExpr(p, &e.left, kind); + rewriteExpr(p, &e.right, kind); + }, + .e_call => |e| { + rewriteExpr(p, &e.target, kind); + for (e.args.slice()) |*a| rewriteExpr(p, a, kind); + }, + .e_new => |e| { + rewriteExpr(p, &e.target, kind); + for (e.args.slice()) |*a| rewriteExpr(p, a, kind); + }, + .e_index => |e| { + rewriteExpr(p, &e.target, kind); + rewriteExpr(p, &e.index, kind); + }, + .e_dot => |e| rewriteExpr(p, &e.target, kind), + .e_spread => |e| rewriteExpr(p, &e.value, kind), + .e_unary => |e| rewriteExpr(p, &e.value, kind), + .e_if => |e| { + rewriteExpr(p, &e.test_, kind); + rewriteExpr(p, &e.yes, kind); + rewriteExpr(p, &e.no, kind); + }, + .e_array => |e| { + for (e.items.slice()) |*item| rewriteExpr(p, item, kind); + }, + .e_object => |e| { + for (e.properties.slice()) |*prop| { + if (prop.value) |*v| rewriteExpr(p, v, kind); + if (prop.initializer) |*ini| rewriteExpr(p, ini, kind); + } + }, + .e_template => |e| { + if (e.tag) |*t| rewriteExpr(p, t, kind); + }, + .e_arrow => |e| rewriteStmts(p, e.body.stmts, kind), + .e_function => |e| { + switch (kind) { + .replace_this => {}, + .replace_ref => { + if (e.func.body.stmts.len > 0) + rewriteStmts(p, e.func.body.stmts, kind); + }, + } + }, + .e_class => {}, + else => {}, + } + } + + fn rewriteStmts(p: *P, stmts: []Stmt, kind: RewriteKind) void { + for (stmts) |*cur_stmt| { + switch (cur_stmt.data) { + .s_expr => |sexpr| { + var val = sexpr.value; + rewriteExpr(p, &val, kind); + cur_stmt.* = p.s(S.SExpr{ + .value = val, + .does_not_affect_tree_shaking = sexpr.does_not_affect_tree_shaking, + }, cur_stmt.loc); + }, + .s_local => |local| { + for (local.decls.slice()) |*decl| { + if (decl.value) |*v| rewriteExpr(p, v, kind); + } + }, + .s_return => |ret| { + if (ret.value) |*v| rewriteExpr(p, v, kind); + }, + else => {}, + } + } + } + + // ── Private access rewriting ───────────────────────── + + fn privateGetExpr(p: *P, obj: Expr, info: PrivateLoweredInfo, l: logger.Loc) Expr { + if (info.accessor_desc_ref) |desc_ref| { + return callRt(p, l, "__privateGet", &[_]Expr{ + obj, + useRef(p, info.storage_ref, l), + p.newExpr(E.Dot{ .target = useRef(p, desc_ref, l), .name = "get", .name_loc = l }, l), + }); + } else if (info.getter_fn_ref) |fn_ref| { + return callRt(p, l, "__privateGet", &[_]Expr{ obj, useRef(p, info.storage_ref, l), useRef(p, fn_ref, l) }); + } else if (info.method_fn_ref) |fn_ref| { + return callRt(p, l, "__privateMethod", &[_]Expr{ obj, useRef(p, info.storage_ref, l), useRef(p, fn_ref, l) }); + } else { + return callRt(p, l, "__privateGet", &[_]Expr{ obj, useRef(p, info.storage_ref, l) }); + } + } + + fn privateSetExpr(p: *P, obj: Expr, info: PrivateLoweredInfo, val: Expr, l: logger.Loc) Expr { + if (info.accessor_desc_ref) |desc_ref| { + return callRt(p, l, "__privateSet", &[_]Expr{ + obj, + useRef(p, info.storage_ref, l), + val, + p.newExpr(E.Dot{ .target = useRef(p, desc_ref, l), .name = "set", .name_loc = l }, l), + }); + } else if (info.setter_fn_ref) |fn_ref| { + return callRt(p, l, "__privateSet", &[_]Expr{ obj, useRef(p, info.storage_ref, l), val, useRef(p, fn_ref, l) }); + } else { + return callRt(p, l, "__privateSet", &[_]Expr{ obj, useRef(p, info.storage_ref, l), val }); + } + } + + fn rewritePrivateAccessesInExpr(p: *P, expr: *Expr, map: *const PrivateLoweredMap) void { + switch (expr.data) { + .e_index => |e| { + rewritePrivateAccessesInExpr(p, &e.target, map); + if (e.index.data == .e_private_identifier) { + if (map.get(e.index.data.e_private_identifier.ref.innerIndex())) |info| { + expr.* = privateGetExpr(p, e.target, info, expr.loc); + return; + } + } + rewritePrivateAccessesInExpr(p, &e.index, map); + }, + .e_binary => |e| { + if (e.op == .bin_assign and e.left.data == .e_index) { + if (e.left.data.e_index.index.data == .e_private_identifier) { + if (map.get(e.left.data.e_index.index.data.e_private_identifier.ref.innerIndex())) |info| { + rewritePrivateAccessesInExpr(p, &e.left.data.e_index.target, map); + rewritePrivateAccessesInExpr(p, &e.right, map); + expr.* = privateSetExpr(p, e.left.data.e_index.target, info, e.right, expr.loc); + return; + } + } + } + if (e.op == .bin_in and e.left.data == .e_private_identifier) { + if (map.get(e.left.data.e_private_identifier.ref.innerIndex())) |info| { + rewritePrivateAccessesInExpr(p, &e.right, map); + expr.* = callRt(p, expr.loc, "__privateIn", &[_]Expr{ + useRef(p, info.storage_ref, expr.loc), + e.right, + }); + return; + } + } + rewritePrivateAccessesInExpr(p, &e.left, map); + rewritePrivateAccessesInExpr(p, &e.right, map); + }, + .e_call => |e| { + if (e.target.data == .e_index) { + if (e.target.data.e_index.index.data == .e_private_identifier) { + if (map.get(e.target.data.e_index.index.data.e_private_identifier.ref.innerIndex())) |info| { + rewritePrivateAccessesInExpr(p, &e.target.data.e_index.target, map); + const obj_expr = e.target.data.e_index.target; + const private_access = privateGetExpr(p, obj_expr, info, expr.loc); + const call_target = p.newExpr(E.Dot{ + .target = private_access, + .name = "call", + .name_loc = expr.loc, + }, expr.loc); + const orig_args = e.args.slice(); + const new_args = bun.handleOom(p.allocator.alloc(Expr, 1 + orig_args.len)); + new_args[0] = obj_expr; + for (orig_args, 0..) |*arg, ai| { + rewritePrivateAccessesInExpr(p, arg, map); + new_args[1 + ai] = arg.*; + } + e.target = call_target; + e.args = ExprNodeList.fromOwnedSlice(new_args); + return; + } + } + } + rewritePrivateAccessesInExpr(p, &e.target, map); + for (e.args.slice()) |*arg| rewritePrivateAccessesInExpr(p, arg, map); + }, + .e_unary => |e| rewritePrivateAccessesInExpr(p, &e.value, map), + .e_dot => |e| rewritePrivateAccessesInExpr(p, &e.target, map), + .e_spread => |e| rewritePrivateAccessesInExpr(p, &e.value, map), + .e_if => |e| { + rewritePrivateAccessesInExpr(p, &e.test_, map); + rewritePrivateAccessesInExpr(p, &e.yes, map); + rewritePrivateAccessesInExpr(p, &e.no, map); + }, + .e_await => |e| rewritePrivateAccessesInExpr(p, &e.value, map), + .e_yield => |e| { + if (e.value) |*v| rewritePrivateAccessesInExpr(p, v, map); + }, + .e_new => |e| { + rewritePrivateAccessesInExpr(p, &e.target, map); + for (e.args.slice()) |*arg| rewritePrivateAccessesInExpr(p, arg, map); + }, + .e_array => |e| { + for (e.items.slice()) |*item| rewritePrivateAccessesInExpr(p, item, map); + }, + .e_object => |e| { + for (e.properties.slice()) |*prop| { + if (prop.value) |*v| rewritePrivateAccessesInExpr(p, v, map); + if (prop.initializer) |*ini| rewritePrivateAccessesInExpr(p, ini, map); + } + }, + .e_template => |e| { + if (e.tag) |*t| rewritePrivateAccessesInExpr(p, t, map); + for (e.parts) |*part| rewritePrivateAccessesInExpr(p, &part.value, map); + }, + .e_function => |e| rewritePrivateAccessesInStmts(p, e.func.body.stmts, map), + .e_arrow => |e| rewritePrivateAccessesInStmts(p, e.body.stmts, map), + else => {}, + } + } + + fn rewritePrivateAccessesInStmts(p: *P, stmts: []Stmt, map: *const PrivateLoweredMap) void { + for (stmts) |*stmt_item| { + switch (stmt_item.data) { + .s_expr => |data| rewritePrivateAccessesInExpr(p, &data.value, map), + .s_return => |data| { + if (data.value) |*v| rewritePrivateAccessesInExpr(p, v, map); + }, + .s_throw => |data| rewritePrivateAccessesInExpr(p, &data.value, map), + .s_local => |data| { + for (data.decls.slice()) |*decl| { + if (decl.value) |*v| rewritePrivateAccessesInExpr(p, v, map); + } + }, + .s_if => |data| { + rewritePrivateAccessesInExpr(p, &data.test_, map); + rewritePrivateAccessesInStmts(p, (&data.yes)[0..1], map); + if (data.no) |*no| rewritePrivateAccessesInStmts(p, no[0..1], map); + }, + .s_block => |data| rewritePrivateAccessesInStmts(p, data.stmts, map), + .s_for => |data| { + if (data.init) |*fi| rewritePrivateAccessesInStmts(p, fi[0..1], map); + if (data.test_) |*t| rewritePrivateAccessesInExpr(p, t, map); + if (data.update) |*u| rewritePrivateAccessesInExpr(p, u, map); + rewritePrivateAccessesInStmts(p, (&data.body)[0..1], map); + }, + .s_for_in => |data| { + rewritePrivateAccessesInExpr(p, &data.value, map); + rewritePrivateAccessesInStmts(p, (&data.body)[0..1], map); + }, + .s_for_of => |data| { + rewritePrivateAccessesInExpr(p, &data.value, map); + rewritePrivateAccessesInStmts(p, (&data.body)[0..1], map); + }, + .s_while => |data| { + rewritePrivateAccessesInExpr(p, &data.test_, map); + rewritePrivateAccessesInStmts(p, (&data.body)[0..1], map); + }, + .s_do_while => |data| { + rewritePrivateAccessesInExpr(p, &data.test_, map); + rewritePrivateAccessesInStmts(p, (&data.body)[0..1], map); + }, + .s_switch => |data| { + rewritePrivateAccessesInExpr(p, &data.test_, map); + for (data.cases) |*case| { + if (case.value) |*v| rewritePrivateAccessesInExpr(p, v, map); + rewritePrivateAccessesInStmts(p, case.body, map); + } + }, + .s_try => |data| { + rewritePrivateAccessesInStmts(p, data.body, map); + if (data.catch_) |c| rewritePrivateAccessesInStmts(p, c.body, map); + if (data.finally) |f| rewritePrivateAccessesInStmts(p, f.stmts, map); + }, + .s_label => |data| rewritePrivateAccessesInStmts(p, (&data.stmt)[0..1], map), + .s_with => |data| { + rewritePrivateAccessesInExpr(p, &data.value, map); + rewritePrivateAccessesInStmts(p, (&data.body)[0..1], map); + }, + else => {}, + } + } + } + + // ── Public API ─────────────────────────────────────── + + pub fn lowerStandardDecoratorsStmt(p: *P, stmt: Stmt) []Stmt { + return lowerImpl(p, stmt, .stmt); + } + + pub fn lowerStandardDecoratorsExpr(p: *P, class: *G.Class, l: logger.Loc, name_from_context: ?[]const u8) Expr { + const result = lowerImpl(p, Stmt.empty(), .{ .expr = .{ + .class = class, + .loc = l, + .name_from_context = name_from_context, + } }); + if (result.len == 0) return p.newExpr(E.Missing{}, l); + return result[0].data.s_expr.value; + } + + // ── Core lowering ──────────────────────────────────── + + fn lowerImpl(p: *P, stmt: Stmt, mode: StdDecMode) []Stmt { + const is_expr = mode == .expr; + var class = switch (mode) { + .stmt => &stmt.data.s_class.class, + .expr => |e| e.class, + }; + const loc = switch (mode) { + .stmt => stmt.loc, + .expr => |e| e.loc, + }; + const name_from_context: ?[]const u8 = switch (mode) { + .expr => |e| e.name_from_context, + .stmt => null, + }; + + // ── Phase 1: Setup ─────────────────────────────── + var class_name_ref: Ref = undefined; + var class_name_loc: logger.Loc = undefined; + var expr_class_ref: ?Ref = null; + var expr_class_is_anonymous = false; + var expr_var_decls = ListManaged(G.Decl).init(p.allocator); + + if (is_expr) { + expr_class_ref = newSym(p, .other, "_class"); + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = expr_class_ref.? }, loc) }) catch unreachable; + if (class.class_name) |cn| { + class_name_ref = cn.ref.?; + class_name_loc = cn.loc; + } else { + class_name_ref = expr_class_ref.?; + class_name_loc = loc; + expr_class_is_anonymous = true; + if (name_from_context) |name| { + class.class_name = .{ .ref = newSym(p, .other, name), .loc = loc }; + } + } + } else { + class_name_ref = class.class_name.?.ref.?; + class_name_loc = class.class_name.?.loc; + } + + var inner_class_ref: Ref = class_name_ref; + if (!is_expr) { + const cns = p.symbols.items[class_name_ref.innerIndex()].original_name; + inner_class_ref = newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}", .{cns}) catch unreachable); + } + + const class_decorators = class.ts_decorators; + class.ts_decorators = .{}; + + const init_ref = newSym(p, .other, "_init"); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = init_ref }, loc) }) catch unreachable; + } + + var base_ref: ?Ref = null; + if (class.extends != null) { + base_ref = newSym(p, .other, "_base"); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = base_ref.? }, loc) }) catch unreachable; + } + } + + // ── Phase 2: Pre-evaluate decorators/keys ──────── + var dec_counter: usize = 0; + var class_dec_ref: ?Ref = null; + var class_dec_stmt: Stmt = Stmt.empty(); + var class_dec_assign_expr: ?Expr = null; + if (class_decorators.len > 0) { + dec_counter += 1; + class_dec_ref = newSym(p, .other, "_dec"); + const arr = p.newExpr(E.Array{ .items = class_decorators }, loc); + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = class_dec_ref.? }, loc) }) catch unreachable; + class_dec_assign_expr = assignTo(p, class_dec_ref.?, arr, loc); + } else { + class_dec_stmt = varDecl(p, class_dec_ref.?, arr, loc); + } + } + + var prop_dec_refs = std.AutoHashMapUnmanaged(usize, Ref){}; + var computed_key_refs = std.AutoHashMapUnmanaged(usize, Ref){}; + var pre_eval_stmts = ListManaged(Stmt).init(p.allocator); + var computed_key_counter: usize = 0; + + for (class.properties, 0..) |*prop, prop_idx| { + if (prop.kind == .class_static_block) continue; + if (prop.ts_decorators.len > 0) { + dec_counter += 1; + const dec_name = if (dec_counter == 1) + "_dec" + else + std.fmt.allocPrint(p.allocator, "_dec{d}", .{dec_counter}) catch unreachable; + const dec_ref = newSym(p, .other, dec_name); + prop_dec_refs.put(p.allocator, prop_idx, dec_ref) catch unreachable; + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = dec_ref }, loc) }) catch unreachable; + } + pre_eval_stmts.append(varDecl(p, dec_ref, p.newExpr(E.Array{ .items = prop.ts_decorators }, loc), loc)) catch unreachable; + } + if (prop.flags.contains(.is_computed) and prop.key != null and prop.ts_decorators.len > 0) { + computed_key_counter += 1; + const key_name = if (computed_key_counter == 1) + "_computedKey" + else + std.fmt.allocPrint(p.allocator, "_computedKey{d}", .{computed_key_counter}) catch unreachable; + const key_ref = newSym(p, .other, key_name); + computed_key_refs.put(p.allocator, prop_idx, key_ref) catch unreachable; + if (is_expr) { + expr_var_decls.append(.{ .binding = p.b(B.Identifier{ .ref = key_ref }, loc) }) catch unreachable; + } + pre_eval_stmts.append(varDecl(p, key_ref, prop.key.?, loc)) catch unreachable; + prop.key = useRef(p, key_ref, prop.key.?.loc); + } + } + + // Replace class name refs in pre-eval expressions for inner binding + { + const replacement_ref = if (is_expr) (expr_class_ref orelse class_name_ref) else inner_class_ref; + if (!replacement_ref.eql(class_name_ref)) { + const rk: RewriteKind = .{ .replace_ref = .{ .old = class_name_ref, .new = replacement_ref } }; + for (pre_eval_stmts.items) |*pre_stmt| { + if (pre_stmt.data == .s_local) { + for (pre_stmt.data.s_local.decls.slice()) |*decl| { + if (decl.value) |*v| rewriteExpr(p, v, rk); + } + } + } + } + } + + // For named class expressions: swap to expr_class_ref for suffix ops + var original_class_name_for_decorator: ?[]const u8 = null; + if (is_expr and !expr_class_is_anonymous and expr_class_ref != null) { + original_class_name_for_decorator = p.symbols.items[class_name_ref.inner_index].original_name; + class_name_ref = expr_class_ref.?; + class_name_loc = loc; + } + + // ── Phase 3: __decoratorStart + base decls ─────── + const init_start_expr: Expr = brk: { + const base_expr = if (base_ref) |br| + p.newExpr(E.Identifier{ .ref = br }, loc) + else + p.newExpr(E.Undefined{}, loc); + break :brk callRt(p, loc, "__decoratorStart", &[_]Expr{base_expr}); + }; + + var base_decl_stmt: Stmt = Stmt.empty(); + if (!is_expr) { + if (base_ref) |br| base_decl_stmt = varDecl(p, br, class.extends, loc); + } + + const base_assign_expr: ?Expr = if (is_expr and base_ref != null) + assignTo(p, base_ref.?, class.extends.?, loc) + else + null; + + if (base_ref) |br| class.extends = useRef(p, br, loc); + + const init_decl_stmt: Stmt = if (!is_expr) + varDecl(p, init_ref, init_start_expr, loc) + else + Stmt.empty(); + + // ── Phase 4: Property loop ─────────────────────── + var suffix_exprs = ListManaged(Expr).init(p.allocator); + var constructor_inject_stmts = ListManaged(Stmt).init(p.allocator); + var new_properties = ListManaged(Property).init(p.allocator); + var static_non_field_elements = ListManaged(Expr).init(p.allocator); + var instance_non_field_elements = ListManaged(Expr).init(p.allocator); + var has_static_private_methods = false; + var has_instance_private_methods = false; + var static_field_decorate = ListManaged(Expr).init(p.allocator); + var instance_field_decorate = ListManaged(Expr).init(p.allocator); + var static_accessor_count: usize = 0; + var instance_accessor_count: usize = 0; + var static_init_entries = ListManaged(FieldInitEntry).init(p.allocator); + var instance_init_entries = ListManaged(FieldInitEntry).init(p.allocator); + var static_element_order = ListManaged(StaticElement).init(p.allocator); + var extracted_static_blocks = ListManaged(*G.ClassStaticBlock).init(p.allocator); + var prefix_stmts = ListManaged(Stmt).init(p.allocator); + var private_lowered_map = PrivateLoweredMap{}; + var accessor_storage_counter: usize = 0; + var emitted_private_adds = std.AutoHashMapUnmanaged(u32, void){}; + var static_private_add_blocks = ListManaged(Property).init(p.allocator); + + // Pre-scan: determine if all private members need lowering + var lower_all_private = false; + { + var has_any_private = false; + var has_any_decorated = false; + for (class.properties) |cprop| { + if (cprop.kind == .class_static_block) continue; + if (cprop.ts_decorators.len > 0) { + has_any_decorated = true; + if (cprop.key != null and cprop.key.?.data == .e_private_identifier) { + lower_all_private = true; + break; + } + } + if (cprop.key != null and cprop.key.?.data == .e_private_identifier) { + has_any_private = true; + } + } + if (!lower_all_private and has_any_private and has_any_decorated) + lower_all_private = true; + } + + for (class.properties, 0..) |*prop, prop_idx| { + if (prop.ts_decorators.len == 0) { + // ── Non-decorated property ── + if (lower_all_private and prop.key != null and + prop.key.?.data == .e_private_identifier and prop.kind != .class_static_block) + { + const nk_expr = prop.key.?; + const npriv_orig = p.symbols.items[nk_expr.data.e_private_identifier.ref.inner_index].original_name; + const npriv_inner = nk_expr.data.e_private_identifier.ref.innerIndex(); + + if (prop.flags.contains(.is_method)) { + // Non-decorated private method/getter/setter → WeakSet + fn extraction + const nk = methodKind(prop); + const existing = private_lowered_map.get(npriv_inner); + const ws_ref = if (existing) |ex| ex.storage_ref else brk: { + break :brk newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}", .{npriv_orig[1..]}) catch unreachable); + }; + const fn_nm = std.fmt.allocPrint(p.allocator, "_{s}{s}", .{ npriv_orig[1..], fnSuffix(nk) }) catch unreachable; + const fn_ref = newSym(p, .other, fn_nm); + + var new_info = if (existing) |ex| ex else PrivateLoweredInfo{ .storage_ref = ws_ref }; + if (nk == 1) new_info.method_fn_ref = fn_ref else if (nk == 2) new_info.getter_fn_ref = fn_ref else new_info.setter_fn_ref = fn_ref; + private_lowered_map.put(p.allocator, npriv_inner, new_info) catch unreachable; + + if (existing == null) { + prefix_stmts.append(varDecl2(p, ws_ref, newWeakSetExpr(p, loc), fn_ref, null, loc)) catch unreachable; + } else { + prefix_stmts.append(varDecl(p, fn_ref, null, loc)) catch unreachable; + } + + // Assign function: _fn = function() { ... } + prefix_stmts.append(p.s(S.SExpr{ + .value = assignTo(p, fn_ref, if (prop.value) |v| v else p.newExpr(E.Undefined{}, loc), loc), + }, loc)) catch unreachable; + + // __privateAdd (once per name) + if (!emitted_private_adds.contains(npriv_inner)) { + emitted_private_adds.put(p.allocator, npriv_inner, {}) catch unreachable; + emitPrivateAdd(p, prop.flags.contains(.is_static), ws_ref, null, loc, &constructor_inject_stmts, &static_private_add_blocks); + } + continue; + } else { + // Non-decorated private field → WeakMap + const wm_nm = std.fmt.allocPrint(p.allocator, "_{s}", .{npriv_orig[1..]}) catch unreachable; + const wm_ref = newSym(p, .other, wm_nm); + private_lowered_map.put(p.allocator, npriv_inner, .{ .storage_ref = wm_ref }) catch unreachable; + prefix_stmts.append(varDecl(p, wm_ref, newWeakMapExpr(p, loc), loc)) catch unreachable; + + const init_val = if (prop.initializer) |iv| iv else p.newExpr(E.Undefined{}, loc); + if (!prop.flags.contains(.is_static)) { + constructor_inject_stmts.append(p.s(S.SExpr{ + .value = callRt(p, loc, "__privateAdd", &[_]Expr{ + p.newExpr(E.This{}, loc), + useRef(p, wm_ref, loc), + init_val, + }), + }, loc)) catch unreachable; + } else { + static_private_add_blocks.append(makeStaticBlock( + p, + callRt(p, loc, "__privateAdd", &[_]Expr{ + p.newExpr(E.This{}, loc), + useRef(p, wm_ref, loc), + init_val, + }), + loc, + )) catch unreachable; + } + continue; + } + } + // Undecorated auto-accessor → WeakMap + getter/setter + if (prop.kind == .auto_accessor) { + const accessor_name = brk: { + if (prop.key.?.data == .e_string) + break :brk std.fmt.allocPrint(p.allocator, "_{s}", .{prop.key.?.data.e_string.data}) catch unreachable; + const name = std.fmt.allocPrint(p.allocator, "_accessor_storage{d}", .{accessor_storage_counter}) catch unreachable; + accessor_storage_counter += 1; + break :brk name; + }; + const wm_ref = newSym(p, .other, accessor_name); + prefix_stmts.append(varDecl(p, wm_ref, newWeakMapExpr(p, loc), loc)) catch unreachable; + + // Getter: get foo() { return __privateGet(this, _foo); } + const get_body = bun.handleOom(p.allocator.alloc(Stmt, 1)); + get_body[0] = p.s(S.Return{ .value = callRt(p, loc, "__privateGet", &[_]Expr{ + p.newExpr(E.This{}, loc), + useRef(p, wm_ref, loc), + }) }, loc); + const get_fn = bun.handleOom(p.allocator.create(G.Fn)); + get_fn.* = .{ .body = .{ .stmts = get_body, .loc = loc } }; + + // Setter: set foo(v) { __privateSet(this, _foo, v); } + const setter_param_ref = newSym(p, .other, "v"); + const set_body = bun.handleOom(p.allocator.alloc(Stmt, 1)); + set_body[0] = p.s(S.SExpr{ .value = callRt(p, loc, "__privateSet", &[_]Expr{ + p.newExpr(E.This{}, loc), + useRef(p, wm_ref, loc), + useRef(p, setter_param_ref, loc), + }) }, loc); + const setter_fn_args = bun.handleOom(p.allocator.alloc(G.Arg, 1)); + setter_fn_args[0] = .{ .binding = p.b(B.Identifier{ .ref = setter_param_ref }, loc) }; + const set_fn = bun.handleOom(p.allocator.create(G.Fn)); + set_fn.* = .{ .args = setter_fn_args, .body = .{ .stmts = set_body, .loc = loc } }; + + var getter_flags = prop.flags; + getter_flags.insert(.is_method); + new_properties.append(.{ + .key = prop.key, + .value = p.newExpr(E.Function{ .func = get_fn.* }, loc), + .kind = .get, + .flags = getter_flags, + }) catch unreachable; + new_properties.append(.{ + .key = prop.key, + .value = p.newExpr(E.Function{ .func = set_fn.* }, loc), + .kind = .set, + .flags = getter_flags, + }) catch unreachable; + + const init_val = if (prop.initializer) |iv| iv else p.newExpr(E.Undefined{}, loc); + if (!prop.flags.contains(.is_static)) { + constructor_inject_stmts.append(p.s(S.SExpr{ + .value = callRt(p, loc, "__privateAdd", &[_]Expr{ + p.newExpr(E.This{}, loc), + useRef(p, wm_ref, loc), + init_val, + }), + }, loc)) catch unreachable; + } else { + suffix_exprs.append(callRt(p, loc, "__privateAdd", &[_]Expr{ + useRef(p, class_name_ref, class_name_loc), + useRef(p, wm_ref, loc), + init_val, + })) catch unreachable; + } + continue; + } + // Static blocks → extract to suffix + if (prop.kind == .class_static_block) { + if (prop.class_static_block) |sb| { + static_element_order.append(.{ .kind = .block, .index = extracted_static_blocks.items.len }) catch unreachable; + extracted_static_blocks.append(sb) catch unreachable; + } + continue; + } + new_properties.append(prop.*) catch unreachable; + continue; + } + + // ── Decorated property ── + var flags: f64 = 0; + if (prop.flags.contains(.is_method)) { + flags = switch (prop.kind) { + .get => 2, + .set => 3, + else => 1, + }; + } else { + flags = switch (prop.kind) { + .auto_accessor => 4, + else => 5, + }; + } + if (prop.flags.contains(.is_static)) flags += 8; + const is_private = prop.key.?.data == .e_private_identifier; + if (is_private) flags += 16; + + const decorator_array = if (prop_dec_refs.get(prop_idx)) |dec_ref| + useRef(p, dec_ref, loc) + else + p.newExpr(E.Array{ .items = prop.ts_decorators }, loc); + + const key_expr = prop.key.?; + const k = @as(u8, @intFromFloat(flags)) & 7; + + var dec_arg_count: usize = 5; + var private_storage_ref: ?Ref = null; + var private_extra_ref: ?Ref = null; + var private_method_fn_ref: ?Ref = null; + + if (is_private) { + const private_orig = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; + const priv_inner = key_expr.data.e_private_identifier.ref.innerIndex(); + + if (k >= 1 and k <= 3) { + // Decorated private method/getter/setter → WeakSet + const existing = private_lowered_map.get(priv_inner); + const ws_ref = if (existing) |ex| ex.storage_ref else newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig[1..]}) catch unreachable); + private_storage_ref = ws_ref; + const fn_ref = newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}{s}", .{ private_orig[1..], fnSuffix(k) }) catch unreachable); + private_method_fn_ref = fn_ref; + + var new_info = if (existing) |ex| ex else PrivateLoweredInfo{ .storage_ref = ws_ref }; + if (k == 1) new_info.method_fn_ref = fn_ref else if (k == 2) new_info.getter_fn_ref = fn_ref else new_info.setter_fn_ref = fn_ref; + private_lowered_map.put(p.allocator, priv_inner, new_info) catch unreachable; + + if (existing == null) { + prefix_stmts.append(varDecl2(p, ws_ref, newWeakSetExpr(p, loc), fn_ref, null, loc)) catch unreachable; + } else { + prefix_stmts.append(varDecl(p, fn_ref, null, loc)) catch unreachable; + } + dec_arg_count = 6; + } else if (k == 5) { + // Decorated private field → WeakMap + const wm_ref = newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig[1..]}) catch unreachable); + private_storage_ref = wm_ref; + private_lowered_map.put(p.allocator, priv_inner, .{ .storage_ref = wm_ref }) catch unreachable; + prefix_stmts.append(varDecl(p, wm_ref, newWeakMapExpr(p, loc), loc)) catch unreachable; + dec_arg_count = 5; + } else if (k == 4) { + // Decorated private auto-accessor → WeakMap + descriptor + const wm_ref = newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}", .{private_orig[1..]}) catch unreachable); + private_storage_ref = wm_ref; + const acc_ref = newSym(p, .other, std.fmt.allocPrint(p.allocator, "_{s}_acc", .{private_orig[1..]}) catch unreachable); + private_method_fn_ref = acc_ref; + private_lowered_map.put(p.allocator, priv_inner, .{ + .storage_ref = wm_ref, + .accessor_desc_ref = acc_ref, + }) catch unreachable; + prefix_stmts.append(varDecl2(p, wm_ref, newWeakMapExpr(p, loc), acc_ref, null, loc)) catch unreachable; + dec_arg_count = 6; + } + } else if (k == 4) { + // Decorated public auto-accessor → WeakMap + const accessor_name = brk: { + if (key_expr.data == .e_string) + break :brk std.fmt.allocPrint(p.allocator, "_{s}", .{key_expr.data.e_string.data}) catch unreachable; + const name = std.fmt.allocPrint(p.allocator, "_accessor_storage{d}", .{accessor_storage_counter}) catch unreachable; + accessor_storage_counter += 1; + break :brk name; + }; + const wm_ref = newSym(p, .other, accessor_name); + private_extra_ref = wm_ref; + prefix_stmts.append(varDecl(p, wm_ref, newWeakMapExpr(p, loc), loc)) catch unreachable; + dec_arg_count = 6; + } + + // Build __decorateElement args + const target_ref = if (is_expr and expr_class_ref != null) expr_class_ref.? else class_name_ref; + const dec_args = bun.handleOom(p.allocator.alloc(Expr, dec_arg_count)); + dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + dec_args[1] = p.newExpr(E.Number{ .value = flags }, loc); + dec_args[2] = if (is_private) + p.newExpr(E.String{ .data = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name }, loc) + else + key_expr; + dec_args[3] = decorator_array; + + if (is_private and private_storage_ref != null) { + dec_args[4] = useRef(p, private_storage_ref.?, loc); + if (dec_arg_count == 6) { + if (k >= 1 and k <= 3) { + dec_args[5] = if (prop.value) |v| v else p.newExpr(E.Undefined{}, loc); + } else if (k == 4) { + dec_args[5] = useRef(p, private_storage_ref.?, loc); + } else { + dec_args[5] = p.newExpr(E.Undefined{}, loc); + } + } + } else { + p.recordUsage(target_ref); + dec_args[4] = p.newExpr(E.Identifier{ .ref = target_ref }, class_name_loc); + if (dec_arg_count == 6) { + if (private_extra_ref) |extra_ref| { + dec_args[5] = useRef(p, extra_ref, loc); + } else { + dec_args[5] = p.newExpr(E.Undefined{}, loc); + } + } + } + + const raw_element = p.callRuntime(loc, "__decorateElement", dec_args); + const element = if (private_method_fn_ref) |fn_ref| + assignTo(p, fn_ref, raw_element, loc) + else + raw_element; + + // Categorize the element + if (k >= 4) { + // Field (k=5) or accessor (k=4) — remove from class body + var prop_copy = prop.*; + prop_copy.ts_decorators = .{}; + if (is_private) { + if (private_storage_ref) |ps_ref| + prop_copy.key = p.newExpr(E.Identifier{ .ref = ps_ref }, loc); + } + if (private_extra_ref) |pe_ref| + prop_copy.value = p.newExpr(E.Identifier{ .ref = pe_ref }, loc); + + const is_accessor = (k == 4); + const init_entry = FieldInitEntry{ .prop = prop_copy, .is_private = is_private, .is_accessor = is_accessor }; + + if (prop.flags.contains(.is_static)) { + if (is_accessor) { + static_non_field_elements.append(element) catch unreachable; + static_accessor_count += 1; + } else { + static_field_decorate.append(element) catch unreachable; + } + static_element_order.append(.{ .kind = .field_or_accessor, .index = static_init_entries.items.len }) catch unreachable; + static_init_entries.append(init_entry) catch unreachable; + } else { + if (is_accessor) { + instance_non_field_elements.append(element) catch unreachable; + instance_accessor_count += 1; + } else { + instance_field_decorate.append(element) catch unreachable; + } + instance_init_entries.append(init_entry) catch unreachable; + } + } else if (is_private and private_storage_ref != null) { + // Private method/getter/setter — remove from class body + const priv_inner2 = key_expr.data.e_private_identifier.ref.innerIndex(); + if (!emitted_private_adds.contains(priv_inner2)) { + emitted_private_adds.put(p.allocator, priv_inner2, {}) catch unreachable; + emitPrivateAdd(p, prop.flags.contains(.is_static), private_storage_ref.?, null, loc, &constructor_inject_stmts, &static_private_add_blocks); + } + if (prop.flags.contains(.is_static)) { + static_non_field_elements.append(element) catch unreachable; + has_static_private_methods = true; + } else { + instance_non_field_elements.append(element) catch unreachable; + has_instance_private_methods = true; + } + } else { + // Public method/getter/setter — keep in class body + var new_prop = prop.*; + new_prop.ts_decorators = .{}; + new_properties.append(new_prop) catch unreachable; + if (prop.flags.contains(.is_static)) { + static_non_field_elements.append(element) catch unreachable; + } else { + instance_non_field_elements.append(element) catch unreachable; + } + } + } + + // ── Phase 5: Rewrite private accesses ──────────── + if (private_lowered_map.count() > 0) { + for (new_properties.items) |*nprop| { + if (nprop.value) |*v| rewritePrivateAccessesInExpr(p, v, &private_lowered_map); + if (nprop.class_static_block) |sb| + rewritePrivateAccessesInStmts(p, sb.stmts.slice(), &private_lowered_map); + } + for (instance_init_entries.items) |*entry| { + if (entry.prop.initializer) |*ini| rewritePrivateAccessesInExpr(p, ini, &private_lowered_map); + } + for (static_init_entries.items) |*entry| { + if (entry.prop.initializer) |*ini| rewritePrivateAccessesInExpr(p, ini, &private_lowered_map); + } + for (extracted_static_blocks.items) |sb| + rewritePrivateAccessesInStmts(p, sb.stmts.slice(), &private_lowered_map); + for (static_non_field_elements.items) |*elem| rewritePrivateAccessesInExpr(p, elem, &private_lowered_map); + for (instance_non_field_elements.items) |*elem| rewritePrivateAccessesInExpr(p, elem, &private_lowered_map); + for (static_field_decorate.items) |*elem| rewritePrivateAccessesInExpr(p, elem, &private_lowered_map); + for (instance_field_decorate.items) |*elem| rewritePrivateAccessesInExpr(p, elem, &private_lowered_map); + rewritePrivateAccessesInStmts(p, pre_eval_stmts.items, &private_lowered_map); + rewritePrivateAccessesInStmts(p, prefix_stmts.items, &private_lowered_map); + } + + // ── Phase 6: Emit suffix ───────────────────────── + const static_field_count = static_field_decorate.items.len; + const total_accessor_count = static_accessor_count + instance_accessor_count; + const static_field_base_idx = total_accessor_count; + const instance_accessor_base_idx = static_accessor_count; + const instance_field_base_idx = total_accessor_count + static_field_count; + + // 1-4: __decorateElement calls in spec order + suffix_exprs.appendSlice(static_non_field_elements.items) catch unreachable; + suffix_exprs.appendSlice(instance_non_field_elements.items) catch unreachable; + suffix_exprs.appendSlice(static_field_decorate.items) catch unreachable; + suffix_exprs.appendSlice(instance_field_decorate.items) catch unreachable; + + // 5: Class decorator + if (class_decorators.len > 0) { + p.recordUsage(class_name_ref); + const class_name_str: []const u8 = if (original_class_name_for_decorator) |name| + name + else if (is_expr and expr_class_is_anonymous) + (name_from_context orelse "") + else + p.symbols.items[class_name_ref.inner_index].original_name; + + const cls_dec_args = bun.handleOom(p.allocator.alloc(Expr, 5)); + cls_dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); + cls_dec_args[1] = p.newExpr(E.Number{ .value = 0 }, loc); + cls_dec_args[2] = p.newExpr(E.String{ .data = class_name_str }, loc); + cls_dec_args[3] = if (class_dec_ref) |cdr| useRef(p, cdr, loc) else p.newExpr(E.Array{ .items = class_decorators }, loc); + cls_dec_args[4] = if (is_expr) + useRef(p, expr_class_ref.?, loc) + else + p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc); + + suffix_exprs.append(assignTo(p, class_name_ref, p.callRuntime(loc, "__decorateElement", cls_dec_args), class_name_loc)) catch unreachable; + } + + // 6: Static method extra initializers + if (static_non_field_elements.items.len > 0 or has_static_private_methods) { + suffix_exprs.append(callRt(p, loc, "__runInitializers", &[_]Expr{ + useRef(p, init_ref, loc), + p.newExpr(E.Number{ .value = 3 }, loc), + useRef(p, class_name_ref, class_name_loc), + })) catch unreachable; + } + + // 7: Static elements in source order + { + var s_accessor_idx: usize = 0; + var s_field_idx: usize = 0; + for (static_element_order.items) |elem| { + switch (elem.kind) { + .block => { + const sb = extracted_static_blocks.items[elem.index]; + rewriteStmts(p, sb.stmts.slice(), .{ .replace_this = .{ .ref = class_name_ref, .loc = class_name_loc } }); + for (sb.stmts.slice()) |sb_stmt| { + if (sb_stmt.data == .s_expr) + suffix_exprs.append(sb_stmt.data.s_expr.value) catch unreachable; + } + }, + .field_or_accessor => { + const entry = static_init_entries.items[elem.index]; + const field_idx: usize = if (entry.is_accessor) brk: { + const idx = s_accessor_idx; + s_accessor_idx += 1; + break :brk idx; + } else brk: { + const idx = static_field_base_idx + s_field_idx; + s_field_idx += 1; + break :brk idx; + }; + + const run_args_count: usize = if (entry.prop.initializer != null) 4 else 3; + const run_args = bun.handleOom(p.allocator.alloc(Expr, run_args_count)); + run_args[0] = useRef(p, init_ref, loc); + run_args[1] = p.newExpr(E.Number{ .value = initFlag(field_idx) }, loc); + run_args[2] = useRef(p, class_name_ref, class_name_loc); + if (entry.prop.initializer) |init_val| run_args[3] = init_val; + const run_init_call = p.callRuntime(loc, "__runInitializers", run_args); + + if (entry.is_accessor or entry.is_private) { + const wm_ref_expr = if (entry.is_accessor and !entry.is_private) + entry.prop.value.? + else + entry.prop.key.?; + suffix_exprs.append(callRt(p, loc, "__privateAdd", &[_]Expr{ + useRef(p, class_name_ref, class_name_loc), + wm_ref_expr, + run_init_call, + })) catch unreachable; + } else { + const assign_target = memberTarget(p, useRef(p, class_name_ref, class_name_loc), entry.prop); + suffix_exprs.append(Expr.assign(assign_target, run_init_call)) catch unreachable; + } + + // Extra initializer + suffix_exprs.append(callRt(p, loc, "__runInitializers", &[_]Expr{ + useRef(p, init_ref, loc), + p.newExpr(E.Number{ .value = extraInitFlag(field_idx) }, loc), + useRef(p, class_name_ref, class_name_loc), + })) catch unreachable; + }, + } + } + } + + // 8: Class extra initializers + if (class_decorators.len > 0) { + suffix_exprs.append(callRt(p, loc, "__runInitializers", &[_]Expr{ + useRef(p, init_ref, loc), + p.newExpr(E.Number{ .value = 1 }, loc), + useRef(p, class_name_ref, class_name_loc), + })) catch unreachable; + } + + // 9: __decoratorMetadata + suffix_exprs.append(callRt(p, loc, "__decoratorMetadata", &[_]Expr{ + useRef(p, init_ref, loc), + useRef(p, class_name_ref, class_name_loc), + })) catch unreachable; + + // ── Phase 7: Constructor injection ─────────────── + if (instance_non_field_elements.items.len > 0 or has_instance_private_methods) { + constructor_inject_stmts.append(p.s(S.SExpr{ + .value = callRt(p, loc, "__runInitializers", &[_]Expr{ + useRef(p, init_ref, loc), + p.newExpr(E.Number{ .value = 5 }, loc), + p.newExpr(E.This{}, loc), + }), + }, loc)) catch unreachable; + } + + // Instance field/accessor init + extra-init + { + var i_accessor_idx: usize = 0; + var i_field_idx: usize = 0; + for (instance_init_entries.items) |entry| { + const field_idx: usize = if (entry.is_accessor) brk: { + const idx = instance_accessor_base_idx + i_accessor_idx; + i_accessor_idx += 1; + break :brk idx; + } else brk: { + const idx = instance_field_base_idx + i_field_idx; + i_field_idx += 1; + break :brk idx; + }; + + const run_args_count: usize = if (entry.prop.initializer != null) 4 else 3; + const run_args = bun.handleOom(p.allocator.alloc(Expr, run_args_count)); + run_args[0] = useRef(p, init_ref, loc); + run_args[1] = p.newExpr(E.Number{ .value = initFlag(field_idx) }, loc); + run_args[2] = p.newExpr(E.This{}, loc); + if (entry.prop.initializer) |init_val| run_args[3] = init_val; + const run_init_call = p.callRuntime(loc, "__runInitializers", run_args); + + if (entry.is_accessor or entry.is_private) { + const wm_ref_expr = if (entry.is_accessor and !entry.is_private) + entry.prop.value.? + else + entry.prop.key.?; + constructor_inject_stmts.append(p.s(S.SExpr{ + .value = callRt(p, loc, "__privateAdd", &[_]Expr{ + p.newExpr(E.This{}, loc), + wm_ref_expr, + run_init_call, + }), + }, loc)) catch unreachable; + } else { + constructor_inject_stmts.append( + Stmt.assign(memberTarget(p, p.newExpr(E.This{}, loc), entry.prop), run_init_call), + ) catch unreachable; + } + + // Extra initializer + constructor_inject_stmts.append(p.s(S.SExpr{ + .value = callRt(p, loc, "__runInitializers", &[_]Expr{ + useRef(p, init_ref, loc), + p.newExpr(E.Number{ .value = extraInitFlag(field_idx) }, loc), + p.newExpr(E.This{}, loc), + }), + }, loc)) catch unreachable; + } + } + + // Inject into constructor + if (constructor_inject_stmts.items.len > 0) { + var found_constructor = false; + for (new_properties.items) |*nprop| { + if (nprop.flags.contains(.is_method) and nprop.key != null and + nprop.key.?.data == .e_string and nprop.key.?.data.e_string.eqlComptime("constructor")) + { + const func = nprop.value.?.data.e_function; + var body_stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, func.func.body.stmts); + var super_index: ?usize = null; + for (body_stmts.items, 0..) |item, index| { + if (item.data != .s_expr) continue; + if (item.data.s_expr.value.data != .e_call) continue; + if (item.data.s_expr.value.data.e_call.target.data != .e_super) continue; + super_index = index; + break; + } + const insert_at = if (super_index) |j| j + 1 else 0; + body_stmts.insertSlice(insert_at, constructor_inject_stmts.items) catch unreachable; + func.func.body.stmts = body_stmts.items; + found_constructor = true; + break; + } + } + + if (!found_constructor) { + var ctor_stmts = ListManaged(Stmt).init(p.allocator); + if (class.extends != null) { + const target = p.newExpr(E.Super{}, loc); + const args_ref = newSym(p, .unbound, arguments_str); + const spread = p.newExpr(E.Spread{ .value = p.newExpr(E.Identifier{ .ref = args_ref }, loc) }, loc); + const call_args = bun.handleOom(ExprNodeList.initOne(p.allocator, spread)); + ctor_stmts.append( + p.s(S.SExpr{ .value = p.newExpr(E.Call{ .target = target, .args = call_args }, loc) }, loc), + ) catch unreachable; + } + ctor_stmts.appendSlice(constructor_inject_stmts.items) catch unreachable; + new_properties.insert(0, G.Property{ + .flags = Flags.Property.init(.{ .is_method = true }), + .key = p.newExpr(E.String{ .data = "constructor" }, loc), + .value = p.newExpr(E.Function{ .func = G.Fn{ + .name = null, + .open_parens_loc = logger.Loc.Empty, + .args = &[_]Arg{}, + .body = .{ .loc = loc, .stmts = ctor_stmts.items }, + .flags = Flags.Function.init(.{}), + } }, loc), + }) catch unreachable; + } + } + + // Static private __privateAdd blocks at beginning + if (static_private_add_blocks.items.len > 0) { + new_properties.insertSlice(0, static_private_add_blocks.items) catch unreachable; + } + + class.properties = new_properties.items; + class.has_decorators = false; + class.should_lower_standard_decorators = false; + + // ── Phase 8: Assemble output ───────────────────── + if (is_expr) { + var comma_parts = ListManaged(Expr).init(p.allocator); + if (class_dec_assign_expr) |cda| comma_parts.append(cda) catch unreachable; + if (base_assign_expr) |ba| comma_parts.append(ba) catch unreachable; + + // Convert S.Local decls to comma assignments + const appendDeclsAsAssigns = struct { + fn f(parts: *ListManaged(Expr), var_decls: *ListManaged(G.Decl), stmts_list: []const Stmt, parser: *P, l: logger.Loc) void { + for (stmts_list) |pstmt| { + if (pstmt.data == .s_local) { + for (pstmt.data.s_local.decls.slice()) |decl_item| { + const ref = decl_item.binding.data.b_identifier.ref; + var_decls.append(.{ .binding = parser.b(B.Identifier{ .ref = ref }, l) }) catch unreachable; + if (decl_item.value) |val| { + parser.recordUsage(ref); + parts.append( + Expr.assign(parser.newExpr(E.Identifier{ .ref = ref }, l), val), + ) catch unreachable; + } + } + } + } + } + }.f; + + appendDeclsAsAssigns(&comma_parts, &expr_var_decls, pre_eval_stmts.items, p, loc); + appendDeclsAsAssigns(&comma_parts, &expr_var_decls, prefix_stmts.items, p, loc); + + // _init = __decoratorStart(...) + comma_parts.append(assignTo(p, init_ref, init_start_expr, loc)) catch unreachable; + + // _class = class { ... } + comma_parts.append(assignTo(p, expr_class_ref.?, p.newExpr(class.*, loc), loc)) catch unreachable; + + comma_parts.appendSlice(suffix_exprs.items) catch unreachable; + + // Final value + const final_ref = if (class_decorators.len > 0) class_name_ref else expr_class_ref.?; + comma_parts.append(useRef(p, final_ref, loc)) catch unreachable; + + // Build comma chain + var result = comma_parts.items[0]; + for (comma_parts.items[1..]) |part| { + result = p.newExpr(E.Binary{ + .op = .bin_comma, + .left = result, + .right = part, + }, loc); + } + + // Emit var declarations + if (expr_var_decls.items.len > 0) { + const var_decl_stmt = p.s(S.Local{ + .decls = Decl.List.fromOwnedSlice(expr_var_decls.items), + }, loc); + if (p.nearest_stmt_list) |stmt_list| { + stmt_list.append(var_decl_stmt) catch unreachable; + } + } + + var out = ListManaged(Stmt).initCapacity(p.allocator, 1) catch unreachable; + out.appendAssumeCapacity(p.s(S.SExpr{ .value = result }, loc)); + return out.items; + } + + // Statement mode + var out = ListManaged(Stmt).initCapacity(p.allocator, prefix_stmts.items.len + pre_eval_stmts.items.len + 5 + suffix_exprs.items.len) catch unreachable; + if (class_dec_stmt.data != .s_empty) out.append(class_dec_stmt) catch unreachable; + if (base_decl_stmt.data != .s_empty) out.append(base_decl_stmt) catch unreachable; + out.appendSlice(pre_eval_stmts.items) catch unreachable; + out.appendSlice(prefix_stmts.items) catch unreachable; + out.appendAssumeCapacity(init_decl_stmt); + out.appendAssumeCapacity(stmt); + for (suffix_exprs.items) |expr| { + out.append(p.s(S.SExpr{ .value = expr }, expr.loc)) catch unreachable; + } + // Inner class binding: let _Foo = Foo + if (!inner_class_ref.eql(class_name_ref)) { + p.recordUsage(class_name_ref); + const inner_decls = bun.handleOom(p.allocator.alloc(G.Decl, 1)); + inner_decls[0] = .{ + .binding = p.b(B.Identifier{ .ref = inner_class_ref }, loc), + .value = p.newExpr(E.Identifier{ .ref = class_name_ref }, class_name_loc), + }; + out.append(p.s(S.Local{ + .kind = .k_let, + .decls = Decl.List.fromOwnedSlice(inner_decls), + }, loc)) catch unreachable; + } + + return out.items; + } + }; +} + +const string = []const u8; + +const bun = @import("bun"); +const logger = bun.logger; + +const js_ast = bun.ast; +const B = js_ast.B; +const E = js_ast.E; +const Expr = js_ast.Expr; +const ExprNodeList = js_ast.ExprNodeList; +const Flags = js_ast.Flags; +const S = js_ast.S; +const Stmt = js_ast.Stmt; +const Symbol = js_ast.Symbol; +const G = js_ast.G; +const Arg = G.Arg; +const Decl = G.Decl; +const Property = G.Property; + +const js_parser = bun.js_parser; +const JSXTransformType = js_parser.JSXTransformType; +const Ref = js_parser.Ref; +const arguments_str = js_parser.arguments_str; + +const std = @import("std"); +const ListManaged = std.array_list.Managed; From bff4726798196abbd4e87d7ce80cc0ca37efbaac Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:49:46 +0000 Subject: [PATCH 03/12] [autofix.ci] apply automated fixes --- src/ast/P.zig | 1 - src/ast/lowerDecorators.zig | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ast/P.zig b/src/ast/P.zig index 81b696f991d7..56ca06ca277e 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -4860,7 +4860,6 @@ pub fn NewParser_( stmts.append(closure) catch unreachable; } - pub fn lowerClass( noalias p: *P, stmtorexpr: js_ast.StmtOrExpr, diff --git a/src/ast/lowerDecorators.zig b/src/ast/lowerDecorators.zig index 7c46a82c0a3d..b46488ec84bb 100644 --- a/src/ast/lowerDecorators.zig +++ b/src/ast/lowerDecorators.zig @@ -1398,7 +1398,8 @@ pub fn LowerDecorators( }; } -const string = []const u8; +const std = @import("std"); +const ListManaged = std.array_list.Managed; const bun = @import("bun"); const logger = bun.logger; @@ -1412,6 +1413,7 @@ const Flags = js_ast.Flags; const S = js_ast.S; const Stmt = js_ast.Stmt; const Symbol = js_ast.Symbol; + const G = js_ast.G; const Arg = G.Arg; const Decl = G.Decl; @@ -1421,6 +1423,3 @@ const js_parser = bun.js_parser; const JSXTransformType = js_parser.JSXTransformType; const Ref = js_parser.Ref; const arguments_str = js_parser.arguments_str; - -const std = @import("std"); -const ListManaged = std.array_list.Managed; From 90d20c6693d948408acb16eedfa673efb2d7bdd4 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 08:53:24 +0100 Subject: [PATCH 04/12] fix(test): copy esbuild decorator tests into test dir for CI The vendor/esbuild submodule isn't available in CI. Copy decorator-tests.ts into test/bundler/transpiler/ and update the import path. Co-Authored-By: Claude --- .../transpiler/es-decorators-esbuild.test.ts | 2 +- .../transpiler/esbuild-decorator-tests.ts | 3912 +++++++++++++++++ 2 files changed, 3913 insertions(+), 1 deletion(-) create mode 100644 test/bundler/transpiler/esbuild-decorator-tests.ts diff --git a/test/bundler/transpiler/es-decorators-esbuild.test.ts b/test/bundler/transpiler/es-decorators-esbuild.test.ts index 490f9143b3e9..c3ca422885f9 100644 --- a/test/bundler/transpiler/es-decorators-esbuild.test.ts +++ b/test/bundler/transpiler/es-decorators-esbuild.test.ts @@ -76,7 +76,7 @@ async function runDecoratorTest(code: string) { } // Read the esbuild decorator test source and extract individual tests -const esbuildTestPath = join(import.meta.dir, "../../../vendor/esbuild/scripts/decorator-tests.ts"); +const esbuildTestPath = join(import.meta.dir, "esbuild-decorator-tests.ts"); const esbuildSource = readFileSync(esbuildTestPath, "utf8"); interface TestEntry { diff --git a/test/bundler/transpiler/esbuild-decorator-tests.ts b/test/bundler/transpiler/esbuild-decorator-tests.ts new file mode 100644 index 000000000000..b29336b008e0 --- /dev/null +++ b/test/bundler/transpiler/esbuild-decorator-tests.ts @@ -0,0 +1,3912 @@ +// Polyfill this in case it's missing +if (!('metadata' in Symbol as any)) { + (Symbol as any).metadata = Symbol('Symbol.metadata') +} +if (!(Symbol.metadata in Function)) { + Object.defineProperty((Function as any).prototype, Symbol.metadata, { value: null }) +} + +const tests: Record Promise | void> = { + // Class decorators + 'Class decorators: Basic statement': () => { + let old: { new(): Foo } + const dec = (name: string) => (cls: { new(): Foo }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + @dec('Foo') class Foo { } + assertEq(() => Foo, old!) + }, + 'Class decorators: Basic expression: Anonymous': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + const Foo = (x => x)(@dec('') class { }) + assertEq(() => Foo, old!) + const Bar = (x => x)(@dec('Baz') class Baz { }) + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Property value': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + const obj = { + Foo: @dec('Foo') class { }, + } + assertEq(() => obj.Foo, old!) + const obj2 = { + Bar: @dec('Baz') class Baz { }, + } + assertEq(() => obj2.Bar, old!) + }, + 'Class decorators: Basic expression: Variable initializer': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + const Foo = @dec('Foo') class { } + assertEq(() => Foo, old!) + const Bar = @dec('Baz') class Baz { } + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Array binding': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + const [Foo = @dec('Foo') class { }] = [] + assertEq(() => Foo, old!) + const [Bar = @dec('Baz') class Baz { }] = [] + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Object binding': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + const { Foo = @dec('Foo') class { } } = {} + assertEq(() => Foo, old!) + const { Bar = @dec('Baz') class Baz { } } = {} + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Assignment initializer': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + let Foo: { new(): unknown } + Foo = @dec('Foo') class { } + assertEq(() => Foo, old!) + let Bar: { new(): unknown } + Bar = @dec('Baz') class Baz { } + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Assignment array binding': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + let Foo: { new(): unknown }; + [Foo = @dec('Foo') class { }] = [] + assertEq(() => Foo, old!) + let Bar: { new(): unknown }; + [Bar = @dec('Baz') class Baz { }] = [] + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Assignment object binding': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + let Foo: { new(): unknown }; + ({ Foo = @dec('Foo') class { } } = {}) + assertEq(() => Foo, old!) + let Bar: { new(): unknown }; + ({ Bar = @dec('Baz') class Baz { } } = {}) + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Instance field initializer': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + class Class { + Foo = @dec('Foo') class { } + } + const Foo = new Class().Foo + assertEq(() => Foo, old!) + class Class2 { + Bar = @dec('Baz') class Baz { } + } + const Bar = new Class2().Bar + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Static field initializer': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + class Class { + static Foo = @dec('Foo') class { } + } + assertEq(() => Class.Foo, old!) + class Class2 { + static Bar = @dec('Baz') class Baz { } + } + assertEq(() => Class2.Bar, old!) + }, + 'Class decorators: Basic expression: Instance auto-accessor initializer': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + class Class { + accessor Foo = @dec('Foo') class { } + } + const Foo = new Class().Foo + assertEq(() => Foo, old!) + class Class2 { + accessor Bar = @dec('Baz') class Baz { } + } + const Bar = new Class2().Bar + assertEq(() => Bar, old!) + }, + 'Class decorators: Basic expression: Static auto-accessor initializer': () => { + let old: { new(): unknown } + const dec = (name: string) => (cls: { new(): unknown }, ctx: ClassDecoratorContext) => { + assertEq(() => typeof cls, 'function') + assertEq(() => cls.name, name) + assertEq(() => ctx.kind, 'class') + assertEq(() => ctx.name, name) + assertEq(() => 'static' in ctx, false) + assertEq(() => 'private' in ctx, false) + assertEq(() => 'access' in ctx, false) + old = cls + } + class Class { + static accessor Foo = @dec('Foo') class { } + } + assertEq(() => Class.Foo, old!) + class Class2 { + static accessor Bar = @dec('Baz') class Baz { } + } + assertEq(() => Class2.Bar, old!) + }, + 'Class decorators: Order': () => { + const log: number[] = [] + let Bar: { new(): Foo } + let Baz: { new(): Foo } + const dec1 = (cls: { new(): Foo }, ctx: ClassDecoratorContext) => { + log.push(2) + Bar = function () { + log.push(4) + return new cls + } as any + return Bar + } + const dec2 = (cls: { new(): Foo }, ctx: ClassDecoratorContext) => { + log.push(1) + Baz = function () { + log.push(5) + return new cls + } as any + return Baz + } + log.push(0) + @dec1 @dec2 class Foo { + constructor() { log.push(6) } + } + log.push(3) + new Foo + log.push(7) + assertEq(() => Foo, Bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Class decorators: Return null': () => { + assertThrows(() => { + const dec = (cls: { new(): Foo }, ctx: ClassDecoratorContext): any => { + return null + } + @dec class Foo { } + }, TypeError) + }, + 'Class decorators: Return object': () => { + assertThrows(() => { + const dec = (cls: { new(): Foo }, ctx: ClassDecoratorContext): any => { + return {} + } + @dec class Foo { } + }, TypeError) + }, + 'Class decorators: Extra initializer': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (cls: { new(): Foo }, ctx: ClassDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + @dec @dec class Foo { } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + + // Method decorators + 'Method decorators: Basic (instance method)': () => { + const old: Record void> = {} + const dec = (key: PropertyKey, name: string) => + (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, name) + assertEq(() => ctx.kind, 'method') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => (ctx.access.get as any)({ [key]: 123 }), 123) + assertEq(() => 'set' in ctx.access, false) + old[key] = fn + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + @dec('foo', 'foo') foo() { } + @dec(bar, '[bar]') [bar]() { } + @dec(baz, '') [baz]() { } + } + assertEq(() => Foo.prototype.foo, old['foo']) + assertEq(() => Foo.prototype[bar], old[bar]) + assertEq(() => Foo.prototype[baz], old[baz]) + }, + 'Method decorators: Basic (static method)': () => { + const old: Record void> = {} + const dec = (key: PropertyKey, name: string) => + (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, name) + assertEq(() => ctx.kind, 'method') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => (ctx.access.get as any)({ [key]: 123 }), 123) + assertEq(() => 'set' in ctx.access, false) + old[key] = fn + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + @dec('foo', 'foo') static foo() { } + @dec(bar, '[bar]') static [bar]() { } + @dec(baz, '') static [baz]() { } + } + assertEq(() => Foo.foo, old['foo']) + assertEq(() => Foo[bar], old[bar]) + assertEq(() => Foo[baz], old[baz]) + }, + 'Method decorators: Basic (private instance method)': () => { + let old: (this: Foo) => void + let lateAsserts: () => void + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, '#foo') + assertEq(() => ctx.kind, 'method') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(new Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(new Foo), $foo) + assertEq(() => 'set' in ctx.access, false) + } + old = fn + } + let $foo: Function + class Foo { + @dec #foo() { } + static { $foo = new Foo().#foo } + } + assertEq(() => $foo, old!) + lateAsserts!() + }, + 'Method decorators: Basic (private static method)': () => { + let old: (this: typeof Foo) => void + let lateAsserts: () => void + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, '#foo') + assertEq(() => ctx.kind, 'method') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(Foo), $foo) + assertEq(() => 'set' in ctx.access, false) + } + old = fn + } + let $foo: Function + class Foo { + @dec static #foo() { } + static { $foo = this.#foo } + } + assertEq(() => $foo, old!) + lateAsserts!() + }, + 'Method decorators: Shim (instance method)': () => { + let bar: (this: Foo) => number + const dec = (fn: (this: Foo) => number, ctx: ClassMethodDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + class Foo { + bar = 123 + @dec foo() { return this.bar } + } + assertEq(() => Foo.prototype.foo, bar!) + assertEq(() => new Foo().foo(), 124) + }, + 'Method decorators: Shim (static method)': () => { + let bar: (this: typeof Foo) => number + const dec = (fn: (this: typeof Foo) => number, ctx: ClassMethodDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + class Foo { + static bar = 123 + @dec static foo() { return this.bar } + } + assertEq(() => Foo.foo, bar!) + assertEq(() => Foo.foo(), 124) + }, + 'Method decorators: Shim (private instance method)': () => { + let bar: (this: Foo) => number + const dec = (fn: (this: Foo) => number, ctx: ClassMethodDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + let $foo: (this: Foo) => number + class Foo { + bar = 123 + @dec #foo() { return this.bar } + static { $foo = new Foo().#foo } + } + assertEq(() => $foo, bar!) + assertEq(() => bar.call(new Foo), 124) + }, + 'Method decorators: Shim (private static method)': () => { + let bar: (this: typeof Foo) => number + const dec = (fn: (this: typeof Foo) => number, ctx: ClassMethodDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + let $foo: (this: Foo) => number + class Foo { + static bar = 123 + @dec static #foo() { return this.bar } + static { $foo = this.#foo } + } + assertEq(() => $foo, bar!) + assertEq(() => bar.call(Foo), 124) + }, + 'Method decorators: Order (instance method)': () => { + const log: number[] = [] + let bar: (this: Foo) => number + let baz: (this: Foo) => number + const dec1 = (fn: (this: Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + class Foo { + @dec1 @dec2 foo() { return log.push(6) } + } + log.push(3) + new Foo().foo() + log.push(7) + assertEq(() => Foo.prototype.foo, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Method decorators: Order (static method)': () => { + const log: number[] = [] + let bar: (this: typeof Foo) => number + let baz: (this: typeof Foo) => number + const dec1 = (fn: (this: typeof Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: typeof Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + class Foo { + @dec1 @dec2 static foo() { return log.push(6) } + } + log.push(3) + Foo.foo() + log.push(7) + assertEq(() => Foo.foo, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Method decorators: Order (private instance method)': () => { + const log: number[] = [] + let bar: (this: Foo) => number + let baz: (this: Foo) => number + const dec1 = (fn: (this: Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + let $foo: Function + class Foo { + @dec1 @dec2 #foo() { return log.push(6) } + static { $foo = new Foo().#foo } + } + log.push(3) + $foo.call(new Foo) + log.push(7) + assertEq(() => $foo, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Method decorators: Order (private static method)': () => { + const log: number[] = [] + let bar: (this: typeof Foo) => number + let baz: (this: typeof Foo) => number + const dec1 = (fn: (this: typeof Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: typeof Foo) => number, ctx: ClassMethodDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + let $foo: (this: Foo) => number + class Foo { + @dec1 @dec2 static #foo() { return log.push(6) } + static { $foo = Foo.#foo } + } + log.push(3) + $foo.call(Foo) + log.push(7) + assertEq(() => $foo, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Method decorators: Return null (instance method)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return null + } + class Foo { @dec foo() { } } + }, TypeError) + }, + 'Method decorators: Return null (static method)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return null + } + class Foo { @dec static foo() { } } + }, TypeError) + }, + 'Method decorators: Return null (private instance method)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return null + } + class Foo { @dec #foo() { } } + }, TypeError) + }, + 'Method decorators: Return null (private static method)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return null + } + class Foo { @dec static #foo() { } } + }, TypeError) + }, + 'Method decorators: Return object (instance method)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return {} + } + class Foo { @dec foo() { } } + }, TypeError) + }, + 'Method decorators: Return object (static method)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return {} + } + class Foo { @dec static foo() { } } + }, TypeError) + }, + 'Method decorators: Return object (private instance method)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return {} + } + class Foo { @dec #foo() { } } + }, TypeError) + }, + 'Method decorators: Return object (private static method)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext): any => { + return {} + } + class Foo { @dec static #foo() { } } + }, TypeError) + }, + 'Method decorators: Extra initializer (instance method)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec foo() { } } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Method decorators: Extra initializer (static method)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static foo() { } } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + 'Method decorators: Extra initializer (private instance method)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: Foo) => void, ctx: ClassMethodDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec #foo() { } } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Method decorators: Extra initializer (private static method)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: typeof Foo) => void, ctx: ClassMethodDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static #foo() { } } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + + // Field decorators + 'Field decorators: Basic (instance field)': () => { + const dec = (key: PropertyKey) => + (value: undefined, ctx: ClassFieldDecoratorContext) => { + assertEq(() => value, undefined) + assertEq(() => ctx.kind, 'field') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => ctx.access.get({ [key]: 123 }), 123) + assertEq(() => { + const obj: any = {} + ctx.access.set(obj, 321) + return obj[key] + }, 321) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + @dec('foo') foo = 123 + @dec(bar) [bar] = 123 + @dec(baz) [baz] = 123 + } + assertEq(() => new Foo().foo, 123) + assertEq(() => new Foo()[bar], 123) + assertEq(() => new Foo()[baz], 123) + }, + 'Field decorators: Basic (static field)': () => { + const dec = (key: PropertyKey) => + (value: undefined, ctx: ClassFieldDecoratorContext) => { + assertEq(() => value, undefined) + assertEq(() => ctx.kind, 'field') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => ctx.access.get({ [key]: 123 }), 123) + assertEq(() => { + const obj: any = {} + ctx.access.set(obj, 321) + return obj[key] + }, 321) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + @dec('foo') static foo = 123 + @dec(bar) static [bar] = 123 + @dec(baz) static [baz] = 123 + } + assertEq(() => Foo.foo, 123) + assertEq(() => Foo[bar], 123) + assertEq(() => Foo[baz], 123) + }, + 'Field decorators: Basic (private instance field)': () => { + let lateAsserts: () => void + const dec = (value: undefined, ctx: ClassFieldDecoratorContext) => { + assertEq(() => value, undefined) + assertEq(() => ctx.kind, 'field') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(new Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(new Foo), 123) + assertEq(() => { + const obj = new Foo + ctx.access.set(obj, 321) + return get$foo(obj) + }, 321) + } + } + let get$foo: (x: Foo) => number + class Foo { + @dec #foo = 123 + static { get$foo = x => x.#foo } + } + assertEq(() => get$foo(new Foo()), 123) + lateAsserts!() + }, + 'Field decorators: Basic (private static field)': () => { + let lateAsserts: () => void + const dec = (value: undefined, ctx: ClassFieldDecoratorContext) => { + assertEq(() => value, undefined) + assertEq(() => ctx.kind, 'field') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(Foo), 123) + assertEq(() => { + ctx.access.set(Foo, 321) + return get$foo(Foo) + }, 321) + } + } + let get$foo: (x: typeof Foo) => number + class Foo { + @dec static #foo = 123 + static { get$foo = x => x.#foo } + } + assertEq(() => get$foo(Foo), 123) + lateAsserts!() + }, + 'Field decorators: Shim (instance field)': () => { + let log: (boolean | number)[] = [] + const dec = (value: undefined, ctx: ClassFieldDecoratorContext) => { + return function (this: Foo, x: number) { + assertEq(() => this instanceof Foo, true) + return log.push('foo' in this, 'bar' in this, x) + } + } + class Foo { + @dec foo = 123 + @dec bar!: number + } + assertEq(() => log + '', '') + var obj = new Foo + assertEq(() => obj.foo, 3) + assertEq(() => obj.bar, 6) + assertEq(() => log + '', 'false,false,123,true,false,') + }, + 'Field decorators: Shim (static field)': () => { + let foo: typeof Foo + let log: (boolean | number)[] = [] + const dec = (value: undefined, ctx: ClassFieldDecoratorContext) => { + return function (this: typeof Foo, x: number) { + assertEq(() => this, foo) + return log.push('foo' in this, 'bar' in this, x) + } + } + assertEq(() => log + '', '') + class Foo { + static { + foo = Foo + } + @dec static foo = 123 + @dec static bar: number + } + assertEq(() => Foo.foo, 3) + assertEq(() => Foo.bar, 6) + assertEq(() => log + '', 'false,false,123,true,false,') + }, + 'Field decorators: Shim (private instance field)': () => { + let log: (boolean | number)[] = [] + const dec = (value: undefined, ctx: ClassFieldDecoratorContext) => { + return function (this: Foo, x: number) { + assertEq(() => this instanceof Foo, true) + return log.push(has$foo(this), has$bar(this), x) + } + } + let has$foo: (x: Foo) => boolean + let has$bar: (x: Foo) => boolean + let get$foo: (x: Foo) => number + let get$bar: (x: Foo) => number + class Foo { + @dec #foo = 123 + @dec #bar!: number + static { + has$foo = x => #foo in x + has$bar = x => #bar in x + get$foo = x => x.#foo + get$bar = x => x.#bar + } + } + assertEq(() => log + '', '') + var obj = new Foo + assertEq(() => get$foo(obj), 3) + assertEq(() => get$bar(obj), 6) + assertEq(() => log + '', 'false,false,123,true,false,') + }, + 'Field decorators: Shim (private static field)': () => { + let foo: typeof Foo + let log: (boolean | number)[] = [] + const dec = (value: undefined, ctx: ClassFieldDecoratorContext) => { + return function (this: typeof Foo, x: number) { + assertEq(() => this, foo) + return log.push(has$foo(this), has$bar(this), x) + } + } + assertEq(() => log + '', '') + let has$foo: (x: typeof Foo) => boolean + let has$bar: (x: typeof Foo) => boolean + let get$foo: (x: typeof Foo) => number + let get$bar: (x: typeof Foo) => number + class Foo { + static { + foo = Foo + has$foo = x => #foo in x + has$bar = x => #bar in x + get$foo = x => x.#foo + get$bar = x => x.#bar + } + @dec static #foo = 123 + @dec static #bar: number + } + assertEq(() => get$foo(Foo), 3) + assertEq(() => get$bar(Foo), 6) + assertEq(() => log + '', 'false,false,123,true,false,') + }, + 'Field decorators: Order (instance field)': () => { + const log: number[] = [] + const dec1 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(2) + return () => log.push(4) + } + const dec2 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(1) + return () => log.push(5) + } + log.push(0) + class Foo { + @dec1 @dec2 foo = 123 + } + log.push(3) + var obj = new Foo() + log.push(6) + assertEq(() => obj.foo, 6) + assertEq(() => log + '', '0,1,2,3,4,5,6') + }, + 'Field decorators: Order (static field)': () => { + const log: number[] = [] + const dec1 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(2) + return () => log.push(3) + } + const dec2 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(1) + return () => log.push(4) + } + log.push(0) + class Foo { + @dec1 @dec2 static foo = 123 + } + log.push(5) + assertEq(() => Foo.foo, 5) + assertEq(() => log + '', '0,1,2,3,4,5') + }, + 'Field decorators: Order (private instance field)': () => { + const log: number[] = [] + const dec1 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(2) + return () => log.push(4) + } + const dec2 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(1) + return () => log.push(5) + } + log.push(0) + let get$foo: (x: Foo) => number + class Foo { + @dec1 @dec2 #foo = 123 + static { get$foo = x => x.#foo } + } + log.push(3) + var obj = new Foo() + log.push(6) + assertEq(() => get$foo(obj), 6) + assertEq(() => log + '', '0,1,2,3,4,5,6') + }, + 'Field decorators: Order (private static field)': () => { + const log: number[] = [] + const dec1 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(2) + return () => log.push(3) + } + const dec2 = (value: undefined, ctx: ClassFieldDecoratorContext) => { + log.push(1) + return () => log.push(4) + } + log.push(0) + let get$foo: (x: typeof Foo) => number + class Foo { + @dec1 @dec2 static #foo = 123 + static { get$foo = x => x.#foo } + } + log.push(5) + assertEq(() => get$foo(Foo), 5) + assertEq(() => log + '', '0,1,2,3,4,5') + }, + 'Field decorators: Return null (instance field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return null + } + class Foo { @dec foo: undefined } + }, TypeError) + }, + 'Field decorators: Return null (static field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return null + } + class Foo { @dec static foo: undefined } + }, TypeError) + }, + 'Field decorators: Return null (private instance field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return null + } + class Foo { @dec #foo: undefined } + }, TypeError) + }, + 'Field decorators: Return null (private static field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return null + } + class Foo { @dec static #foo: undefined } + }, TypeError) + }, + 'Field decorators: Return object (instance field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return {} + } + class Foo { @dec foo: undefined } + }, TypeError) + }, + 'Field decorators: Return object (static field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return {} + } + class Foo { @dec static foo: undefined } + }, TypeError) + }, + 'Field decorators: Return object (private instance field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return {} + } + class Foo { @dec #foo: undefined } + }, TypeError) + }, + 'Field decorators: Return object (private static field)': () => { + assertThrows(() => { + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + return {} + } + class Foo { @dec static #foo: undefined } + }, TypeError) + }, + 'Field decorators: Extra initializer (instance field)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec foo: undefined } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Field decorators: Extra initializer (static field)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static foo: undefined } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + 'Field decorators: Extra initializer (private instance field)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec #foo: undefined } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Field decorators: Extra initializer (private static field)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (value: undefined, ctx: ClassFieldDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static #foo: undefined } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + + // Getter decorators + 'Getter decorators: Basic (instance getter)': () => { + const dec = (key: PropertyKey, name: string) => + (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, name) + assertEq(() => ctx.kind, 'getter') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => ctx.access.get({ [key]: 123 }), 123) + assertEq(() => 'set' in ctx.access, false) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + bar = 123 + @dec('foo', 'get foo') get foo() { return this.bar } + @dec(bar, 'get [bar]') get [bar]() { return this.bar } + @dec(baz, 'get ') get [baz]() { return this.bar } + } + assertEq(() => new Foo().foo, 123) + assertEq(() => new Foo()[bar], 123) + assertEq(() => new Foo()[baz], 123) + }, + 'Getter decorators: Basic (static getter)': () => { + const dec = (key: PropertyKey, name: string) => + (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, name) + assertEq(() => ctx.kind, 'getter') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => ctx.access.get({ [key]: 123 }), 123) + assertEq(() => 'set' in ctx.access, false) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + static bar = 123 + @dec('foo', 'get foo') static get foo() { return this.bar } + @dec(bar, 'get [bar]') static get [bar]() { return this.bar } + @dec(baz, 'get ') static get [baz]() { return this.bar } + } + assertEq(() => Foo.foo, 123) + assertEq(() => Foo[bar], 123) + assertEq(() => Foo[baz], 123) + }, + 'Getter decorators: Basic (private instance getter)': () => { + let lateAsserts: () => void + const dec = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, 'get #foo') + assertEq(() => ctx.kind, 'getter') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(new Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(new Foo), 123) + assertEq(() => 'set' in ctx.access, false) + } + } + let get$foo: (x: Foo) => number + class Foo { + #bar = 123 + @dec get #foo() { return this.#bar } + static { get$foo = x => x.#foo } + } + assertEq(() => get$foo(new Foo), 123) + lateAsserts!() + }, + 'Getter decorators: Basic (private static getter)': () => { + let lateAsserts: () => void + const dec = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, 'get #foo') + assertEq(() => ctx.kind, 'getter') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(Foo), 123) + assertEq(() => 'set' in ctx.access, false) + } + } + let get$foo: (x: typeof Foo) => number + class Foo { + static #bar = 123 + @dec static get #foo() { return this.#bar } + static { get$foo = x => x.#foo } + } + assertEq(() => get$foo(Foo), 123) + lateAsserts!() + }, + 'Getter decorators: Shim (instance getter)': () => { + let bar: (this: Foo) => number + const dec = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + class Foo { + bar = 123 + @dec get foo() { return this.bar } + } + assertEq(() => Object.getOwnPropertyDescriptor(Foo.prototype, 'foo')!.get, bar!) + assertEq(() => new Foo().foo, 124) + }, + 'Getter decorators: Shim (static getter)': () => { + let bar: (this: typeof Foo) => number + const dec = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + class Foo { + static bar = 123 + @dec static get foo() { return this.bar } + } + assertEq(() => Object.getOwnPropertyDescriptor(Foo, 'foo')!.get, bar!) + assertEq(() => Foo.foo, 124) + }, + 'Getter decorators: Shim (private instance getter)': () => { + let bar: (this: Foo) => number + const dec = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + let get$foo: (x: Foo) => number + class Foo { + #bar = 123 + @dec get #foo() { return this.#bar } + static { get$foo = x => x.#foo } + } + assertEq(() => get$foo(new Foo), 124) + }, + 'Getter decorators: Shim (private static getter)': () => { + let bar: (this: typeof Foo) => number + const dec = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + bar = function () { return fn.call(this) + 1 } + return bar + } + let get$foo: (x: typeof Foo) => number + class Foo { + static #bar = 123 + @dec static get #foo() { return this.#bar } + static { get$foo = x => x.#foo } + } + assertEq(() => get$foo(Foo), 124) + }, + 'Getter decorators: Order (instance getter)': () => { + const log: number[] = [] + let bar: (this: Foo) => number + let baz: (this: Foo) => number + const dec1 = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + class Foo { + @dec1 @dec2 get foo() { return log.push(6) } + } + log.push(3) + new Foo().foo + log.push(7) + assertEq(() => Object.getOwnPropertyDescriptor(Foo.prototype, 'foo')!.get, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Getter decorators: Order (static getter)': () => { + const log: number[] = [] + let bar: (this: typeof Foo) => number + let baz: (this: typeof Foo) => number + const dec1 = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + class Foo { + @dec1 @dec2 static get foo() { return log.push(6) } + } + log.push(3) + Foo.foo + log.push(7) + assertEq(() => Object.getOwnPropertyDescriptor(Foo, 'foo')!.get, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Getter decorators: Order (private instance getter)': () => { + const log: number[] = [] + let bar: (this: Foo) => number + let baz: (this: Foo) => number + const dec1 = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + let get$foo: (x: Foo) => number + class Foo { + @dec1 @dec2 get #foo() { return log.push(6) } + static { get$foo = x => x.#foo } + } + log.push(3) + assertEq(() => get$foo(new Foo), 7) + log.push(7) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Getter decorators: Order (private static getter)': () => { + const log: number[] = [] + let bar: (this: typeof Foo) => number + let baz: (this: typeof Foo) => number + const dec1 = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(2) + bar = function () { + log.push(4) + return fn.call(this) + } + return bar + } + const dec2 = (fn: (this: typeof Foo) => number, ctx: ClassGetterDecoratorContext) => { + log.push(1) + baz = function () { + log.push(5) + return fn.call(this) + } + return baz + } + log.push(0) + let get$foo: (x: typeof Foo) => number + class Foo { + @dec1 @dec2 static get #foo() { return log.push(6) } + static { get$foo = x => x.#foo } + } + log.push(3) + assertEq(() => get$foo(Foo), 7) + log.push(7) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Getter decorators: Return null (instance getter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return null + } + class Foo { @dec get foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return null (static getter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return null + } + class Foo { @dec static get foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return null (private instance getter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return null + } + class Foo { @dec get #foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return null (private static getter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return null + } + class Foo { @dec static get #foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return object (instance getter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return {} + } + class Foo { @dec get foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return object (static getter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return {} + } + class Foo { @dec static get foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return object (private instance getter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return {} + } + class Foo { @dec get #foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Return object (private static getter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + return {} + } + class Foo { @dec static get #foo(): undefined { return } } + }, TypeError) + }, + 'Getter decorators: Extra initializer (instance getter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec get foo(): undefined { return } } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Getter decorators: Extra initializer (static getter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: typeof Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static get foo(): undefined { return } } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + 'Getter decorators: Extra initializer (private instance getter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec get #foo(): undefined { return } } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Getter decorators: Extra initializer (private static getter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: typeof Foo) => undefined, ctx: ClassGetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static get #foo(): undefined { return } } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + + // Setter decorators + 'Setter decorators: Basic (instance setter)': () => { + const dec = (key: PropertyKey, name: string) => + (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, name) + assertEq(() => ctx.kind, 'setter') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => 'get' in ctx.access, false) + const obj: any = {} + ctx.access.set(obj, 123) + assertEq(() => obj[key], 123) + assertEq(() => 'bar' in obj, false) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + bar = 0 + @dec('foo', 'set foo') set foo(x: number) { this.bar = x } + @dec(bar, 'set [bar]') set [bar](x: number) { this.bar = x } + @dec(baz, 'set ') set [baz](x: number) { this.bar = x } + } + var obj = new Foo + obj.foo = 321 + assertEq(() => obj.bar, 321) + obj[bar] = 4321 + assertEq(() => obj.bar, 4321) + obj[baz] = 54321 + assertEq(() => obj.bar, 54321) + }, + 'Setter decorators: Basic (static setter)': () => { + const dec = (key: PropertyKey, name: string) => + (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, name) + assertEq(() => ctx.kind, 'setter') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => 'get' in ctx.access, false) + const obj: any = {} + ctx.access.set(obj, 123) + assertEq(() => obj[key], 123) + assertEq(() => 'bar' in obj, false) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + static bar = 0 + @dec('foo', 'set foo') static set foo(x: number) { this.bar = x } + @dec(bar, 'set [bar]') static set [bar](x: number) { this.bar = x } + @dec(baz, 'set ') static set [baz](x: number) { this.bar = x } + } + Foo.foo = 321 + assertEq(() => Foo.bar, 321) + Foo[bar] = 4321 + assertEq(() => Foo.bar, 4321) + Foo[baz] = 54321 + assertEq(() => Foo.bar, 54321) + }, + 'Setter decorators: Basic (private instance setter)': () => { + let lateAsserts: () => void + const dec = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, 'set #foo') + assertEq(() => ctx.kind, 'setter') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(new Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => 'get' in ctx.access, false) + assertEq(() => { + const obj = new Foo + ctx.access.set(obj, 123) + return obj.bar + }, 123) + } + } + let set$foo: (x: Foo, y: number) => void + class Foo { + bar = 0 + @dec set #foo(x: number) { this.bar = x } + static { set$foo = (x, y) => { x.#foo = y } } + } + lateAsserts!() + var obj = new Foo + assertEq(() => set$foo(obj, 321), undefined) + assertEq(() => obj.bar, 321) + }, + 'Setter decorators: Basic (private static setter)': () => { + let lateAsserts: () => void + const dec = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + assertEq(() => typeof fn, 'function') + assertEq(() => fn.name, 'set #foo') + assertEq(() => ctx.kind, 'setter') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => 'get' in ctx.access, false) + assertEq(() => { + ctx.access.set(Foo, 123) + return Foo.bar + }, 123) + } + } + let set$foo: (x: typeof Foo, y: number) => void + class Foo { + static bar = 0 + @dec static set #foo(x: number) { this.bar = x } + static { set$foo = (x, y) => { x.#foo = y } } + } + lateAsserts!() + assertEq(() => set$foo(Foo, 321), undefined) + assertEq(() => Foo.bar, 321) + }, + 'Setter decorators: Shim (instance setter)': () => { + let bar: (this: Foo, x: number) => void + const dec = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + bar = function (x) { fn.call(this, x + 1) } + return bar + } + class Foo { + bar = 123 + @dec set foo(x: number) { this.bar = x } + } + assertEq(() => Object.getOwnPropertyDescriptor(Foo.prototype, 'foo')!.set, bar!) + var obj = new Foo + obj.foo = 321 + assertEq(() => obj.bar, 322) + }, + 'Setter decorators: Shim (static setter)': () => { + let bar: (this: typeof Foo, x: number) => void + const dec = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + bar = function (x) { fn.call(this, x + 1) } + return bar + } + class Foo { + static bar = 123 + @dec static set foo(x: number) { this.bar = x } + } + assertEq(() => Object.getOwnPropertyDescriptor(Foo, 'foo')!.set, bar!) + Foo.foo = 321 + assertEq(() => Foo.bar, 322) + }, + 'Setter decorators: Shim (private instance setter)': () => { + let bar: (this: Foo, x: number) => void + const dec = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + bar = function (x) { fn.call(this, x + 1) } + return bar + } + let set$foo: (x: Foo, y: number) => void + class Foo { + bar = 123 + @dec set #foo(x: number) { this.bar = x } + static { set$foo = (x, y) => { x.#foo = y } } + } + var obj = new Foo + assertEq(() => set$foo(obj, 321), undefined) + assertEq(() => obj.bar, 322) + }, + 'Setter decorators: Shim (private static setter)': () => { + let bar: (this: typeof Foo, x: number) => void + const dec = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + bar = function (x) { fn.call(this, x + 1) } + return bar + } + let set$foo: (x: typeof Foo, y: number) => void + class Foo { + static bar = 123 + @dec static set #foo(x: number) { this.bar = x } + static { set$foo = (x, y) => { x.#foo = y } } + } + assertEq(() => set$foo(Foo, 321), undefined) + assertEq(() => Foo.bar, 322) + }, + 'Setter decorators: Order (instance setter)': () => { + const log: number[] = [] + let bar: (this: Foo, x: number) => void + let baz: (this: Foo, x: number) => void + const dec1 = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(2) + bar = function (x) { + log.push(4) + fn.call(this, x) + } + return bar + } + const dec2 = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(1) + baz = function (x) { + log.push(5) + fn.call(this, x) + } + return baz + } + log.push(0) + class Foo { + @dec1 @dec2 set foo(x: number) { log.push(6) } + } + log.push(3) + new Foo().foo = 123 + log.push(7) + assertEq(() => Object.getOwnPropertyDescriptor(Foo.prototype, 'foo')!.set, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Setter decorators: Order (static setter)': () => { + const log: number[] = [] + let bar: (this: typeof Foo, x: number) => void + let baz: (this: typeof Foo, x: number) => void + const dec1 = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(2) + bar = function (x) { + log.push(4) + fn.call(this, x) + } + return bar + } + const dec2 = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(1) + baz = function (x) { + log.push(5) + fn.call(this, x) + } + return baz + } + log.push(0) + class Foo { + @dec1 @dec2 static set foo(x: number) { log.push(6) } + } + log.push(3) + Foo.foo = 123 + log.push(7) + assertEq(() => Object.getOwnPropertyDescriptor(Foo, 'foo')!.set, bar!) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Setter decorators: Order (private instance setter)': () => { + const log: number[] = [] + let bar: (this: Foo, x: number) => void + let baz: (this: Foo, x: number) => void + const dec1 = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(2) + bar = function (x) { + log.push(4) + fn.call(this, x) + } + return bar + } + const dec2 = (fn: (this: Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(1) + baz = function (x) { + log.push(5) + fn.call(this, x) + } + return baz + } + log.push(0) + let set$foo: (x: Foo, y: number) => void + class Foo { + @dec1 @dec2 set #foo(x: number) { log.push(6) } + static { set$foo = (x, y) => { x.#foo = y } } + } + log.push(3) + assertEq(() => set$foo(new Foo, 123), undefined) + log.push(7) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Setter decorators: Order (private static setter)': () => { + const log: number[] = [] + let bar: (this: typeof Foo, x: number) => void + let baz: (this: typeof Foo, x: number) => void + const dec1 = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(2) + bar = function (x) { + log.push(4) + fn.call(this, x) + } + return bar + } + const dec2 = (fn: (this: typeof Foo, x: number) => void, ctx: ClassSetterDecoratorContext) => { + log.push(1) + baz = function (x) { + log.push(5) + fn.call(this, x) + } + return baz + } + log.push(0) + let set$foo: (x: typeof Foo, y: number) => void + class Foo { + @dec1 @dec2 static set #foo(x: number) { log.push(6) } + static { set$foo = (x, y) => { x.#foo = y } } + } + log.push(3) + assertEq(() => set$foo(Foo, 123), undefined) + log.push(7) + assertEq(() => log + '', '0,1,2,3,4,5,6,7') + }, + 'Setter decorators: Return null (instance setter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return null + } + class Foo { @dec set foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return null (static setter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return null + } + class Foo { @dec static set foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return null (private instance setter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return null + } + class Foo { @dec set #foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return null (private static setter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return null + } + class Foo { @dec static set #foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return object (instance setter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return {} + } + class Foo { @dec set foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return object (static setter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return {} + } + class Foo { @dec static set foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return object (private instance setter)': () => { + assertThrows(() => { + const dec = (fn: (this: Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return {} + } + class Foo { @dec set #foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Return object (private static setter)': () => { + assertThrows(() => { + const dec = (fn: (this: typeof Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + return {} + } + class Foo { @dec static set #foo(x: undefined) { } } + }, TypeError) + }, + 'Setter decorators: Extra initializer (instance setter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec set foo(x: undefined) { } } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Setter decorators: Extra initializer (static setter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: typeof Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static set foo(x: undefined) { } } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + 'Setter decorators: Extra initializer (private instance setter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec set #foo(x: undefined) { } } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Setter decorators: Extra initializer (private static setter)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (fn: (this: typeof Foo, x: undefined) => void, ctx: ClassSetterDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static set #foo(x: undefined) { } } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + + // Auto-accessor decorators + 'Auto-accessor decorators: Basic (instance auto-accessor)': () => { + const dec = (key: PropertyKey, getName: string, setName: string) => + (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext) => { + assertEq(() => typeof target.get, 'function') + assertEq(() => typeof target.set, 'function') + assertEq(() => target.get.name, getName) + assertEq(() => target.set.name, setName) + assertEq(() => ctx.kind, 'accessor') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => ctx.access.get({ [key]: 123 }), 123) + assertEq(() => { + const obj: any = {} + ctx.access.set(obj, 123) + return obj[key] + }, 123) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + @dec('foo', 'get foo', 'set foo') accessor foo = 0 + @dec(bar, 'get [bar]', 'set [bar]') accessor [bar] = 0 + @dec(baz, 'get ', 'set ') accessor [baz] = 0 + } + var obj = new Foo + obj.foo = 321 + assertEq(() => obj.foo, 321) + obj[bar] = 4321 + assertEq(() => obj[bar], 4321) + obj[baz] = 54321 + assertEq(() => obj[baz], 54321) + }, + 'Auto-accessor decorators: Basic (static auto-accessor)': () => { + const dec = (key: PropertyKey, getName: string, setName: string) => + (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext) => { + assertEq(() => typeof target.get, 'function') + assertEq(() => typeof target.set, 'function') + assertEq(() => target.get.name, getName) + assertEq(() => target.set.name, setName) + assertEq(() => ctx.kind, 'accessor') + assertEq(() => ctx.name, key) + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, false) + assertEq(() => ctx.access.has({ [key]: false }), true) + assertEq(() => ctx.access.has({ bar: true }), false) + assertEq(() => ctx.access.get({ [key]: 123 }), 123) + assertEq(() => { + const obj: any = {} + ctx.access.set(obj, 123) + return obj[key] + }, 123) + } + const bar = Symbol('bar') + const baz = Symbol() + class Foo { + @dec('foo', 'get foo', 'set foo') static accessor foo = 0 + @dec(bar, 'get [bar]', 'set [bar]') static accessor [bar] = 0 + @dec(baz, 'get ', 'set ') static accessor [baz] = 0 + } + Foo.foo = 321 + assertEq(() => Foo.foo, 321) + Foo[bar] = 4321 + assertEq(() => Foo[bar], 4321) + Foo[baz] = 54321 + assertEq(() => Foo[baz], 54321) + }, + 'Auto-accessor decorators: Basic (private instance auto-accessor)': () => { + let lateAsserts: () => void + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext) => { + assertEq(() => typeof target.get, 'function') + assertEq(() => typeof target.set, 'function') + assertEq(() => target.get.name, 'get #foo') + assertEq(() => target.set.name, 'set #foo') + assertEq(() => ctx.kind, 'accessor') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, false) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(new Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(new Foo), 0) + assertEq(() => { + const obj = new Foo + ctx.access.set(obj, 123) + return get$foo(obj) + }, 123) + } + } + let get$foo: (x: Foo) => number + let set$foo: (x: Foo, y: number) => void + class Foo { + @dec accessor #foo = 0 + static { + get$foo = x => x.#foo + set$foo = (x, y) => { x.#foo = y } + } + } + lateAsserts!() + var obj = new Foo + assertEq(() => set$foo(obj, 321), undefined) + assertEq(() => get$foo(obj), 321) + }, + 'Auto-accessor decorators: Basic (private static auto-accessor)': () => { + let lateAsserts: () => void + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext) => { + assertEq(() => typeof target.get, 'function') + assertEq(() => typeof target.set, 'function') + assertEq(() => target.get.name, 'get #foo') + assertEq(() => target.set.name, 'set #foo') + assertEq(() => ctx.kind, 'accessor') + assertEq(() => ctx.name, '#foo') + assertEq(() => ctx.static, true) + assertEq(() => ctx.private, true) + lateAsserts = () => { + assertEq(() => ctx.access.has(Foo), true) + assertEq(() => ctx.access.has({}), false) + assertEq(() => ctx.access.get(Foo), 0) + assertEq(() => { + ctx.access.set(Foo, 123) + return get$foo(Foo) + }, 123) + } + } + let get$foo: (x: typeof Foo) => number + let set$foo: (x: typeof Foo, y: number) => void + class Foo { + @dec static accessor #foo = 0 + static { + get$foo = x => x.#foo + set$foo = (x, y) => { x.#foo = y } + } + } + lateAsserts!() + assertEq(() => set$foo(Foo, 321), undefined) + assertEq(() => get$foo(Foo), 321) + }, + 'Auto-accessor decorators: Shim (instance auto-accessor)': () => { + let get: (this: Foo) => number + let set: (this: Foo, x: number) => void + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult => { + function init(this: Foo, x: number): number { + assertEq(() => this instanceof Foo, true) + return x + 1 + } + get = function () { return target.get.call(this) * 10 } + set = function (x) { target.set.call(this, x * 2) } + return { get, set, init } + } + class Foo { + @dec accessor foo = 123 + } + assertEq(() => Object.getOwnPropertyDescriptor(Foo.prototype, 'foo')!.get, get!) + assertEq(() => Object.getOwnPropertyDescriptor(Foo.prototype, 'foo')!.set, set!) + var obj = new Foo + assertEq(() => obj.foo, (123 + 1) * 10) + obj.foo = 321 + assertEq(() => obj.foo, (321 * 2) * 10) + }, + 'Auto-accessor decorators: Shim (static auto-accessor)': () => { + let foo: typeof Foo + let get: (this: typeof Foo) => number + let set: (this: typeof Foo, x: number) => void + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult => { + function init(this: typeof Foo, x: number): number { + assertEq(() => this, foo) + return x + 1 + } + get = function () { return target.get.call(this) * 10 } + set = function (x) { target.set.call(this, x * 2) } + return { get, set, init } + } + class Foo { + static { + foo = Foo + } + @dec static accessor foo = 123 + } + assertEq(() => Object.getOwnPropertyDescriptor(Foo, 'foo')!.get, get!) + assertEq(() => Object.getOwnPropertyDescriptor(Foo, 'foo')!.set, set!) + assertEq(() => Foo.foo, (123 + 1) * 10) + Foo.foo = 321 + assertEq(() => Foo.foo, (321 * 2) * 10) + }, + 'Auto-accessor decorators: Shim (private instance auto-accessor)': () => { + let get: (this: Foo) => number + let set: (this: Foo, x: number) => void + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult => { + function init(this: Foo, x: number): number { + assertEq(() => this instanceof Foo, true) + return x + 1 + } + get = function () { return target.get.call(this) * 10 } + set = function (x) { target.set.call(this, x * 2) } + return { get, set, init } + } + let get$foo: (x: Foo) => number + let set$foo: (x: Foo, y: number) => void + class Foo { + @dec accessor #foo = 123 + static { + get$foo = x => x.#foo + set$foo = (x, y) => { x.#foo = y } + } + } + var obj = new Foo + assertEq(() => get$foo(obj), (123 + 1) * 10) + assertEq(() => set$foo(obj, 321), undefined) + assertEq(() => get$foo(obj), (321 * 2) * 10) + }, + 'Auto-accessor decorators: Shim (private static auto-accessor)': () => { + let foo: typeof Foo + let get: (this: typeof Foo) => number + let set: (this: typeof Foo, x: number) => void + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult => { + function init(this: typeof Foo, x: number): number { + assertEq(() => this, foo) + return x + 1 + } + get = function () { return target.get.call(this) * 10 } + set = function (x) { target.set.call(this, x * 2) } + return { get, set, init } + } + let get$foo: (x: typeof Foo) => number + let set$foo: (x: typeof Foo, y: number) => void + class Foo { + static { + foo = Foo + get$foo = x => x.#foo + set$foo = (x, y) => { x.#foo = y } + } + @dec static accessor #foo = 123 + } + assertEq(() => get$foo(Foo), (123 + 1) * 10) + assertEq(() => set$foo(Foo, 321), undefined) + assertEq(() => get$foo(Foo), (321 * 2) * 10) + }, + 'Auto-accessor decorators: Return null (instance auto-accessor)': () => { + assertThrows(() => { + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + return null + } + class Foo { @dec accessor foo: undefined } + }, TypeError) + }, + 'Auto-accessor decorators: Return null (static auto-accessor)': () => { + assertThrows(() => { + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + return null + } + class Foo { @dec static accessor foo: undefined } + }, TypeError) + }, + 'Auto-accessor decorators: Return null (private instance auto-accessor)': () => { + assertThrows(() => { + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + return null + } + class Foo { @dec accessor #foo: undefined } + }, TypeError) + }, + 'Auto-accessor decorators: Return null (private static auto-accessor)': () => { + assertThrows(() => { + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + return null + } + class Foo { @dec static accessor #foo: undefined } + }, TypeError) + }, + 'Auto-accessor decorators: Extra initializer (instance auto-accessor)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec accessor foo: undefined } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Auto-accessor decorators: Extra initializer (static auto-accessor)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static accessor foo: undefined } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + 'Auto-accessor decorators: Extra initializer (private instance auto-accessor)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec accessor #foo: undefined } + assertEq(() => got, undefined) + const instance = new Foo + assertEq(() => got.this, instance) + assertEq(() => got.args.length, 0) + }, + 'Auto-accessor decorators: Extra initializer (private static auto-accessor)': () => { + let oldAddInitializer: DecoratorContext['addInitializer'] | null + let got: { this: any, args: any[] } + const dec = (target: ClassAccessorDecoratorTarget, ctx: ClassAccessorDecoratorContext): any => { + ctx.addInitializer(function (...args) { + got = { this: this, args } + }) + if (oldAddInitializer) assertThrows(() => oldAddInitializer!(() => { }), TypeError) + assertThrows(() => ctx.addInitializer({} as any), TypeError) + oldAddInitializer = ctx.addInitializer + } + class Foo { @dec @dec static accessor #foo: undefined } + assertEq(() => got.this, Foo) + assertEq(() => got.args.length, 0) + }, + + // Decorator list evaluation + 'Decorator list evaluation: Computed names (class statement)': () => { + const log: number[] = [] + const foo = (n: number): Function => { + log.push(n) + return () => { } + } + + const computed: { + readonly method: unique symbol, + readonly field: unique symbol, + readonly getter: unique symbol, + readonly setter: unique symbol, + readonly accessor: unique symbol, + } = { + get method() { log.push(log.length); return Symbol('method') }, + get field() { log.push(log.length); return Symbol('field') }, + get getter() { log.push(log.length); return Symbol('getter') }, + get setter() { log.push(log.length); return Symbol('setter') }, + get accessor() { log.push(log.length); return Symbol('accessor') }, + } as any + + @foo(0) class Foo + extends (foo(1), Object) + { + @foo(2) [computed.method]() { } + @foo(4) static [computed.method]() { } + + @foo(6) [computed.field]: undefined + @foo(8) static [computed.field]: undefined + + @foo(10) get [computed.getter](): undefined { return } + @foo(12) static get [computed.getter](): undefined { return } + + @foo(14) set [computed.setter](x: undefined) { } + @foo(16) static set [computed.setter](x: undefined) { } + + @foo(18) accessor [computed.accessor]: undefined + @foo(20) static accessor [computed.accessor]: undefined + } + + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21') + }, + 'Decorator list evaluation: Computed names (class expression)': () => { + const log: number[] = [] + const foo = (n: number): Function => { + log.push(n) + return () => { } + } + + const computed: { + readonly method: unique symbol, + readonly field: unique symbol, + readonly getter: unique symbol, + readonly setter: unique symbol, + readonly accessor: unique symbol, + } = { + get method() { log.push(log.length); return Symbol('method') }, + get field() { log.push(log.length); return Symbol('field') }, + get getter() { log.push(log.length); return Symbol('getter') }, + get setter() { log.push(log.length); return Symbol('setter') }, + get accessor() { log.push(log.length); return Symbol('accessor') }, + } as any + + (@foo(0) class + extends (foo(1), Object) + { + @foo(2) [computed.method]() { } + @foo(4) static [computed.method]() { } + + @foo(6) [computed.field]: undefined + @foo(8) static [computed.field]: undefined + + @foo(10) get [computed.getter](): undefined { return } + @foo(12) static get [computed.getter](): undefined { return } + + @foo(14) set [computed.setter](x: undefined) { } + @foo(16) static set [computed.setter](x: undefined) { } + + @foo(18) accessor [computed.accessor]: undefined + @foo(20) static accessor [computed.accessor]: undefined + }) + + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21') + }, + 'Decorator list evaluation: "this" (class statement)': () => { + const log: number[] = [] + const dummy: Function = () => { } + const ctx = { + foo(n: number) { + log.push(n) + } + } + + function wrapper(this: typeof ctx) { + @(assertEq(() => this.foo(0), undefined), dummy) class Foo + extends (assertEq(() => this.foo(1), undefined), Object) + { + @(assertEq(() => this.foo(2), undefined), dummy) method() { } + @(assertEq(() => this.foo(3), undefined), dummy) static method() { } + + @(assertEq(() => this.foo(4), undefined), dummy) field: undefined + @(assertEq(() => this.foo(5), undefined), dummy) static field: undefined + + @(assertEq(() => this.foo(6), undefined), dummy) get getter(): undefined { return } + @(assertEq(() => this.foo(7), undefined), dummy) static get getter(): undefined { return } + + @(assertEq(() => this.foo(8), undefined), dummy) set setter(x: undefined) { } + @(assertEq(() => this.foo(9), undefined), dummy) static set setter(x: undefined) { } + + @(assertEq(() => this.foo(10), undefined), dummy) accessor accessor: undefined + @(assertEq(() => this.foo(11), undefined), dummy) static accessor accessor: undefined + } + } + + wrapper.call(ctx) + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11') + }, + 'Decorator list evaluation: "this" (class expression)': () => { + const log: number[] = [] + const dummy: Function = () => { } + const ctx = { + foo(n: number) { + log.push(n) + } + } + + function wrapper(this: typeof ctx) { + (@(assertEq(() => this.foo(0), undefined), dummy) class + extends (assertEq(() => this.foo(1), undefined), Object) + { + @(assertEq(() => this.foo(2), undefined), dummy) method() { } + @(assertEq(() => this.foo(3), undefined), dummy) static method() { } + + @(assertEq(() => this.foo(4), undefined), dummy) field: undefined + @(assertEq(() => this.foo(5), undefined), dummy) static field: undefined + + @(assertEq(() => this.foo(6), undefined), dummy) get getter(): undefined { return } + @(assertEq(() => this.foo(7), undefined), dummy) static get getter(): undefined { return } + + @(assertEq(() => this.foo(8), undefined), dummy) set setter(x: undefined) { } + @(assertEq(() => this.foo(9), undefined), dummy) static set setter(x: undefined) { } + + @(assertEq(() => this.foo(10), undefined), dummy) accessor accessor: undefined + @(assertEq(() => this.foo(11), undefined), dummy) static accessor accessor: undefined + }) + } + + wrapper.call(ctx) + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11') + }, + 'Decorator list evaluation: "await" (class statement)': async () => { + const log: number[] = [] + const dummy: Function = () => { } + + async function wrapper() { + @(log.push(await Promise.resolve(0)), dummy) class Foo + extends (log.push(await Promise.resolve(1)), Object) + { + @(log.push(await Promise.resolve(2)), dummy) method() { } + @(log.push(await Promise.resolve(3)), dummy) static method() { } + + @(log.push(await Promise.resolve(4)), dummy) field: undefined + @(log.push(await Promise.resolve(5)), dummy) static field: undefined + + @(log.push(await Promise.resolve(6)), dummy) get getter(): undefined { return } + @(log.push(await Promise.resolve(7)), dummy) static get getter(): undefined { return } + + @(log.push(await Promise.resolve(8)), dummy) set setter(x: undefined) { } + @(log.push(await Promise.resolve(9)), dummy) static set setter(x: undefined) { } + + @(log.push(await Promise.resolve(10)), dummy) accessor accessor: undefined + @(log.push(await Promise.resolve(11)), dummy) static accessor accessor: undefined + } + } + + await wrapper() + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11') + }, + 'Decorator list evaluation: "await" (class expression)': async () => { + const log: number[] = [] + const dummy: Function = () => { } + + async function wrapper() { + (@(log.push(await Promise.resolve(0)), dummy) class + extends (log.push(await Promise.resolve(1)), Object) + { + @(log.push(await Promise.resolve(2)), dummy) method() { } + @(log.push(await Promise.resolve(3)), dummy) static method() { } + + @(log.push(await Promise.resolve(4)), dummy) field: undefined + @(log.push(await Promise.resolve(5)), dummy) static field: undefined + + @(log.push(await Promise.resolve(6)), dummy) get getter(): undefined { return } + @(log.push(await Promise.resolve(7)), dummy) static get getter(): undefined { return } + + @(log.push(await Promise.resolve(8)), dummy) set setter(x: undefined) { } + @(log.push(await Promise.resolve(9)), dummy) static set setter(x: undefined) { } + + @(log.push(await Promise.resolve(10)), dummy) accessor accessor: undefined + @(log.push(await Promise.resolve(11)), dummy) static accessor accessor: undefined + }) + } + + await wrapper() + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11') + }, + 'Decorator list evaluation: Outer private name (class statement)': () => { + const log: number[] = [] + + class Dummy { + static #foo(n: number): Function { + log.push(n) + return () => { } + } + + static { + const dummy = this + @(dummy.#foo(0)) class Foo + extends (dummy.#foo(1), Object) + { + @(dummy.#foo(2)) method() { } + @(dummy.#foo(3)) static method() { } + + @(dummy.#foo(4)) field: undefined + @(dummy.#foo(5)) static field: undefined + + @(dummy.#foo(6)) get getter(): undefined { return } + @(dummy.#foo(7)) static get getter(): undefined { return } + + @(dummy.#foo(8)) set setter(x: undefined) { } + @(dummy.#foo(9)) static set setter(x: undefined) { } + + @(dummy.#foo(10)) accessor accessor: undefined + @(dummy.#foo(11)) static accessor accessor: undefined + } + } + } + + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11') + }, + 'Decorator list evaluation: Outer private name (class expression)': () => { + const log: number[] = [] + + class Dummy { + static #foo(n: number): Function { + log.push(n) + return () => { } + } + + static { + const dummy = this; + (@(dummy.#foo(0)) class + extends (dummy.#foo(1), Object) + { + @(dummy.#foo(2)) method() { } + @(dummy.#foo(3)) static method() { } + + @(dummy.#foo(4)) field: undefined + @(dummy.#foo(5)) static field: undefined + + @(dummy.#foo(6)) get getter(): undefined { return } + @(dummy.#foo(7)) static get getter(): undefined { return } + + @(dummy.#foo(8)) set setter(x: undefined) { } + @(dummy.#foo(9)) static set setter(x: undefined) { } + + @(dummy.#foo(10)) accessor accessor: undefined + @(dummy.#foo(11)) static accessor accessor: undefined + }) + } + } + + assertEq(() => '' + log, '0,1,2,3,4,5,6,7,8,9,10,11') + }, + 'Decorator list evaluation: Inner private name (class statement)': () => { + const fns: (() => number)[] = [] + const capture = (fn: () => number): Function => { + fns.push(fn) + return () => { } + } + + class Dummy { + static #foo = NaN + + static { + @(capture(() => (new Foo() as any).#foo + 0)) + class Foo { + #foo = 10 + + @(capture(() => new Foo().#foo + 1)) method() { } + @(capture(() => new Foo().#foo + 2)) static method() { } + + @(capture(() => new Foo().#foo + 3)) field: undefined + @(capture(() => new Foo().#foo + 4)) static field: undefined + + @(capture(() => new Foo().#foo + 5)) get getter(): undefined { return } + @(capture(() => new Foo().#foo + 6)) static get getter(): undefined { return } + + @(capture(() => new Foo().#foo + 7)) set setter(x: undefined) { } + @(capture(() => new Foo().#foo + 8)) static set setter(x: undefined) { } + + @(capture(() => new Foo().#foo + 9)) accessor accessor: undefined + @(capture(() => new Foo().#foo + 10)) static accessor accessor: undefined + } + } + } + + // Accessing "#foo" in the class decorator should fail. The "#foo" should + // refer to the outer "#foo", not the inner "#foo". + const firstFn = fns.shift()! + assertEq(() => { + try { + firstFn() + throw new Error('Expected a TypeError to be thrown') + } catch (err) { + if (err instanceof TypeError) return true + throw err + } + }, true) + + // Accessing "#foo" from any of the class element decorators should succeed. + // Each "#foo" should refer to the inner "#foo", not the outer "#foo". + const log: number[] = [] + for (const fn of fns) log.push(fn()) + assertEq(() => '' + log, '11,12,13,14,15,16,17,18,19,20') + }, + 'Decorator list evaluation: Inner private name (class expression)': () => { + const fns: (() => number)[] = [] + const capture = (fn: () => number): Function => { + fns.push(fn) + return () => { } + } + + class Outer { + static #foo = 0 + + static { + (@(capture(() => Outer.#foo + 0)) + class Foo { + #foo = 10 + + @(capture(() => new Foo().#foo + 1)) method() { } + @(capture(() => new Foo().#foo + 2)) static method() { } + + @(capture(() => new Foo().#foo + 3)) field: undefined + @(capture(() => new Foo().#foo + 4)) static field: undefined + + @(capture(() => new Foo().#foo + 5)) get getter(): undefined { return } + @(capture(() => new Foo().#foo + 6)) static get getter(): undefined { return } + + @(capture(() => new Foo().#foo + 7)) set setter(x: undefined) { } + @(capture(() => new Foo().#foo + 8)) static set setter(x: undefined) { } + + @(capture(() => new Foo().#foo + 9)) accessor accessor: undefined + @(capture(() => new Foo().#foo + 10)) static accessor accessor: undefined + }) + } + } + + // Accessing the outer "#foo" on "Outer" from within the class decorator + // should succeed. Class decorators are evaluated in the outer private + // environment before entering "ClassDefinitionEvaluation". + // + // Accessing the inner "#foo" on "Foo" from within any of the class element + // decorators should also succeed. Class element decorators are evaluated + // in the inner private environment inside "ClassDefinitionEvaluation". + const log: number[] = [] + for (const fn of fns) log.push(fn()) + assertEq(() => '' + log, '0,11,12,13,14,15,16,17,18,19,20') + }, + 'Decorator list evaluation: Class binding (class statement)': () => { + const fns: (() => typeof Foo)[] = [] + + const capture = (fn: () => typeof Foo): Function => { + fns.push(fn) + + // Note: As far as I can tell, early reference to the class name should + // throw a reference error because: + // + // 1. Class decorators run first in the top-level scope before entering + // BindingClassDeclarationEvaluation. + // + // 2. Class element decorators run in ClassDefinitionEvaluation, which + // runs ClassElementEvaluation for each class element before eventually + // running classEnv.InitializeBinding(classBinding, F). + // + assertThrows(() => fn(), ReferenceError) + return () => { } + } + + @(capture(() => Foo)) class Foo { + @(capture(() => Foo)) method() { } + @(capture(() => Foo)) static method() { } + + @(capture(() => Foo)) field: undefined + @(capture(() => Foo)) static field: undefined + + @(capture(() => Foo)) get getter(): undefined { return } + @(capture(() => Foo)) static get getter(): undefined { return } + + @(capture(() => Foo)) set setter(x: undefined) { } + @(capture(() => Foo)) static set setter(x: undefined) { } + + @(capture(() => Foo)) accessor accessor: undefined + @(capture(() => Foo)) static accessor accessor: undefined + } + + const originalFoo = Foo + + // Once we get here, these should all reference the now-initialized class, + // either through classBinding (for class element decorators) or through + // className (for decorators on the class itself). + for (const fn of fns) { + assertEq(() => fn(), originalFoo) + } + + // Mutating a class binding is allowed in JavaScript. Let's test what + // happens when we do this. + (Foo as any) = null as any + + // As far as I can tell, class decorators should observe this change because + // they are evaluated in the top-level scope. + const firstFn = fns.shift()! + assertEq(() => firstFn(), null) + + // But I believe class element decorators should not observe this change + // because they are evaluated in the environment that exists for the + // duration of ClassDefinitionEvaluation (i.e. classEnv, not env). + for (const fn of fns) { + assertEq(() => fn(), originalFoo) + } + }, + 'Decorator list evaluation: Class binding (class expression)': () => { + const fns: (() => { new(): Object })[] = [] + + const capture = (fn: () => { new(): Object }): Function => { + fns.push(fn) + return () => { } + } + + const originalFoo = (@(capture(() => Foo)) class Foo { + @(capture(() => Foo)) method() { } + @(capture(() => Foo)) static method() { } + + @(capture(() => Foo)) field: undefined + @(capture(() => Foo)) static field: undefined + + @(capture(() => Foo)) get getter(): undefined { return } + @(capture(() => Foo)) static get getter(): undefined { return } + + @(capture(() => Foo)) set setter(x: undefined) { } + @(capture(() => Foo)) static set setter(x: undefined) { } + + @(capture(() => Foo)) accessor accessor: undefined + @(capture(() => Foo)) static accessor accessor: undefined + }) + + // Decorators on the class itself should reference a global called "Foo", + // which should still be a reference error. This is because a class + // expression runs "DecoratorListEvaluation" in the outer environment and + // then passes the evaluated decorators to "ClassDefinitionEvaluation". + const firstFn = fns.shift()! + assertThrows(() => firstFn(), ReferenceError) + + // All other decorators should reference the classBinding called "Foo", + // which should now be initialized. This is because all other decorators + // are evaluated within "ClassDefinitionEvaluation" while the running + // execution context's environment is the nested class environment. + for (const fn of fns) { + assertEq(() => fn(), originalFoo) + } + }, + + // Decorator metadata + 'Decorator metadata: class statement': () => { + let counter = 0 + const dec = (_: any, ctx: DecoratorContext) => { + ctx.metadata[ctx.name!] = counter++ + } + @dec class Foo { + @dec instanceField: undefined + @dec accessor instanceAccessor: undefined + @dec instanceMethod() { } + @dec get instanceGetter() { return } + @dec set instanceSetter(_: undefined) { } + + @dec static staticField: undefined + @dec static accessor staticAccessor: undefined + @dec static staticMethod() { } + @dec static get staticGetter() { return } + @dec static set staticSetter(_: undefined) { } + } + @dec class Bar extends Foo { + @dec #instanceField: undefined + @dec accessor #instanceAccessor: undefined + @dec #instanceMethod() { } + @dec get #instanceGetter() { return } + @dec set #instanceSetter(_: undefined) { } + + @dec static #staticField: undefined + @dec static accessor #staticAccessor: undefined + @dec static #staticMethod() { } + @dec static get #staticGetter() { return } + @dec static set #staticSetter(_: undefined) { } + } + const order = (meta: DecoratorMetadataObject) => '' + [ + meta['staticAccessor'], meta['staticMethod'], meta['staticGetter'], meta['staticSetter'], + meta['#staticAccessor'], meta['#staticMethod'], meta['#staticGetter'], meta['#staticSetter'], + meta['instanceAccessor'], meta['instanceMethod'], meta['instanceGetter'], meta['instanceSetter'], + meta['#instanceAccessor'], meta['#instanceMethod'], meta['#instanceGetter'], meta['#instanceSetter'], + meta['staticField'], meta['#staticField'], + meta['instanceField'], meta['#instanceField'], + meta['Foo'], meta['Bar'], + ] + const foo = Foo[Symbol.metadata]! + const bar = Bar[Symbol.metadata]! + assertEq(() => order(foo), '0,1,2,3,,,,,4,5,6,7,,,,,8,,9,,10,') + assertEq(() => Object.getPrototypeOf(foo), null) + assertEq(() => order(bar), '0,1,2,3,11,12,13,14,4,5,6,7,15,16,17,18,8,19,9,20,10,21') + assertEq(() => Object.getPrototypeOf(bar), foo) + + // Test an undecorated class + class FooNoDec { } + class BarNoDec extends FooNoDec { } + assertEq(() => FooNoDec[Symbol.metadata], null) + assertEq(() => BarNoDec[Symbol.metadata], null) + + // Test a class with no class decorator + class FooOneDec { @dec x: undefined } + class BarOneDec extends FooOneDec { @dec y: undefined } + assertEq(() => JSON.stringify(FooOneDec[Symbol.metadata]!), JSON.stringify({ x: 22 })) + assertEq(() => JSON.stringify(BarOneDec[Symbol.metadata]!), JSON.stringify({ y: 23 })) + assertEq(() => Object.getPrototypeOf(BarOneDec[Symbol.metadata]!), FooOneDec[Symbol.metadata]!) + }, + 'Decorator metadata: class expression': () => { + let counter = 0 + const dec = (_: any, ctx: DecoratorContext) => { + ctx.metadata[ctx.name!] = counter++ + } + const Foo = @dec class { + @dec instanceField: undefined + @dec accessor instanceAccessor: undefined + @dec instanceMethod() { } + @dec get instanceGetter() { return } + @dec set instanceSetter(_: undefined) { } + + @dec static staticField: undefined + @dec static accessor staticAccessor: undefined + @dec static staticMethod() { } + @dec static get staticGetter() { return } + @dec static set staticSetter(_: undefined) { } + }, Bar = @dec class extends Foo { + @dec #instanceField: undefined + @dec accessor #instanceAccessor: undefined + @dec #instanceMethod() { } + @dec get #instanceGetter() { return } + @dec set #instanceSetter(_: undefined) { } + + @dec static #staticField: undefined + @dec static accessor #staticAccessor: undefined + @dec static #staticMethod() { } + @dec static get #staticGetter() { return } + @dec static set #staticSetter(_: undefined) { } + } + const order = (meta: DecoratorMetadataObject) => '' + [ + meta['staticAccessor'], meta['staticMethod'], meta['staticGetter'], meta['staticSetter'], + meta['#staticAccessor'], meta['#staticMethod'], meta['#staticGetter'], meta['#staticSetter'], + meta['instanceAccessor'], meta['instanceMethod'], meta['instanceGetter'], meta['instanceSetter'], + meta['#instanceAccessor'], meta['#instanceMethod'], meta['#instanceGetter'], meta['#instanceSetter'], + meta['staticField'], meta['#staticField'], + meta['instanceField'], meta['#instanceField'], + meta['Foo'], meta['Bar'], + ] + const foo = Foo[Symbol.metadata]! + const bar = Bar[Symbol.metadata]! + assertEq(() => order(foo), '0,1,2,3,,,,,4,5,6,7,,,,,8,,9,,10,') + assertEq(() => Object.getPrototypeOf(foo), null) + assertEq(() => order(bar), '0,1,2,3,11,12,13,14,4,5,6,7,15,16,17,18,8,19,9,20,10,21') + assertEq(() => Object.getPrototypeOf(bar), foo) + + // Test an undecorated class + const FooNoDec = class { } + const BarNoDec = class extends FooNoDec { } + assertEq(() => FooNoDec[Symbol.metadata], null) + assertEq(() => BarNoDec[Symbol.metadata], null) + + // Test a class with no class decorator + const FooOneDec = class { @dec x: undefined } + const BarOneDec = class extends FooOneDec { @dec y: undefined } + assertEq(() => JSON.stringify(FooOneDec[Symbol.metadata]!), JSON.stringify({ x: 22 })) + assertEq(() => JSON.stringify(BarOneDec[Symbol.metadata]!), JSON.stringify({ y: 23 })) + assertEq(() => Object.getPrototypeOf(BarOneDec[Symbol.metadata]!), FooOneDec[Symbol.metadata]!) + }, + + // Initializer order + 'Initializer order (public members, class statement)': () => { + const log: string[] = [] + + // Class decorators + const classDec1 = (cls: { new(): Foo }, ctxClass: ClassDecoratorContext) => { + log.push('c2') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c5')) + ctxClass.addInitializer(() => log.push('c6')) + } + const classDec2 = (cls: { new(): Foo }, ctxClass: ClassDecoratorContext) => { + log.push('c1') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c3')) + ctxClass.addInitializer(() => log.push('c4')) + } + + // Method decorators + const methodDec1 = (fn: (this: Foo) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m2') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m5')) + ctxMethod.addInitializer(() => log.push('m6')) + } + const methodDec2 = (fn: (this: Foo) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m1') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m3')) + ctxMethod.addInitializer(() => log.push('m4')) + } + const staticMethodDec1 = (fn: (this: Foo) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M2') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M5')) + ctxStaticMethod.addInitializer(() => log.push('M6')) + } + const staticMethodDec2 = (fn: (this: Foo) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M1') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M3')) + ctxStaticMethod.addInitializer(() => log.push('M4')) + } + + // Field decorators + const fieldDec1 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Foo, value: undefined) => undefined) | undefined => { + log.push('f2') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f5')) + ctxField.addInitializer(() => log.push('f6')) + return () => { log.push('f7') } + } + const fieldDec2 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Foo, value: undefined) => undefined) | undefined => { + log.push('f1') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f3')) + ctxField.addInitializer(() => log.push('f4')) + return () => { log.push('f8') } + } + const staticFieldDec1 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: typeof Foo, value: undefined) => undefined) | undefined => { + log.push('F2') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F5')) + ctxStaticField.addInitializer(() => log.push('F6')) + return () => { log.push('F7') } + } + const staticFieldDec2 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: typeof Foo, value: undefined) => undefined) | undefined => { + log.push('F1') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F3')) + ctxStaticField.addInitializer(() => log.push('F4')) + return () => { log.push('F8') } + } + + // Getter decorators + const getterDec1 = (fn: (this: Foo) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g2') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g5')) + ctxGetter.addInitializer(() => log.push('g6')) + } + const getterDec2 = (fn: (this: Foo) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g1') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g3')) + ctxGetter.addInitializer(() => log.push('g4')) + } + const staticGetterDec1 = (fn: (this: Foo) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G2') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G5')) + ctxStaticGetter.addInitializer(() => log.push('G6')) + } + const staticGetterDec2 = (fn: (this: Foo) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G1') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G3')) + ctxStaticGetter.addInitializer(() => log.push('G4')) + } + + // Setter decorators + const setterDec1 = (fn: (this: Foo, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s2') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s5')) + ctxSetter.addInitializer(() => log.push('s6')) + } + const setterDec2 = (fn: (this: Foo, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s1') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s3')) + ctxSetter.addInitializer(() => log.push('s4')) + } + const staticSetterDec1 = (fn: (this: Foo, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S2') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S5')) + ctxStaticSetter.addInitializer(() => log.push('S6')) + } + const staticSetterDec2 = (fn: (this: Foo, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S1') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S3')) + ctxStaticSetter.addInitializer(() => log.push('S4')) + } + + // Auto-accessor decorators + const accessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a2') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a5')) + ctxAccessor.addInitializer(() => log.push('a6')) + return { init() { log.push('a7') } } + } + const accessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a1') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a3')) + ctxAccessor.addInitializer(() => log.push('a4')) + return { init() { log.push('a8') } } + } + const staticAccessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A2') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A5')) + ctxStaticAccessor.addInitializer(() => log.push('A6')) + return { init() { log.push('A7') } } + } + const staticAccessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A1') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A3')) + ctxStaticAccessor.addInitializer(() => log.push('A4')) + return { init() { log.push('A8') } } + } + + log.push('start') + @classDec1 @classDec2 class Foo extends (log.push('extends'), Object) { + static { log.push('static:start') } + + constructor() { + log.push('ctor:start') + super() + log.push('ctor:end') + } + + @methodDec1 @methodDec2 method() { } + @staticMethodDec1 @staticMethodDec2 static method() { } + + @fieldDec1 @fieldDec2 field: undefined + @staticFieldDec1 @staticFieldDec2 static field: undefined + + @getterDec1 @getterDec2 get getter(): undefined { return } + @staticGetterDec1 @staticGetterDec2 static get getter(): undefined { return } + + @setterDec1 @setterDec2 set setter(x: undefined) { } + @staticSetterDec1 @staticSetterDec2 static set setter(x: undefined) { } + + @accessorDec1 @accessorDec2 accessor accessor: undefined + @staticAccessorDec1 @staticAccessorDec2 static accessor accessor: undefined + + static { log.push('static:end') } + } + log.push('after') + new Foo + log.push('end') + assertEq(() => log + '', 'start,extends,' + + 'M1,M2,G1,G2,S1,S2,A1,A2,' + // For each element e of staticElements if e.[[Kind]] is not field + 'm1,m2,g1,g2,s1,s2,a1,a2,' + // For each element e of instanceElements if e.[[Kind]] is not field + 'F1,F2,' + // For each element e of staticElements if e.[[Kind]] is field + 'f1,f2,' + // For each element e of instanceElements if e.[[Kind]] is field + 'c1,c2,' + // ApplyDecoratorsToClassDefinition + 'M3,M4,M5,M6,G3,G4,G5,G6,S3,S4,S5,S6,' + // For each element initializer of staticMethodExtraInitializers + 'static:start,' + // For each element elementRecord of staticElements + 'F7,F8,F3,F4,F5,F6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'A7,A8,A3,A4,A5,A6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'static:end,' + // For each element elementRecord of staticElements + 'c3,c4,c5,c6,' + // For each element initializer of classExtraInitializers + 'after,' + + 'ctor:start,' + + 'm3,m4,m5,m6,g3,g4,g5,g6,s3,s4,s5,s6,' + // For each element initializer of constructor.[[Initializers]] (a.k.a. instanceMethodExtraInitializers) + 'f7,f8,f3,f4,f5,f6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'a7,a8,a3,a4,a5,a6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'ctor:end,' + + 'end') + }, + 'Initializer order (public members, class expression)': () => { + const log: string[] = [] + + // Class decorators + const classDec1 = (cls: { new(): Object }, ctxClass: ClassDecoratorContext) => { + log.push('c2') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c5')) + ctxClass.addInitializer(() => log.push('c6')) + } + const classDec2 = (cls: { new(): Object }, ctxClass: ClassDecoratorContext) => { + log.push('c1') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c3')) + ctxClass.addInitializer(() => log.push('c4')) + } + + // Method decorators + const methodDec1 = (fn: (this: Object) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m2') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m5')) + ctxMethod.addInitializer(() => log.push('m6')) + } + const methodDec2 = (fn: (this: Object) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m1') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m3')) + ctxMethod.addInitializer(() => log.push('m4')) + } + const staticMethodDec1 = (fn: (this: Object) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M2') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M5')) + ctxStaticMethod.addInitializer(() => log.push('M6')) + } + const staticMethodDec2 = (fn: (this: Object) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M1') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M3')) + ctxStaticMethod.addInitializer(() => log.push('M4')) + } + + // Field decorators + const fieldDec1 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('f2') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f5')) + ctxField.addInitializer(() => log.push('f6')) + return () => { log.push('f7') } + } + const fieldDec2 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('f1') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f3')) + ctxField.addInitializer(() => log.push('f4')) + return () => { log.push('f8') } + } + const staticFieldDec1 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('F2') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F5')) + ctxStaticField.addInitializer(() => log.push('F6')) + return () => { log.push('F7') } + } + const staticFieldDec2 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('F1') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F3')) + ctxStaticField.addInitializer(() => log.push('F4')) + return () => { log.push('F8') } + } + + // Getter decorators + const getterDec1 = (fn: (this: Object) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g2') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g5')) + ctxGetter.addInitializer(() => log.push('g6')) + } + const getterDec2 = (fn: (this: Object) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g1') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g3')) + ctxGetter.addInitializer(() => log.push('g4')) + } + const staticGetterDec1 = (fn: (this: Object) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G2') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G5')) + ctxStaticGetter.addInitializer(() => log.push('G6')) + } + const staticGetterDec2 = (fn: (this: Object) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G1') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G3')) + ctxStaticGetter.addInitializer(() => log.push('G4')) + } + + // Setter decorators + const setterDec1 = (fn: (this: Object, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s2') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s5')) + ctxSetter.addInitializer(() => log.push('s6')) + } + const setterDec2 = (fn: (this: Object, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s1') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s3')) + ctxSetter.addInitializer(() => log.push('s4')) + } + const staticSetterDec1 = (fn: (this: Object, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S2') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S5')) + ctxStaticSetter.addInitializer(() => log.push('S6')) + } + const staticSetterDec2 = (fn: (this: Object, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S1') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S3')) + ctxStaticSetter.addInitializer(() => log.push('S4')) + } + + // Auto-accessor decorators + const accessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a2') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a5')) + ctxAccessor.addInitializer(() => log.push('a6')) + return { init() { log.push('a7') } } + } + const accessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a1') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a3')) + ctxAccessor.addInitializer(() => log.push('a4')) + return { init() { log.push('a8') } } + } + const staticAccessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A2') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A5')) + ctxStaticAccessor.addInitializer(() => log.push('A6')) + return { init() { log.push('A7') } } + } + const staticAccessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A1') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A3')) + ctxStaticAccessor.addInitializer(() => log.push('A4')) + return { init() { log.push('A8') } } + } + + log.push('start') + const Foo = @classDec1 @classDec2 class extends (log.push('extends'), Object) { + static { log.push('static:start') } + + constructor() { + log.push('ctor:start') + super() + log.push('ctor:end') + } + + @methodDec1 @methodDec2 method() { } + @staticMethodDec1 @staticMethodDec2 static method() { } + + @fieldDec1 @fieldDec2 field: undefined + @staticFieldDec1 @staticFieldDec2 static field: undefined + + @getterDec1 @getterDec2 get getter(): undefined { return } + @staticGetterDec1 @staticGetterDec2 static get getter(): undefined { return } + + @setterDec1 @setterDec2 set setter(x: undefined) { } + @staticSetterDec1 @staticSetterDec2 static set setter(x: undefined) { } + + @accessorDec1 @accessorDec2 accessor accessor: undefined + @staticAccessorDec1 @staticAccessorDec2 static accessor accessor: undefined + + static { log.push('static:end') } + } + log.push('after') + new Foo + log.push('end') + assertEq(() => log + '', 'start,extends,' + + 'M1,M2,G1,G2,S1,S2,A1,A2,' + // For each element e of staticElements if e.[[Kind]] is not field + 'm1,m2,g1,g2,s1,s2,a1,a2,' + // For each element e of instanceElements if e.[[Kind]] is not field + 'F1,F2,' + // For each element e of staticElements if e.[[Kind]] is field + 'f1,f2,' + // For each element e of instanceElements if e.[[Kind]] is field + 'c1,c2,' + // ApplyDecoratorsToClassDefinition + 'M3,M4,M5,M6,G3,G4,G5,G6,S3,S4,S5,S6,' + // For each element initializer of staticMethodExtraInitializers + 'static:start,' + // For each element elementRecord of staticElements + 'F7,F8,F3,F4,F5,F6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'A7,A8,A3,A4,A5,A6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'static:end,' + // For each element elementRecord of staticElements + 'c3,c4,c5,c6,' + // For each element initializer of classExtraInitializers + 'after,' + + 'ctor:start,' + + 'm3,m4,m5,m6,g3,g4,g5,g6,s3,s4,s5,s6,' + // For each element initializer of constructor.[[Initializers]] (a.k.a. instanceMethodExtraInitializers) + 'f7,f8,f3,f4,f5,f6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'a7,a8,a3,a4,a5,a6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'ctor:end,' + + 'end') + }, + 'Initializer order (private members, class statement)': () => { + const log: string[] = [] + + // Class decorators + const classDec1 = (cls: { new(): Foo }, ctxClass: ClassDecoratorContext) => { + log.push('c2') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c5')) + ctxClass.addInitializer(() => log.push('c6')) + } + const classDec2 = (cls: { new(): Foo }, ctxClass: ClassDecoratorContext) => { + log.push('c1') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c3')) + ctxClass.addInitializer(() => log.push('c4')) + } + + // Method decorators + const methodDec1 = (fn: (this: Foo) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m2') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m5')) + ctxMethod.addInitializer(() => log.push('m6')) + } + const methodDec2 = (fn: (this: Foo) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m1') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m3')) + ctxMethod.addInitializer(() => log.push('m4')) + } + const staticMethodDec1 = (fn: (this: Foo) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M2') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M5')) + ctxStaticMethod.addInitializer(() => log.push('M6')) + } + const staticMethodDec2 = (fn: (this: Foo) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M1') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M3')) + ctxStaticMethod.addInitializer(() => log.push('M4')) + } + + // Field decorators + const fieldDec1 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Foo, value: undefined) => undefined) | undefined => { + log.push('f2') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f5')) + ctxField.addInitializer(() => log.push('f6')) + return () => { log.push('f7') } + } + const fieldDec2 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Foo, value: undefined) => undefined) | undefined => { + log.push('f1') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f3')) + ctxField.addInitializer(() => log.push('f4')) + return () => { log.push('f8') } + } + const staticFieldDec1 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: typeof Foo, value: undefined) => undefined) | undefined => { + log.push('F2') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F5')) + ctxStaticField.addInitializer(() => log.push('F6')) + return () => { log.push('F7') } + } + const staticFieldDec2 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: typeof Foo, value: undefined) => undefined) | undefined => { + log.push('F1') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F3')) + ctxStaticField.addInitializer(() => log.push('F4')) + return () => { log.push('F8') } + } + + // Getter decorators + const getterDec1 = (fn: (this: Foo) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g2') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g5')) + ctxGetter.addInitializer(() => log.push('g6')) + } + const getterDec2 = (fn: (this: Foo) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g1') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g3')) + ctxGetter.addInitializer(() => log.push('g4')) + } + const staticGetterDec1 = (fn: (this: Foo) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G2') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G5')) + ctxStaticGetter.addInitializer(() => log.push('G6')) + } + const staticGetterDec2 = (fn: (this: Foo) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G1') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G3')) + ctxStaticGetter.addInitializer(() => log.push('G4')) + } + + // Setter decorators + const setterDec1 = (fn: (this: Foo, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s2') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s5')) + ctxSetter.addInitializer(() => log.push('s6')) + } + const setterDec2 = (fn: (this: Foo, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s1') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s3')) + ctxSetter.addInitializer(() => log.push('s4')) + } + const staticSetterDec1 = (fn: (this: Foo, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S2') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S5')) + ctxStaticSetter.addInitializer(() => log.push('S6')) + } + const staticSetterDec2 = (fn: (this: Foo, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S1') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S3')) + ctxStaticSetter.addInitializer(() => log.push('S4')) + } + + // Auto-accessor decorators + const accessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a2') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a5')) + ctxAccessor.addInitializer(() => log.push('a6')) + return { init() { log.push('a7') } } + } + const accessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a1') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a3')) + ctxAccessor.addInitializer(() => log.push('a4')) + return { init() { log.push('a8') } } + } + const staticAccessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A2') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A5')) + ctxStaticAccessor.addInitializer(() => log.push('A6')) + return { init() { log.push('A7') } } + } + const staticAccessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A1') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A3')) + ctxStaticAccessor.addInitializer(() => log.push('A4')) + return { init() { log.push('A8') } } + } + + log.push('start') + @classDec1 @classDec2 class Foo extends (log.push('extends'), Object) { + static { log.push('static:start') } + + constructor() { + log.push('ctor:start') + super() + log.push('ctor:end') + } + + @methodDec1 @methodDec2 #method() { } + @staticMethodDec1 @staticMethodDec2 static #staticMethod() { } + + @fieldDec1 @fieldDec2 #field: undefined + @staticFieldDec1 @staticFieldDec2 static #staticField: undefined + + @getterDec1 @getterDec2 get #getter(): undefined { return } + @staticGetterDec1 @staticGetterDec2 static get #staticGetter(): undefined { return } + + @setterDec1 @setterDec2 set #setter(x: undefined) { } + @staticSetterDec1 @staticSetterDec2 static set #staticSetter(x: undefined) { } + + @accessorDec1 @accessorDec2 accessor #accessor: undefined + @staticAccessorDec1 @staticAccessorDec2 static accessor #staticAccessor: undefined + + static { log.push('static:end') } + } + log.push('after') + new Foo + log.push('end') + assertEq(() => log + '', 'start,extends,' + + 'M1,M2,G1,G2,S1,S2,A1,A2,' + // For each element e of staticElements if e.[[Kind]] is not field + 'm1,m2,g1,g2,s1,s2,a1,a2,' + // For each element e of instanceElements if e.[[Kind]] is not field + 'F1,F2,' + // For each element e of staticElements if e.[[Kind]] is field + 'f1,f2,' + // For each element e of instanceElements if e.[[Kind]] is field + 'c1,c2,' + // ApplyDecoratorsToClassDefinition + 'M3,M4,M5,M6,G3,G4,G5,G6,S3,S4,S5,S6,' + // For each element initializer of staticMethodExtraInitializers + 'static:start,' + // For each element elementRecord of staticElements + 'F7,F8,F3,F4,F5,F6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'A7,A8,A3,A4,A5,A6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'static:end,' + // For each element elementRecord of staticElements + 'c3,c4,c5,c6,' + // For each element initializer of classExtraInitializers + 'after,' + + 'ctor:start,' + + 'm3,m4,m5,m6,g3,g4,g5,g6,s3,s4,s5,s6,' + // For each element initializer of constructor.[[Initializers]] (a.k.a. instanceMethodExtraInitializers) + 'f7,f8,f3,f4,f5,f6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'a7,a8,a3,a4,a5,a6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'ctor:end,' + + 'end') + }, + 'Initializer order (private members, class expression)': () => { + const log: string[] = [] + + // Class decorators + const classDec1 = (cls: { new(): Object }, ctxClass: ClassDecoratorContext) => { + log.push('c2') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c5')) + ctxClass.addInitializer(() => log.push('c6')) + } + const classDec2 = (cls: { new(): Object }, ctxClass: ClassDecoratorContext) => { + log.push('c1') + if (!assertEq(() => typeof ctxClass.addInitializer, 'function')) return + ctxClass.addInitializer(() => log.push('c3')) + ctxClass.addInitializer(() => log.push('c4')) + } + + // Method decorators + const methodDec1 = (fn: (this: Object) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m2') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m5')) + ctxMethod.addInitializer(() => log.push('m6')) + } + const methodDec2 = (fn: (this: Object) => void, ctxMethod: ClassMethodDecoratorContext) => { + log.push('m1') + if (!assertEq(() => typeof ctxMethod.addInitializer, 'function')) return + ctxMethod.addInitializer(() => log.push('m3')) + ctxMethod.addInitializer(() => log.push('m4')) + } + const staticMethodDec1 = (fn: (this: Object) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M2') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M5')) + ctxStaticMethod.addInitializer(() => log.push('M6')) + } + const staticMethodDec2 = (fn: (this: Object) => void, ctxStaticMethod: ClassMethodDecoratorContext) => { + log.push('M1') + if (!assertEq(() => typeof ctxStaticMethod.addInitializer, 'function')) return + ctxStaticMethod.addInitializer(() => log.push('M3')) + ctxStaticMethod.addInitializer(() => log.push('M4')) + } + + // Field decorators + const fieldDec1 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('f2') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f5')) + ctxField.addInitializer(() => log.push('f6')) + return () => { log.push('f7') } + } + const fieldDec2 = ( + value: undefined, + ctxField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('f1') + if (!assertEq(() => typeof ctxField.addInitializer, 'function')) return + ctxField.addInitializer(() => log.push('f3')) + ctxField.addInitializer(() => log.push('f4')) + return () => { log.push('f8') } + } + const staticFieldDec1 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('F2') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F5')) + ctxStaticField.addInitializer(() => log.push('F6')) + return () => { log.push('F7') } + } + const staticFieldDec2 = ( + value: undefined, + ctxStaticField: ClassFieldDecoratorContext, + ): ((this: Object, value: undefined) => undefined) | undefined => { + log.push('F1') + if (!assertEq(() => typeof ctxStaticField.addInitializer, 'function')) return + ctxStaticField.addInitializer(() => log.push('F3')) + ctxStaticField.addInitializer(() => log.push('F4')) + return () => { log.push('F8') } + } + + // Getter decorators + const getterDec1 = (fn: (this: Object) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g2') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g5')) + ctxGetter.addInitializer(() => log.push('g6')) + } + const getterDec2 = (fn: (this: Object) => undefined, ctxGetter: ClassGetterDecoratorContext) => { + log.push('g1') + if (!assertEq(() => typeof ctxGetter.addInitializer, 'function')) return + ctxGetter.addInitializer(() => log.push('g3')) + ctxGetter.addInitializer(() => log.push('g4')) + } + const staticGetterDec1 = (fn: (this: Object) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G2') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G5')) + ctxStaticGetter.addInitializer(() => log.push('G6')) + } + const staticGetterDec2 = (fn: (this: Object) => undefined, ctxStaticGetter: ClassGetterDecoratorContext) => { + log.push('G1') + if (!assertEq(() => typeof ctxStaticGetter.addInitializer, 'function')) return + ctxStaticGetter.addInitializer(() => log.push('G3')) + ctxStaticGetter.addInitializer(() => log.push('G4')) + } + + // Setter decorators + const setterDec1 = (fn: (this: Object, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s2') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s5')) + ctxSetter.addInitializer(() => log.push('s6')) + } + const setterDec2 = (fn: (this: Object, x: undefined) => void, ctxSetter: ClassSetterDecoratorContext) => { + log.push('s1') + if (!assertEq(() => typeof ctxSetter.addInitializer, 'function')) return + ctxSetter.addInitializer(() => log.push('s3')) + ctxSetter.addInitializer(() => log.push('s4')) + } + const staticSetterDec1 = (fn: (this: Object, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S2') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S5')) + ctxStaticSetter.addInitializer(() => log.push('S6')) + } + const staticSetterDec2 = (fn: (this: Object, x: undefined) => void, ctxStaticSetter: ClassSetterDecoratorContext) => { + log.push('S1') + if (!assertEq(() => typeof ctxStaticSetter.addInitializer, 'function')) return + ctxStaticSetter.addInitializer(() => log.push('S3')) + ctxStaticSetter.addInitializer(() => log.push('S4')) + } + + // Auto-accessor decorators + const accessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a2') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a5')) + ctxAccessor.addInitializer(() => log.push('a6')) + return { init() { log.push('a7') } } + } + const accessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('a1') + if (!assertEq(() => typeof ctxAccessor.addInitializer, 'function')) return + ctxAccessor.addInitializer(() => log.push('a3')) + ctxAccessor.addInitializer(() => log.push('a4')) + return { init() { log.push('a8') } } + } + const staticAccessorDec1 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A2') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A5')) + ctxStaticAccessor.addInitializer(() => log.push('A6')) + return { init() { log.push('A7') } } + } + const staticAccessorDec2 = ( + target: ClassAccessorDecoratorTarget, + ctxStaticAccessor: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | undefined => { + log.push('A1') + if (!assertEq(() => typeof ctxStaticAccessor.addInitializer, 'function')) return + ctxStaticAccessor.addInitializer(() => log.push('A3')) + ctxStaticAccessor.addInitializer(() => log.push('A4')) + return { init() { log.push('A8') } } + } + + log.push('start') + const Foo = @classDec1 @classDec2 class extends (log.push('extends'), Object) { + static { log.push('static:start') } + + constructor() { + log.push('ctor:start') + super() + log.push('ctor:end') + } + + @methodDec1 @methodDec2 #method() { } + @staticMethodDec1 @staticMethodDec2 static #staticMethod() { } + + @fieldDec1 @fieldDec2 #field: undefined + @staticFieldDec1 @staticFieldDec2 static #staticField: undefined + + @getterDec1 @getterDec2 get #getter(): undefined { return } + @staticGetterDec1 @staticGetterDec2 static get #staticGetter(): undefined { return } + + @setterDec1 @setterDec2 set #setter(x: undefined) { } + @staticSetterDec1 @staticSetterDec2 static set #staticSetter(x: undefined) { } + + @accessorDec1 @accessorDec2 accessor #accessor: undefined + @staticAccessorDec1 @staticAccessorDec2 static accessor #staticAccessor: undefined + + static { log.push('static:end') } + } + log.push('after') + new Foo + log.push('end') + assertEq(() => log + '', 'start,extends,' + + 'M1,M2,G1,G2,S1,S2,A1,A2,' + // For each element e of staticElements if e.[[Kind]] is not field + 'm1,m2,g1,g2,s1,s2,a1,a2,' + // For each element e of instanceElements if e.[[Kind]] is not field + 'F1,F2,' + // For each element e of staticElements if e.[[Kind]] is field + 'f1,f2,' + // For each element e of instanceElements if e.[[Kind]] is field + 'c1,c2,' + // ApplyDecoratorsToClassDefinition + 'M3,M4,M5,M6,G3,G4,G5,G6,S3,S4,S5,S6,' + // For each element initializer of staticMethodExtraInitializers + 'static:start,' + // For each element elementRecord of staticElements + 'F7,F8,F3,F4,F5,F6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'A7,A8,A3,A4,A5,A6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'static:end,' + // For each element elementRecord of staticElements + 'c3,c4,c5,c6,' + // For each element initializer of classExtraInitializers + 'after,' + + 'ctor:start,' + + 'm3,m4,m5,m6,g3,g4,g5,g6,s3,s4,s5,s6,' + // For each element initializer of constructor.[[Initializers]] (a.k.a. instanceMethodExtraInitializers) + 'f7,f8,f3,f4,f5,f6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'a7,a8,a3,a4,a5,a6,' + // InitializeFieldOrAccessor + For each element initializer of elementRecord.[[ExtraInitializers]] + 'ctor:end,' + + 'end') + }, +} + +function prettyPrint(x: any): any { + if (x && x.prototype && x.prototype.constructor === x) return 'class' + if (typeof x === 'string') return JSON.stringify(x) + try { + return x + '' + } catch { + return 'typeof ' + typeof x // Handle values that don't implement "toString" + } +} + +function assertEq(callback: () => T, expected: T): boolean { + let details: string + try { + let x: any = callback() + if (x === expected) return true + details = ` Expected: ${prettyPrint(expected)}\n Observed: ${prettyPrint(x)}` + } catch (error) { + details = ` Throws: ${error}` + } + + const code = callback.toString().replace(/^\(\) => /, '').replace(/\s+/g, ' ') + console.log(`❌ ${testName}\n Code: ${code}\n${details}\n`) + failures++ + return false +} + +function assertThrows(callback: () => void, expected: T): boolean { + let details: string + try { + let x: any = callback() + details = ` Expected: throws instanceof ${expected.name}\n Observed: returns ${prettyPrint(x)}` + } catch (error) { + if (error instanceof expected) return true + details = ` Expected: throws instanceof ${expected.name}\n Observed: throws ${error}` + } + + const code = callback.toString().replace(/^\(\) => /, '').replace(/\s+/g, ' ') + console.log(`❌ ${testName}\n Code: ${code}\n${details}\n`) + failures++ + return false +} + +let testName: string +let failures = 0 + +async function run() { + for (const [name, test] of Object.entries(tests)) { + testName = name + try { + await test() + } catch (err) { + console.log(`❌ ${name}\n Throws: ${err}\n`) + failures++ + } + } + + if (failures > 0) { + console.log(`❌ ${failures} checks failed`) + } else { + console.log(`✅ All checks passed`) + } +} + +const promise = run() From f621babdae90be3bc1291f0a1f034ebd6d167466 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 09:03:36 +0100 Subject: [PATCH 05/12] fix(transpiler): address review feedback for ES decorators - Save/restore decorator_class_name instead of hard-resetting to null in visitBinaryExpression.zig and visitStmt.zig to avoid clobbering outer decorator name scopes in nested contexts - Use per-file task.emit_decorator_metadata and task.experimental_decorators instead of global transpiler.options in ParseTask.zig so per-file tsconfig settings are respected - Make newWeakSetExpr use findSymbol for proper lexical scope resolution, consistent with newWeakMapExpr - Exclude auto_accessor from private field lowering path so private auto-accessors get proper getter/setter pairs in decorated classes - Handle S.SExpr statements in appendDeclsAsAssigns so side-effect statements are preserved in expression-mode decorator lowering - Remove unnecessary return from setter in runtime.js Co-Authored-By: Claude --- src/ast/lowerDecorators.zig | 10 ++++++---- src/ast/visitBinaryExpression.zig | 3 ++- src/ast/visitStmt.zig | 3 ++- src/bundler/ParseTask.zig | 4 ++-- src/runtime.js | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ast/lowerDecorators.zig b/src/ast/lowerDecorators.zig index b46488ec84bb..6217b33bef32 100644 --- a/src/ast/lowerDecorators.zig +++ b/src/ast/lowerDecorators.zig @@ -94,9 +94,8 @@ pub fn LowerDecorators( /// new WeakSet() expression. fn newWeakSetExpr(p: *P, l: logger.Loc) Expr { - const ref = newSym(p, .unbound, "WeakSet"); return p.newExpr(E.New{ - .target = p.newExpr(E.Identifier{ .ref = ref }, l), + .target = p.newExpr(E.Identifier{ .ref = (p.findSymbol(l, "WeakSet") catch unreachable).ref }, l), .args = ExprNodeList.empty, .close_parens_loc = l, }, l); @@ -702,7 +701,8 @@ pub fn LowerDecorators( if (prop.ts_decorators.len == 0) { // ── Non-decorated property ── if (lower_all_private and prop.key != null and - prop.key.?.data == .e_private_identifier and prop.kind != .class_static_block) + prop.key.?.data == .e_private_identifier and prop.kind != .class_static_block and + prop.kind != .auto_accessor) { const nk_expr = prop.key.?; const npriv_orig = p.symbols.items[nk_expr.data.e_private_identifier.ref.inner_index].original_name; @@ -1312,7 +1312,9 @@ pub fn LowerDecorators( const appendDeclsAsAssigns = struct { fn f(parts: *ListManaged(Expr), var_decls: *ListManaged(G.Decl), stmts_list: []const Stmt, parser: *P, l: logger.Loc) void { for (stmts_list) |pstmt| { - if (pstmt.data == .s_local) { + if (pstmt.data == .s_expr) { + parts.append(pstmt.data.s_expr.value) catch unreachable; + } else if (pstmt.data == .s_local) { for (pstmt.data.s_local.decls.slice()) |decl_item| { const ref = decl_item.binding.data.b_identifier.ref; var_decls.append(.{ .binding = parser.b(B.Identifier{ .ref = ref }, l) }) catch unreachable; diff --git a/src/ast/visitBinaryExpression.zig b/src/ast/visitBinaryExpression.zig index 69143cc2018f..ac761680641a 100644 --- a/src/ast/visitBinaryExpression.zig +++ b/src/ast/visitBinaryExpression.zig @@ -69,6 +69,7 @@ pub fn CreateBinaryExpressionVisitor( const is_call_target = @as(Expr.Tag, p.call_target) == .e_binary and e_ == p.call_target.e_binary; // const is_stmt_expr = @as(Expr.Tag, p.stmt_expr_value) == .e_binary and expr.data.e_binary == p.stmt_expr_value.e_binary; const was_anonymous_named_expr = e_.right.isAnonymousNamed(); + const prev_decorator_class_name = p.decorator_class_name; // Propagate name for anonymous decorated class expressions in assignments if (e_.op == .bin_assign and was_anonymous_named_expr and @@ -120,7 +121,7 @@ pub fn CreateBinaryExpressionVisitor( e_.right = p.visitExpr(e_.right); }, } - p.decorator_class_name = null; + p.decorator_class_name = prev_decorator_class_name; // Always put constants on the right for equality comparisons to help // reduce the number of cases we have to check during pattern matching. We diff --git a/src/ast/visitStmt.zig b/src/ast/visitStmt.zig index e35d90c2c983..ccd7f9e8359b 100644 --- a/src/ast/visitStmt.zig +++ b/src/ast/visitStmt.zig @@ -215,11 +215,12 @@ pub fn VisitStmt( switch (data.value) { .expr => |expr| { const was_anonymous_named_expr = expr.isAnonymousNamed(); + const prev_decorator_class_name = p.decorator_class_name; if (was_anonymous_named_expr and expr.data == .e_class and expr.data.e_class.should_lower_standard_decorators) { p.decorator_class_name = js_ast.ClauseItem.default_alias; } data.value.expr = p.visitExpr(expr); - p.decorator_class_name = null; + p.decorator_class_name = prev_decorator_class_name; if (p.is_control_flow_dead) { return; diff --git a/src/bundler/ParseTask.zig b/src/bundler/ParseTask.zig index 4c6125897d3c..92bf67206167 100644 --- a/src/bundler/ParseTask.zig +++ b/src/bundler/ParseTask.zig @@ -1197,8 +1197,8 @@ fn runWithSourceCode( opts.features.minify_identifiers = transpiler.options.minify_identifiers; opts.features.minify_keep_names = transpiler.options.keep_names; opts.features.minify_whitespace = transpiler.options.minify_whitespace; - opts.features.emit_decorator_metadata = transpiler.options.emit_decorator_metadata; - opts.features.standard_decorators = !loader.isTypeScript() or !transpiler.options.experimental_decorators; + opts.features.emit_decorator_metadata = task.emit_decorator_metadata; + opts.features.standard_decorators = !loader.isTypeScript() or !task.experimental_decorators; opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages; opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags; opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime(); diff --git a/src/runtime.js b/src/runtime.js index e4d440f48419..833daeb4ad5a 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -242,7 +242,7 @@ export var __decorateElement = (array, flags, name, decorators, target, extra) = return __privateGet(this, extra); }, set [name](x) { - return __privateSet(this, extra, x); + __privateSet(this, extra, x); }, }, name, From 3048ff4eab2922f6bdbf6b91534bc4e195b2122c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 09:08:33 +0100 Subject: [PATCH 06/12] fix(test): clean up esbuild decorator test boilerplate Remove unused _testName/_failures variables from test boilerplate and add explicit stdout: "pipe" to Bun.spawn call. Co-Authored-By: Claude --- test/bundler/transpiler/es-decorators-esbuild.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/bundler/transpiler/es-decorators-esbuild.test.ts b/test/bundler/transpiler/es-decorators-esbuild.test.ts index c3ca422885f9..4c01d8b1c3da 100644 --- a/test/bundler/transpiler/es-decorators-esbuild.test.ts +++ b/test/bundler/transpiler/es-decorators-esbuild.test.ts @@ -23,9 +23,6 @@ function prettyPrint(x) { try { return x + ''; } catch { return 'typeof ' + typeof x; } } -let _testName = ''; -let _failures = 0; - function assertEq(callback, expected) { let x; try { x = callback(); } catch (e) { @@ -68,6 +65,7 @@ async function runDecoratorTest(code: string) { cmd: [bunExe(), "test.ts"], env: bunEnv, cwd: String(dir), + stdout: "pipe", stderr: "pipe", }); From 3f89145fbdf3a7c7ebd53f8e3a7b854b29b9f005 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 12:37:55 +0100 Subject: [PATCH 07/12] fix(test): update snapshot for cyclic-imports-async-bundler test The debugId hash changed due to the runtime.js setter fix. Co-Authored-By: Claude --- test/regression/issue/cyclic-imports-async-bundler.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/regression/issue/cyclic-imports-async-bundler.test.js b/test/regression/issue/cyclic-imports-async-bundler.test.js index 16fbe4ed274c..a20f2ec7b3f2 100644 --- a/test/regression/issue/cyclic-imports-async-bundler.test.js +++ b/test/regression/issue/cyclic-imports-async-bundler.test.js @@ -176,7 +176,7 @@ test("cyclic imports with async dependencies should generate async wrappers", as var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint); AsyncEntryPoint2(); - //# debugId=68A023AE1F6BCD1164756E2164756E21 + //# debugId=986E7BD819E590FD64756E2164756E21 //# sourceMappingURL=entryBuild.js.map " `); From 02d6a87305417ca0d23c31a9a62264c960dd0933 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 12:42:17 +0100 Subject: [PATCH 08/12] fix(test): update snapshots and add experimentalDecorators to legacy tests - Update debugId snapshots in cyclic-imports-async-bundler.test.js and bundler_promiseall_deadcode.test.ts (changed by runtime.js setter fix) - Add experimentalDecorators: true tsconfig to TypeScriptDecoratorsSimpleCase and TypeScriptDecoratorScopeESBuildIssue2147 tests which expect legacy decorator behavior (now that standard decorators are default for TS) Co-Authored-By: Claude --- test/bundler/bundler_promiseall_deadcode.test.ts | 4 ++-- test/bundler/esbuild/ts.test.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/bundler/bundler_promiseall_deadcode.test.ts b/test/bundler/bundler_promiseall_deadcode.test.ts index 91148a3f48d1..c631584211d7 100644 --- a/test/bundler/bundler_promiseall_deadcode.test.ts +++ b/test/bundler/bundler_promiseall_deadcode.test.ts @@ -160,7 +160,7 @@ describe("bundler", () => { var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint); AsyncEntryPoint2(); - //# debugId=741426CCC2D50C3364756E2164756E21 + //# debugId=5E85CC0956C6307964756E2164756E21 //# sourceMappingURL=out.js.map " `); @@ -402,7 +402,7 @@ describe("bundler", () => { var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint); AsyncEntryPoint2(); - //# debugId=27B6A8D5F1ED83DA64756E2164756E21 + //# debugId=C92CBF0103732ECC64756E2164756E21 //# sourceMappingURL=out.js.map " `); diff --git a/test/bundler/esbuild/ts.test.ts b/test/bundler/esbuild/ts.test.ts index 196fa9a24263..d026d000f1dc 100644 --- a/test/bundler/esbuild/ts.test.ts +++ b/test/bundler/esbuild/ts.test.ts @@ -843,6 +843,7 @@ describe("bundler", () => { new Foo().bar() `, + "/tsconfig.json": JSON.stringify({ compilerOptions: { experimentalDecorators: true } }), }, run: { stdout: ` @@ -1029,6 +1030,7 @@ describe("bundler", () => { } console.log(Foo, Bar) `, + "/tsconfig.json": JSON.stringify({ compilerOptions: { experimentalDecorators: true } }), }, bundling: false, onAfterBundle(api) { From 01311566ff4e63e1ff83e73c449cbeae3387f2a5 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 Jan 2026 22:32:26 +0100 Subject: [PATCH 09/12] fix(transpiler): fix export default class crash and accessor ! parsing with decorators Fix two bugs in ES decorator lowering: 1. `export default class` with decorators crashed because the code assumed the first statement from lowerClass was always s_class, but standard decorator lowering can produce prefix variable declarations. 2. `accessor field!: Type` (definite assignment assertion) failed to parse because the `!` operator was only recognized for `kind == .normal`, excluding `auto_accessor`. Also fix DCE check panic on non-s_class statements in export default. Co-Authored-By: Claude --- src/ast/P.zig | 5 +- src/ast/parseProperty.zig | 2 +- src/ast/visitStmt.zig | 30 ++-- test/bundler/transpiler/es-decorators.test.ts | 143 ++++++++++++++++++ 4 files changed, 169 insertions(+), 11 deletions(-) diff --git a/src/ast/P.zig b/src/ast/P.zig index 56ca06ca277e..3923f2c4c139 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -3768,7 +3768,10 @@ pub fn NewParser_( } }, else => { - Output.panic("Unexpected type in export default", .{}); + // Standard decorator lowering can produce non-class + // statements as the export default value; conservatively + // assume they have side effects. + return false; }, } }, diff --git a/src/ast/parseProperty.zig b/src/ast/parseProperty.zig index 24e81306f2da..670f3ca88901 100644 --- a/src/ast/parseProperty.zig +++ b/src/ast/parseProperty.zig @@ -421,7 +421,7 @@ pub fn ParseProperty( try p.lexer.next(); } else if (p.lexer.token == .t_exclamation and !p.lexer.has_newline_before and - kind == .normal and + (kind == .normal or kind == .auto_accessor) and !opts.is_async and !opts.is_generator) { diff --git a/src/ast/visitStmt.zig b/src/ast/visitStmt.zig index ccd7f9e8359b..e7d71e9a947e 100644 --- a/src/ast/visitStmt.zig +++ b/src/ast/visitStmt.zig @@ -461,17 +461,29 @@ pub fn VisitStmt( } } - // This is to handle TS decorators, mostly. + // Lower the class (handles both TS legacy and standard decorators). + // Standard decorator lowering may produce prefix statements + // (variable declarations) before the class statement. var class_stmts = p.lowerClass(.{ .stmt = s2 }); - bun.assert(class_stmts[0].data == .s_class); - if (class_stmts.len > 1) { - data.value.stmt = class_stmts[0]; - stmts.append(stmt.*) catch {}; - stmts.appendSlice(class_stmts[1..]) catch {}; - } else { - data.value.stmt = class_stmts[0]; - stmts.append(stmt.*) catch {}; + // Find the s_class statement in the returned list + var class_stmt_idx: usize = 0; + for (class_stmts, 0..) |cs, idx| { + if (cs.data == .s_class) { + class_stmt_idx = idx; + break; + } + } + + // Emit any prefix statements before the export default + stmts.appendSlice(class_stmts[0..class_stmt_idx]) catch {}; + + data.value.stmt = class_stmts[class_stmt_idx]; + stmts.append(stmt.*) catch {}; + + // Emit any suffix statements after the export default + if (class_stmt_idx + 1 < class_stmts.len) { + stmts.appendSlice(class_stmts[class_stmt_idx + 1 ..]) catch {}; } if (p.options.features.server_components.wrapsExports()) { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index a22d9110547c..6a0563109dac 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -472,4 +472,147 @@ describe("ES Decorators", () => { expect(exitCode).toBe(0); }); }); + + describe("export default class", () => { + test("export default class with method decorator", async () => { + using dir = tempDir("es-dec-export-default", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.foo()); + `, + "mod.js": ` + function dec(target, ctx) { return target; } + export default class { + @dec foo() { return 42; } + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("42\n"); + expect(exitCode).toBe(0); + }); + + test("export default class with class decorator", async () => { + using dir = tempDir("es-dec-export-default-cls", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.value); + `, + "mod.js": ` + function addValue(cls, ctx) { + return class extends cls { value = "decorated"; }; + } + @addValue export default class {} + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("decorated\n"); + expect(exitCode).toBe(0); + }); + + test("export default named class with decorator", async () => { + using dir = tempDir("es-dec-export-default-named", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.foo()); + `, + "mod.js": ` + function dec(target, ctx) { return target; } + export default class MyClass { + @dec foo() { return "named"; } + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("named\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("accessor with TypeScript annotations", () => { + test("accessor with definite assignment assertion (!)", async () => { + using dir = tempDir("es-dec-accessor-bang", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "test.ts": ` + function dec(target: any, ctx: any) { return target; } + class Foo { + @dec accessor child!: string; + } + const f = new Foo(); + f.child = "hello"; + console.log(f.child); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("hello\n"); + expect(exitCode).toBe(0); + }); + + test("accessor with optional marker (?)", async () => { + using dir = tempDir("es-dec-accessor-optional", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "test.ts": ` + function dec(target: any, ctx: any) { return target; } + class Foo { + @dec accessor child?: string; + } + const f = new Foo(); + console.log(f.child); + f.child = "world"; + console.log(f.child); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("undefined\nworld\n"); + expect(exitCode).toBe(0); + }); + }); }); From c5f6b248474c63b739871cb3184caa424bf16429 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 Jan 2026 23:18:58 +0100 Subject: [PATCH 10/12] fix(transpiler): improve decorator lowering robustness - Fix e_template rewriting to iterate over template parts, not just tag - Fix static block lowering to wrap non-expression stmts in IIFE instead of silently dropping let/if/for/try statements - Add parse error for `accessor foo() {}` (auto-accessor as method) - Save/restore decorator_class_name in visit.zig instead of clobbering outer value with null Co-Authored-By: Claude --- src/ast/lowerDecorators.zig | 28 +++++++++++++++++++++++++--- src/ast/parseProperty.zig | 6 ++++++ src/ast/visit.zig | 15 ++++++++++----- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/ast/lowerDecorators.zig b/src/ast/lowerDecorators.zig index 6217b33bef32..47adc6950ff3 100644 --- a/src/ast/lowerDecorators.zig +++ b/src/ast/lowerDecorators.zig @@ -235,6 +235,7 @@ pub fn LowerDecorators( }, .e_template => |e| { if (e.tag) |*t| rewriteExpr(p, t, kind); + for (e.parts) |*part| rewriteExpr(p, &part.value, kind); }, .e_arrow => |e| rewriteStmts(p, e.body.stmts, kind), .e_function => |e| { @@ -1112,10 +1113,31 @@ pub fn LowerDecorators( switch (elem.kind) { .block => { const sb = extracted_static_blocks.items[elem.index]; - rewriteStmts(p, sb.stmts.slice(), .{ .replace_this = .{ .ref = class_name_ref, .loc = class_name_loc } }); - for (sb.stmts.slice()) |sb_stmt| { - if (sb_stmt.data == .s_expr) + const stmts_slice = sb.stmts.slice(); + rewriteStmts(p, stmts_slice, .{ .replace_this = .{ .ref = class_name_ref, .loc = class_name_loc } }); + + // Check if all statements are simple expressions + const all_exprs = blk: { + for (stmts_slice) |sb_stmt| { + if (sb_stmt.data != .s_expr) break :blk false; + } + break :blk true; + }; + + if (all_exprs) { + for (stmts_slice) |sb_stmt| { suffix_exprs.append(sb_stmt.data.s_expr.value) catch unreachable; + } + } else { + // Wrap in IIFE to preserve non-expression statements + const iife_body = p.newExpr(E.Arrow{ + .body = .{ .loc = loc, .stmts = stmts_slice }, + .is_async = false, + }, loc); + suffix_exprs.append(p.newExpr(E.Call{ + .target = iife_body, + .args = ExprNodeList.empty, + }, loc)) catch unreachable; } }, .field_or_accessor => { diff --git a/src/ast/parseProperty.zig b/src/ast/parseProperty.zig index 670f3ca88901..010765fa603b 100644 --- a/src/ast/parseProperty.zig +++ b/src/ast/parseProperty.zig @@ -528,6 +528,12 @@ pub fn ParseProperty( }; } + // Auto-accessor fields cannot be methods + if (kind == .auto_accessor and p.lexer.token == .t_open_paren) { + p.log.addRangeError(p.source, key_range, "auto-accessor properties cannot have a method body") catch unreachable; + return error.SyntaxError; + } + // Parse a method expression if (p.lexer.token == .t_open_paren or kind != .normal or opts.is_class or opts.is_async or opts.is_generator) { return parseMethodExpression(p, kind, opts, is_computed, &key, key_range); diff --git a/src/ast/visit.zig b/src/ast/visit.zig index 79136c77b3fa..c992b980519a 100644 --- a/src/ast/visit.zig +++ b/src/ast/visit.zig @@ -187,13 +187,14 @@ pub fn Visit( @compileError("only_scan_imports_and_do_not_visit must not run this."); } // Propagate name from binding to anonymous decorated class expressions + const prev_decorator_class_name = p.decorator_class_name; if (was_anonymous_named_expr and val.data == .e_class and val.data.e_class.should_lower_standard_decorators and decl.binding.data == .b_identifier) { p.decorator_class_name = p.loadNameFromRef(decl.binding.data.b_identifier.ref); } decl.value = p.visitExprInOut(val, .{ .is_immediately_assigned_to_decl = true, }); - p.decorator_class_name = null; + p.decorator_class_name = prev_decorator_class_name; if (p.options.features.react_fast_refresh) { // When hooks are immediately assigned to something, we need to hash the binding. @@ -412,6 +413,7 @@ pub fn Visit( p.visitBinding(item.binding, duplicate_arg_check); if (item.default_value) |default_value| { const was_anonymous_named_expr = default_value.isAnonymousNamed(); + const prev_decorator_class_name2 = p.decorator_class_name; if (was_anonymous_named_expr and default_value.data == .e_class and default_value.data.e_class.should_lower_standard_decorators and item.binding.data == .b_identifier) @@ -419,7 +421,7 @@ pub fn Visit( p.decorator_class_name = p.loadNameFromRef(item.binding.data.b_identifier.ref); } item.default_value = p.visitExpr(default_value); - p.decorator_class_name = null; + p.decorator_class_name = prev_decorator_class_name2; switch (item.binding.data) { .b_identifier => |bind_| { @@ -443,6 +445,7 @@ pub fn Visit( p.visitBinding(property.value, duplicate_arg_check); if (property.default_value) |default_value| { const was_anonymous_named_expr = default_value.isAnonymousNamed(); + const prev_decorator_class_name3 = p.decorator_class_name; if (was_anonymous_named_expr and default_value.data == .e_class and default_value.data.e_class.should_lower_standard_decorators and property.value.data == .b_identifier) @@ -450,7 +453,7 @@ pub fn Visit( p.decorator_class_name = p.loadNameFromRef(property.value.data.b_identifier.ref); } property.default_value = p.visitExpr(default_value); - p.decorator_class_name = null; + p.decorator_class_name = prev_decorator_class_name3; switch (property.value.data) { .b_identifier => |bind_| { @@ -646,11 +649,12 @@ pub fn Visit( if (property.value) |val| { if (name_to_keep) |name| { const was_anon = val.isAnonymousNamed(); + const prev_dcn = p.decorator_class_name; if (val.data == .e_class and val.data.e_class.class_name == null and val.data.e_class.should_lower_standard_decorators) { p.decorator_class_name = name; } property.value = p.maybeKeepExprSymbolName(p.visitExpr(val), name, was_anon); - p.decorator_class_name = null; + p.decorator_class_name = prev_dcn; } else { property.value = p.visitExpr(val); } @@ -666,11 +670,12 @@ pub fn Visit( // if (property.flags.is_static and ) if (name_to_keep) |name| { const was_anon = val.isAnonymousNamed(); + const prev_dcn2 = p.decorator_class_name; if (val.data == .e_class and val.data.e_class.class_name == null and val.data.e_class.should_lower_standard_decorators) { p.decorator_class_name = name; } property.initializer = p.maybeKeepExprSymbolName(p.visitExpr(val), name, was_anon); - p.decorator_class_name = null; + p.decorator_class_name = prev_dcn2; } else { property.initializer = p.visitExpr(val); } From 20c47da3bb600d2cb2dddbe8afc535a85d9eb2a1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 Jan 2026 23:58:08 +0100 Subject: [PATCH 11/12] fix(transpiler): complete rewriteStmts coverage and use innerIndex() accessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add all control-flow statement cases to rewriteStmts (s_if, s_for, s_for_in, s_for_of, s_while, s_do_while, s_switch, s_try, s_block, s_label, s_with, s_throw) mirroring rewritePrivateAccessesInStmts, so this→className replacement works inside if/for/while/switch/try in static blocks and other rewritten contexts - Replace all .inner_index field accesses with .innerIndex() method calls for consistent bounds-checked access into p.symbols.items Co-Authored-By: Claude --- src/ast/lowerDecorators.zig | 56 +++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/ast/lowerDecorators.zig b/src/ast/lowerDecorators.zig index 47adc6950ff3..a3faf07b0557 100644 --- a/src/ast/lowerDecorators.zig +++ b/src/ast/lowerDecorators.zig @@ -271,6 +271,52 @@ pub fn LowerDecorators( .s_return => |ret| { if (ret.value) |*v| rewriteExpr(p, v, kind); }, + .s_throw => |data| rewriteExpr(p, &data.value, kind), + .s_if => |data| { + rewriteExpr(p, &data.test_, kind); + rewriteStmts(p, (&data.yes)[0..1], kind); + if (data.no) |*no| rewriteStmts(p, no[0..1], kind); + }, + .s_block => |data| rewriteStmts(p, data.stmts, kind), + .s_for => |data| { + if (data.init) |*fi| rewriteStmts(p, fi[0..1], kind); + if (data.test_) |*t| rewriteExpr(p, t, kind); + if (data.update) |*u| rewriteExpr(p, u, kind); + rewriteStmts(p, (&data.body)[0..1], kind); + }, + .s_for_in => |data| { + rewriteExpr(p, &data.value, kind); + rewriteStmts(p, (&data.body)[0..1], kind); + }, + .s_for_of => |data| { + rewriteExpr(p, &data.value, kind); + rewriteStmts(p, (&data.body)[0..1], kind); + }, + .s_while => |data| { + rewriteExpr(p, &data.test_, kind); + rewriteStmts(p, (&data.body)[0..1], kind); + }, + .s_do_while => |data| { + rewriteExpr(p, &data.test_, kind); + rewriteStmts(p, (&data.body)[0..1], kind); + }, + .s_switch => |data| { + rewriteExpr(p, &data.test_, kind); + for (data.cases) |*case| { + if (case.value) |*v| rewriteExpr(p, v, kind); + rewriteStmts(p, case.body, kind); + } + }, + .s_try => |data| { + rewriteStmts(p, data.body, kind); + if (data.catch_) |c| rewriteStmts(p, c.body, kind); + if (data.finally) |f| rewriteStmts(p, f.stmts, kind); + }, + .s_label => |data| rewriteStmts(p, (&data.stmt)[0..1], kind), + .s_with => |data| { + rewriteExpr(p, &data.value, kind); + rewriteStmts(p, (&data.body)[0..1], kind); + }, else => {}, } } @@ -623,7 +669,7 @@ pub fn LowerDecorators( // For named class expressions: swap to expr_class_ref for suffix ops var original_class_name_for_decorator: ?[]const u8 = null; if (is_expr and !expr_class_is_anonymous and expr_class_ref != null) { - original_class_name_for_decorator = p.symbols.items[class_name_ref.inner_index].original_name; + original_class_name_for_decorator = p.symbols.items[class_name_ref.innerIndex()].original_name; class_name_ref = expr_class_ref.?; class_name_loc = loc; } @@ -706,7 +752,7 @@ pub fn LowerDecorators( prop.kind != .auto_accessor) { const nk_expr = prop.key.?; - const npriv_orig = p.symbols.items[nk_expr.data.e_private_identifier.ref.inner_index].original_name; + const npriv_orig = p.symbols.items[nk_expr.data.e_private_identifier.ref.innerIndex()].original_name; const npriv_inner = nk_expr.data.e_private_identifier.ref.innerIndex(); if (prop.flags.contains(.is_method)) { @@ -881,7 +927,7 @@ pub fn LowerDecorators( var private_method_fn_ref: ?Ref = null; if (is_private) { - const private_orig = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name; + const private_orig = p.symbols.items[key_expr.data.e_private_identifier.ref.innerIndex()].original_name; const priv_inner = key_expr.data.e_private_identifier.ref.innerIndex(); if (k >= 1 and k <= 3) { @@ -943,7 +989,7 @@ pub fn LowerDecorators( dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); dec_args[1] = p.newExpr(E.Number{ .value = flags }, loc); dec_args[2] = if (is_private) - p.newExpr(E.String{ .data = p.symbols.items[key_expr.data.e_private_identifier.ref.inner_index].original_name }, loc) + p.newExpr(E.String{ .data = p.symbols.items[key_expr.data.e_private_identifier.ref.innerIndex()].original_name }, loc) else key_expr; dec_args[3] = decorator_array; @@ -1081,7 +1127,7 @@ pub fn LowerDecorators( else if (is_expr and expr_class_is_anonymous) (name_from_context orelse "") else - p.symbols.items[class_name_ref.inner_index].original_name; + p.symbols.items[class_name_ref.innerIndex()].original_name; const cls_dec_args = bun.handleOom(p.allocator.alloc(Expr, 5)); cls_dec_args[0] = p.newExpr(E.Identifier{ .ref = init_ref }, loc); From cbeeec9e1956c953c21c0c3b4f2af607f9e02cf5 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 9 Feb 2026 21:57:10 -0800 Subject: [PATCH 12/12] Update html-import-manifest.test.ts --- test/bundler/html-import-manifest.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bundler/html-import-manifest.test.ts b/test/bundler/html-import-manifest.test.ts index a6c93c0f2522..e05ebe63c38e 100644 --- a/test/bundler/html-import-manifest.test.ts +++ b/test/bundler/html-import-manifest.test.ts @@ -103,11 +103,11 @@ console.log(favicon); "files": [ { "input": "client.html", - "path": "./client-5y90hwq3.js", + "path": "./client-s249t5qg.js", "loader": "js", "isEntry": true, "headers": { - "etag": "xGxKikG0dN0", + "etag": "fxoJ6L-0X3o", "content-type": "text/javascript;charset=utf-8" } },