Don't emit reserved words as class names when lowering decorated anonymous class expressions (#31265)
### What does this PR do?
Fixes a fuzzer-found invariant violation (`printed output does not
reparse`) in the standard (TC39) decorator lowering: decorating an
anonymous class expression whose inferred name is a reserved word
produced syntactically invalid output.
```ts
export default (@dec class {})
```
was lowered to
```js
export default (_dec = [dec], _init = __decoratorStart(undefined), _class = class default {
}, _class = __decorateElement(_init, 0, "default", _dec, _class), ...);
```
`class default {}` is a syntax error, so:
- `Bun.Transpiler` / `bun build --target=node|browser` emitted output
that fails to parse
- running a plain `.js` module containing `export default (@dec class
{})` (or `export default @dec class {}`) failed at runtime with
`SyntaxError: Unexpected keyword 'default'`
The same thing happened for any other context-inferred name that can't
be a binding identifier, e.g. `{ default: (@dec class {}) }`, `{ "foo
bar!": (@dec class {}) }`, `{ 0: (@dec class {}) }`, `{ eval: (@dec
class {}) }`.
### How did we fix it?
When the lowering rewrites an anonymous decorated class expression, it
attaches the context-inferred name (from `export default`, object
property keys, assignment targets, …) as the class's syntactic binding
name so `Class.name` survives the rewrite. That is only legal when the
name is a valid strict-mode binding identifier (class bodies are always
strict and the output may be a module).
`lower_impl` now only attaches the name when it passes that check (valid
identifier, not a keyword, not strict-mode reserved, not
`await`/`eval`/`arguments`); otherwise the class expression stays
anonymous — the same shape esbuild emits. The decorator context `name`
string and the `__name` call inside `__decorateElement` are unchanged,
so `ctx.name` and `Class.name` still evaluate to `"default"` for a
decorated anonymous default export. Valid inferred names (`let x = (@dec
class {})` → `class x {}`) are unaffected.
### How did you verify your code works?
Repro (before): `bun -e 'new Bun.Transpiler({loader:"ts", target:"node",
deadCodeElimination:true}).transformSync("export default(@c class{})")'`
emitted `_class = class default {}`; running a `.js` module with `export
default (@dec class {})` threw `SyntaxError: Unexpected keyword
'default'`.
Added tests to `test/bundler/transpiler/es-decorators.test.ts`:
- `export default (@dec class {})` and `export default @dec class {}`
run, `Class.name` and `ctx.name` are `"default"`
- `export default (class { @dec foo() {} })` (elements-only decoration)
runs
- `{ default: (@dec class {}) }` runs with correct name
- `Bun.Transpiler` output for the fuzz repro contains no `class default`
and reparses
All 5 fail on bun without this change (SyntaxError / invalid output) and
pass with it; the rest of `es-decorators.test.ts`,
`es-decorators-esbuild.test.ts`, `decorators.test.ts`,
`decorator-metadata.test.ts`, and `bundler_decorator_metadata.test.ts`
(180 tests) pass. R
robobun committed
c80b71085b4bc63d660d77a5a658e2f006eacaba
Parent: 3aa8a0e
Committed by GitHub <noreply@github.com>
on 5/23/2026, 6:45:07 AM