diff --git a/src/ast/e.rs b/src/ast/e.rs index bcdd9490294d..896fd24bbd79 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -721,7 +721,7 @@ impl Number { /// by calling out to the APIs in WebKit which are responsible for this operation. /// /// This can return `None` in wasm builds to avoid linking JSC - pub(crate) fn to_string(self, bump: &Bump) -> Option { + pub fn to_string(self, bump: &Bump) -> Option { Self::to_string_from_f64(self.value(), bump) } @@ -819,7 +819,7 @@ impl BigInt { /// a syntax error, so any literal that starts with `0` and has more than /// one character is a radix literal. #[inline] - pub(crate) fn has_radix(v: &[u8]) -> bool { + pub fn has_radix(v: &[u8]) -> bool { v.len() >= 2 && v[0] == b'0' } diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 77c2620dac5e..969dbfbdb26e 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -5,9 +5,8 @@ use bun_alloc::ArenaVecExt as _; use bun_collections::{HashMap, VecExt}; -use crate::lexer as js_lexer; use crate::p::P; -use crate::parser::{ARGUMENTS_STR as arguments_str, Ref, is_eval_or_arguments}; +use crate::parser::{ARGUMENTS_STR as arguments_str, Ref}; use bun_ast::g::{DeclList, Property, PropertyKind}; use bun_ast::{self as js_ast, B, E, Expr, ExprNodeList, Flags, G, S, Stmt}; @@ -124,19 +123,20 @@ fn class_copy(c: &G::Class) -> G::Class { } } -/// Whether a context-inferred name (`export default` → "default", object -/// property keys, assignment targets) can be attached to a lowered anonymous -/// class expression as its syntactic binding name. Class bodies are always -/// strict mode code and the output may be a module, so reserved words -/// ("default", "let", "await", …), `eval`/`arguments`, and non-identifier -/// strings would turn `_class = class {}` into a syntax error. -#[inline] -fn can_be_class_binding_name(name: &[u8]) -> bool { - js_lexer::is_identifier(name) - && js_lexer::keyword(name).is_none() - && !js_lexer::is_strict_mode_reserved_word(name) - && name != b"await" - && !is_eval_or_arguments(name) +/// Installed before static blocks run; a `static name` field instead wins on its own. +fn defines_static_name_method(props: &[Property]) -> bool { + props.iter().any(|prop| { + prop.flags.contains(Flags::Property::IsStatic) + && (prop.flags.contains(Flags::Property::IsMethod) + // A decorated accessor is installed from the suffix instead. + || (prop.kind == PropertyKind::AutoAccessor && prop.ts_decorators.len_u32() == 0)) + && match &prop.key { + Some(key) => { + matches!(&key.data, js_ast::ExprData::EString(s) if s.eql_comptime(b"name")) + } + None => false, + } + }) } // ── impl P ─────────────────────────────────────────────────────────────────── @@ -1142,19 +1142,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O class_name_ref = ecr; class_name_loc = loc; expr_class_is_anonymous = true; - if let Some(name) = name_from_context - && can_be_class_binding_name(name) - { - class.class_name = Some(js_ast::LocRef { - ref_: p.new_sym(js_ast::symbol::Kind::Other, name), - loc, - }); - } } } else { class_name_ref = class.class_name.as_ref().unwrap().ref_; class_name_loc = class.class_name.as_ref().unwrap().loc; } + // Decided before Phase 2 replaces decorated computed keys with temporaries. + let restore_inferred_name = + expr_class_is_anonymous && !defines_static_name_method(class.properties.slice()); let mut inner_class_ref: Ref = class_name_ref; if !is_expr { @@ -2422,6 +2417,21 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O new_properties = merged; } + // A string literal, unlike a class binding, survives the bundler's renaming. + if restore_inferred_name { + let this_e = p.new_expr(E::This {}, loc); + let name_e = p.new_expr( + E::EString { + data: name_from_context.unwrap_or(b"").into(), + ..Default::default() + }, + loc, + ); + let set_name = p.call_rt(loc, b"__name", &[this_e, name_e]); + let block = p.make_static_block(set_name, loc); + new_properties.insert(0, block); + } + class.properties = bun_ast::StoreSlice::new_mut(new_properties.into_bump_slice_mut()); class.has_decorators = false; class.should_lower_standard_decorators = false; diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs index dd62b2fc2ed2..72111dadb7a7 100644 --- a/src/js_parser/visit/mod.rs +++ b/src/js_parser/visit/mod.rs @@ -771,6 +771,30 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.stmts_to_single_stmt(stmt.loc, stmts.into_bump_slice_mut()) } + /// The `.name` that `key` gives an anonymous class value; lowering the class would lose it. + pub(crate) fn decorator_class_name_from_key( + &self, + key: Option, + value: &Expr, + ) -> Option<&'a [u8]> { + let ExprData::EClass(class) = value.data else { + return None; + }; + if class.class_name.is_some() || !class.should_lower_standard_decorators { + return None; + } + match key?.unwrap_inlined().data { + // `slice` flattens ropes and transcodes UTF-16; `data` alone does not. + ExprData::EString(mut str_) => Some(str_.slice(self.arena)), + ExprData::ENumber(num) => num.to_string(self.arena).map(|s| s.slice()), + ExprData::EBigInt(bigint) if !E::BigInt::has_radix(&bigint.value) => { + Some(bigint.value.slice()) + } + ExprData::EPrivateIdentifier(private) => Some(self.load_name_from_ref(private.ref_)), + _ => None, + } + } + pub(crate) fn visit_class( &mut self, name_scope_loc: bun_ast::Loc, @@ -953,23 +977,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } if let Some(val) = property.value { + let was_anon = val.is_anonymous_named(); + let prev_dcn = self.decorator_class_name; + self.decorator_class_name = + self.decorator_class_name_from_key(property.key, &val); + self.visit_expr(property.value.as_mut().unwrap()); + self.decorator_class_name = prev_dcn; if let Some(name) = name_to_keep { - let was_anon = val.is_anonymous_named(); - let prev_dcn = self.decorator_class_name; - if let ExprData::EClass(e_class) = &val.data { - if e_class.class_name.is_none() - && e_class.should_lower_standard_decorators - { - self.decorator_class_name = Some(name); - } - } - let mut visited = val; - self.visit_expr(&mut visited); - property.value = - Some(self.maybe_keep_expr_symbol_name(visited, name, was_anon)); - self.decorator_class_name = prev_dcn; - } else { - self.visit_expr(property.value.as_mut().unwrap()); + property.value = Some(self.maybe_keep_expr_symbol_name( + property.value.expect("unreachable"), + name, + was_anon, + )); } if Self::IS_TYPESCRIPT_ENABLED { @@ -984,24 +1003,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } if let Some(val) = property.initializer { - // if (property.flags.is_static and ) + let was_anon = val.is_anonymous_named(); + let prev_dcn = self.decorator_class_name; + self.decorator_class_name = + self.decorator_class_name_from_key(property.key, &val); + self.visit_expr(property.initializer.as_mut().unwrap()); + self.decorator_class_name = prev_dcn; if let Some(name) = name_to_keep { - let was_anon = val.is_anonymous_named(); - let prev_dcn2 = self.decorator_class_name; - if let ExprData::EClass(e_class) = &val.data { - if e_class.class_name.is_none() - && e_class.should_lower_standard_decorators - { - self.decorator_class_name = Some(name); - } - } - let mut visited = val; - self.visit_expr(&mut visited); - property.initializer = - Some(self.maybe_keep_expr_symbol_name(visited, name, was_anon)); - self.decorator_class_name = prev_dcn2; - } else { - self.visit_expr(property.initializer.as_mut().unwrap()); + property.initializer = Some(self.maybe_keep_expr_symbol_name( + property.initializer.expect("unreachable"), + name, + was_anon, + )); } } diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index 260a937e154b..0413fccc0d45 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -1705,32 +1705,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } if let Some(value) = &mut property.value { - // 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 == js_ast::AssignTarget::None - && matches!(value.data, Data::EClass(..)) - && value - .data - .e_class() - .unwrap() - .should_lower_standard_decorators - && value - .data - .e_class() - .expect("infallible: variant checked") - .class_name - .is_none() - && let Some(key) = property.key - && matches!(key.data, Data::EString(..)) - { - let key_str = key.data.e_string().expect("infallible: variant checked"); - // While E.rs has duplicate impls (E0034), reach the bytes directly - // — class-name keys are parser-produced (UTF-8, no rope). - p.decorator_class_name = if !key_str.is_utf16 { - Some(key_str.data.slice()) - } else { - None - }; + // { Foo: @dec class {} } gives the class .name = "Foo" + if in_.assign_target == js_ast::AssignTarget::None { + p.decorator_class_name = p.decorator_class_name_from_key(property.key, value); } p.visit_expr_in_out( value, diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 7fe388d3a623..67013a72a011 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -3170,6 +3170,51 @@ describe("bundler", () => { }, run: { stdout: "try:false" }, }); + // Standard-decorator lowering rewrites `const Bar = class { ... }` into + // `_class = class { ... }`, so the class no longer infers its name from the + // binding. The name the lowering attaches instead has to survive the + // bundler's renaming of symbols that collide in the enclosing scope (the + // function-local `Bar` here, the CommonJS-wrapped module scope below) and + // identifier minification. + const decoratedAnonymousClassNames = /* js */ ` + function dec() {} + function f() { + const Bar = class { @dec m() {} }; + const Baz = class { accessor x; }; + let Qux; Qux = class { @dec static s() {} }; + const obj = { "not-an-identifier": class { @dec m() {} } }; + return [Bar.name, Baz.name, Qux.name, obj["not-an-identifier"].name]; + } + console.log(JSON.stringify(f())); + `; + itBundled("edgecase/DecoratedAnonymousClassExprKeepsInferredName", { + files: { + "/entry.js": decoratedAnonymousClassNames, + }, + run: { stdout: '["Bar","Baz","Qux","not-an-identifier"]' }, + }); + itBundled("edgecase/DecoratedAnonymousClassExprKeepsInferredNameMinified", { + files: { + "/entry.js": decoratedAnonymousClassNames, + }, + minifyIdentifiers: true, + minifySyntax: true, + minifyWhitespace: true, + run: { stdout: '["Bar","Baz","Qux","not-an-identifier"]' }, + }); + itBundled("edgecase/DecoratedAnonymousClassExprKeepsInferredNameInCJSWrapper", { + files: { + "/entry.js": /* js */ ` + console.log(require("./mod.cjs").name); + `, + "/mod.cjs": /* js */ ` + function dec() {} + const Bar = class { @dec m() {} }; + module.exports = Bar; + `, + }, + run: { stdout: "Bar" }, + }); }); for (const backend of ["api", "cli"] as const) { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index bdc61e68c48d..ba48e091cd17 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -325,6 +325,168 @@ describe("ES Decorators", () => { }); }); + describe("anonymous class expressions named by the property key", () => { + // An anonymous class expression used as a property value is named after the + // property key. The lowering moves the class out of that position, so the + // parser has to hand it the key's string form, whatever kind of key it is, + // and the lowering must apply it as a string (a class binding of that name + // would shadow an outer variable spelled like the key inside the body). + // Expected values are what the same code prints without the decorators. + test.concurrent("object literal keys reach a class decorator as ctx.name and .name", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const names = []; + function dec(cls, ctx) { names.push(ctx.name); } + const o = { + 123: @dec class {}, + 1_000: @dec class {}, + 0x10: @dec class {}, + 0.5: @dec class {}, + 1e21: @dec class {}, + [-1]: @dec class {}, + 1n: @dec class {}, + "héllo": @dec class {}, + "\\u{20BB7}": @dec class {}, + ["a" + "b"]: @dec class {}, + "a-b": @dec class {}, + }; + const keys = [123, 1000, 16, 0.5, 1e21, -1, 1, "héllo", "\\u{20BB7}", "ab", "a-b"]; + console.log(JSON.stringify([names, keys.map(key => o[key].name)])); + `); + expect(stderr).toBe(""); + const expected = ["123", "1000", "16", "0.5", "1e+21", "-1", "1", "héllo", "\u{20BB7}", "ab", "a-b"]; + expect(JSON.parse(stdout)).toEqual([expected, expected]); + expect(exitCode).toBe(0); + }); + + test.concurrent("keys name a class that only has member decorators or accessors", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const o = { + ascii: class { @dec m() {} }, + 123: class { @dec m() {} }, + "héllo": class { @dec m() {} }, + "\\u{20BB7}": class { accessor x; }, + [-1]: class { accessor x; }, + 1n: class { @dec m() {} }, + ["a" + "b"]: class { accessor x; }, + }; + class Holder { + static 456 = class { @dec m() {} }; + static ["x-y"] = class { accessor x; }; + #secret = class { @dec m() {} }; + get secret() { return this.#secret; } + } + console.log(JSON.stringify([ + o.ascii.name, o[123].name, o["héllo"].name, o["\\u{20BB7}"].name, o[-1].name, o[1].name, o.ab.name, + Holder[456].name, Holder["x-y"].name, new Holder().secret.name, + ])); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([ + "ascii", + "123", + "héllo", + "\u{20BB7}", + "-1", + "1", + "ab", + "456", + "x-y", + "#secret", + ]); + expect(exitCode).toBe(0); + }); + + test.concurrent("the key only names the class, it does not shadow a same-named outer binding", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const wörld = "outer", Widget = "outer", Model = "outer"; + const o = { + "wörld": @dec class { static outer() { return wörld; } }, + ["Wid" + "get"]: class { @dec m() {} static outer() { return Widget; } }, + }; + class Holder { + static ["Model"] = class { accessor x; static outer() { return Model; } }; + } + const classes = [o["wörld"], o.Widget, Holder.Model]; + console.log(JSON.stringify(classes.map(cls => [cls.name, cls.outer()]))); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([ + ["wörld", "outer"], + ["Widget", "outer"], + ["Model", "outer"], + ]); + expect(exitCode).toBe(0); + }); + + test.concurrent("class member keys reach a class decorator as ctx.name and .name", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const names = []; + function dec(cls, ctx) { names.push(ctx.name); } + class Holder { + static 456 = @dec class {}; + static "wörld" = @dec class {}; + static ["x-y"] = @dec class {}; + static #hidden = @dec class {}; + 123 = @dec class {}; + 2n = @dec class {}; + #secret = @dec class {}; + static classNames() { + const instance = new Holder(); + return [ + Holder[456].name, + Holder["wörld"].name, + Holder["x-y"].name, + Holder.#hidden.name, + instance[123].name, + instance[2].name, + instance.#secret.name, + ]; + } + } + const classNames = Holder.classNames(); + console.log(JSON.stringify([names, classNames])); + `); + expect(stderr).toBe(""); + const expected = ["456", "wörld", "x-y", "#hidden", "123", "2", "#secret"]; + expect(JSON.parse(stdout)).toEqual([expected, expected]); + expect(exitCode).toBe(0); + }); + + test.concurrent("inlined enum member keys name the class after the enum value", async () => { + using dir = tempDir("es-dec-enum-key", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "test.ts": ` + enum Kind { User = "User", Num = 7 } + class User {} + const names: string[] = []; + function dec(cls: unknown, ctx: ClassDecoratorContext) { names.push(String(ctx.name)); } + const registry = { + [Kind.User]: @dec class { static model() { return User; } }, + [Kind.Num]: class { accessor x; }, + }; + console.log(JSON.stringify([ + names, + registry[Kind.User].name, + registry[Kind.User].model() === User, + registry[Kind.Num].name, + ])); + `, + }); + 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(JSON.parse(stdout)).toEqual([["User"], "User", true, "7"]); + expect(exitCode).toBe(0); + }); + }); + describe("decorator ordering", () => { test("decorators on different elements evaluate in source order", async () => { const { stdout, stderr, exitCode } = await runDecorator(` @@ -845,6 +1007,7 @@ describe("ES Decorators", () => { import Cls from "./mod.js"; const c = new Cls(); console.log(c.foo()); + console.log(Cls.name); `, "mod.js": ` function dec(fn, ctx) { console.log("decorated", ctx.name); return fn; } @@ -863,7 +1026,7 @@ describe("ES Decorators", () => { const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(filterStderr(rawStderr)).toBe(""); - expect(stdout).toBe("decorated foo\n42\n"); + expect(stdout).toBe("decorated foo\n42\ndefault\n"); expect(exitCode).toBe(0); }); @@ -982,6 +1145,162 @@ describe("ES Decorators", () => { }); }); + describe("inferred names of lowered anonymous class expressions", () => { + // Lowering turns `const Bar = class { ... }` into `_class = class { ... }`, + // which would otherwise make the class infer the name "_class". The name + // the source position would have inferred must be restored without adding + // a class binding: a binding can only hold identifier names, shadows the + // outer variable inside the class body, and is renamed by the bundler. + test.concurrent("names that are not valid identifiers", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const obj = { + "foo-bar": class { @dec m() {} }, + "": class { @dec m() {} }, + default: class { @dec m() {} }, + "with space": class { accessor x; }, + }; + console.log(JSON.stringify([obj["foo-bar"].name, obj[""].name, obj.default.name, obj["with space"].name])); + `); + expect(stderr).toBe(""); + expect(stdout).toBe('["foo-bar","","default","with space"]\n'); + expect(exitCode).toBe(0); + }); + + test.concurrent("console.log and Bun.inspect show the inferred name", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const Item = class { accessor id; }; + const Decorated = class { @dec m() {} }; + class Child extends Item {} + console.log(Item, Bun.inspect(new Item(), { compact: true }), Bun.inspect({ item: new Item() }, { compact: true })); + console.log(Decorated, Bun.inspect(new Decorated(), { compact: true }), Child); + `); + expect(stderr).toBe(""); + expect(stdout).toBe( + [ + "[class Item] Item { id: [Getter/Setter] } { item: Item { id: [Getter/Setter] } }", + "[class Decorated] Decorated { m: [Function: m] } [class Child extends Item]", + ].join("\n") + "\n", + ); + expect(exitCode).toBe(0); + }); + + test.concurrent("every naming context", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const A = class { @dec m() {} }; + let B; B = class { @dec m() {} }; + const { C = class { @dec m() {} } } = {}; + const [D = class { @dec m() {} }] = []; + const obj = { E: class { @dec m() {} } }; + class Holder { + static F = class { @dec m() {} }; + G = class { @dec m() {} }; + } + const H = @dec class {}; + console.log(JSON.stringify([A.name, B.name, C.name, D.name, obj.E.name, Holder.F.name, new Holder().G.name, H.name])); + `); + expect(stderr).toBe(""); + expect(stdout).toBe('["A","B","C","D","E","F","G","H"]\n'); + expect(exitCode).toBe(0); + }); + + test.concurrent("no naming context leaves the name empty", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const id = x => x; + console.log(JSON.stringify([id(class { @dec m() {} }).name, id(@dec class {}).name, id(class { accessor x; }).name])); + `); + expect(stderr).toBe(""); + expect(stdout).toBe('["","",""]\n'); + expect(exitCode).toBe(0); + }); + + test.concurrent("class body still refers to the outer binding", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + let Bar = class { + @dec m() { return Bar; } + static s() { return Bar; } + }; + const Original = Bar; + Bar = "reassigned"; + console.log(Original.name, new Original().m(), Original.s()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("Bar reassigned reassigned\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent("name is set before static initializers run", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const Bar = class { + static field = this.name; + static #priv = this.name; + static priv() { return this.#priv; } + static { console.log("static block:", this.name); } + @dec m() {} + }; + console.log(Bar.name, Bar.field, Bar.priv()); + const Baz = @dec class { + static field = this.name; + }; + console.log(Baz.name, Baz.field); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("static block: Bar\nBar Bar Bar\nBaz Baz\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent("a static member named `name` declared by the class wins", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const Getter = class { static get name() { return "from getter"; } @dec m() {} }; + const Setter = class { static set name(v) {} @dec m() {} }; + const Method = class { static name() {} @dec m() {} }; + const DecoratedMethod = class { @dec static name() {} }; + const DecoratedComputed = class { @dec static ["name"]() {} }; + const Accessor = class { static accessor name = "from accessor"; @dec m() {} }; + const Field = class { static seenBefore = this.name; static name = "from field"; @dec m() {} }; + const Uninitialized = class { static name; @dec m() {} }; + const Instance = class { name = "instance"; @dec m() {} }; + console.log(JSON.stringify([ + Getter.name, + Setter.name, + typeof Method.name, + typeof DecoratedMethod.name, + typeof DecoratedComputed.name, + Accessor.name, + Field.seenBefore, + Field.name, + Uninitialized.name, + Instance.name, + ])); + `); + expect(stderr).toBe(""); + expect(stdout).toBe( + '["from getter",null,"function","function","function","from accessor","Field","from field",null,"Instance"]\n', + ); + expect(exitCode).toBe(0); + }); + + test.concurrent("a decorated static accessor named `name` is installed after the body runs", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + const Foo = class { + static seenBefore = this.name; + @dec static accessor name = "from accessor"; + }; + console.log(JSON.stringify([Foo.seenBefore, Foo.name])); + `); + expect(stderr).toBe(""); + expect(stdout).toBe('["Foo","from accessor"]\n'); + expect(exitCode).toBe(0); + }); + }); + describe("private member calls in lowered classes", () => { // When a class is lowered for standard decorators, `recv.#m(...)` becomes // `__privateGet(recv, _m).call(recv, ...)`. The receiver must be evaluated