SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 207 Rust

Reject export clauses inside a non-declare TypeScript namespace instead of panicking (#31266)

### Repro

Found by transpiler fuzzing (80 bytes, minimized):

```
bun -e 'new Bun.Transpiler({loader:"tsx", target:"node", minifyWhitespace:true, deadCodeElimination:true}).transformSync(atob("bmFtZXNwYWNlIE0gew1leHBvcnQgaW1wb3J0IE1fQSA9IE07fQ0KZXhwb3J0IG5hbWVzcGFjZSBNIHsNZXhwb3J0IHtNX0EgYXMgYX07fQ0="))'
panic: index out of bounds: the len is 0 but the index is 4294967295
Crashed while printing input.tsx
```

Decoded, the input is:

```ts
namespace M {
export import M_A = M;}
export namespace M {
export {M_A as a};}
```

The minify/DCE/target options are irrelevant; any `transformSync` of
this source panics.

### Cause

`export { ... }` / `export * from ...` clauses inside a non-declare
`namespace`/`module` block are invalid TypeScript — tsc reports TS1194
("Export declarations are not permitted in a namespace") and esbuild
rejects them with `Unexpected "{"` / `Unexpected "*"`. Bun accepted them
because the guard in `t_export` (`src/js_parser/parse/parse_stmt.rs`)
has a misplaced negation relative to esbuild:

```rust
// Bun (TAsterisk / TOpenBrace):    error iff !module && !namespace && declare   ← unreachable, dead code
!opts.is_module_scope && !(opts.is_namespace_scope || !opts.is_typescript_declare)
// esbuild (and Bun's own TDefault arm): error iff !module && (!namespace || !declare)
!opts.is_module_scope && (!opts.is_namespace_scope || !opts.is_typescript_declare)
```

So the clause parses, and `export {M_A as a}` in the second namespace
block resolves `M_A` through the sibling namespace's exported members.
That lazily creates a property-access symbol whose `namespace_alias` has
the default `import_record_index: u32::MAX` (there is no import record —
it aliases the namespace IIFE argument). The printer's `SExportClause`
arm then does

```rust
let import_record = self.import_record(namespace.import_record_index as usize);
```

before checking `was_originally_property_access`, indexing
`import_records` (len 0) with `u32::MAX` → panic. The Zig implementation
had the same pattern but only "worked" because release builds skip the
bounds check and the bogus pointer is never dereferenced when
`was_originally_property_access` is false; it silently emitted invalid
JS instead:

```js
// bun 1.3.13 output for the repro — `export` inside an arrow body, SyntaxError when loaded
((M) => {

  export { M_A as a };
})(M ||= {});
```

### Fix

- `src/js_parser/parse/parse_stmt.rs`: use esbuild's condition for the
`export *` and `export {` arms, so these clauses are a syntax error
inside a non-declare namespace (`Unexpected {` / `Unexpected *`).
Ambient contexts (`declare namespace`, `declare module "m"`, nested
namespaces inside a `declare`) still allow them and erase the body as
before, and `export import A = B`, `export
const/function/class/enum/namespace ...` inside namespaces are
untouched.
- `src/js_printer/lib.rs`: in the two `SExportClause` arms, only look up
the import record inside the `was_originally_property_access` branch —
the only place it's used and the only case where the index is guaranteed
valid. This mirrors the bounds-guarded `EImportIdentifier` path so a
namespace-alias symbol without an import record can never panic the
printer.

### Verification

New test `Bun.Transpiler > TypeScript > rejects export clauses inside a
non-declare namespace` in `test/bundler/transpiler/transpiler.test.js`
covers the fuzz input (plus simpler variants), the ambient-context cases
that must keep working, and `export import` inside a namespace.

```
USE_SYSTEM_BUN=1 bun test test/bundler/transpiler/transpiler.test.js -t "rejects export clauses"   # panics: index out of bounds (the bug)
bun bd test test/bundler/transpiler/transpiler.test.js   # 127 pass, 0 fail
bun bd test test/bundler/esbuild/ts.test.ts              # 57 pass, 0 fail
bun bd test test/bundler/transpiler/scope-mismatch-panic.test.ts   # 8 pass, 0 fail
bun bd test test/bundler/bundler_edgecase.test.ts -t "Namespace"   # 6 pass, 0 fail
```

esbuild parity spot-checks: `namespace M { export {x}; }` → `Unexpected
"{"`, `namespace M { export * from "y"; }` → `Unexpected "*"`, `declare
namespace M { export {x}; }` / `declare module "foo" { export {x}; }` →
accepted and erased.
R
robobun committed
5da1ff86de6139ac08942992459e51eb1baec057
Parent: 41aa672
Committed by GitHub <noreply@github.com> on 5/24/2026, 2:29:30 AM