aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/js_ast.zig1
-rw-r--r--src/js_parser.zig225
-rw-r--r--src/runtime.footer.bun.js2
-rw-r--r--src/runtime.footer.js2
-rw-r--r--src/runtime.footer.node.js2
-rw-r--r--src/runtime.footer.with-refresh.js2
-rw-r--r--src/runtime.js12
-rw-r--r--src/runtime.zig16
-rw-r--r--test/bun.js/decorators.test.ts787
-rw-r--r--test/scripts/snippets.json1
-rw-r--r--test/snippets/package.json1
-rw-r--r--test/snippets/simple-lit-example.ts76
12 files changed, 1123 insertions, 4 deletions
diff --git a/src/js_ast.zig b/src/js_ast.zig
index 452f6044c..31bc056fb 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -539,6 +539,7 @@ pub const G = struct {
body_loc: logger.Loc = logger.Loc.Empty,
close_brace_loc: logger.Loc = logger.Loc.Empty,
properties: []Property = &([_]Property{}),
+ has_decorators: bool = false,
};
// invalid shadowing if left as Comment
diff --git a/src/js_parser.zig b/src/js_parser.zig
index 052edc6b1..af71b5453 100644
--- a/src/js_parser.zig
+++ b/src/js_parser.zig
@@ -1014,6 +1014,8 @@ const StaticSymbolName = struct {
pub const __HMRClient = NewStaticSymbol("Bun");
pub const __FastRefreshModule = NewStaticSymbol("FastHMR");
pub const __FastRefreshRuntime = NewStaticSymbol("FastRefresh");
+ pub const __decorateClass = NewStaticSymbol("__decorateClass");
+ pub const __decorateParam = NewStaticSymbol("__decorateParam");
pub const @"$$m" = NewStaticSymbol("$$m");
@@ -2010,6 +2012,8 @@ const FnOrArrowDataParse = struct {
is_constructor: bool = false,
is_typescript_declare: bool = false,
+ has_argument_decorators: bool = false,
+
is_return_disallowed: bool = false,
is_this_disallowed: bool = false,
@@ -2140,6 +2144,7 @@ const PropertyOpts = struct {
class_has_extends: bool = false,
allow_ts_decorators: bool = false,
ts_decorators: []Expr = &[_]Expr{},
+ has_argument_decorators: bool = false,
};
pub const ScanPassResult = struct {
@@ -5504,6 +5509,7 @@ fn NewParser_(
// Only allow omitting the body if we're parsing TypeScript
.allow_missing_body_for_type_script = is_typescript_enabled,
});
+ p.fn_or_arrow_data_parse.has_argument_decorators = false;
if (comptime is_typescript_enabled) {
// Don't output anything if it's just a forward declaration of a function
@@ -5606,6 +5612,7 @@ fn NewParser_(
p.fn_or_arrow_data_parse.allow_super_call = opts.allow_super_call;
p.fn_or_arrow_data_parse.allow_super_property = opts.allow_super_property;
+ var arg_has_decorators: bool = false;
var args = List(G.Arg){};
while (p.lexer.token != T.t_close_paren) {
// Skip over "this" type annotations
@@ -5626,6 +5633,9 @@ fn NewParser_(
var ts_decorators: []ExprNodeIndex = &([_]ExprNodeIndex{});
if (opts.allow_ts_decorators) {
ts_decorators = try p.parseTypeScriptDecorators();
+ if (ts_decorators.len > 0) {
+ arg_has_decorators = true;
+ }
}
if (!func.flags.contains(.has_rest_arg) and p.lexer.token == T.t_dot_dot_dot) {
@@ -5734,6 +5744,8 @@ fn NewParser_(
try p.lexer.expect(.t_close_paren);
p.fn_or_arrow_data_parse = std.mem.bytesToValue(@TypeOf(p.fn_or_arrow_data_parse), &old_fn_or_arrow_data);
+ p.fn_or_arrow_data_parse.has_argument_decorators = arg_has_decorators;
+
// "function foo(): any {}"
if (is_typescript_enabled and p.lexer.token == .t_colon) {
try p.lexer.next();
@@ -9418,6 +9430,7 @@ fn NewParser_(
.allow_await = if (is_async) .allow_expr else .allow_ident,
.allow_yield = if (is_generator) .allow_expr else .allow_ident,
});
+ p.fn_or_arrow_data_parse.has_argument_decorators = false;
p.validateFunctionName(func, .expr);
p.popScope();
@@ -10232,6 +10245,9 @@ fn NewParser_(
.allow_missing_body_for_type_script = is_typescript_enabled and opts.is_class,
});
+ opts.has_argument_decorators = opts.has_argument_decorators or p.fn_or_arrow_data_parse.has_argument_decorators;
+ p.fn_or_arrow_data_parse.has_argument_decorators = false;
+
// "class Foo { foo(): void; foo(): void {} }"
if (func.flags.contains(.is_forward_declaration)) {
// Skip this property entirely
@@ -10335,6 +10351,7 @@ fn NewParser_(
// been parsed. We need to start parsing from the "extends" clause.
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;
if (p.lexer.token == .t_extends) {
try p.lexer.next();
@@ -10386,12 +10403,13 @@ fn NewParser_(
continue;
}
- opts = PropertyOpts{ .is_class = true, .allow_ts_decorators = class_opts.allow_ts_decorators, .class_has_extends = extends != null };
+ opts = PropertyOpts{ .is_class = true, .allow_ts_decorators = class_opts.allow_ts_decorators, .class_has_extends = extends != null, .has_argument_decorators = false };
// Parse decorators for this property
const first_decorator_loc = p.lexer.loc();
if (opts.allow_ts_decorators) {
opts.ts_decorators = try p.parseTypeScriptDecorators();
+ has_decorators = has_decorators or opts.ts_decorators.len > 0;
} else {
opts.ts_decorators = &[_]Expr{};
}
@@ -10411,6 +10429,8 @@ fn NewParser_(
else => {},
}
}
+
+ has_decorators = has_decorators or opts.has_argument_decorators;
}
}
@@ -10433,6 +10453,7 @@ fn NewParser_(
.class_keyword = class_keyword,
.body_loc = body_loc,
.properties = properties.toOwnedSlice(),
+ .has_decorators = has_decorators or class_opts.ts_decorators.len > 0,
};
}
@@ -17355,9 +17376,205 @@ fn NewParser_(
) []Stmt {
switch (stmtorexpr) {
.stmt => |stmt| {
- var stmts = p.allocator.alloc(Stmt, 1) catch unreachable;
- stmts[0] = stmt;
- return stmts;
+ if (!stmt.data.s_class.class.has_decorators) {
+ var stmts = p.allocator.alloc(Stmt, 1) catch unreachable;
+ stmts[0] = stmt;
+ return stmts;
+ }
+
+ var class = &stmt.data.s_class.class;
+ var constructor_function: ?*E.Function = null;
+
+ var static_decorators = ListManaged(Stmt).init(p.allocator);
+ var instance_decorators = ListManaged(Stmt).init(p.allocator);
+ var instance_members = ListManaged(Stmt).init(p.allocator);
+ var static_members = ListManaged(Stmt).init(p.allocator);
+ var class_properties = ListManaged(Property).init(p.allocator);
+
+ for (class.properties) |*prop| {
+ // merge parameter decorators with method decorators
+ if (prop.flags.contains(.is_method)) {
+ if (prop.value) |prop_value| {
+ switch (prop_value.data) {
+ .e_function => |func| {
+ const is_constructor = (prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor"));
+ for (func.func.args) |arg, i| {
+ for (arg.ts_decorators.ptr[0..arg.ts_decorators.len]) |arg_decorator| {
+ var decorators = if (is_constructor) class.ts_decorators.listManaged(p.allocator) else prop.ts_decorators.listManaged(p.allocator);
+ const args = p.allocator.alloc(Expr, 2) catch unreachable;
+ args[0] = p.e(E.Number{ .value = @intToFloat(f64, i) }, arg_decorator.loc);
+ args[1] = arg_decorator;
+ decorators.append(p.callRuntime(arg_decorator.loc, "__decorateParam", args)) catch unreachable;
+ if (is_constructor) {
+ class.ts_decorators.update(decorators);
+ } else {
+ prop.ts_decorators.update(decorators);
+ }
+ }
+ }
+ },
+ else => unreachable,
+ }
+ }
+ }
+
+ if (prop.flags.contains(.is_method)) {
+ if (prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor")) {
+ if (prop.value) |prop_value| {
+ switch (prop_value.data) {
+ .e_function => |func| {
+ constructor_function = func;
+ },
+ else => unreachable,
+ }
+ }
+ }
+ }
+
+ if (prop.ts_decorators.len > 0) {
+ const loc = prop.key.?.loc;
+ const descriptor_key = switch (prop.key.?.data) {
+ .e_identifier => |k| p.e(E.Identifier{ .ref = k.ref }, loc),
+ .e_number => |k| p.e(E.Number{ .value = k.value }, loc),
+ .e_string => |k| p.e(E.String{ .data = k.data }, loc),
+ else => undefined,
+ };
+
+ const descriptor_kind: f64 = if (!prop.flags.contains(.is_method)) 2 else 1;
+
+ var target: Expr = undefined;
+ if (prop.flags.contains(.is_static)) {
+ p.recordUsage(class.class_name.?.ref.?);
+ target = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc);
+ } else {
+ target = p.e(E.Dot{ .target = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc), .name = "prototype", .name_loc = loc }, loc);
+ }
+
+ const args = p.allocator.alloc(Expr, 4) catch unreachable;
+ args[0] = p.e(E.Array{ .items = prop.ts_decorators }, loc);
+ args[1] = target;
+ args[2] = descriptor_key;
+ args[3] = p.e(E.Number{ .value = descriptor_kind }, loc);
+
+ const decorator = p.callRuntime(prop.key.?.loc, "__decorateClass", args);
+ const decorator_stmt = p.s(S.SExpr{ .value = decorator }, decorator.loc);
+
+ if (prop.flags.contains(.is_static)) {
+ static_decorators.append(decorator_stmt) catch unreachable;
+ } else {
+ instance_decorators.append(decorator_stmt) catch unreachable;
+ }
+ }
+
+ if (!prop.flags.contains(.is_method) and prop.key.?.data != .e_private_identifier and prop.ts_decorators.len > 0) {
+ // remove decorated fields without initializers to avoid assigning undefined.
+ const initializer = if (prop.initializer) |initializer_value| initializer_value else continue;
+
+ var target: Expr = undefined;
+ if (prop.flags.contains(.is_static)) {
+ p.recordUsage(class.class_name.?.ref.?);
+ target = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc);
+ } else {
+ target = p.e(E.This{}, prop.key.?.loc);
+ }
+
+ if (prop.flags.contains(.is_computed)) {
+ target = p.e(E.Index{
+ .target = target,
+ .index = prop.key.?,
+ }, prop.key.?.loc);
+ } else {
+ target = p.e(E.Dot{
+ .target = target,
+ .name = prop.key.?.data.e_string.data,
+ .name_loc = prop.key.?.loc,
+ }, prop.key.?.loc);
+ }
+
+ // remove fields with decorators from class body. Move static members outside of class.
+ if (prop.flags.contains(.is_static)) {
+ static_members.append(Expr.assignStmt(target, initializer, p.allocator)) catch unreachable;
+ } else {
+ instance_members.append(Expr.assignStmt(target, initializer, p.allocator)) catch unreachable;
+ }
+ continue;
+ }
+
+ class_properties.append(prop.*) catch unreachable;
+ }
+
+ class.properties = class_properties.items;
+
+ if (instance_members.items.len > 0 or class.extends != null) {
+ if (constructor_function == null) {
+ var properties = ListManaged(Property).fromOwnedSlice(p.allocator, class.properties);
+ var constructor_stmts = ListManaged(Stmt).init(p.allocator);
+
+ if (class.extends != null) {
+ const target = p.e(E.Super{}, stmt.loc);
+ const arguments_ref = p.newSymbol(.unbound, "arguments") catch unreachable;
+ p.current_scope.generated.append(p.allocator, arguments_ref) catch unreachable;
+
+ const super = p.e(E.Spread{ .value = p.e(E.Identifier{ .ref = arguments_ref }, stmt.loc) }, stmt.loc);
+ const args = ExprNodeList.one(p.allocator, super) catch unreachable;
+
+ constructor_stmts.append(p.s(S.SExpr{ .value = p.e(E.Call{ .target = target, .args = args }, stmt.loc) }, stmt.loc)) catch unreachable;
+ }
+
+ constructor_stmts.appendSlice(instance_members.items) catch unreachable;
+
+ properties.insert(0, G.Property{
+ .flags = Flags.Property.init(.{ .is_method = true }),
+ .key = p.e(E.String{ .data = "constructor" }, stmt.loc),
+ .value = p.e(E.Function{ .func = G.Fn{
+ .name = null,
+ .open_parens_loc = logger.Loc.Empty,
+ .args = &[_]Arg{},
+ .body = .{ .loc = stmt.loc, .stmts = constructor_stmts.items },
+ .flags = Flags.Function.init(.{}),
+ } }, stmt.loc),
+ }) catch unreachable;
+
+ class.properties = properties.items;
+ } else {
+ var constructor_stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, constructor_function.?.func.body.stmts);
+ // statements coming from class body inserted after super call or beginning of constructor.
+ var has_super = false;
+ for (constructor_stmts.items) |item, index| {
+ if (item.data != .s_expr or item.data.s_expr.value.data != .e_call or item.data.s_expr.value.data.e_call.target.data != .e_super) continue;
+ has_super = true;
+ constructor_stmts.insertSlice(index + 1, instance_members.items) catch unreachable;
+ }
+ if (!has_super) {
+ constructor_stmts.insertSlice(0, instance_members.items) catch unreachable;
+ }
+
+ constructor_function.?.func.body.stmts = constructor_stmts.items;
+ }
+ }
+
+ var stmts_count: usize = 1 + static_members.items.len + instance_decorators.items.len + static_decorators.items.len;
+ if (class.ts_decorators.len > 0) stmts_count += 1;
+ var stmts = ListManaged(Stmt).initCapacity(p.allocator, stmts_count) catch unreachable;
+ stmts.appendAssumeCapacity(stmt);
+ stmts.appendSliceAssumeCapacity(static_members.items);
+ stmts.appendSliceAssumeCapacity(instance_decorators.items);
+ stmts.appendSliceAssumeCapacity(static_decorators.items);
+ if (class.ts_decorators.len > 0) {
+ const args = p.allocator.alloc(Expr, 2) catch unreachable;
+ args[0] = p.e(E.Array{ .items = class.ts_decorators }, stmt.loc);
+ args[1] = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc);
+
+ stmts.appendAssumeCapacity(Expr.assignStmt(
+ p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc),
+ p.callRuntime(stmt.loc, "__decorateClass", args),
+ p.allocator,
+ ));
+
+ p.recordUsage(class.class_name.?.ref.?);
+ p.recordUsage(class.class_name.?.ref.?);
+ }
+ return stmts.items;
},
.expr => |expr| {
var stmts = p.allocator.alloc(Stmt, 1) catch unreachable;
diff --git a/src/runtime.footer.bun.js b/src/runtime.footer.bun.js
index a5466f53a..0c83ebc49 100644
--- a/src/runtime.footer.bun.js
+++ b/src/runtime.footer.bun.js
@@ -11,6 +11,8 @@ export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime;
export var __exportValue = BUN_RUNTIME.__exportValue;
export var __exportDefault = BUN_RUNTIME.__exportDefault;
export var __merge = BUN_RUNTIME.__merge;
+export var __decorateClass = BUN_RUNTIME.__decorateClass;
+export var __decorateParam = BUN_RUNTIME.__decorateParam;
export var $$bun_runtime_json_parse = JSON.parse;
export var __internalIsCommonJSNamespace =
BUN_RUNTIME.__internalIsCommonJSNamespace;
diff --git a/src/runtime.footer.js b/src/runtime.footer.js
index be4516473..062961b1f 100644
--- a/src/runtime.footer.js
+++ b/src/runtime.footer.js
@@ -19,6 +19,8 @@ export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime;
export var __exportValue = BUN_RUNTIME.__exportValue;
export var __exportDefault = BUN_RUNTIME.__exportDefault;
export var __merge = BUN_RUNTIME.__merge;
+export var __decorateClass = BUN_RUNTIME.__decorateClass;
+export var __decorateParam = BUN_RUNTIME.__decorateParam;
export var $$bun_runtime_json_parse = JSON.parse;
export var __internalIsCommonJSNamespace =
BUN_RUNTIME.__internalIsCommonJSNamespace;
diff --git a/src/runtime.footer.node.js b/src/runtime.footer.node.js
index 7d2b3a649..a6e425e44 100644
--- a/src/runtime.footer.node.js
+++ b/src/runtime.footer.node.js
@@ -12,6 +12,8 @@ export var __cJS2eSM = BUN_RUNTIME.__cJS2eSM;
export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime;
export var __exportValue = BUN_RUNTIME.__exportValue;
export var __exportDefault = BUN_RUNTIME.__exportDefault;
+export var __decorateClass = BUN_RUNTIME.__decorateClass;
+export var __decorateParam = BUN_RUNTIME.__decorateParam;
export var $$bun_runtime_json_parse = JSON.parse;
export var __internalIsCommonJSNamespace =
BUN_RUNTIME.__internalIsCommonJSNamespace;
diff --git a/src/runtime.footer.with-refresh.js b/src/runtime.footer.with-refresh.js
index 618329baf..a5b5a3b79 100644
--- a/src/runtime.footer.with-refresh.js
+++ b/src/runtime.footer.with-refresh.js
@@ -18,6 +18,8 @@ export var __cJS2eSM = BUN_RUNTIME.__cJS2eSM;
export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime;
export var __exportValue = BUN_RUNTIME.__exportValue;
export var __exportDefault = BUN_RUNTIME.__exportDefault;
+export var __decorateClass = BUN_RUNTIME.__decorateClass;
+export var __decorateParam = BUN_RUNTIME.__decorateParam;
export var $$bun_runtime_json_parse = JSON.parse;
export var __FastRefreshRuntime = BUN_RUNTIME.__FastRefreshRuntime;
export var __internalIsCommonJSNamespace =
diff --git a/src/runtime.js b/src/runtime.js
index e1cf9e3df..fe2f5b9df 100644
--- a/src/runtime.js
+++ b/src/runtime.js
@@ -235,3 +235,15 @@ export var __merge = (props, defaultProps) => {
? defaultProps
: mergeDefaultProps(props, defaultProps);
};
+
+export var __decorateClass = (decorators, target, key, kind) => {
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
+ if (decorator = decorators[i])
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
+ if (kind && result)
+ __defProp(target, key, result);
+ return result;
+};
+
+export var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index); \ No newline at end of file
diff --git a/src/runtime.zig b/src/runtime.zig
index 1a490e4d2..87daae829 100644
--- a/src/runtime.zig
+++ b/src/runtime.zig
@@ -364,6 +364,8 @@ pub const Runtime = struct {
__exportDefault: ?GeneratedSymbol = null,
__FastRefreshRuntime: ?GeneratedSymbol = null,
__merge: ?GeneratedSymbol = null,
+ __decorateClass: ?GeneratedSymbol = null,
+ __decorateParam: ?GeneratedSymbol = null,
pub const all = [_][]const u8{
// __HMRClient goes first
@@ -384,6 +386,8 @@ pub const Runtime = struct {
"__exportDefault",
"__FastRefreshRuntime",
"__merge",
+ "__decorateClass",
+ "__decorateParam",
};
pub const Name = "bun:wrap";
pub const alt_name = "bun:wrap";
@@ -483,6 +487,16 @@ pub const Runtime = struct {
return Entry{ .key = 15, .value = val.ref };
}
},
+ 16 => {
+ if (@field(this.runtime_imports, all[16])) |val| {
+ return Entry{ .key = 16, .value = val.ref };
+ }
+ },
+ 17 => {
+ if (@field(this.runtime_imports, all[17])) |val| {
+ return Entry{ .key = 17, .value = val.ref };
+ }
+ },
else => {
return null;
},
@@ -543,6 +557,8 @@ pub const Runtime = struct {
13 => (@field(imports, all[13]) orelse return null).ref,
14 => (@field(imports, all[14]) orelse return null).ref,
15 => (@field(imports, all[15]) orelse return null).ref,
+ 16 => (@field(imports, all[16]) orelse return null).ref,
+ 17 => (@field(imports, all[17]) orelse return null).ref,
else => null,
};
}
diff --git a/test/bun.js/decorators.test.ts b/test/bun.js/decorators.test.ts
new file mode 100644
index 000000000..545030ef1
--- /dev/null
+++ b/test/bun.js/decorators.test.ts
@@ -0,0 +1,787 @@
+import { test, expect } from "bun:test";
+
+test("decorator order of evaluation", () => {
+ let counter = 0;
+ const computedProp: unique symbol = Symbol("computedProp");
+
+ @decorator1
+ @decorator2
+ class BugReport {
+ @decorator7
+ type: string;
+
+ @decorator3
+ x: number = 20;
+
+ @decorator5
+ private _y: number = 12;
+
+ @decorator10
+ get y() {
+ return this._y;
+ }
+ @decorator11
+ set y(newY: number) {
+ this._y = newY;
+ }
+
+ @decorator9
+ [computedProp]: string = "yes";
+
+ constructor(@decorator8 type: string) {
+ this.type = type;
+ }
+
+ @decorator6
+ move(newX: number, @decorator12 newY: number) {
+ this.x = newX;
+ this._y = newY;
+ }
+
+ @decorator4
+ jump() {
+ this._y += 30;
+ }
+ }
+
+ function decorator1(target, propertyKey) {
+ expect(counter++).toBe(11);
+ expect(target === BugReport).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ }
+
+ function decorator2(target, propertyKey) {
+ expect(counter++).toBe(10);
+ expect(target === BugReport).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ }
+
+ function decorator3(target, propertyKey) {
+ expect(counter++).toBe(1);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("x");
+ }
+
+ function decorator4(target, propertyKey) {
+ expect(counter++).toBe(8);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("jump");
+ }
+
+ function decorator5(target, propertyKey) {
+ expect(counter++).toBe(2);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("_y");
+ }
+
+ function decorator6(target, propertyKey) {
+ expect(counter++).toBe(7);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("move");
+ }
+
+ function decorator7(target, propertyKey) {
+ expect(counter++).toBe(0);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("type");
+ }
+
+ function decorator8(target, propertyKey) {
+ expect(counter++).toBe(9);
+ expect(target === BugReport).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ }
+
+ function decorator9(target, propertyKey) {
+ expect(counter++).toBe(5);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe(computedProp);
+ }
+
+ function decorator10(target, propertyKey) {
+ expect(counter++).toBe(3);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("y");
+ }
+
+ function decorator11(target, propertyKey) {
+ expect(counter++).toBe(4);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("y");
+ }
+
+ function decorator12(target, propertyKey) {
+ expect(counter++).toBe(6);
+ expect(target === BugReport.prototype).toBe(true);
+ expect(propertyKey).toBe("move");
+ }
+});
+
+test("decorator factories order of evaluation", () => {
+ let counter = 0;
+ const computedProp: unique symbol = Symbol("computedProp");
+
+ @decorator1()
+ @decorator2()
+ class BugReport {
+ @decorator7()
+ type: string;
+
+ @decorator3()
+ x: number = 20;
+
+ @decorator5()
+ private _y: number = 12;
+
+ @decorator10()
+ get y() {
+ return this._y;
+ }
+ @decorator11()
+ set y(newY: number) {
+ this._y = newY;
+ }
+
+ @decorator9()
+ [computedProp]: string = "yes";
+
+ constructor(@decorator8() type: string) {
+ this.type = type;
+ }
+
+ @decorator6()
+ move(newX: number, @decorator12() newY: number) {
+ this.x = newX;
+ this._y = newY;
+ }
+
+ @decorator4()
+ jump() {
+ this._y += 30;
+ }
+ }
+
+ function decorator1() {
+ expect(counter++).toBe(18);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(23);
+ };
+ }
+
+ function decorator2() {
+ expect(counter++).toBe(19);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(22);
+ };
+ }
+
+ function decorator3() {
+ expect(counter++).toBe(2);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(3);
+ };
+ }
+
+ function decorator4() {
+ expect(counter++).toBe(16);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(17);
+ };
+ }
+
+ function decorator5() {
+ expect(counter++).toBe(4);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(5);
+ };
+ }
+
+ function decorator6() {
+ expect(counter++).toBe(12);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(15);
+ };
+ }
+
+ function decorator7() {
+ expect(counter++).toBe(0);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(1);
+ };
+ }
+
+ function decorator8() {
+ expect(counter++).toBe(20);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(21);
+ };
+ }
+
+ function decorator9() {
+ expect(counter++).toBe(10);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(11);
+ };
+ }
+
+ function decorator10() {
+ expect(counter++).toBe(6);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(7);
+ };
+ }
+
+ function decorator11() {
+ expect(counter++).toBe(8);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(9);
+ };
+ }
+
+ function decorator12() {
+ expect(counter++).toBe(13);
+ return function (target, descriptorKey) {
+ expect(counter++).toBe(14);
+ };
+ }
+});
+
+test("parameter decorators", () => {
+ let counter = 0;
+ class HappyDecorator {
+ width: number;
+ height: number;
+ x: number;
+ y: number;
+
+ move(@d4 x: number, @d5 @d6 y: number) {
+ this.x = x;
+ this.y = y;
+ }
+
+ constructor(
+ one: number,
+ two: string,
+ three: boolean,
+ @d1 @d2 width: number,
+ @d3 height: number
+ ) {
+ this.width = width;
+ this.height = height;
+ }
+
+ dance(@d7 @d8 intensity: number) {
+ this.width *= intensity;
+ this.height *= intensity;
+ }
+ }
+
+ function d1(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(7);
+ expect(target === HappyDecorator).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ expect(parameterIndex).toBe(3);
+ }
+
+ function d2(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(6);
+ expect(target === HappyDecorator).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ expect(parameterIndex).toBe(3);
+ }
+
+ function d3(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(5);
+ expect(target === HappyDecorator).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ expect(parameterIndex).toBe(4);
+ }
+
+ function d4(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(2);
+ expect(target === HappyDecorator.prototype).toBe(true);
+ expect(propertyKey).toBe("move");
+ expect(parameterIndex).toBe(0);
+ }
+
+ function d5(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(1);
+ expect(target === HappyDecorator.prototype).toBe(true);
+ expect(propertyKey).toBe("move");
+ expect(parameterIndex).toBe(1);
+ }
+
+ function d6(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(0);
+ expect(target === HappyDecorator.prototype).toBe(true);
+ expect(propertyKey).toBe("move");
+ expect(parameterIndex).toBe(1);
+ }
+
+ function d7(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(4);
+ expect(target === HappyDecorator.prototype).toBe(true);
+ expect(propertyKey).toBe("dance");
+ expect(parameterIndex).toBe(0);
+ }
+
+ function d8(target, propertyKey, parameterIndex) {
+ expect(counter++).toBe(3);
+ expect(target === HappyDecorator.prototype).toBe(true);
+ expect(propertyKey).toBe("dance");
+ expect(parameterIndex).toBe(0);
+ }
+
+ class Maybe {
+ constructor(
+ @m1 private x: number,
+ @m2 public y: boolean,
+ @m3 protected z: string
+ ) {}
+ }
+
+ function m1(target, propertyKey, index) {
+ expect(target === Maybe).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ expect(index).toBe(0);
+ }
+
+ function m2(target, propertyKey, index) {
+ expect(target === Maybe).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ expect(index).toBe(1);
+ }
+
+ function m3(target, propertyKey, index) {
+ expect(target === Maybe).toBe(true);
+ expect(propertyKey).toBe(undefined);
+ expect(index).toBe(2);
+ }
+});
+
+test("decorators random", () => {
+ @Frozen
+ class IceCream {}
+
+ function Frozen(constructor: Function) {
+ Object.freeze(constructor);
+ Object.freeze(constructor.prototype);
+ }
+
+ expect(Object.isFrozen(IceCream)).toBe(true);
+
+ class IceCreamComponent {
+ @Emoji()
+ flavor = "vanilla";
+ }
+
+ // Property Decorator
+ function Emoji() {
+ return function (target: Object, key: string | symbol) {
+ let val = target[key];
+
+ const getter = () => {
+ return val;
+ };
+ const setter = (next) => {
+ val = `๐Ÿฆ ${next} ๐Ÿฆ`;
+ };
+
+ Object.defineProperty(target, key, {
+ get: getter,
+ set: setter,
+ enumerable: true,
+ configurable: true,
+ });
+ };
+ }
+
+ const iceCream = new IceCreamComponent();
+ expect(iceCream.flavor === "๐Ÿฆ vanilla ๐Ÿฆ").toBe(true);
+ iceCream.flavor = "chocolate";
+ expect(iceCream.flavor === "๐Ÿฆ chocolate ๐Ÿฆ").toBe(true);
+
+ const i: unique symbol = Symbol.for("i");
+ const h: unique symbol = Symbol.for("h");
+ const t: unique symbol = Symbol.for("t");
+ const q: unique symbol = Symbol.for("q");
+ const p: unique symbol = Symbol.for("p");
+ const u3: unique symbol = Symbol.for("u3");
+ const u5: unique symbol = Symbol.for("u5");
+ const u6: unique symbol = Symbol.for("u6");
+ const u8: unique symbol = Symbol.for("u8");
+
+ class S {
+ @StringAppender("๐Ÿ˜›") k = 35;
+ @StringAppender("๐Ÿค ") static j = 4;
+ @StringAppender("๐Ÿ˜ตโ€๐Ÿ’ซ") private static [h] = 30;
+ @StringAppender("๐Ÿคฏ") private static u = 60;
+ @StringAppender("๐Ÿคช") private [t] = 32;
+ @StringAppender("๐Ÿค‘") [i] = 8;
+ @StringAppender("๐ŸŽƒ") private e = 10;
+ @StringAppender("๐Ÿ‘ป") static [q] = 202;
+ @StringAppender("๐Ÿ˜‡") r = S[h];
+ _y: number;
+ @StringAppender("๐Ÿคก") get y() {
+ return this._y;
+ }
+ set y(next) {
+ this._y = next;
+ }
+ #o = 100;
+
+ @StringAppender("๐Ÿ˜") u1: number;
+ @StringAppender("๐Ÿฅณ") static u2: number;
+ @StringAppender("๐Ÿค“") private static [u3]: number;
+ @StringAppender("๐Ÿฅบ") private static u4: number;
+ @StringAppender("๐Ÿคฏ") private [u5]: number;
+ @StringAppender("๐Ÿคฉ") [u6]: number;
+ @StringAppender("โ˜น๏ธ") private u7: number;
+ @StringAppender("๐Ÿ™ƒ") static [u8]: number;
+
+ @StringAppender("๐Ÿค”") u9 = this.u1;
+ @StringAppender("๐Ÿคจ") u10 = this.u2;
+ @StringAppender("๐Ÿ™‚") u11 = S[u3];
+ @StringAppender("๐Ÿ™") u12 = S.u4;
+ @StringAppender("๐Ÿ˜") u13 = this[u5];
+ @StringAppender("๐Ÿ˜‘") u14 = this[u6];
+ @StringAppender("๐Ÿ˜ถ") u15 = this.u7;
+ @StringAppender("๐Ÿ˜") u16 = S[u8];
+
+ constructor() {
+ this.k = 3;
+ expect(this.k).toBe("3 ๐Ÿ˜›");
+ expect(S.j).toBe(4);
+ expect(this[i]).toBe("8 ๐Ÿค‘");
+ expect(this.e).toBe("10 ๐ŸŽƒ");
+ expect(S[h]).toBe(30);
+ expect(S.u).toBe(60);
+ expect(this[t]).toBe("32 ๐Ÿคช");
+ expect(S[q]).toBe(202);
+ expect(this.#o).toBe(100);
+ expect(this.r).toBe("30 ๐Ÿ˜‡");
+ expect(this.y).toBe(undefined);
+ this.y = 100;
+ expect(this.y).toBe(100);
+
+ expect(this.u1).toBe(undefined);
+ expect(S.u2).toBe(undefined);
+ expect(S[u3]).toBe(undefined);
+ expect(S.u4).toBe(undefined);
+ expect(this[u5]).toBe(undefined);
+ expect(this[u6]).toBe(undefined);
+ expect(this.u7).toBe(undefined);
+ expect(S[u8]).toBe(undefined);
+
+ expect(this.u9).toBe("undefined ๐Ÿค”");
+ expect(this.u10).toBe("undefined ๐Ÿคจ");
+ expect(this.u11).toBe("undefined ๐Ÿ™‚");
+ expect(this.u12).toBe("undefined ๐Ÿ™");
+ expect(this.u13).toBe("undefined ๐Ÿ˜");
+ expect(this.u14).toBe("undefined ๐Ÿ˜‘");
+ expect(this.u15).toBe("undefined ๐Ÿ˜ถ");
+ expect(this.u16).toBe("undefined ๐Ÿ˜");
+
+ this.u1 = 100;
+ expect(this.u1).toBe("100 ๐Ÿ˜");
+ S.u2 = 100;
+ expect(S.u2).toBe("100 ๐Ÿฅณ");
+ S[u3] = 100;
+ expect(S[u3]).toBe("100 ๐Ÿค“");
+ S.u4 = 100;
+ expect(S.u4).toBe("100 ๐Ÿฅบ");
+ this[u5] = 100;
+ expect(this[u5]).toBe("100 ๐Ÿคฏ");
+ this[u6] = 100;
+ expect(this[u6]).toBe("100 ๐Ÿคฉ");
+ this.u7 = 100;
+ expect(this.u7).toBe("100 โ˜น๏ธ");
+ S[u8] = 100;
+ expect(S[u8]).toBe("100 ๐Ÿ™ƒ");
+
+ expect(this.u9).toBe("undefined ๐Ÿค”");
+ expect(this.u10).toBe("undefined ๐Ÿคจ");
+ expect(this.u11).toBe("undefined ๐Ÿ™‚");
+ expect(this.u12).toBe("undefined ๐Ÿ™");
+ expect(this.u13).toBe("undefined ๐Ÿ˜");
+ expect(this.u14).toBe("undefined ๐Ÿ˜‘");
+ expect(this.u15).toBe("undefined ๐Ÿ˜ถ");
+ expect(this.u16).toBe("undefined ๐Ÿ˜");
+ }
+ }
+
+ let s = new S();
+ expect(s.u9).toBe("undefined ๐Ÿค”");
+ expect(s.u10).toBe("undefined ๐Ÿคจ");
+ expect(s.u11).toBe("undefined ๐Ÿ™‚");
+ expect(s.u12).toBe("undefined ๐Ÿ™");
+ expect(s.u13).toBe("undefined ๐Ÿ˜");
+ expect(s.u14).toBe("undefined ๐Ÿ˜‘");
+ expect(s.u15).toBe("undefined ๐Ÿ˜ถ");
+ expect(s.u16).toBe("undefined ๐Ÿ˜");
+
+ s.u9 = 35;
+ expect(s.u9).toBe("35 ๐Ÿค”");
+ s.u10 = 36;
+ expect(s.u10).toBe("36 ๐Ÿคจ");
+ s.u11 = 37;
+ expect(s.u11).toBe("37 ๐Ÿ™‚");
+ s.u12 = 38;
+ expect(s.u12).toBe("38 ๐Ÿ™");
+ s.u13 = 39;
+ expect(s.u13).toBe("39 ๐Ÿ˜");
+ s.u14 = 40;
+ expect(s.u14).toBe("40 ๐Ÿ˜‘");
+ s.u15 = 41;
+ expect(s.u15).toBe("41 ๐Ÿ˜ถ");
+ s.u16 = 42;
+ expect(s.u16).toBe("42 ๐Ÿ˜");
+
+ function StringAppender(emoji: string) {
+ return function (target: Object, key: string | symbol) {
+ let val = target[key];
+
+ const getter = () => {
+ return val;
+ };
+ const setter = (value) => {
+ val = `${value} ${emoji}`;
+ };
+
+ Object.defineProperty(target, key, {
+ get: getter,
+ set: setter,
+ enumerable: true,
+ configurable: true,
+ });
+ };
+ }
+});
+
+test("class field order", () => {
+ class N {
+ l = 455;
+ }
+ class M {
+ u = 4;
+ @d1 w = 9;
+ constructor() {
+ // this.w = 9 should be moved here
+ expect(this.u).toBe(4);
+ expect(this.w).toBe(9);
+ this.u = 3;
+ this.w = 6;
+ expect(this.u).toBe(3);
+ expect(this.w).toBe(6);
+ }
+ }
+
+ function d1(target, propertyKey) {
+ expect(target === M.prototype).toBe(true);
+ expect(propertyKey).toBe("w");
+ }
+
+ let m = new M();
+ expect(m.u).toBe(3);
+ expect(m.w).toBe(6);
+});
+
+test("changing static method", () => {
+ class A {
+ static bar() {
+ return 1;
+ }
+ }
+
+ @changeMethodReturn("bar", 5)
+ class A_2 {
+ static bar() {
+ return 7;
+ }
+ }
+
+ function changeMethodReturn(method, value) {
+ return function (target) {
+ target[method] = function () {
+ return value;
+ };
+ return target;
+ };
+ }
+
+ @changeMethodReturn("bar", 2)
+ class B extends A {}
+
+ @changeMethodReturn("bar", 9)
+ class C extends B {}
+
+ expect(A_2.bar()).toBe(5);
+ expect(A.bar()).toBe(1);
+ expect(B.bar()).toBe(2);
+ expect(C.bar()).toBe(9);
+});
+
+test("class extending from another class", () => {
+ class A {
+ a: number;
+ constructor() {
+ this.a = 3;
+ }
+ }
+
+ class B extends A {
+ a: number = 9;
+ }
+
+ expect(new A().a).toBe(3);
+ expect(new B().a).toBe(9);
+
+ class C {
+ a: number = 80;
+ }
+
+ class D extends C {
+ a: number = 32;
+ constructor() {
+ super();
+ }
+ }
+
+ expect(new C().a).toBe(80);
+ expect(new D().a).toBe(32);
+
+ class E {
+ a: number = 40;
+ constructor() {
+ expect(this.a).toBe(40);
+ }
+ }
+
+ class F extends E {
+ @d1 a: number = 50;
+ constructor() {
+ super();
+ expect(this.a).toBe(50);
+ this.a = 60;
+ expect(this.a).toBe(60);
+ }
+ }
+
+ function d1(target) {
+ target.a = 100;
+ }
+});
+
+test("decorated fields moving to constructor", () => {
+ class A {
+ @d1 a = 3;
+ @d2 b = 4;
+ @d3 c = 5;
+ }
+
+ function d1(target, propertyKey) {
+ expect(target === A.prototype).toBe(true);
+ expect(propertyKey).toBe("a");
+ }
+
+ function d2(target, propertyKey) {
+ expect(target === A.prototype).toBe(true);
+ expect(propertyKey).toBe("b");
+ }
+
+ function d3(target, propertyKey) {
+ expect(target === A.prototype).toBe(true);
+ expect(propertyKey).toBe("c");
+ }
+
+ let a = new A();
+ expect(a.a).toBe(3);
+ expect(a.b).toBe(4);
+ expect(a.c).toBe(5);
+});
+
+test("only class decorator", () => {
+ let a = 0;
+ @d1
+ class A {}
+
+ let aa = new A();
+
+ function d1(target) {
+ a = 1;
+ expect(target).toBe(A);
+ }
+
+ expect(a).toBe(1);
+});
+
+test("only property decorators", () => {
+ let a = 0;
+ class A {
+ @d1 a() {}
+ }
+
+ let b = 0;
+ class B {
+ @d2 b = 3;
+ }
+
+ let c = 0;
+ class C {
+ @d3 get c() {
+ return 3;
+ }
+ }
+
+ function d1(target, propertyKey) {
+ a = 1;
+ expect(target === A.prototype).toBe(true);
+ expect(propertyKey).toBe("a");
+ }
+ expect(a).toBe(1);
+
+ function d2(target, propertyKey) {
+ b = 1;
+ expect(target === B.prototype).toBe(true);
+ expect(propertyKey).toBe("b");
+ }
+ expect(b).toBe(1);
+
+ function d3(target, propertyKey) {
+ c = 1;
+ expect(target === C.prototype).toBe(true);
+ expect(propertyKey).toBe("c");
+ }
+ expect(c).toBe(1);
+});
+
+test("only argument decorators", () => {
+ let a = 0;
+ class A {
+ a(@d1 a: string) {}
+ }
+
+ function d1(target, propertyKey, parameterIndex) {
+ a = 1;
+ expect(target === A.prototype).toBe(true);
+ expect(propertyKey).toBe("a");
+ expect(parameterIndex).toBe(0);
+ }
+
+ expect(a).toBe(1);
+});
+
+test("no decorators", () => {
+ let a = 0;
+ class A {
+ b: number;
+ constructor() {
+ a = 1;
+ this.b = 300000;
+ }
+ }
+
+ let aa = new A();
+ expect(a).toBe(1);
+ expect(aa.b).toBe(300000);
+});
diff --git a/test/scripts/snippets.json b/test/scripts/snippets.json
index ebdec23d3..1829eb9c4 100644
--- a/test/scripts/snippets.json
+++ b/test/scripts/snippets.json
@@ -25,6 +25,7 @@
"/optional-chain-with-function.js",
"/template-literal.js",
"/number-literal-bug.js",
+ "/simple-lit-example.ts",
"/caught-require.js",
"/package-json-utf8.js",
"/multiple-var.js",
diff --git a/test/snippets/package.json b/test/snippets/package.json
index 07b349f86..0c05b97be 100644
--- a/test/snippets/package.json
+++ b/test/snippets/package.json
@@ -4,6 +4,7 @@
"dependencies": {
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.4.1",
+ "lit": "^2.4.0",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/test/snippets/simple-lit-example.ts b/test/snippets/simple-lit-example.ts
new file mode 100644
index 000000000..34446e418
--- /dev/null
+++ b/test/snippets/simple-lit-example.ts
@@ -0,0 +1,76 @@
+import { LitElement, html, css } from "lit";
+import { customElement, property, eventOptions } from "lit/decorators.js";
+
+var loadedResolve;
+var loadedPromise = new Promise((resolve) => {
+ loadedResolve = resolve;
+});
+
+if (document?.readyState === "loading") {
+ document.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ loadedResolve();
+ },
+ { once: true }
+ );
+} else {
+ loadedResolve();
+}
+
+@customElement("my-element")
+export class MyElement extends LitElement {
+ static styles = css`
+ :host {
+ display: inline-block;
+ padding: 10px;
+ background: lightgray;
+ }
+ .planet {
+ color: var(--planet-color, blue);
+ }
+ `;
+
+ @property() planet = "Earth";
+
+ render() {
+ return html`
+ <span @click=${this.togglePlanet} class="planet" id="planet-id"
+ >${this.planet}</span
+ >
+ `;
+ }
+
+ @eventOptions({ once: true })
+ togglePlanet() {
+ this.planet = this.planet === "Earth" ? "Mars" : "Earth";
+ }
+}
+
+function setup() {
+ let element = document.createElement("my-element");
+ element.id = "my-element-id";
+ document.body.appendChild(element);
+}
+
+export async function test() {
+ setup();
+ await loadedPromise;
+
+ let element = document.getElementById("my-element-id");
+ let shadowRoot = element.shadowRoot;
+ let planet = shadowRoot.getElementById("planet-id");
+ if (element.__planet !== "Earth") {
+ throw new Error("Unexpected planet name: " + element.__planet);
+ }
+ planet.click();
+ if (element.__planet !== "Mars") {
+ throw new Error("Unexpected planet name: " + element.__planet);
+ }
+ planet.click();
+ if (element.__planet !== "Mars") {
+ throw new Error("Unexpected planet name: " + element.__planet);
+ }
+
+ return testDone(import.meta.url);
+}