Skip to content

Commit da9fe2a

Browse files
committed
fix(transformer): keep enum IIFE when a non-inlinable value reference remains
The optimize pass dropped the enum declaration whenever the members were all evaluable — regardless of whether some non-inlinable value reference still named the enum. The result referenced a binding that no longer existed: const enum Phase { one = 'one' } export default Phase; // -> export default Phase; // ReferenceError at runtime enum Direction { Up } Direction.Up.toString(); // -> Direction.Up.toString(); // ReferenceError at runtime Decide based on the remaining resolved references: drop only when none of them is a value reference (`Read`/`Write`). Type references (`as E`, `: E`) are kept here and stripped later by `annotations.rs`, so they don't block removal. `typeof E` in JS expression position is `Read` and correctly keeps the IIFE; `typeof E` in TS type position is `ValueAsType` (not in `Value`) and doesn't. This unifies the rule for regular and const enums — the previous asymmetry (const always dropped, regular dropped only when no refs at all) had no principled basis. Diverges from Babel's `optimize-const-enums/local` and `/merged` fixtures, which expect the declaration dropped. tsc under `--isolatedModules` keeps the IIFE; oxc now matches that. Refs vitejs/vite#22452 (comment)
1 parent b7fe1e2 commit da9fe2a

10 files changed

Lines changed: 79 additions & 44 deletions

File tree

crates/oxc_transformer/src/typescript/enum.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,12 +432,21 @@ impl<'a> TypeScriptEnum {
432432
}
433433

434434
/// Check if an enum declaration can be safely removed (post-inlining).
435-
/// Const enums are always removed when `optimize_const_enums` is set.
436-
/// Regular enums are removed only if all references were inlined away by `enter_expression`.
435+
///
436+
/// The decl is removable when no value references (`Read`/`Write`) remain —
437+
/// `enter_expression` inlines member accesses and deletes those references. Type
438+
/// references (e.g. from `as E` / `: E`) are kept here and stripped later by
439+
/// `annotations.rs`, so they don't block removal.
440+
///
441+
/// If a non-inlinable value reference remains (e.g. `export default E`, `E.toString()`),
442+
/// we emit the IIFE form so the binding still exists at runtime — matching tsc under
443+
/// `--isolatedModules`. Babel drops the declaration in this case, leaving dangling
444+
/// references; oxc diverges intentionally to preserve runtime correctness.
437445
fn can_remove_enum(&self, decl: &TSEnumDeclaration<'a>, ctx: &TraverseCtx<'a>) -> bool {
438-
self.may_remove_enum(decl, ctx)
439-
&& (decl.r#const
440-
|| ctx.scoping().get_resolved_reference_ids(decl.id.symbol_id()).is_empty())
446+
if !self.may_remove_enum(decl, ctx) {
447+
return false;
448+
}
449+
ctx.scoping().get_resolved_references(decl.id.symbol_id()).all(|r| !r.is_value())
441450
}
442451

443452
/// Check if all members of an enum declaration have known constant values.

tasks/transform_conformance/snapshots/babel.snap.md

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,26 +2303,10 @@ x Output mismatch
23032303
x Output mismatch
23042304

23052305
* optimize-const-enums/local/input.ts
2306-
Reference symbol mismatch for "A":
2307-
after transform: SymbolId(0) "A"
2308-
rebuilt : <None>
2309-
Reference symbol mismatch for "A":
2310-
after transform: SymbolId(0) "A"
2311-
rebuilt : <None>
2312-
Unresolved references mismatch:
2313-
after transform: []
2314-
rebuilt : ["A"]
2306+
x Output mismatch
23152307

23162308
* optimize-const-enums/merged/input.ts
2317-
Reference symbol mismatch for "A":
2318-
after transform: SymbolId(0) "A"
2319-
rebuilt : <None>
2320-
Reference symbol mismatch for "A":
2321-
after transform: SymbolId(0) "A"
2322-
rebuilt : <None>
2323-
Unresolved references mismatch:
2324-
after transform: []
2325-
rebuilt : ["A"]
2309+
x Output mismatch
23262310

23272311
* optimize-const-enums/merged-exported/input.ts
23282312
x Output mismatch

tasks/transform_conformance/snapshots/oxc.snap.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
commit: 4079bcda
22

3-
Passed: 235/383
3+
Passed: 236/385
44

55
# All Passed:
66
* babel-plugin-transform-class-static-block
@@ -63,7 +63,7 @@ after transform: SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), R
6363
rebuilt : SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), ReferenceId(10)]
6464

6565

