Skip to content

js_parser: name an anonymous decorated export default class "default" - #38758

Open
robobun wants to merge 14 commits into
mainfrom
farm/b0c910ff/decorated-anonymous-class-name
Open

robobun wants to merge 14 commits into
mainfrom
farm/b0c910ff/decorated-anonymous-class-name

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Status: the former base of this PR, #38757, is closed. #40833 superseded it. This PR now has main as its base and needs a rebase. Until then the diff also shows the old commits of #38757 and #38922. The own commits of this PR are 8b5c474, a547191, d7497b6 and 9ca0448. The text below describes the PR as it was on top of #38757. See #38758 (comment) for the cases that still fail on main.

Problem

  • With standard (TC39) decorators, an anonymous export default class statement gets the wrong name:
    • export default class { @dec m() {} } in mod.js has .name === "mod_default"; without the decorator it is "default".
    • @dec export default class {} (decorator before export) sets .name to "mod_default" and passes it to the class decorator as context.name. The export default @dec class {} and export default (class { ... }) forms are expressions and are handled by js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757.
  • Cause: s_export_default (src/js_parser/visit/visit_stmt.rs:824) gives an anonymous class the module's default-export symbol as its class name, because decorator lowering needs a binding to refer to the class by. lower_impl in src/js_parser/lower/lower_decorators.rs cannot tell that binding apart from a user-written name, so the emitted declaration is class mod_default { ... } and the __decorateElement name argument is "mod_default".

