SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

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

0 0 150 Rust

parser: lower `using` in switch cases by wrapping the whole switch (#31262)

### Repro

Found by parser fuzzing (invariant: printed output does not reparse,
loader=jsx):

```js
new Bun.Transpiler({ loader: "jsx", target: "node", minifyWhitespace: true }).transformSync(
  "switch (dom()) {\n case 0:\n using d23 = { [Se]() {} };\n default:\n using d24 = { [ose]() {} };\n }",
);
```

The printed output declared `let __bun_temp_ref_1$` twice in the same
switch block scope, so re-parsing it fails with `"__bun_temp_ref_1$" has
already been declared`. Any downstream consumer of the transpiled output
(e.g. a second build pass) hits the same SyntaxError.

### Cause

Two problems compound:

1. **`using` in switch cases was lowered per case body.** Case bodies
were visited with `StmtsKind::None`, so the generic statement-list
lowering in `visit_stmts` wrapped each case body in its own `let stack /
try / catch / finally`. All cases share the switch's single block scope,
so the per-case `let` stack declarations collide. It is also
semantically wrong: the TC39 proposal scopes `using` in a case clause to
the whole switch block (disposal happens when the switch exits, after
fall-through), and the rewritten `const` binding ended up inside the
generated `try` block where later cases can't see it. esbuild instead
wraps the entire switch statement once.

2. **`temp_ref_count` was reset per body and never restored.** Visiting
the `[Se]() {}` methods inside the case values resets the counter to 0,
so the second case's lowering reused `__bun_temp_ref_1$`…`_4$`. When the
renamer isn't used (no bundling / identifier minification), the
generated name is printed verbatim, so the counter must stay unique for
the whole file.

### Fix

`src/js_parser/visit/visit_stmt.rs`:
- Visit case bodies with `StmtsKind::SwitchStmt` (the existing guard in
`visit_stmts` already skips per-list lowering for that kind).
- After visiting the cases — while the switch's block scope is still
current — if any case body contains a `using` / `await using` that needs
lowering, scan all case bodies with one `LowerUsingDeclarationsContext`
and wrap the entire switch statement in a single `let stack / try /
catch / finally`, matching esbuild's lowering:

```js
let _stack = [];
try {
  switch (dom()) {
    case 0:
      const d23 = __using(_stack, { [Se]() {} }, 0);
    default:
      const d24 = __using(_stack, { [ose]() {} }, 0);
  }
} catch (_catch) {
  var _err = _catch, _hasErr = 1;
} finally {
  __callDispose(_stack, _err, _hasErr);
}
```

`src/js_parser/visit/mod.rs`:
- Stop resetting `temp_ref_count` in `visit_stmts_and_prepend_temp_refs`
so `__bun_temp_ref_{n}$` names stay monotonic (unique) across the file.
Existing outputs (including the `using` snapshots) are unchanged because
list-level lowering always runs after the nested bodies in that list
have been visited.

### Verification

New tests in `test/bundler/transpiler/transpiler.test.js` (`using
declarations in switch statements`):

- the fuzz input reparses and is wrapped in exactly one try/finally
(minified and non-minified)
- `await using` in switch cases gets the same treatment
- sibling switches in one scope keep distinct temp refs
- switch + top-level `using` combination reparses and the case bindings
stay `const` (they can never be exported, so they are not rewritten to
`var` by the module-level wrap; esbuild emits `var` here, this
intentionally keeps `const`)
- running the lowered output disposes at switch exit in reverse order
(`dispose b`, `dispose a` after fall-through) and the binding from `case
0` stays visible in `default`

```
USE_SYSTEM_BUN=1 bun test test/bundler/transpiler/transpiler.test.js -t "using declarations in switch"   # 4 fail (reparse error, ReferenceError, early disposal)
bun bd test test/bundler/transpiler/transpiler.test.js    # 131 pass, 0 fail (snapshots unchanged)
bun bd test test/js/bun/resolve/lower-using-bun-target.test.ts   # 11 pass
bun bd test test/bundler/esbuild/default.test.ts / ts.test.ts / dce.test.ts / lower.test.ts   # 0 fail
bun bd test test/bundler/transpiler/es-decorators.test.ts        # 27 pass
```
R
robobun committed
8d7c08040c83feec772169f800c0df07febdd128
Parent: c80b710
Committed by GitHub <noreply@github.com> on 5/23/2026, 6:45:53 AM