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).
Given some code like
```js
const {
[({ ...rest }) => {
let { ...b } = {};
}]: a,
[({ ...d } = {})]: c,
} = {};
export {}; // necessary so we are in strict mode
```
It would previously be transformed to:
```js
const { [(_ref) => {
let rest = _extends({}, (_objectDestructuringEmpty(_ref), _ref));
let _ref2 = {}, b = _extends({}, (_objectDestructuringEmpty(_ref2), _ref2));
}]: a, [(_ref3 = {}, {} = _ref3, d = _extends({}, (_objectDestructuringEmpty(_ref3), _ref3)), _ref3)]: c } = {};
export {};
```
This is a problem because there was no declaration to `_ref3`
This causes runtime errors.
After this PR, we now emit:
This new code includes `var _ref3`, fixing the unresolved reference issue.
```js
var _ref3;
const {
[_ref => {
let rest = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_ref), _ref));
let _ref2 = {},
b = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_ref2), _ref2));
}]: a,
[(_ref3 = {}, {} = _ref3, d = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_ref3), _ref3)), _ref3)]: c
} = {};
```
This aligns with Babel.
----
This has been fixed by walking up to find the containing statement, and inserting the var declaration before it.
Previously, the `var` declaration would only be inserted if we were inside `ExpressionStatementExpression` which was not guaranteed.
## Summary
Fixes a long-standing inconsistency in the TypeScript transformer's handling of constructor parameter properties under the documented default (`useDefineForClassFields: true` per [`compiler_assumptions.rs:137-139`](https://github.com/oxc-project/oxc/blob/main/crates/oxc_transformer/src/compiler_assumptions.rs#L137-L139)).
The transformer already emitted TS-true-aligned output for **declared** fields (kept `boom: number;` as `boom;`) but silently dropped the field declaration for **parameter properties**, so downstream tools that re-analyze the transformed AST saw the property only as a `this.*` write, not as a declared class field.
```ts
// input
class Foo {
boom: number; // declared field
constructor(public foo) {} // parameter property
}
```
```js
// before — inconsistent
class Foo {
boom; // ✅ kept (TS-true)
constructor(foo) { // ❌ missing `foo;` (TS-false-style for params only)
this.foo = foo;
}
}
// after — consistent
class Foo {
foo; // ✅ now matches `boom;`
boom;
constructor(foo) {
this.foo = foo;
}
}
```
This is **not a policy change** — the documented default has always been `useDefineForClassFields: true` (set both `set_public_class_fields` and `remove_class_fields_without_initializer` to `true` to opt into `false` semantics). The fix just closes the missing case for parameter properties.
## Implementation
- Add `add_parameter_property_fields` to the `enter_class` path that runs when `set_public_class_fields` is `false`, mirroring the existing branch that calls `transform_class_fields` when it is `true`.
- Skip parameter properties whose name matches an existing instance field on the same class (matches tsc's dedup). Static, private, and `declare`-only fields are different bindings, so they don't suppress the new field.
- Early-bail before allocating the dedup set when the class has no parameter properties — keeps the transformer-allocations snapshot at baseline.
No new option is added; the existing `set_public_class_fields` assumption already maps cleanly to `useDefineForClassFields` (the playground's `useDefineForClassFields: true` already toggles `set_public_class_fields = false`).
Closes#21600Closes#21831
## Summary
- Skip the trailing reference (e.g. `_c`) in the generated sequence expression when the destructuring assignment is directly inside an `ExpressionStatement`, since the expression result is not consumed
- Before: `_c = c2, {a2} = _c, b2 = objectWithoutProperties(_c, ["a2"]), _c;`
- After: `_c = c2, {a2} = _c, b2 = objectWithoutProperties(_c, ["a2"]);`
- The trailing reference is still kept when the result is consumed, e.g. `console.log((..., _c))`
Closes#7389 (issue 1)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* close https://github.com/oxc-project/oxc/issues/12460
Injecting top-level statements after the last import statement is unsound because once there is an import statement in the last line, it would be broken if other code relies on the injected statements. Learn more to see the following example.
I suppose the reason we did this is that we had a test case that misled us. I overrode that test with the expected output.
For example:
Input:
```js
import { observer } from 'mobx-react-lite'
import { useFoo } from './useFoo'
export const BazComponent = observer(function BazComponent_() {
const foo = useFoo()
return (
<>
{foo}
{bar}
</>
)
})
import { bar } from './bar'
```
Before output:
```js
import { observer } from "mobx-react-lite";
import { useFoo } from "./useFoo";
export const BazComponent = _s(observer(_c = _s(function BazComponent_() {
_s();
const foo = useFoo();
return /* @__PURE__ */ _jsxs(_Fragment, { children: [foo, bar] });
}, "useFoo{foo}", false, function() {
return [useFoo];
})), "useFoo{foo}", false, function() {
return [useFoo];
});
_c2 = BazComponent;
import { bar } from "./bar";
import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
var _s = $RefreshSig$();
var _c, _c2;
$RefreshReg$(_c, "BazComponent$observer");
$RefreshReg$(_c2, "BazComponent");
```
After output:
```js
import { observer } from "mobx-react-lite";
import { useFoo } from "./useFoo";
import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
var _s = $RefreshSig$();
export const BazComponent = _s(observer(_c = _s(function BazComponent_() {
_s();
const foo = useFoo();
return /* @__PURE__ */ _jsxs(_Fragment, { children: [foo, bar] });
}, "useFoo{foo}", false, function() {
return [useFoo];
})), "useFoo{foo}", false, function() {
return [useFoo];
});
_c2 = BazComponent;
import { bar } from "./bar";
var _c, _c2;
$RefreshReg$(_c, "BazComponent$observer");
$RefreshReg$(_c2, "BazComponent");
```
The difference is that `import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; var _s = $RefreshSig$();` has injected after `import { useFoo } from "./useFoo";`
Alter the algorithm for generating UIDs, to improve its performance.
Previously we stored all existing symbol names which start with `_` in an `FxHashSet`. When creating a UID based on `foo`, it would first see if `_foo` is available, then try `_foo2`, then `_foo3`, then `_foo4` etc.
When creating a lot of UIDs from the same base name (`foo` in example above), this has poor performance as each attempt requires a separate hashset lookup.
Instead, use an `FxHashMap` mapping base name to:
1. Largest number of underscores at start.
2. Largest numeric prefix on end.
i.e. if there are existing symbols `_foo`, `_bar3`, `_bar8`, `_qux`, `___qux2`, this produces a map:
```
{
"foo": { postfix: 1, underscore_count: 1 },
"bar": { postfix: 8, underscore_count: 1 },
"qux": { postfix: 2, underscore_count: 3 },
}
```
When generating a UID, `postfix` is incremented and UID is constructed as `<underscores><base><postfix>`.
This requires a maximum of 1 x hashmap lookup and 1 x hashmap insert for each UID. It also reduces the size of the hashmap where many UIDs are created for same base name.
This new algorithm produces almost same UIDs as Babel, but not quite. 3 tests require their outputs to be changed due to UIDs being different.
Benefits from https://github.com/oxc-project/oxc/pull/10350. `NamespaceModule` means this `TSModuleDeclaration` is a type-only declaration, so that we can get rid of it early without going through its block.
This PR supports transforming private `getter` or `setter` methods, and the output is a little different from Babel's output, For example:
Input:
```js
class Cls {
#prop = 0;
get #accessor() {
return this.#prop;
}
set #accessor(v) {
console.log(arguments);
this.#prop = v;
}
constructor() {
console.log(this.#accessor)
[this.#accessor] = [1];
}
}
```
[Babel's output](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=MYGwhgzhAEDCIwN4ChrQMQAcBOB7T0AvNAAwDcyq0A5gKYAuGYwwtUu2AFAJTQpppsDAK7YAdtHoALAJYQAdFjyYKaAL5UIDJizYQOnAG69-A4LjH6QteSFzVOYbNWEBbWmPoRuqgdLmKOPhE0Ia-GlTmlvTYwsD0BiZUaFFWNnYO_grozKzs2NzJ0ADaWYq5ehwAuiHFAIxV4cgaQA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&evaluate=false&fileSize=false&timeTravel=false&sourceType=script&lineWrap=true&presets=&prettier=false&targets=&version=7.26.4&externalPlugins=%40babel%2Fplugin-transform-class-properties%407.25.9%2C%40babel%2Fplugin-transform-private-methods%407.25.9%2C%40babel%2Fplugin-external-helpers%407.25.9&assumptions=%7B%7D):
```js
var _prop = new WeakMap();
var _Cls_brand = new WeakSet();
class Cls {
constructor() {
babelHelpers.classPrivateMethodInitSpec(this, _Cls_brand);
babelHelpers.classPrivateFieldInitSpec(this, _prop, 0);
console.log(babelHelpers.classPrivateGetter(_Cls_brand, this, _get_accessor));
[babelHelpers.classPrivateGetter(_Cls_brand, this, _get_accessor)] = [1];
}
}
function _get_accessor(_this) {
return babelHelpers.classPrivateFieldGet(_prop, _this);
}
function _set_accessor(_this2, v) {
var _arguments = [].slice.call(arguments, 1);
console.log(_arguments);
babelHelpers.classPrivateFieldSet(_prop, _this2, v);
}
```
Oxc's output:
```js
var _prop = new WeakMap();
var _Cls_brand = new WeakSet();
class Cls {
constructor() {
babelHelpers.classPrivateMethodInitSpec(this, _Cls_brand);
babelHelpers.classPrivateFieldInitSpec(this, _prop, 0);
console.log(_get_accessor.call(babelHelpers.assertClassBrand(_Cls_brand, this)));
[babelHelpers.toSetter(_set_accessor.bind(babelHelpers.assertClassBrand(_Cls_brand, this)), [])._] = [1];
}
}
function _get_accessor() {
return babelHelpers.classPrivateFieldGet2(_prop, this);
}
function _set_accessor(v) {
console.log(arguments);
babelHelpers.classPrivateFieldSet2(_prop, this, v);
}
```
### Main difference
```diff
// Babel
+ console.log(babelHelpers.classPrivateGetter(_Cls_brand, this, _get_accessor));
+ [babelHelpers.classPrivateGetter(_Cls_brand, this, _get_accessor)] = [1];
+ function _get_accessor(_this) {
+ return babelHelpers.classPrivateFieldGet(_prop, _this);
+ }
+ function _set_accessor(_this2, v) {
+ var _arguments = [].slice.call(arguments, 1);
+ console.log(_arguments);
+ babelHelpers.classPrivateFieldSet(_prop, _this2, v);
+ }
// Oxc
- console.log(_get_accessor.call(babelHelpers.assertClassBrand(_Cls_brand, this)));- -
- [babelHelpers.toSetter(_set_accessor.bind(babelHelpers.assertClassBrand(_Cls_brand, this)), [])._] = [1];
- function _get_accessor() {
- return babelHelpers.classPrivateFieldGet2(_prop, this);
- }
- function _set_accessor(v) {
- console.log(arguments);
- babelHelpers.classPrivateFieldSet2(_prop, this, v);
- }
```
From the main differences, we can see that Babel handles `getter` and `setter` methods using `classPrivateGetter` and `classPrivateSetter` helper functions, which causes all use `this` and `arguments` needs to rewrite to use a temp var instead in `getter` and `setter` methods. This is unnecessary and is not an efficient transformation for us.
Instead, I adapt binding a `this` instead of passing in `this`, this way we don't need to rewrite anything. We can't control the `helper` library for now, so I just transformed the AST to bind `this`, in the future, we can create a helper function to do the same thing.
Large re-architecting of class properties transform. Split transform into 3 phases:
1. Transform instance properties when entering class.
2. Transform private fields during traversal of class body.
3. Transform static properties and static blocks when exiting class.
This ensures that code which has to be moved outside of the class (static property initializers, static blocks, computed keys) can get transformed by other transforms before they're moved out.
Also fixes a problem where we previously registered private properties too early - on entering the class, rather than the class *body*, so private fields in `extends` clause of a nested class were misinterpretted.
When creating class constructor for a class which has super class, use UID `_args` for temp var (rather than `args`). This avoids shadowing a var called `args` used in an instance property initializer.
This diverges from Babel. Babel uses `args` unless it finds a var called `args` in an instance property initializer. But searching the AST of initializers can be fairly expensive, so it's better to skip it. The overrides for test fixtures included in this PR are just to account for that difference.
This PR does the following things.
1. Move the override output of the `snapshots` folder to the `overrides` folder.
2. Support `override` mode to replace `takeover` mode
3. The `update_fixtures.js` no longer uses `overrides`'s `output.js` to replace Babel's `output.js`.
### How does `override` mode work?
When running each test, it checks whether an output file for that test exists in the `overrides` directory. If it does, the output file will be used to compare with the transformed code.
In two of the overridden text fixtures for class properties transform, there was no `output.js` file because what was overridden was just `options.json` and `update_fixtures.js` script then generated new output files using Babel with the new options.
That was fine, but doesn't work with #7771. So add `output.js` files to the these overrides too.
Support `private_fields_as_properties` assumption in class properties transform. This assumption is also enabled by the transform's `loose` option.
Optional chain (e.g. `this?.#prop`) is not yet implemented, but all other usages of private fields are supported. We'll handle optional chain in a follow-on PR.
`TransformCtx::duplicate_expression` (introduced in #7754) don't create temp vars for literals. This produces more compact output for the logical assignment operators transform.
This diverges from Babel (it's better!) so add an override for one of Babel's fixtures. Also add further tests for all literal types.
When create a `_super` function outside class, ensure it's always strict mode. The code it contains was previously inside the class, so was strict mode.
Override some transform conformance test fixtures for class properties transform, where:
* Our output differs from Babel in cosmetic manner only.
* Our transform intentionally works differently from Babel.
* Babel's fixtures enable arrow functions transform, which malfunctions in our implementation. But we're not trying to test arrow functions transform here, so disable it.