Skip to content

parser: apply ASI when TS contextual keywords precede a newline - #34258

Merged
dylan-conway merged 11 commits into
mainfrom
farm/80275baa/ts-contextual-keyword-asi
Jul 15, 2026
Merged

parser: apply ASI when TS contextual keywords precede a newline#34258
dylan-conway merged 11 commits into
mainfrom
farm/80275baa/ts-contextual-keyword-asi

Conversation

@robobun

@robobun robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

Bun's TS parser treats declare, abstract, interface, and accessor as modifiers even when a newline follows them, silently deleting the declaration that comes after. esbuild and tsc both apply ASI and treat the keyword as a standalone identifier expression (statement level) or class field (class body).

$ printf 'declare\nfunction foo() { return 1 }\nconsole.log(foo())\n' > r.ts
$ bun build r.ts --no-bundle
console.log(foo());                 # function body gone; ReferenceError at runtime
$ npx esbuild r.ts
declare;
function foo() { return 1; }
console.log(foo());
$ printf 'class Foo { declare\n foo() { return 1 } }\nconsole.log(new Foo().foo())\n' > r2.ts
$ bun build r2.ts --no-bundle
class Foo {}                        # method deleted; new Foo().foo() throws
console.log(new Foo().foo());

Sibling shapes with the same bug: abstract\nclass A {}, interface\nA\n{ sideEffect() }, abstract class A { abstract\nfoo() {} }, export default abstract\nclass A {}, class A { accessor\n x = 1 }. Also class A { get\n*x() {} } was rejected with Unexpected *; esbuild/tsc parse a field get followed by a generator *x.

Cause

esbuild (internal/js_parser/js_parser.go) gates each of these paths on !p.lexer.HasNewlineBefore before committing to the modifier interpretation, and validates the body of every declare statement after the recursive parse. Bun's port lost both:

  • TsStmtDeclare, TsStmtInterface, TsStmtAbstract in parse_stmt_fallthrough_ts_keyword had no newline check.
  • The export default abstract class path had no newline check.
  • Class-body PDeclare, PAbstract, PAccessor in parse_property had no newline check.
  • could_be_modifier_keyword counted * after get/set as a modifier follow-up; esbuild excludes it.
  • TsStmtDeclare unconditionally wrapped whatever the recursive parse returned in S::TypeScript{}, so a newline-split keyword that fell through to SExpr silently left the rest of the input as live runtime statements (declare type\nFoo = number emitted Foo = number;).

Fix

  • Add the missing !p.lexer.has_newline_before gates at each site above.
  • In TsStmtDeclare, after the recursive parse_stmt, reject any result that is not STypeScript/SLocal/SEmpty with Unexpected "<token>" pointing at the token captured before recursion. This uniformly catches declare {interface,abstract,type,namespace,module,declare}\n, declare foo, and declare foo: bar while accepting every valid ambient form and everything inside ambient bodies (declare namespace { ... }, declare module "m" { ... }, declare global { ... }).
  • export default interface \n Foo {} stays accepted (esbuild explicitly allows a newline there) via the existing is_name_optional flag. export interface \n Foo {} now reports Unexpected "interface" like esbuild. @decorator followed by declare/abstract split by a newline reports Unexpected "declare"/"abstract" instead of the self-contradictory Expected "class" but found "class".
  • Drop the redundant is_typescript_declare = true pre-set in t_export's SDeclare arm (the declare arm sets it itself).

Verification

New test in test/bundler/transpiler/transpiler.test.js (contextual keywords followed by a newline apply ASI instead of acting as modifiers) covers 50 assertions across every shape above: the ASI cases, no-newline control cases, every valid declare X form, every rejected declare X form, ambient-body cases (accepted), export abstract\n/export declare\n (accepted), and the decorator variants. Fails at the first assertion on the unfixed build.

Two cases in test/bundler/transpiler/scope-mismatch-panic.test.ts (declare foo: bar and declare module : es2015) were previously accepted and are now parse errors matching esbuild; their assertions moved to the new test block.

  • bun bd test test/bundler/transpiler/transpiler.test.js — 172 pass, 0 fail
  • bun bd test test/bundler/esbuild/ts.test.ts — 57 pass, 0 fail
  • bun bd test test/bundler/transpiler/{decorators,decorator-metadata,es-decorators}.test.ts test/bundler/bundler_decorator_metadata.test.ts — 78 pass, 0 fail

