Keep the TS namespace closure argument from colliding with namespace members (#31261)
### Repro
Found by parser fuzzing (invariant: printed output does not reparse,
loader=tsx):
```ts
bun -e 'new Bun.Transpiler({loader:"tsx", target:"browser"}).transformSync("namespace m2{class m2 { } class _m2 { }}")'
```
produced
```js
var m2;
((_m2) => {
class m2 {
}
class _m2 {
}
})(m2 ||= {});
```
which is not valid JavaScript — the closure argument `_m2` re-declares
the class `_m2`. The same input as a regular `.ts` file fails at
runtime, so this breaks executing valid TypeScript, not just
`Bun.Transpiler`:
```ts
namespace m2 {
class m2 {}
class _m2 {}
export const x = 42;
}
console.log(m2.x);
// SyntaxError: Cannot declare a class twice: '_m2'.
```
`tsc` compiles this fine (it names the argument `m2_1`), and the bundler
path is unaffected because the linker runs the symbol renamer.
### Cause
In `parse_type_script_namespace`
(`src/js_parser/parse/parse_typescript.rs`), when the namespace name
collides with one of its own members, the generated closure argument
falls back to `"_" + name` without checking whether that prefixed name
is *also* declared in the namespace body. The comment inherited from
esbuild says the renamer will fix collisions, but the non-bundler print
paths (runtime transpiler, `Bun.Transpiler`, `bun build --no-bundle`)
print symbols with `NoOpRenamer`, i.e. by their original names, so the
collision ends up in the output as a duplicate block-scoped declaration.
### Fix
Keep prepending `_` until the generated argument name no longer collides
with any symbol declared in the namespace scope (`_m2` → `__m2` for the
repro). Enum closures don't need the same treatment because their bodies
only contain property assignments, never declarations.
### Verification
- Original repro now prints `((__m2) => { ... })(m2 ||= {})` and the
output reparses; the runtime example prints `42`.
- New tests in `test/bundler/transpiler/transpiler.test.js` ("generated
closures"): exact printed output for the colliding case, a baseline for
the existing single-collision rename, and a spawn test that runs the
colliding namespace and checks its exports. The two collision tests fail
on bun without this change and pass with it:
```
bun bd test test/bundler/transpiler/transpiler.test.js # 129 pass, 0 fail
bun bd test test/bundler/esbuild/ts.test.ts # 57 pass, 0 fail
bun bd test test/bundler/transpiler/runtime-transpiler.test.ts # 13 pass, 0 fail
``` R
robobun committed
b733df44bcef0d9c691e99253c0c67eca681effb
Parent: aad5561
Committed by GitHub <noreply@github.com>
on 5/23/2026, 6:30:33 AM