66-
# babel-plugin-transform-typescript (23/58)
66+
# babel-plugin-transform-typescript (24/60)
6767
* allow-declare-fields-false/input.ts
6868
Unresolved references mismatch:
6969
after transform: ["dce"]
@@ -118,6 +118,28 @@ Unresolved reference IDs mismatch for "Infinity":
118118
after transform: [ReferenceId(0), ReferenceId(1), ReferenceId(2), ReferenceId(3), ReferenceId(8), ReferenceId(11), ReferenceId(14), ReferenceId(18)]
119119
rebuilt : [ReferenceId(2), ReferenceId(5), ReferenceId(8), ReferenceId(12)]
120120

121+
* const-enum-mixed-refs/input.ts
122+
Bindings mismatch:
123+
after transform: ScopeId(1): ["Phase", "one", "two"]
124+
rebuilt : ScopeId(1): ["Phase"]
125+
Scope flags mismatch:
126+
after transform: ScopeId(1): ScopeFlags(0x0)
127+
rebuilt : ScopeId(1): ScopeFlags(Function)
128+
Symbol flags mismatch for "Phase":
129+
after transform: SymbolId(0): SymbolFlags(ConstEnum)
130+
rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable)
131+
132+
* const-enum-value-ref-kept/input.ts
133+
Bindings mismatch:
134+
after transform: ScopeId(1): ["Phase", "one", "two"]
135+
rebuilt : ScopeId(1): ["Phase"]
136+
Scope flags mismatch:
137+
after transform: ScopeId(1): ScopeFlags(0x0)
138+
rebuilt : ScopeId(1): ScopeFlags(Function)
139+
Symbol flags mismatch for "Phase":
140+
after transform: SymbolId(0): SymbolFlags(ConstEnum)
141+
rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable)
142+
121143
* elimination-declare/input.ts
122144
Bindings mismatch:
123145
after transform: ScopeId(0): ["A", "ReactiveMarker", "ReactiveMarkerSymbol"]
@@ -526,20 +548,6 @@ Symbol flags mismatch for "B":
526548
after transform: SymbolId(2): SymbolFlags(RegularEnum)
527549
rebuilt : SymbolId(2): SymbolFlags(FunctionScopedVariable)
528550

529-
* optimize-enums/type-cast/input.ts
530-
Bindings mismatch:
531-
after transform: ScopeId(1): ["Direction", "Down", "Up"]
532-
rebuilt : ScopeId(1): ["Direction"]
533-
Scope flags mismatch:
534-
after transform: ScopeId(1): ScopeFlags(0x0)
535-
rebuilt : ScopeId(1): ScopeFlags(Function)
536-
Symbol flags mismatch for "Direction":
537-
after transform: SymbolId(0): SymbolFlags(RegularEnum)
538-
rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable)
539-
Symbol reference IDs mismatch for "Direction":
540-
after transform: SymbolId(0): [ReferenceId(5), ReferenceId(1), ReferenceId(7), ReferenceId(3), ReferenceId(14)]
541-
rebuilt : SymbolId(0): [ReferenceId(5)]
542-
543551
* optimize-enums/typeof-kept/input.ts
544552
Bindings mismatch:
545553
after transform: ScopeId(1): ["Bar", "X"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Member accesses are inlined; the bare value reference forces the IIFE
2+
// to be kept so the runtime binding still exists.
3+
const enum Phase {
4+
one = "one",
5+
two = "two",
6+
}
7+
8+
const a = Phase.one;
9+
const b = Phase.two;
10+
const c = Phase;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"plugins": [["transform-typescript", { "optimizeConstEnums": true }]]
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
var Phase = /* @__PURE__ */ function(Phase) {
2+
Phase["one"] = "one";
3+
Phase["two"] = "two";
4+
return Phase;
5+
}(Phase || {});
6+
const a = "one";
7+
const b = "two";
8+
const c = Phase;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// `export default <enum>` is a bare value reference that can't be inlined.
2+
// The IIFE form must be emitted so the binding still exists at runtime,
3+
// otherwise the export resolves to `undefined`/ReferenceError.
4+
const enum Phase {
5+
one = "one",
6+
two = "two",
7+
}
8+
9+
export default Phase;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"plugins": [["transform-typescript", { "optimizeConstEnums": true }]]
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var Phase = /* @__PURE__ */ function(Phase) {
2+
Phase["one"] = "one";
3+
Phase["two"] = "two";
4+
return Phase;
5+
}(Phase || {});
6+
export default Phase;

tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/optimize-enums/type-cast/output.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
var Direction = /* @__PURE__ */ function(Direction) {
2-
Direction[Direction["Up"] = 0] = "Up";
3-
Direction[Direction["Down"] = 1] = "Down";
4-
return Direction;
5-
}(Direction || {});
61
0;
72
1;
83
0;

0 commit comments

Comments
 (0)