Related: #29201 independently adds the accessor newline check while lifting the standard_decorators gate; whichever lands second has a one-hunk rebase on that branch.


[review] gate passed · iteration 3 · 4 files touched

fails on main (without fix)
ASAN without fix: 1 failed, 22 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/scope-mismatch-panic.test.ts test/bundler/transpiler/transpiler.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (8e0d9d11c)

test/bundler/transpiler/scope-mismatch-panic.test.ts:
(pass) scope mismatch panic regression test > should not panic with scope mismatch when arrow function is followed by array literal [741.41ms]
(pass) scope mismatch panic regression test > should not panic with simpler arrow function followed by array [440.32ms]
(pass) scope mismatch panic regression test > correctly rejects direct indexing into block body arrow function [428.17ms]
(pass) macro tagged templates visit their interpolations > tagged template macro with arrow interpolation in dead code is erased [431.69ms]
(pass) macro tagged templates visit their interpolations > tagged template macro with arr
... (truncated)

release without fix: 22 skipped
bun test v1.4.0-canary.1 (21df53534)

test/bundler/transpiler/scope-mismatch-panic.test.ts:
(pass) scope mismatch panic regression test > should not panic with scope mismatch when arrow function is followed by array literal [22.06ms]
(pass) scope mismatch panic regression test > should not panic with simpler arrow function followed by array [13.58ms]
(pass) scope mismatch panic regression test > correctly rejects direct indexing into block body arrow function [11.90ms]
(pass) macro tagged templates visit their interpolations > tagged template macro with arrow interpolation in dead code is erased [12.44ms]
(pass) macro tagged templates visit their interpolations > tagged template macro with arrow interpolation reports the macro error [13.21ms]
(pass) macro tagged templates visit their interpolations > member-expression macro tag with function interpolation reports the macro error [11.90ms]
(pass) TypeScript 'declare' statements discard scopes of dropped statements > declare global containing nested blocks followed by a class [24.10ms]
(pass) TypeScript 'declare' statements discard scopes of dropped statements > declare const with an arrow function initializer followe
... (truncated)
passes on PR (with fix)
ASAN with fix: 22 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/scope-mismatch-panic.test.ts test/bundler/transpiler/transpiler.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (8e0d9d11c)

test/bundler/transpiler/scope-mismatch-panic.test.ts:
(pass) scope mismatch panic regression test > should not panic with scope mismatch when arrow function is followed by array literal [740.34ms]
(pass) scope mismatch panic regression test > should not panic with simpler arrow function followed by array [440.90ms]
(pass) scope mismatch panic regression test > correctly rejects direct indexing into block body arrow function [435.01ms]
(pass) macro tagged templates visit their interpolations > tagged template macro with arrow interpolation reports the macro error [438.93ms]
(pass) macro tagged templates visit their interpolations > tagged template macro with ar
... (truncated)

release with fix: 22 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 761ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl
... (truncated)
diff hotspot
src/js_parser/parse/parse_property.rs              |  8 +-
 src/js_parser/parse/parse_stmt.rs                  | 75 ++++++++++++++++--
 .../transpiler/scope-mismatch-panic.test.ts        |  9 +--
 test/bundler/transpiler/transpiler.test.js         | 90 ++++++++++++++++++++++
 4 files changed, 165 insertions(+), 17 deletions(-)

gate history · 4 passed · 0 rejected · iteration 3

evidence per changed file
file                                                  reads  edits  tests
src/js_parser/parse/parse_property.rs                     3      4      0
src/js_parser/parse/parse_stmt.rs                        11     15      0
test/bundler/transpiler/scope-mismatch-panic.test.ts      3      5      0
test/bundler/transpiler/transpiler.test.js                4     10      0

…newline

Bun's TS parser treated declare, abstract, interface (statement level and
class-body level) and accessor (class body) as modifiers even when a
newline followed, silently deleting the declaration that came after.
esbuild and tsc both apply ASI and treat the word as a standalone
identifier/field.

Also fixes class-body get/set followed by '*' (esbuild parses the word
as a field and '*' as a generator; bun rejected with 'Unexpected *').

Statement keywords now gate on !has_newline_before to match esbuild:
TsStmtDeclare, TsStmtInterface, TsStmtAbstract, and the export-default
abstract class path. Class-body declare/abstract/accessor gate on
!has_newline_before. could_be_modifier_keyword now excludes '*' after
get/set (when not async), matching esbuild.
@coderabbitai