Fix

  • s_export_default passes "default" down as the class's name-from-context (through lower_class and lower_standard_decorators_stmt) exactly when it injects the default-export symbol; every other statement passes None, so a named class is unchanged.
  • lower_impl treats a statement with a name-from-context as anonymous: it gets the same leading static { __name(this, "default"); } block js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 emits for anonymous class expressions (same static name member guard), and "default" is what __decorateElement receives when there are class decorators, so .name and context.name are both "default".
  • Why this is correct: the spec names an anonymous default-export class "default" at class definition, before its static elements run and before decorators are applied; the block runs at that point, and the mod_default symbol stays a purely internal binding (it is still what the module exports as default). Legacy experimentalDecorators lowering ignores the argument and is unchanged (tsc also emits a generated name there, default_1).
  • Verified with test/bundler/transpiler/es-decorators.test.ts, export default class block: the statement form (checking a static field initializer, a relocated static block and a static method initializer all observe "default", and that console.log shows [class default] and default {} rather than the mod_default binding), the TypeScript variant, @dec export default class {} (context.name, the class passed to the decorator, the import, and its console.log output), a default export declaring its own static name getter (block suppressed) next to one with a decorated static accessor name (block emitted, an earlier static field sees "default"), and a named @dec export default class Named that must keep reporting Named. Built on js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 (68a8a87) without the source change, four of these fail with mod_default and the named one passes; with it all 72 tests in the file pass.
  • Also passing on the debug build of this branch: es-decorators-esbuild.test.ts (esbuild's 147 decorator tests), decorators.test.ts, decorator-metadata.test.ts, bundler_decorator_metadata.test.ts.

Background

  • An anonymous export default class {} has no binding of its own in the source; the module's default export refers to it through a synthesized symbol (printed as mod_default). Decorator lowering emits the class as a declaration and then statements that reference it (__decorateElement(...), relocated static blocks), so it needs that symbol to be the declaration's name.
  • js_parser: keep the inferred name of lowered anonymous decorated class expressions #38757 stops the expression path from turning an inferred name into a class binding and instead restores the name with a static { __name(this, "<name>") } block at the top of the body, which runs before any other static element; name_from_context is the inferred name it uses. This PR feeds the statement path into that same mechanism. inspect: display a class or function name set with Object.defineProperty #38922 makes console.log / Bun.inspect display a name set this way, which is why the display assertions here need the full stack.
Emitted code and earlier revisions

export default class { @dec m() {} } now lowers to:

export default class d_default {
  static {
    __name(this, "default");
  }
  constructor() { __runInitializers(_init, 5, this); }
  m() {}
}
__decorateElement(_init, 1, "m", _dec, d_default);
__decoratorMetadata(_init, d_default);

and @dec export default class {} passes "default": d_default = __decorateElement(_init, 0, "default", _dec, d_default).

Earlier revisions of this PR (50085c0, 4bf3526) also replaced the expression path's class binding, first with a __name call after the class and then with the leading static block. #38757, opened a minute earlier, makes the same expression-path change with the same guard, so this PR was reduced to the statement-form delta on top of it. The review findings on the earlier revisions (undecorated static fields running before an appended __name; the guard missing @dec static ["name"]()) are addressed in #38757's shape, which this commit reuses.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8551d7ca-3f2f-42a5-b303-6f625e2dcfac

📥 Commits

Reviewing files that changed from the base of the PR and between c2fa121 and 7c08532.

📒 Files selected for processing (4)
  • src/js_parser/lower/lower_decorators.rs
  • src/js_parser/p.rs
  • src/js_parser/visit/visit_stmt.rs
  • test/bundler/transpiler/es-decorators.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:05 AM PT - Aug 15th, 2026

❌ @robobun, your commit 9ca0448 has some failures in Build #97785 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38758

That installs a local version of the PR into your bun-38758 executable, so you can run:

bun-38758 --bun

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review, stacked on #38757 (which is stacked on #38922); base is #38757's branch at 68a8a87, and this lands after it and gets rebased onto main once it is in.

Reproduced on main with bun run of a two-file module: export default class { @dec m() {} } imported from another file prints mod_default for .name (and [class mod_default] in console.log), and @dec export default class {} passes mod_default to the class decorator as context.name. The expression forms and the bundler rename that earlier revisions of this PR also covered are fixed by #38757.

The delta here is visit_stmt.rs / p.rs passing "default" into the lowering and lower_impl treating that statement as anonymous; tests are the five new cases in the export default class block of test/bundler/transpiler/es-decorators.test.ts (four fail on the base without this change, all pass with it; esbuild's decorator suite and the other decorator files pass on the debug build). Current head: 9ca0448, CI green on every lane that runs (build 97785).

Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread test/bundler/transpiler/es-decorators.test.ts Outdated
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

This overlaps with #38757 (opened a minute earlier), which removes the same binding for the expression path. One difference in placement matters: emitting __name in the suffix runs it after the class body has been evaluated, and undecorated static fields (and the generated __privateAdd blocks for static private fields) stay in the body, so they observe the temporary name. Built at 50085c0:

function dec() {}
const Bar = class {
  static field = this.name;
  static #priv = this.name;
  static priv() { return this.#priv; }
  @dec m() {}
};
console.log(JSON.stringify({ name: Bar.name, field: Bar.field, priv: Bar.priv() }));
const Baz = @dec class { static field = this.name; };
console.log(JSON.stringify({ name: Baz.name, field: Baz.field }));
this branch:  {"name":"Bar","field":"_class","priv":"_class"}  {"name":"Baz","field":"_class"}
bun 1.4.0:    {"name":"Bar","field":"Bar","priv":"Bar"}        {"name":"Baz","field":"Baz"}
node (decorators removed): same as 1.4.0

#38757 avoids this by opening the class body with static { __name(this, "Bar") } (esbuild's --keep-names shape), unconditionally for anonymous expressions, with the same static-name guard as here. The export default class { @dec m() {} } statement fix in this PR is not covered by #38757 and would be welcome as a change on top of it; the same placement concern applies to export default class { static x = this.name; @dec m() {} }, where a leading static block also works.

Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/visit/visit_stmt.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Updated for the review, in 4bf3526:

  • The name is now restored from a leading static { __name(this, "<name>"); } block inside the class body instead of a call appended after the class. Undecorated static fields stay in the body, so static observed = this.name saw "_class" with the previous shape (also with a class decorator, where __decorateElement renames the class later); the block runs before them. It is emitted for every anonymous lowered class.
  • The static name guard now looks at the source properties before Phase 2 hoists decorated computed keys (so @dec static ["name"]() {} is left alone; previously it was renamed and then failed inside __decorateElement), and only covers methods, getters, setters and accessors, since a static name field runs after the block and overrides it on its own. A computed key that only evaluates to "name" at runtime is not detectable; that is documented on the helper and in the description.
  • The build step in the bundling test drains stdout too.

Tests added for each of these (static initializers with and without a class decorator, the computed and accessor name members, a static before = this.name ordering check); 7c08532 only trims comments. The description is updated to describe the current shape. The two CodeRabbit comments above are rate-limit notices.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three findings from my earlier review are addressed in 4bf3526/7c08532 and this pass found nothing new; given the change touches spec-ordering-sensitive decorator lowering and overlaps with #38757, a human sign-off on the approach and coordination is still warranted.

Checked in this pass: has_static_name_method_or_accessor runs on class.properties in Phase 1 before Phase 2 hoists computed keys, and IsMethod covers getters/setters (parse_property.rs:225). The __name static block is inserted at index 0 after the static_private_add_blocks prepend (lower_decorators.rs:2412-2438), so it also runs before lowered static #priv initializers. source_class_name reading the symbol's original_name for named statement classes preserves the pre-PR class-decorator name string. The bun build spawn now drains stdout.

Extended reasoning...

Overview

The PR fixes .name on lowered anonymous classes with standard (TC39) decorators by replacing the previous "attach the inferred name as a syntactic class binding" approach with a leading static { __name(this, "<name>") } block. It threads name_from_context through lower_class → lower_standard_decorators_stmt → lower_impl so the anonymous export default class statement path knows to emit "default" instead of the synthesized mod_default symbol name, and it unifies the class-decorator name string on the same source_class_name. Eleven new tests cover both export default forms, non-identifier and empty inferred names, static-initializer ordering with and without a class decorator, the outer binding staying visible inside the body, declared static name members (field/getter/method/decorated/computed/accessor), and bundler renaming.

Prior review round

My first-round review (on the suffix-__name shape) flagged that undecorated static fields stay in the body and would observe "_class", that the static-name guard ran after Phase 2 hoisted decorated computed keys, and a stdout-drain nit. All three are fixed in the current revision: the name is set from a leading static block inserted at new_properties[0] after the static_private_add_blocks merge (so it precedes both undecorated static fields and lowered static-private __privateAdd blocks), the guard now inspects the source class.properties in Phase 1 and only skips for methods/accessors (a static name field runs after the block and wins on its own), and the build subprocess drains all three streams. All prior inline threads are resolved. The comment-cop flags were addressed by trimming to short pointers in 7c08532; the remaining comments document ordering invariants the first review round tripped over.

Security risks

None. Transpiler output-shape correctness for decorator lowering; no untrusted-input parsing surface added, no auth/crypto/permissions.

Level of scrutiny

Medium-high. This is spec-driven transpiler output in a path with subtle ordering (SetFunctionName vs. static element evaluation vs. relocated blocks vs. __decorateElement). The first revision had a real ordering regression, which argues for a maintainer confirming the static-block approach and its interaction with the rest of Phase 7. The author also noted overlap with #38757 (which uses the same leading-static-block shape for the expression path); a human should decide how the two land relative to each other.

Other factors

Test coverage is thorough (11 new tests, 9 verified to fail on the unfixed build; esbuild's 147-test decorator suite and the other decorator/transpiler files reported passing). The two CI failures (test-http-chunk-problem.js, bake/deinitialization.test.ts) are unrelated to the parser.

@robobun robobun changed the title js_parser: set .name of lowered anonymous decorated classes with __name js_parser: name an anonymous decorated export default class "default" Aug 15, 2026
@robobun
robobun changed the base branch from main to farm/80aecbe5/decorator-inferred-class-name August 15, 2026 01:53
@robobun
robobun force-pushed the farm/b0c910ff/decorated-anonymous-class-name branch from 7c08532 to 48c88dc Compare August 15, 2026 01:53
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Restructured to avoid overlapping with #38757, which was opened a minute before this PR and makes the same expression-path change (drop the class binding, restore the name from a leading static { __name(this, ...) } block, same static name guard) that the previous revisions here ended up with.

This PR is now stacked on that branch (base set to farm/80aecbe5/decorator-inferred-class-name) and only carries the statement form, which #38757 does not cover: export default class { @dec m() {} } and @dec export default class {} still get mod_default as .name and context.name there. The delta is 48c88dc: s_export_default passes "default" down when it injects the default-export symbol, and lower_impl treats such a statement as anonymous. Built on #38757 without it, the three new statement tests fail with mod_default; with it the file passes (69), as do esbuild's decorator suite and the other decorator files.

The description is rewritten for the current diff; the earlier revisions are summarized in its details block. Once #38757 lands this gets rebased onto main.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed after the restructure onto #38757 and the switch to the leading static { __name(this, ...) } block — the three findings from the earlier revision (undecorated static fields observing _class, the defines_static_name_method guard missing @dec static ["name"], and the undrained build stdout) are all addressed, and this run found no new issues. Since it is stacked on an unlanded PR and changes spec-observable decorator lowering, a human look before merge is still worthwhile.

What was reviewed:

  • defines_static_name_method now runs on source properties before Phase 2 rewrites computed keys; verified IsMethod covers getters/setters so the guard matches the test's Getter/Setter/Method/Accessor cases.
  • The !is_expr && name_from_context.is_some() anonymity check — only s_export_default sets it (when injecting the default-export symbol), and the other lower_class call site passes None, so a named class stays untouched (covered by the Named test).
  • new_properties.insert(0, block) places the __name block ahead of every retained static element, including undecorated static fields and the __privateAdd blocks for static privates — the static field/static #priv/Field.seenBefore tests exercise that ordering.
Extended reasoning...

Overview

The PR threads a name_from_context: Option<&[u8]> parameter from visit_stmt.rs (s_export_default) through p.rs::lower_class into lower_decorators.rs::lower_standard_decorators_stmt and lower_impl, so an anonymous export default class { @dec ... } statement is treated as anonymous by the standard-decorator lowering. Combined with the base PR (#38757), the lowered class gets a leading static { __name(this, "default") } block and __decorateElement receives "default" as the class name, instead of the module's synthesized mod_default symbol leaking into .name and context.name. The diff shown is against main, so it also includes #38757's expression-path change (removal of can_be_class_binding_name, the defines_static_name_method helper, and the restore_inferred_name block insertion at Phase 7).

Security risks

None. This is a pure transpiler-output correctness change; no untrusted-input parsing paths, allocation sizing, or privilege boundaries are touched.

Level of scrutiny

Medium-high. Decorator lowering is spec-sensitive and the earlier revisions of this PR had real ordering bugs (undecorated static fields observing the temporary _class name; the name guard running after Phase 2 had rewritten computed keys). Those are now fixed by inserting the __name call as the first static block and computing the guard from class.properties before any rewriting. I traced IsMethod through parse_property.rs to confirm it is set for getters and setters, so the guard's IsMethod || AutoAccessor predicate matches all the method-shaped cases the test exercises; fields are intentionally excluded because they run after the leading block and win on their own. The one undecidable case (a runtime-computed key evaluating to "name") is acknowledged and unchanged from #38757's shape — it is not a regression relative to that base.

Other factors

  • Stacked PR. The base is #38757, not main. Approving this in isolation would be premature; the combined diff should land together or #38757 first.
  • Test coverage. Four new statement-form tests (member decorators, TypeScript variant, class decorator, named-class negative), plus the expression-path suite from #38757 (every naming context, non-identifier names, static-initializer ordering, the nine-variant static name member matrix, bundler rename/minify survival). The Field.seenBefore === "Field" assertion specifically pins the leading-block ordering that the first revision got wrong.
  • Prior review resolved. All three of my earlier inline findings and the comment-cop notes are addressed in the current commits (4bf3526 → 8469ac3); the author's replies match what the diff now does.
  • Legacy path. lower_class only forwards name_from_context to lower_standard_decorators_stmt; the legacy experimentalDecorators branch in the same function ignores it, matching tsc's default_1 behavior as noted in the description.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Agreed on the order: #38757 first, then this gets rebased onto main and re-run; nothing here is meant to be merged on its own.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up since this is now based on #38757: that branch was rebased onto current main (head 4997c18, same content), so this one will want a rebase onto it to keep the diff down to the statement-form change.

…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".
@robobun
robobun force-pushed the farm/80aecbe5/decorator-inferred-class-name branch from e32d6e9 to 68a8a87 Compare August 15, 2026 07:09
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Another rebase heads-up: #38757 is now itself based on #38922 (the formatter has to display a name set with Object.defineProperty, otherwise classes lowered this way print as [class _class] in console.log and bun:test). The statement form here has the same display question: with __name/a static block, export default class { @dec m() {} } will print as [class mod_default] until #38922 lands, and should print [class default] after it, which is worth a console.log assertion in this PR too. Current head of #38757 is 68a8a87.

The visitor gives an anonymous `export default class` the module's
default-export symbol as its class name so that decorator lowering has a
binding to refer to, and the lowering then treated that symbol as the
class's real name: `.name` came out as `mod_default`, and a class decorator
received it as context.name. Pass "default" down as the class's
name-from-context in that case so the lowering restores it with the same
leading static block it already emits for anonymous class expressions and
hands it to __decorateElement.
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased again onto the current tip of #38757 (68a8a87, which now also carries its inspect change); still the same 3-commit statement-form delta, 72/72 in the decorator file and the other decorator suites green on the debug build.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline nit, I checked that both lower_class statement callers are updated (the non-default s_class path passes None, so named declarations are unaffected), that the widened class_is_anonymous at the class_name_str branch is behaviour-identical for expressions (original_class_name_for_decorator still takes precedence for named exprs), and that the legacy-decorator path in lower_class never sees the new argument.

Extended reasoning...

The current delta is a small mechanical extension of #38757's mechanism: thread name_from_context through lower_class → lower_standard_decorators_stmt → lower_impl, and widen two conditions from expr_class_is_anonymous to class_is_anonymous. I traced each condition's callers and confirmed the expression path is unchanged; the only new behaviour is the anonymous export default class statement getting the leading __name block and "default" as the __decorateElement name. The one open item is the 07:12 suggestion for a statement-form console.log(Cls) assertion, which the inline nit covers.

Comment thread test/bundler/transpiler/es-decorators.test.ts
@robobun
robobun force-pushed the farm/b0c910ff/decorated-anonymous-class-name branch from cdec8c8 to 9ca0448 Compare August 15, 2026 09:07

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the console.log/Bun.inspect assertion from the last round is in (9ca0448), and the statement-form delta on top of #38757 looks correct.

What was reviewed:

  • name_from_context threading: only two lower_class callers exist, both updated; only the anonymous-default branch sets it, so named default exports and ordinary class statements pass None and are unaffected.
  • class_is_anonymous widening in lower_impl: for statements it reduces to name_from_context.is_some(), so the __decorateElement name string and restore_inferred_name both pick up "default" only for the injected-symbol case.
  • Tests cover the member-decorator and class-decorator statement forms, TS variant, the named-class negative case, the static name guard, and now formatter output.
Extended reasoning...

Overview

This is the statement-form delta stacked on #38757. It threads an Option<&[u8]> name-from-context through lower_class → lower_standard_decorators_stmt → lower_impl, set to ClauseItem::DEFAULT_ALIAS (b"default") exactly when s_export_default injects the module's default-export symbol as an anonymous class's binding. lower_impl then treats such a statement as anonymous (widening expr_class_is_anonymous to class_is_anonymous), so it gets #38757's leading static { __name(this, "default") } block and __decorateElement receives "default" instead of "mod_default". Five new tests in es-decorators.test.ts cover the variants; a runFiles helper is added alongside the existing runDecorator.

Changes since last review

My only outstanding note (08:24Z) was a nit asking for a console.log(Cls) / Bun.inspect assertion so the formatter path is pinned for the statement form. Commit 9ca0448 adds console.log(Cls.name, Cls, Bun.inspect(new Cls(), { compact: true })) to the first test (asserting [class default] default {}) and console.log(Cls.name, Cls) to the class-decorator test (asserting [class default]). That covers the ask; nothing else changed. All earlier findings (undecorated static field ordering, the defines_static_name_member computed-key gap, undrained stdout) were about the pre-restructure revisions and now live in #38757, whose mechanism this PR reuses without modification.

Security risks

None. This is compile-time transpiler output shaping for a spec-compliance fix (anonymous default-export class .name). No untrusted-input parsing, no allocation sizing driven by external data, no privilege or filesystem surface.

Level of scrutiny

Medium — js_parser is production-critical, but the diff is ~15 source lines that plumb one optional parameter and widen one boolean. I verified there are exactly two lower_class call sites (both updated), that DEFAULT_ALIAS is the existing b"default" constant used elsewhere in visit_stmt.rs for the same purpose, and that the legacy TS-decorator path in lower_class does not consume the new argument (matching the description's note that legacy lowering is unchanged).

Other factors

  • The bug-hunting system found no issues on this revision.
  • Test coverage is thorough for the delta: statement form with a static field / static block / static-method initializer all observing "default", the TypeScript entry point, @dec export default class {} checking ctx.name/cls.name/the import, a named default export as the negative case, and the static-name guard interacting with a getter vs. a decorated accessor. All follow the file's harness conventions (test.concurrent, tempDir, all three pipes drained, stderr/stdout asserted before exit code).
  • This is stacked; it merges into #38757's branch and gets rebased onto main after that lands. The delta reviewed here should be identical post-rebase.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

9ca0448 adds the display assertions: the first statement-form test logs the class and an instance (default [class default] default {}) and the class-decorator statement test logs the class. On the base (68a8a87, which includes #38922) without the source change here both still print mod_default, so the statement shape (named declaration plus the __name block) is pinned separately from the expression shape. The empty retrigger commit is dropped; the branch is 4 commits on 68a8a87 and mergeable against it.

@robobun
robobun force-pushed the farm/80aecbe5/decorator-inferred-class-name branch from 68a8a87 to 1ed1f67 Compare August 15, 2026 13:20
@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

The base of this PR, #38757, is closed. #40833 (a22b2aa) superseded it: the merged lowering has no _class temporary and no synthesized class binding. I changed the base of this PR to main, so that it does not point at the branch of a closed PR. Until the branch is rebased, the diff also shows the old commits of #38757 and #38922.

The statement form that this PR fixes still fails on main. Measured on a debug build of 09bb546 under bun run, with mod.js imported from another file:

mod.js main 09bb546 node, decorators removed
export default class { @dec m() {} } .name is "mod_default" "default"
export default class { static seen = this.name; @dec m() {} } .name and seen are "mod_default" "default"
@dec export default class {} .name and context.name are "mod_default" "default"
export default @dec class {} and export default (class { @dec m() {} }) "default" "default"

The cause on main is the same as before. s_export_default (src/js_parser/visit/visit_stmt.rs:821) gives the anonymous class the default export symbol as its class name when should_lower_standard_decorators is set. lower_standard_decorators_stmt (src/js_parser/lower/lower_decorators.rs:721) passes None as the name from the context, so lower_class_body takes "mod_default" from class_name.

This PR needs new code on top of the lower_decorators.rs of main. A replay of the commits is not sufficient:

@robobun
robobun changed the base branch from farm/80aecbe5/decorator-inferred-class-name to main September 13, 2026 19:23
@robobun

robobun commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

#44723 passes the name default into the statement lowering of standard decorators on current main. It covers the two statement cases of the Problem section here: @dec export default class {} gets .name and context.name default, and export default class { @dec m() {} } keeps .name default without a bundle. It does not touch the display part: Bun.inspect still prints the binding <file>_default for a class-decorated anonymous default class. If #44723 merges, this PR can close or shrink to the display part.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant