mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
e6090e7ccc
Refs vitejs/vite#22452 (comment). 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 at runtime. The result referenced a binding that no longer existed: ```ts 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 ``` ## Fix 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. 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. ## Compatibility Diverges from Babel on two `optimize-const-enums/*` fixtures where the input has a bare value reference to the enum that we can't inline. Three engines disagree on what to do: | Engine | Inline `E.X` member accesses | Preserve `E` binding (IIFE) | Result for `E.unknown` / bare `E` | |---|---|---|---| | Babel `transform-typescript` (optimize-const-enums) | yes | **no** | runtime ReferenceError / TypeError | | tsc `--isolatedModules` | no | yes | `undefined` / resolves | | oxc after this PR | yes | yes | `undefined` / resolves | We now hybridize: inline what's inlinable (Babel) **and** keep the binding (tsc). Local overrides under `tasks/transform_conformance/overrides/` record the new expected output for the two affected Babel fixtures. ### `optimize-const-enums/local` Input: ```ts const enum A { x, y } A.x; A["y"]; A.z; A; ``` Babel (and oxc before this PR): ```js 0; 1; A.z; A; ``` `A.z` is `TypeError: cannot read properties of undefined`; `A;` is `ReferenceError`. tsc (`--isolatedModules`): ```js var A; (function (A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; })(A || (A = {})); A.x; A["y"]; A.z; A; ``` Keeps the IIFE binding, no member-access inlining. `A.z` evaluates to `undefined`; `A;` is a no-op. oxc after this PR (`overrides/.../optimize-const-enums/local/output.mjs`): ```js var A = /* @__PURE__ */ function(A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; return A; }(A || {}); 0; 1; A.z; A; ``` Hybrid: inlines `A.x` / `A["y"]` like Babel, **and** keeps the IIFE binding like tsc — so `A.z` evaluates to `undefined` (not crash) and `A;` resolves. ### `optimize-const-enums/merged` Input: ```ts const enum A { x, y } const enum A { z } A.x; A["y"]; A.z; A.w; A; ``` Babel (and oxc before this PR): ```js 0; 1; 0; A.w; A; ``` `A.w` is `TypeError: cannot read properties of undefined`; `A;` is `ReferenceError`. tsc (`--isolatedModules`): ```js var A; (function (A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; })(A || (A = {})); (function (A) { A[A["z"] = 0] = "z"; })(A || (A = {})); A.x; A["y"]; A.z; A.w; A; ``` Both IIFEs emitted, no inlining. `A.w` is `undefined`; `A;` resolves. oxc after this PR (`overrides/.../optimize-const-enums/merged/output.mjs`): ```js var A = /* @__PURE__ */ function(A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; return A; }(A || {}); A = /* @__PURE__ */ function(A) { A[A["z"] = 0] = "z"; return A; }(A || {}); 0; 1; 0; A.w; A; ``` Hybrid: both enum bodies lower (the second reuses the first's binding via `A = (function(A) { … })(A || {})` — assignment, not `var`) **and** members are inlined like Babel. `A.w` evaluates to `undefined`; `A;` resolves. ## Verification New fixtures cover the bug shapes: - `babel-plugin-transform-typescript/const-enum-value-ref-kept/` — `export default Phase` - `babel-plugin-transform-typescript/const-enum-mixed-refs/` — member access + bare ref combined Existing `optimize-enums/type-cast` updated to drop the IIFE for the all-type-refs case (the regular-enum side of the unified rule).