coderabbitai Bot commented Jul 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: 15 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: 04ae9e80-49af-49c8-9a5a-e07f96c7574e

📥 Commits

Reviewing files that changed from the base of the PR and between be77b65 and bb9014b.

📒 Files selected for processing (4)
  • src/js_parser/parse/parse_property.rs
  • src/js_parser/parse/parse_stmt.rs
  • test/bundler/transpiler/scope-mismatch-panic.test.ts
  • test/bundler/transpiler/transpiler.test.js

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

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:05 PM PT - Jul 15th, 2026

@robobun, your commit 8e0d9d1 is building: #73448

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Parse and lower accessor fields under experimentalDecorators #29201 - Both PRs add the same !p.lexer.has_newline_before ASI guard to the PAccessor branch in parse_property.rs; Parse and lower accessor fields under experimentalDecorators #29201 does so as part of lowering accessor fields under experimentalDecorators

🤖 Generated with Claude Code

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #29201. That PR is about lowering accessor fields under experimentalDecorators and touches only the PAccessor branch. This PR fixes missing ASI for seven contextual-keyword sites (declare/abstract/interface at statement level, declare/abstract/accessor in class bodies, get/set before *, and export default abstract). The overlap is the single accessor newline check, already called out in the PR description; whichever lands second has a one-hunk rebase.

Comment thread src/js_parser/parse/parse_stmt.rs
Comment thread src/js_parser/parse/parse_stmt.rs
Comment thread src/js_parser/parse/parse_stmt.rs
…f emitting live code

When the newline falls after interface/abstract inside an ambient
"declare" context, falling through to SExpr leaves the remaining tokens
to be parsed as live runtime statements (the declare wrapper only
consumes the keyword). esbuild rejects these shapes. Extend the error
checks to cover is_typescript_declare and point the error at the
keyword range to match esbuild, replacing the self-contradictory
"Expected class but found class" wording for the decorator variants.
Comment thread src/js_parser/parse/parse_stmt.rs Outdated
Comment thread src/js_parser/parse/parse_stmt.rs Outdated
robobun added 4 commits July 15, 2026 19:51
…lag checks

The per-arm is_typescript_declare checks over-rejected inside ambient
namespace/global bodies (where the flag is propagated for body semantics)
and for "export declare\n" (where t_export pre-set the flag redundantly),
and under-covered the type/namespace/module arms. Replace with esbuild's
approach: after the recursive parse inside TsStmtDeclare, reject any
result that is not STypeScript/SLocal/SEmpty, pointing at the token
immediately after "declare". This uniformly catches "declare interface\n",
"declare abstract\n", "declare type\n", "declare namespace\n",
"declare module\n", "declare declare\n", and "declare foo" while accepting
all valid ambient forms and all shapes inside ambient bodies.

Also drop the redundant is_typescript_declare pre-set in t_export's
SDeclare arm (TsStmtDeclare sets it itself on the no-newline path).
"declare foo: bar" and "declare module : es2015" are now rejected with
Unexpected "foo"/"module" (matching esbuild) instead of being silently
dropped, so they no longer exercise the scope-discard codepath. Move the
error assertions to transpiler.test.js alongside the other declare-body
validation cases.
…icitly

The JSX fixture runs from a tempDir with no node_modules of its own, so
react/jsx-dev-runtime resolution depends on NODE_PATH being set in the
ambient environment. Set it explicitly so the test passes in environments
that do not pre-seed NODE_PATH.
Comment thread src/js_parser/parse/parse_stmt.rs Outdated

@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.

No new findings this pass. All four earlier review rounds have been addressed — the final shape (post-check in TsStmtDeclare accepting only STypeScript/SLocal/SEmpty, per-arm is_typescript_declare checks removed, redundant t_export pre-set dropped) matches esbuild's approach and the 50-assertion test block covers every variant I'd raised. Deferring to a human for final sign-off since this is core parser control flow that went through several non-trivial correctness iterations.

What was reviewed:

  • Re-checked the declare post-check against every valid ambient form in the tests (function/class/enum/namespace/abstract class/const/let/var) — each returns STypeScript or SLocal under is_typescript_declare, so none are falsely rejected.
  • Confirmed the ambient-body cases (declare namespace N { abstract\n... }, declare global { ... }, declare module "m" { ... }) and export declare\n... no longer trip the removed per-arm is_typescript_declare guard.
  • Checked the get/set + * gate is scoped by opts.is_async so async * x() still parses; the two removed scope-mismatch-panic cases are covered as parse errors in the new test block.
