fix(transformer): sync scoping after lowering an enum to IIFE#22502
fix(transformer): sync scoping after lowering an enum to IIFE#22502Dunqing wants to merge 1 commit into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Merging this PR will not alter performance
Comparing Footnotes
|
3ac367a to
1c28e2d
Compare
f2a4208 to
8591185
Compare
da9fe2a to
ae6439a
Compare
8591185 to
f4778ae
Compare
ae6439a to
c723603
Compare
c723603 to
e6090e7
Compare
f4778ae to
fa62d6c
Compare
`transform_ts_enum` rewrites `enum E { … }` into `var/let E =
(function(E) { … })(E || {})` but left the semantic table describing
the *old* shape:
* the enum's symbol still carried `RegularEnum`/`ConstEnum` flags,
even though the binding now backs a plain `var`/`let`;
* the function-expression body scope (re-used from the enum body)
still had default scope flags instead of `Function`;
* member names (`Up`, `Down`, …) remained bound in that scope despite
living on the runtime enum object now, not as JS-level bindings.
`TransformerReturn` exposes this scoping table, so downstream consumers
— `oxc_minifier`, the mangler, NAPI clients — were reading a model that
no longer described the emitted code. The transform-checker conformance
suite had been snapshotting this as ~20 fixtures' worth of
"Bindings mismatch / Symbol flags mismatch / Scope flags mismatch"
errors (`optimize-enums/typeof-kept`, `exported-not-removed`, and the
new `const-enum-*-kept` fixtures from the parent branch).
Sync the scoping table inside `transform_ts_enum` once the lowering
decides what statement to emit: set the symbol's flags to
`FunctionScopedVariable`/`BlockScopedVariable` based on `var` vs `let`,
add `ScopeFlags::Function` to the body scope, and drop member bindings
from that scope. Skip the `is_already_declared` (enum-merge) path's
symbol-flag update, since that path reuses an existing outer binding.
Watch out for name collisions like `enum y { y = 123 }`: the IIFE param
`y` overwrites the member binding `y` under the same name. Skip
removal when the live binding now points at the param.
Pass count goes 236 → 244 (oxc.snap.md) and 700 → 711 (babel.snap.md):
+19 fixtures move from semantic-mismatch failures to passing.
fa62d6c to
f250d22
Compare

transform_ts_enumrewritesenum E { … }intovar/let E = (function(E) { … })(E || {})but left the semantic table describing the old shape:RegularEnum/ConstEnumflags, even though the binding now backs a plainvar/let;Function;Up,Down, …) remained bound in that scope despite living on the runtime enum object now, not as JS-level bindings.TransformerReturnexposes this scoping table, so downstream consumers —oxc_minifier, the mangler, NAPI clients — were reading a model that no longer described the emitted code. The transform-checker conformance suite was snapshotting this as ~20 fixtures' worth ofBindings mismatch / Symbol flags mismatch / Scope flags mismatcherrors (optimize-enums/typeof-kept,exported-not-removed, and the newconst-enum-*-keptfixtures from the parent PR).Fix
Sync the scoping table when lowering happens:
FunctionScopedVariable/BlockScopedVariablebased onvarvslet;ScopeFlags::Functionto the body scope;Skip the
is_already_declared(enum-merge) path's symbol-flag update, since that path reuses an existing outer binding.Identifying members by their
EnumMembersymbol flag (rather than by name) naturally handlesenum y { y = 123 }: the IIFE param's binding has overwritten the member's entry under that name, so its symbol is no longer reachable viaget_bindings, and the param'sFunctionScopedVariablesymbol survives untouched.Why the updates are queued and drained per scope
transform_ts_enumruns inenter_statement(before sibling references are visited) when the enum can't be fully removed — e.g.enum Mixed { A = "hello", B }(B's auto-increment after a string isn't statically evaluable), orexport enum E { … }. Rewriting the symbol flags eagerly there would make the inliner later seeFunctionScopedVariablefor siblingMixed.A/Direction.Upreferences, fail theis_const_enum/RegularEnumcheck, and skip inlining.Queue the updates on
TypeScriptEnum.pending_scoping_updateswith each enum'senclosing_scope_id, and drain them inexit_statementsfor that scope. By the timeexit_statementsfires for the enclosing scope, the entire subtree (where any reference to the enum could live) has been visited depth-first, so everyenter_expressionthat could inline anE.Xaccess has already run.The flush runs unconditionally because
transform_ts_enumis invoked unconditionally for exported enums regardless of theoptimize_*flags; the optimize-gated loop inexit_statementsonly deals with deferred-removable enums.Impact
Pass count: 236 → 246 (
oxc.snap.md) and 700 → 711 (babel.snap.md); ~19 fixtures move from semantic-mismatch failures to passing, plus 2 more (optimize-enums/auto-increment-after-string,optimize-enums/exported-not-removed) that the deferred-update approach unblocks. ~660 lines of snapshot errors removed.