js_parser: name lowered anonymous classes after numeric, non-ASCII and private property keys - #38787
Conversation
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
Comment |
|
Status: reproduced on bun 1.4.0 and on a debug build of main ( This PR is stacked on #38757 (its base branch) and should land after it: on the CI on f6ad721 finished with 178 of 179 jobs green; the one failure is |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The helper extraction and expanded key handling look correct, and the test coverage is thorough. Because it refactors control flow in visit_class (the parser's visit pass) and explicitly interacts with two other open PRs on the same lowering path (#38757/#38758), a human look to confirm the approach and merge order would be worthwhile.
Checked: the helper's per-key-type stringification matches NamedEvaluation (E::Number::to_string is the same JS-ToString routine template folding uses; EString::slice handles both rope and UTF-16); decorator_class_name is now unconditionally set-then-restored around the visit, which is more consistent than the old conditional-set — the surrounding code already resets it to None after visiting so no stale value could leak either way; the pub(crate) → pub widening on to_string/has_radix is inert.
Extended reasoning...
Overview
Extracts a shared helper decorator_class_name_from_key in src/js_parser/visit/mod.rs that derives the NamedEvaluation name for an anonymous decorated class expression from its property key, and calls it from both visit_class (class member value/initializer) and the e_object arm of visit_expr.rs. The helper handles E::String (either encoding, rope-flattened via EString::slice), E::Number (via E::Number::to_string), decimal E::BigInt, E::PrivateIdentifier, and E::InlinedEnum (via unwrap_inlined). Two bun_ast helpers are widened from pub(crate) to pub. Four new subprocess-driven tests cover the variant matrix.
Security risks
None. Parser-internal name propagation for a lowering; no untrusted-size arithmetic, no allocation from adversarial input beyond what the existing EString::slice/to_string paths already do.
Level of scrutiny
Moderate-to-high. This is the JS parser's visit pass — a hot, correctness-critical path where subtle state (like decorator_class_name) is threaded across recursive visits. The refactor of visit_class changes when was_anon is computed and makes the save/restore of decorator_class_name unconditional; I traced this and believe it is behavior-preserving (the helper returns None for non-class values, and e_class at visit_expr.rs:2608-2609 immediately takes-and-clears the field anyway), but it's the kind of control-flow reshaping in critical code that a maintainer should confirm.
Other factors
- The PR description explicitly documents interaction with #38757 and #38758 (which change how the lowering applies the name); merge order and compatibility should be coordinated by a human.
- Test coverage is strong: object-literal keys of every supported shape (numeric with separators/radix/exponent, negative computed, bigint, non-ASCII BMP and supra-BMP, folded concatenation), class-member keys including private and computed, and TS inlined-enum keys. All assert both
ctx.nameand.name. - The
EString::slicecall mutates the arena-stored key (rope flatten / UTF-16→UTF-8) throughStoreRef::DerefMutfrom a&selfmethod — this is the establishedStoreRefinterior-mutability pattern used throughout the visit pass, not new here. - I confirmed
property.keyis visited before the value/initializer in both call sites, so computed keys that fold to a literal (["a"+"b"],[Kind.A]) reach the helper post-fold.
|
On merge order with #38757 / #38758: it does not matter. This PR only changes the two visitor sites that fill in |
There was a problem hiding this comment.
Beyond the inline nit, I also checked: the visit_class value/initializer refactor preserves behavior — was_anon and the decorator_class_name save/restore moved outside the name_to_keep guard are only consumed inside it, and e_class (visit_expr.rs:2608) captures-and-clears decorator_class_name before visit_class runs, so the unconditional overwrite cannot leak a stale outer name into a method body. Both call sites visit the key before invoking the helper (e_object at :1659, visit_class earlier in the property loop), so folded ropes and EInlinedEnum are already in place.
Extended reasoning...
The one finding is a hermeticity nit; the production change itself checked out. I traced the visit_class restructuring against the old two-branch shape: computing was_anon unconditionally is pure; restoring decorator_class_name before rather than after maybe_keep_expr_symbol_name is safe because that helper does not read the field; and setting decorator_class_name = None for non-EClass values (methods, getters) where the old else-branch left it untouched cannot regress anything because the e_class handler snapshots-and-clears it on entry anyway. EString::slice mutating the arena node through the copied StoreRef is the same in-place rope-flatten pattern used elsewhere and is idempotent.
There was a problem hiding this comment.
LGTM — the tsconfig nit from my earlier pass is addressed in 481bbc2, and no new issues turned up on re-review.
Checked: the shared decorator_class_name_from_key helper covers every key shape via existing primitives (EString::slice for ropes/UTF-16, Number::to_string, BigInt::has_radix, load_name_from_ref, unwrap_inlined); the visit_class refactor preserves the name_to_keep/maybe_keep_expr_symbol_name path while adding coverage for numeric/private/computed keys; e_object's key-visit ordering (key visited before value) means folding/enum-inlining has already happened when the helper runs; the surrounding decorator_class_name = None reset in visit_expr.rs makes the unconditional assignment behavior-preserving.
Extended reasoning...
Overview
Focused fix to the standard-decorator lowering path: two ad-hoc sites that computed p.decorator_class_name from a property key (in visit_expr.rs's e_object and visit_class in visit/mod.rs) are replaced by one helper, decorator_class_name_from_key, that handles EString of either encoding (via slice, which flattens ropes and transcodes UTF-16), ENumber (via the existing JS-ToString Number::to_string), decimal EBigInt, EPrivateIdentifier, and EInlinedEnum (via unwrap_inlined). Two pub(crate) → pub visibility bumps in src/ast/e.rs let the parser crate reach Number::to_string and BigInt::has_radix. Four new test.concurrent blocks in es-decorators.test.ts cover ~20 key shapes across object literals, class members, and TS enum keys.
Security risks
None. This only changes which UTF-8 byte string is threaded through to __decorateElement as the class .name / ctx.name during decorator lowering. No untrusted-input parsing, no allocation-size arithmetic, no FFI.
Level of scrutiny
Medium. js_parser is hot-path code, but the change is narrowly scoped: it only affects anonymous class expressions that (a) will be lowered for standard decorators and (b) sit in a property-value position — the helper returns None early otherwise. Every branch delegates to an existing, already-exercised primitive (EString::slice is the same path template folding uses; Number::to_string is the enum/constant-folding path; BigInt::has_radix gates the same fold in Template::fold). The visit_class refactor is behavior-preserving for the previously-covered case: I traced that decorator_class_name is already None at each property iteration (consumed by e_class at visit_expr.rs:2608-2609 and reset at :1719), so the new unconditional assignment can't clobber a live outer value.
Other factors
- I left one nit on the previous revision (the
.tsenum-key test omitted a pinnedtsconfig.json); commit 481bbc2 adds it, matching every sibling in the file. The comment-cop feedback on long comments was also addressed (trimmed to one-liners). - Test coverage hits the REVIEW.md variant-matrix bar: numeric/hex/underscore/float/exp/computed-negative/bigint keys, non-ASCII and supra-BMP string keys, folded-rope keys, private names (static and instance), computed literal keys, and inlined enum keys of both string and number value; both
ctx.nameand.nameare asserted, with expected values matching Node's undecorated output. - Verified the key is visited before the value in both call sites (
e_objectat visit_expr.rs:1659 andvisit_class's property loop), so constant folding of["a" + "b"]and enum inlining have already happened when the helper readsproperty.key. - No CODEOWNERS on the touched paths.
481bbc2 to
bf584cc
Compare
…ns with __name
Standard-decorator lowering rewrites `const Bar = class { @dec m() {} }`
into `_class = class ... {}`, which no longer infers the name "Bar". The
lowering restored it by giving the class a binding named after the
context. That binding collides with the `Bar` declared in the same scope,
so the bundler renamed it (`class Bar2`, or a minified name), it could
only carry identifier names (object keys such as "foo-bar" or "default"
fell back to "_class"), and it shadowed the outer `Bar` inside the body.
Leave the class anonymous and open its body with
`static { __name(this, "Bar") }` instead, so the name is a string literal
the renamer never touches and is in place before any static initializer
left in the body runs. Classes with no naming context get "" like an
undecorated anonymous class.
Static methods and accessors are installed on the constructor before any static block runs, so the injected __name block would overwrite a `static get name()` the class declares itself. Skip the block when the emitted body has a static member keyed `name`, the same rule esbuild applies for --keep-names.
Only static methods and accessors keyed `name` need to suppress the
__name block: they are installed before static blocks run. A `static
name` field runs after the block and replaces the name itself, and
skipping the block for it left earlier static initializers reading
"_class". The check now also runs before decorated computed keys are
replaced with temporaries, so `@dec static ["name"]() {}` is seen.
A decorated `static accessor name` is installed from the suffix, so the body has nothing the __name block could overwrite; skipping the block left earlier static initializers reading "_class".
e32d6e9 to
68a8a87
Compare
…d private keys
An anonymous class expression that standard decorator lowering rewrites
gets its .name from decorator_class_name, which the object literal and
class member visitors fill in from the property key. Both only handled
keys stored as UTF-8 E::String: the object literal site dropped UTF-16
keys (any string literal with a non-ASCII character) and both ignored
numeric, bigint and private keys, so `{ 123: @dec class {} }` and
`{ "héllo": @dec class {} }` reached the lowering with no name and the
class decorator saw ctx.name === "". A computed key that folded to a
rope (`["a" + "b"]`) passed only the first segment of the string.
Route both sites through one helper that turns the key into the name
the property would give the class: strings of either encoding (ropes
flattened), numbers via E::Number::to_string, decimal bigint literals,
private names, and inlined enum members, matching the names the engine
assigns to the undecorated code.
Based on the branch that makes the lowering apply the inferred name as
a string literal; on top of the old binding-based lowering, the extra
names would have shadowed same-named outer variables in class bodies.
bf584cc to
f6ad721
Compare
|
Restructured after a closer look at how the name is consumed: this branch is now based on #38757 instead of Reason: the lowering on The previous round's comments are all addressed and the description has been rewritten for the new shape. |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
src/js_parser/lower/lower_decorators.rs:126-140—defines_static_name_methodmatches&key.dataagainstEStringwithout calling.unwrap_inlined()first, so a static method key that is a same-file inlined enum member evaluating to"name"(enum K { N = "name" }; const Foo = class { static [K.N]() {} @dec m() {} }) is not detected, and the insertedstatic { __name(this, "Foo") }block overwrites the user's method. This is a regression from the old class-binding approach (whose runtime check handled any computed key), and an inconsistency withdecorator_class_name_from_keyat visit/mod.rs:786, which does callkey?.unwrap_inlined(). Fix:matches!(&key.unwrap_inlined().data, ...).Extended reasoning...
What the bug is
defines_static_name_methodatsrc/js_parser/lower/lower_decorators.rs:126-140decides whether the class already declares a static method (or undecorated auto-accessor) named"name", so that the lowering can skip insertingstatic { __name(this, "<inferred>") }. The check is:Some(key) => { matches!(&key.data, js_ast::ExprData::EString(s) if s.eql_comptime(b"name")) }
This inspects
key.datadirectly. But this function runs aftervisit_classhas visited the property keys, and a computed key[Kind.Name](whereenum Kind { Name = "name" }is defined in the same file) has by then been rewritten toExprData::EInlinedEnum(EString("name")).EInlinedEnumis a distinct variant (it carries the wrapped literal plus the trailing-comment enum member name), somatches!(&key.data, ExprData::EString(_))isfalse,defines_static_name_methodreturnsfalse,restore_inferred_namestaystrue, and the lowering insertsstatic { __name(this, "Foo") }at index 0 ofnew_properties.Why this is wrong at runtime
Per ClassDefinitionEvaluation, static methods (including computed-key ones) are installed on the class object before any static block or field initializer runs. So the emitted body evaluates in this order:
static ["name" /* Name */]() {}— installsFoo.name= the method function.static { __name(this, "Foo") }—__nameatsrc/runtime.jsdoesObject.defineProperty(target, "name", { value, configurable: true }). Class methods are configurable, so this overwrites step 1.
Result:
typeof Foo.name === "string"(it is"Foo") instead of"function". The user's static method is unreachable viaFoo.name. The PR's own new test"a static member named \name` declared by the class wins"establishes that a staticnamemethod (including the computed-literal case["name"]`) must win; this is the same case, but with the literal wrapped by enum inlining.Why this is a regression, and an in-diff inconsistency
Before this PR, the inferred name was applied by giving the anonymous class a class binding (
_class = class Foo { ... }). In that shape the engine sets.namefrom the binding via SetFunctionName before static elements are installed, so a static method whose key evaluates to"name"at runtime always overwrote it — a runtime check that handled ANY computed key. The new__name-static-block mechanism cannot do that runtime check, so it depends ondefines_static_name_methodcatching every knowable case at compile time. It catches plainEString(including ropes viaeql_comptime→eql8_rope) but missesEInlinedEnum.The PR's other new key-inspection helper,
decorator_class_name_from_keyatvisit/mod.rs:786, does exactly the right thing for the same reason:match key?.unwrap_inlined().data { ... }. And the PR's new test"inlined enum member keys name the class after the enum value"proves same-file enum keys areEInlinedEnumafter visiting. So this is a same-class inconsistency between two key-inspection sites added in the same diff (REVIEW.md: "Fix the whole class in the same PR").Step-by-step proof
// tsconfig: { compilerOptions: {} } → standard decorators enum Kind { Name = "name" } function dec() {} const Foo = class { static [Kind.Name]() { return "method"; } @dec m() {} }; console.log(typeof Foo.name);
- Parser sees
[Kind.Name]as a computed keyEIndex(Kind, "Name"). visit_classvisits the key; TS enum inlining substitutes it withEInlinedEnum { value: EString("name"), comment: "Name" }.visit_expron the class callslower_standard_decorators_expr(viashould_lower_standard_decorators);expr_class_is_anonymous = true;defines_static_name_methoditeratesclass.properties. For the static method:IsStatic && IsMethod→ checks the key.key.dataisEInlinedEnum(...), notEString(...), somatches!isfalse. Function returnsfalse.restore_inferred_name = true && !false = true. After phase 2, the lowering insertsstatic { __name(this, "Foo") }atnew_properties[0].- At runtime the class installs the static
["name"]()method, then runs the static block:__nameObject.defineProperty(this, "name", { value: "Foo" })overwrites it. - Output:
string. Expected (node without decorators, and this PR's own contract for the literal-["name"]case):function.
Impact and fix
Impact: extremely narrow — requires TypeScript, a same-file enum member whose value is exactly
"name", used as a computed static method (or undecorated auto-accessor) key, on a lowered anonymous class expression. Not something real code will hit; hence nit. The broader case of a runtime-only computed key (static [someVar]()wheresomeVar === "name"at runtime) is inherently undecidable with the__nameapproach and is out of scope.Fix: unwrap the inlined enum before matching, mirroring
decorator_class_name_from_key:Some(key) => { matches!(&key.unwrap_inlined().data, js_ast::ExprData::EString(s) if s.eql_comptime(b"name")) }
There was a problem hiding this comment.
LGTM — the earlier tsconfig.json nit is addressed and no new issues on this pass.
What was reviewed:
decorator_class_name_from_key: each arm produces the JS ToString of the key (EString::slicefor ropes/UTF-16,Number::to_string, decimal-only bigint viahas_radix,load_name_from_reffor#private,unwrap_inlinedfor enum members); confirmed keys are already visited before the helper reads them at both call sites.visit_classrefactor:name_to_keepstill drivesmaybe_keep_expr_symbol_nameunchanged;decorator_class_nameis now saved/restored around every value/initializer visit (previously only in thename_to_keepbranch), which also prevents an outer name from leaking through.e_objectsite invisit_expr.rs: the surroundingp.decorator_class_name = Nonereset after the visit was already there, so unconditionally overwriting from the helper is safe.
Extended reasoning...
Overview
Consolidates two duplicated blocks that populate p.decorator_class_name from a property key into one helper, decorator_class_name_from_key, and extends the key shapes it understands from "UTF-8 E::String only" to numeric, UTF-16 string, rope-folded string, decimal bigint, private identifier, and inlined enum member. Two visibility widenings in src/ast/e.rs (Number::to_string, BigInt::has_radix) support the helper. Five new subprocess tests in es-decorators.test.ts cover the full key matrix in both object-literal and class-member positions, plus a shadowing check tied to the base branch's __name lowering.
Security risks
None. This is compile-time name propagation in the JS parser; the derived name is a string literal in emitted code, not a binding, so no injection or scoping hazard beyond what the base branch already handles.
Level of scrutiny
Medium. The parser visitor is a hot/critical path, but the change is narrow: it only affects what byte string is stored in decorator_class_name around a class-expression visit, and only for classes that already have should_lower_standard_decorators set. The refactored visit_class value/initializer blocks preserve the pre-existing maybe_keep_expr_symbol_name behavior exactly (moved out of the branch, same inputs). I traced that both call sites visit property.key before invoking the helper, so folded/inlined keys are already in their final form when read. The e_object call site was already resetting decorator_class_name = None after each property visit, so the switch from conditional-set to unconditional-overwrite cannot leak state.
Other factors
This is stacked on #38757 (its base branch), and the description/tests explicitly encode why: the shadowing test would fail against the main lowering. All prior review threads are resolved — the comment-cop long-comment flags were trimmed to one-liners, and my earlier nit about the missing tsconfig.json in the enum-key test was applied in 481bbc2. The tests use test.concurrent, drain all pipes, assert exact JSON output before exit code, and cover the variant matrix (numeric with radix/separator/exponent, UTF-16, supra-BMP, rope, bigint, private, computed, static/instance, enum). The PR description lists the full decorator/transpiler test suite as passing on the stacked build.
68a8a87 to
1ed1f67
Compare
Based on #38757 (this PR's base branch); it retargets to
mainautomatically once that merges. The reason it has to land second is under Fix.Problem
ctx.nameis""for all four decorated classes and every.nameis""(o[456].nameis"_class"onmain,""on js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757); node prints123,héllo,456,789,#pfor the same code without the decorators. Same underbun build. ASCII string keys work. The lowering is also used for classes withaccessormembers, so{ 123: class { accessor x } }is affected with no decorators in the file.{ ["a" + "b"]: @dec class {} }, is named"a"(the first rope segment), and an inlined enum member key in TypeScript,{ [Kind.A]: @dec class {} }, is named"".p.decorator_class_name(Option<&[u8]>), and the two places that fill it from a property key only understood keys stored as UTF-8E::String:src/js_parser/visit/visit_expr.rs(e_object) set it toNoneforis_utf16strings (the lexer stores every literal containing a non-ASCII character as UTF-16), skippedE::Number/E::BigInt/E::InlinedEnumkeys, and read only the head of a rope;src/js_parser/visit/mod.rs(visit_class) derived it fromname_to_keep, which exists only for non-computedE::Stringkeys, so numeric, bigint, private and computed member keys were skipped.Noneand emits""as the name (__decorateElement(_init, 0, "", ...), and on js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757__name(this, "")).Fix
decorator_class_name_from_keyinvisit/mod.rs, replaces both blocks. For a value that is an anonymous class about to be lowered it returns the key's string form:E::Stringof either encoding throughEString::slice(flattens ropes, transcodes UTF-16),E::Numberthrough the existingE::Number::to_string(JSToString, the routine string folding uses:0x10->"16",1e21->"1e+21"), decimal bigint literals (stored canonically by the lexer; radix forms stay unnamed, as into_string_expr_without_side_effects), private names ("#p"), and inlined enum members viaunwrap_inlined. A computed key that did not fold to a literal still givesNone.visit_classsetsdecorator_class_namefrom the key around every member value/initializer visit and restores it afterwards;name_to_keepkeeps its keep-names role unchanged.E::Number::to_stringandE::BigInt::has_radixinsrc/ast/e.rsbecomepub;lower_decorators.rsis not touched.__name(this, "...")/__decorateElement(...), which is valid for any key.main: the lowering onmainturns any identifier-shaped inferred name into a synthesized class binding (_class = class User { ... }), which shadows a same-named outer variable inside the class body. That bug already hits ASCII keys today ({ User: class { accessor x; static make() { return new User() } } }constructs the inner class), and js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 fixes it by emitting__nameinstead. On its own, this PR would extend that shadowing to the keys it starts propagating: with the producer change onmain,enum Kind { User = "User" }; class User {}; { [Kind.User]: class { accessor loaded; static make() { return new User(1) } } }stops constructing the outerUser(it works on 1.4.0, where the class is merely named"_class"), and the same for"wörld",["a" + "b"]and computed member keys. On js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 the name is only ever a string literal, so the new tests include a shadowing check that would fail against themainlowering.test/bundler/transpiler/es-decorators.test.ts, block "anonymous class expressions named by the property key" (5 tests): object keys of every supported shape reaching a class decorator asctx.nameand.name; the same keys naming classes that only have member decorators or accessors (object and class-member positions, including#private); class member keys with a class decorator (static, instance, computed, bigint, private); a.tsenum-keyed registry that must keep resolving the outerUser; and quoted non-ASCII / folded / computed keys not shadowing a same-named outer binding. With this PR'ssrc/change reverted on top of the base branch all 5 fail; with it all 5 pass.es-decorators.test.ts(including js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757's tests),es-decorators-esbuild.test.ts(esbuild's conformance suite),decorators.test.ts,decorator-metadata.test.ts,bundler_edgecase.test.ts,bundler_decorator_metadata.test.ts,transpiler.test.js,property.test.ts,ts-use-define-for-class-fields.test.ts, and the decorator regression tests undertest/regression/issue;cargo clippy -p bun_js_parser -p bun_astis clean.x ??= @dec class {}, parameter defaults,for (let x = @dec class {};;); js_parser: name an anonymous decorated class after the parameter or for loop variable it initializes #38906 covers parameter defaults and the others have owners), andrequire.resolve("./a" + "b")reading only the head of a folded string, which is the same rope mistake in a different reader. While writing the tests I also found thatclass A { static 2n = 1 }fails to parse (bigint key after a modifier keyword), reported separately; the bigint cases here use positions that parse today.Background
.namefrom the position it is created in; for a property value the name is the property key as a string ({ 123: class {} }[123].name === "123",class A { #p = class {} }names the inner class"#p"). Any other position gives"".@dec class {}(and any class withaccessormembers) into_class = class { ... }, __decorateElement(...), ..., which takes the class out of the property position, so the engine cannot infer the name. The visitor records the position's name inp.decorator_class_namejust before visiting the class;e_classpasses it to the lowering asname_from_context. js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 makes the lowering restore it withstatic { __name(this, "<name>") }(and__decorateElement(_init, 0, "<name>", ...), which also exposes it asctx.name); before js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 it used the name as a class binding when it was a valid identifier.E::Stringis stored as UTF-8 for ASCII-only literals and identifiers, and as UTF-16 for any literal with a non-ASCII character;dataof a UTF-16 string is not usable as bytes, which is why the old code refused those keys. Folding"a" + "b"produces a rope (linked segments);EString::sliceresolves both representations to one UTF-8 slice.E::InlinedEnumwraps the literal the parser substitutes for a same-file TypeScript enum member (Kind.Ais printed as"a" /* A */);unwrap_inlinedreturns that literal.Before/after on the repro from the report
["123","456","héllo","wörld"]["","_class","","_class"]["","","",""]["123","456","héllo","wörld"]Emitted code for
{ 123: @dec class {} }changes from__decorateElement(_init, 0, "", _dec, _class)to__decorateElement(_init, 0, "123", _dec, _class); for{ 123: class { @dec m() {} } }the class body starts withstatic { __name(this, "123") }instead of__name(this, "").