Extended reasoning...

Overview

This PR adds !has_newline_before gates to seven TS contextual-keyword parse sites (statement-level declare/abstract/interface, export default abstract, class-body declare/abstract/accessor, and get/set before *) so that a newline between the keyword and what follows applies ASI instead of silently consuming the following declaration as an ambient/modifier body. It also replaces the previous "unconditionally wrap whatever the recursive parse returned in S::TypeScript{}" behaviour in TsStmtDeclare with a post-check that rejects any result other than STypeScript/SLocal/SEmpty, matching esbuild's case "declare": handler. Files: parse_stmt.rs (+77/-8), parse_property.rs (+8/-2), transpiler.test.js (+90), scope-mismatch-panic.test.ts (2 cases moved to the new test block).

Security risks

None. Pure parser control-flow / ASI correctness; no I/O, auth, crypto, or untrusted-length arithmetic. The new error paths return SyntaxError rather than panicking.

Level of scrutiny

High — this is core JS/TS parser logic that runs on every .ts file Bun touches. The change is not mechanical: it went through four review rounds here, three of which surfaced real correctness issues (self-contradictory diagnostic wording; a regression where declare interface\n/declare abstract\n started emitting live code; an over-broad is_typescript_declare guard that falsely rejected export declare\n and ambient-body statements; and incomplete coverage of sibling type/namespace/module arms). Each was fixed, and the final design converged on esbuild's post-check approach — but the number of iterations to get there is itself a signal that a maintainer familiar with the parser should confirm the final shape.

Other factors

  • The 50-assertion test block is comprehensive: ASI cases, no-newline controls, every valid declare X, every rejected declare X, ambient-body acceptance, export {abstract,declare}\n fall-through, and the three decorator+newline error paths. bun bd test passes on both debug-ASAN and release per the evidence block; the test fails on the unfixed build.
  • The two removed scope-mismatch-panic.test.ts cases (declare foo: bar, declare module : es2015) were previously accepted but are now parse errors — a behaviour change, but one that matches esbuild and is covered in the new test block. The unrelated NODE_PATH tweak in that file (resolving react from test/node_modules) is a hermeticity fix.
  • The dropped opts.is_typescript_declare = true in t_export's SDeclare arm is genuinely redundant now that TsStmtDeclare sets it itself, and removing it is what makes export declare\nclass Foo {} fall through correctly.

Given the parser-critical scope and the iteration history, I'm deferring rather than approving, but I have no outstanding concerns with the current diff.

Comment thread test/bundler/transpiler/scope-mismatch-panic.test.ts Outdated

@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.

No new issues found after the latest revision — all prior feedback (the declare <keyword>\n live-code leak, the over-broad is_typescript_declare guard, and both comment-length nits) has been addressed. Deferring to a human for final sign-off since this touches core TS parser control flow.

Checked: the newline gates in parse_property.rs (PDeclare/PAbstract/PAccessor, get/set + *) and parse_stmt.rs (TsStmtInterface/Abstract/Declare, export default abstract) against esbuild's shapes.
Checked: the TsStmtDeclare post-check accepts every valid ambient form (SLocal/STypeScript/SEmpty) including inside declare namespace/declare global/declare module bodies — no false rejections found.
Ruled out: SEmpty arm diverging from esbuild (declare; — unreachable, the newline guard returns first); accessor newline test being vacuous (it asserts the field-named-accessor output either way).

Extended reasoning...

Overview

This PR fixes Bun's TypeScript parser to apply ASI when the contextual keywords declare, abstract, interface, and accessor are followed by a newline, instead of treating them as modifiers that silently consume (and erase) the following declaration. It touches src/js_parser/parse/parse_stmt.rs (~75 lines: newline gates on TsStmtInterface/TsStmtAbstract/TsStmtDeclare and the export default abstract path, plus a post-recursive-parse validation in TsStmtDeclare that rejects any non-STypeScript/SLocal/SEmpty result) and src/js_parser/parse/parse_property.rs (~8 lines: newline gates on class-body PDeclare/PAbstract/PAccessor, and excluding * from the modifier-follow set after get/set). It adds ~90 lines of tests in transpiler.test.js covering 50 assertions across every shape, and removes two now-rejected cases from scope-mismatch-panic.test.ts.

Security risks

None. This is parser control-flow for pathological TypeScript inputs; there is no untrusted-length arithmetic, allocation sizing, or FFI involved. The change tightens acceptance (some previously-accepted inputs now error) and loosens erasure (some previously-erased declarations now survive), both matching esbuild/tsc.

Level of scrutiny

High — the JS/TS parser is a critical, hot code path, and the review history bears this out: three prior review rounds on this PR found real behavioral regressions (declare interface\n leaking live code, an over-broad is_typescript_declare guard rejecting valid ambient bodies, a self-contradictory Expected "class" but found "class" diagnostic). All have been fixed, and the final approach — esbuild's post-check in TsStmtDeclare rather than per-arm is_typescript_declare guards — is the structurally correct one and eliminates the false-positive class entirely.

Other factors

Test coverage is thorough: the new test block exercises the full variant matrix (statement-level and class-body, with and without export/export default/decorators, every valid declare X form as a control, ambient-body cases). The two candidate issues raised this run were both refuted on inspection. The one behavioral divergence from esbuild I'm aware of (declare; — esbuild's SEmpty post-check arm) is unreachable in Bun's implementation because the newline guard fires first and returns Ok(None) before the recursive parse. Given the parser's criticality and the number of iterations this took to converge, a human familiar with the esbuild port should give it a final look.

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

The diff is green: transpiler.test.js (172 pass, 50 new assertions), esbuild/ts.test.ts (57 pass), scope-mismatch-panic.test.ts (16 pass), and the decorator suites all pass on every platform. The automated review has no outstanding concerns.

CI builds #73421 and #73448 both failed only on unrelated flakes (net-connect-memleak, install-registry/patch, webview, bake/deinitialization, spawn, repl, solc, valkey, fetch.stream); none touch the parser. Ready for review.

@dylan-conway
dylan-conway merged commit 0bffb47 into main Jul 15, 2026
77 of 79 checks passed
@dylan-conway
dylan-conway deleted the farm/80275baa/ts-contextual-keyword-asi branch July 15, 2026 23:44
dylan-conway pushed a commit that referenced this pull request Jul 16, 2026
### What

`#34249` changed TypeScript enum lowering so that only **module-scope**
enums emit `var`; an enum in a function, method, or block body now emits
`let`.

`#34250` merged four minutes earlier and added `it("rejects
yield/await/this/super in enum initializers")`, whose expectations still
assert `var x` for **block-scoped** enums. Each PR was green against a
`main` that lacked the other's change, so the collision only appeared
once both had landed — and since `main` pushes run no test shards,
nothing caught it.

The result is that `main` asserts output its own parser no longer
produces. `test/bundler/transpiler/transpiler.test.js` currently fails
on every PR that merges `main`.

### The fix

Updates the five stale expectations to `let`. All five are enums nested
in a function or method body:

| Line | Case |
|---|---|
| 771 | `function *f() { enum x { y = (function*() { yield 1 })() } }` |
| 775 | `async function f() { enum x { y = (async () => await 1)() } }`
|
| 785 | `function *f() { enum x { y = 1 } yield 1; }` |
| 789 | `async function f() { enum x { y = 1 } await 1; }` |
| 793 | `class C extends B { m() { enum x { y = 1 } super.foo(); } }` |

Deliberately unchanged:

- **Line 779** — `enum x { y = (function() { return this })() }` is
top-level, so `var` is still correct.
- **Namespace expectations** — namespaces only appear at module scope or
nested in another namespace, where both the old and new predicates
agree.

### Note for reviewers

Only the line-771 failure is visible in CI: `expectPrinted_` throws at
the first mismatch, masking the other four. A fix touching only the
reported line would go red again on the next one, so all five are
updated together.

### Verification

Ran `test/bundler/transpiler/transpiler.test.js` against a build
containing `#34249`:

- pristine `main`: 176 pass / 4 fail
- with this change: 177 pass / 3 fail

The change flips exactly the one enum test and touches nothing else. The
3 remaining failures are unrelated to this diff — they are skew between
that build and two commits that landed after it (`#34254`, `#34258`),
and build from source in CI.

Also swept the repo for any other assertion of enum-lowering text (the
closure IIFE shape `(x ||= {})` / `(x = x || {})`). Only two files
assert it: this one, and
`test/js/node/module/require-extensions.test.ts:129`, whose fixture
declares a top-level enum and is correctly `var`.
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.

2 participants