Commit Graph

274 Commits

Author SHA1 Message Date
Changsu Seong 116c8b8303 fix(transformer/class-properties): keep private methods in class-expression scope (#26308)
Private methods lowered from a class expression inside a concise arrow
were inserted next to the arrow's outer statement, while private field
storage remained inside the arrow. Starting with Oxc 0.143, this could
leave the generated helper unable to resolve the private field and throw
a `ReferenceError` at runtime.

Concise arrow bodies have no statement list where the helper declaration
can be inserted. In that context, the helper is now emitted as an
assignment in the class-expression sequence, with its `var` binding
inside the expanded arrow body. Ordinary statement-backed class
expressions keep block-local function declarations so classes created by
repeated block evaluation retain their own captured values. The
generated symbol flags and scope parents match the emitted form in both
cases.

## Example

```js
const makeClass = () => class {
  #field = 41;
  #read() { return this.#field; }
  run() { return this.#read() + 1; }
};

new (makeClass())().run();
```

Before Oxc emitted a helper outside `makeClass`, and the call threw
`ReferenceError: _field is not defined`. After this change it returns
`42`.

Verified with the private-method transform conformance suite, runtime
regressions for concise arrows and repeated block evaluation, `cargo
test -p oxc_transformer`, and transformer clippy.

Fixes the upstream cause of
[rolldown/rolldown#10818](https://github.com/rolldown/rolldown/issues/10818).

AI assistance: Cursor was used for the original patch; OpenAI Codex was
used for follow-up review, fixes, and testing.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-09-07 20:47:40 +08:00
Dunqing 3d95477214 fix(transformer): gate async-only rewrites by owning transform (#26326)
For targets that support async functions but not async generators, async-only converter mode still treated every async function as if the enabled transform would move its body. Plain async arrows and methods gained generated `this` and `super` bindings, and class-field arrows were wrapped unnecessarily.

Use the shared body-movement predicate for every async-only rewrite. Functions left native by the selected target now remain untouched, while async generator functions that are lowered keep their required lexical captures.

## Example

A plain async method using `super` previously gained a generated `_superprop_getValue` binding even though the method remained native. This made `eval("typeof _superprop_getValue")` return `"function"` instead of `"undefined"`. The method now remains unchanged.

Verified with focused output/runtime conformance, full transformer output conformance, and `cargo test -p oxc_transformer`.

Stacked on #26317.

AI assistance was used to diagnose the remaining transform boundaries, implement the predicate checks, and add tests. I remain responsible for reviewing and understanding the changes before merge.
2026-09-07 07:21:47 +00:00
Dunqing f45a9ee58b fix(transformer): limit arguments capture to lowered async functions (#26317)
For targets that support async functions but not async generators, plain async methods and arrows remain native. Async-only converter mode still captured `arguments` for all of them, exposing generated `_arguments` bindings even though their bodies never move.

Gate `arguments` capture on the transform that owns the function body. Native async functions now remain untouched, while lowered async generator methods still capture their original arguments.

## Example

This native async method previously changed from:

```js
async method() {
  void arguments;
  return eval("typeof _arguments");
}
```

to:

```js
async method() {
  var _arguments = arguments;
  void _arguments;
  return eval("typeof _arguments");
}
```

The generated binding changed the return value from `"undefined"` to `"object"`. The method now remains unchanged.

Verified with focused output/runtime conformance, full transformer output conformance, and `cargo test -p oxc_transformer`.

Stacked on #26227.

AI assistance was used to diagnose the transform boundary, implement the fix, and add tests. I remain responsible for reviewing and understanding the changes before merge.
2026-09-07 07:05:01 +00:00
Dunqing 789969f8a1 fix(transformer): gate this capture by owning async transform (#26227)
For targets that support async functions but not async generators, plain async methods remain native. The shared arrow converter still captured `this` whenever either async transform was active, injecting an observable `_this` binding into methods whose bodies never move.

Track async-to-generator and async-generator lowering separately. Plain async methods are now left untouched when only async generators require lowering, while transformed async generator methods still preserve their receiver.

## Example

With a Chrome 55 target, this method previously gained a generated binding:

```js
async method() {
  var _this = this;
  void _this;
  return eval("typeof _this");
}
```

It now remains unchanged, so the direct evaluation returns `"undefined"` as it does in the source program.

Verified with a Chrome 55 target output/runtime fixture, full transformer output conformance, and `cargo test -p oxc_transformer`.

AI assistance was used to diagnose the transform boundary, implement the fix, and add tests. I remain responsible for reviewing and understanding the changes before merge.
2026-09-07 07:05:01 +00:00
camc314 243b685c3d fix(transformer/object-rest): lower multiple declarators correctly (#26147)
## Summary

Fix ES2018 object-rest lowering when one variable declaration contains multiple object-rest declarators.

### Before

Given:

```js
let { a, ...r } = x, { b, ...s } = y;
```

Oxc produced:

```js
let { a } = x, { b } = y,
  s = babelHelpers.objectWithoutProperties(y, ["b"]),
  {} = null;
```

The `r` binding was lost.

Each replacement was recorded using its original declarator index. Replacing the first declarator expanded it into multiple entries, shifting the second declarator. The subsequent replacement still used the old index and overwrote part of the first expansion.

### After

Oxc now produces:

```js
let { a } = x,
  r = babelHelpers.objectWithoutProperties(x, ["a"]),
  { b } = y,
  s = babelHelpers.objectWithoutProperties(y, ["b"]);
```

Queued replacements are applied from right to left. Replacing a later declarator cannot change the index of an earlier one, so every recorded index remains valid.

Regression coverage includes multiple `let`, `const`, and `var` declarators.
2026-08-28 14:02:19 +00:00
Dunqing c3dedc9b0c fix(semantic): skip function body bindings in parameters (#26099)
Function parameters, TypeScript signatures, and function bodies currently share one Oxc semantic scope. A reference in a parameter can therefore bind to a declaration inside the body, even though JavaScript does not make body declarations visible there. Rolldown may then treat a global as local, skip a configured define, or remove live code.

## Example

```js
export function load(value = typeof window !== "undefined") {
  var window;
  return value;
}
```

The body `var window` is not visible to the default value. Before this change, Oxc resolved `window` in the default value to that body declaration. It should remain a global reference.

The workaround resolves parameter and signature references before visiting the body, while only parameter bindings are present. If a reference is still unresolved, its `lookup_scope_id` advances to the function's parent. Final resolution resumes there, so declarations added while visiting the body are skipped. References created in the body still begin at the function scope and resolve normally.

```text
Before: parameter reference -> completed function scope -> body binding (wrong)

After:  parameter reference -> parameter bindings -> parent scopes
        body reference      -> completed function scope -> parent scopes
```

Parameter decorators already begin in the enclosing class scope, so they are left for normal final resolution. This emulates separate parameter and body scopes until https://github.com/oxc-project/backlog/issues/176 is implemented.

The corrected semantic data also exposes the transformer case covered by #26105.

Verified with semantic and transformer-plugin tests, Clippy, coverage, allocation snapshots, and diff checks. `just ready` completed workspace checks but stopped at a codegen snapshot recorded with Node.js v26.5.0; the local version is v25.8.1.

Closes #20427
Closes #22783

Downstream reports:

- https://github.com/rolldown/rolldown/issues/10722
- https://github.com/rolldown/rolldown/issues/10779

AI assistance: OpenAI Codex was used to investigate, implement, and test this change. The contributor is responsible for the submitted change.
2026-08-27 17:23:18 +00:00
oxc-guard 7ebbdddce7 chore(submodules): update submodule SHAs to latest commits (#25505)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `b363f29` → `be13516`
- babel: `06b6eae` → `1eac448`
- estree-conformance: `f7a4e0c` → `8d4e1fa`

This PR is automatically generated by the [update_submodules workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).
2026-08-11 16:17:31 +00:00
oxc-guard[bot] 4e7007118d chore(submodules): update submodule SHAs to latest commits (#25261)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `d1d583d` → `b363f29`
- babel: `c86e9e4` → `06b6eae`
- TypeScript: `637d574` → `b465fdb`
- estree-conformance: `e8fd120` → `f7a4e0c`


This PR is automatically generated by the [update_submodules
workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
2026-08-03 22:09:18 +01:00
oxc-guard[bot] 17cb5c13c5 chore(submodules): update submodule SHAs to latest commits (#24463)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `de8e621` → `d1d583d`
- babel: `1fb0b77` → `c86e9e4`
- TypeScript: `7964e22` → `637d574`
- estree-conformance: `6514ad7` → `e8fd120`


This PR is automatically generated by the [update_submodules
workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
2026-07-13 14:24:36 +01:00
camc314 af4922be6d fix(transformer): clear lowered namespace redeclarations (#24300) 2026-07-08 13:56:43 +00:00
camc314 6371fed556 fix(transformer): remove stale enum member bindings (#24272)
## Summary

Remove TypeScript enum-member bindings after transformation.

Enum members are represented as bindings during semantic analysis so the transformer can resolve member values, fold references, and handle merged enums. Once TypeScript transformation finishes, those bindings no longer correspond to emitted JavaScript declarations.

Include `EnumMember` in the existing final TypeScript binding cleanup instead of removing bindings during enum lowering, where later references may still need them.

This fixes `discriminatedUnionTypes4.ts` and removes the same stale binding mismatch from other enum cases.
2026-07-07 18:29:57 +00:00
Dunqing f05dfabe5c fix(transformer): correct symbol flags for lowered namespaces (#24271)
Closes #24224 — the first commit is @camc314's fix from there.

Lowered namespaces kept their `ValueModule`/`NameSpaceModule` symbol flags, so the flags no longer matched the emitted JavaScript:

- Namespaces lowered to a declaration become `let` → `BlockScopedVariable`.
- Namespaces lowered to an assignment only lose their module bits.

This removes over 2,000 stale symbol-flag mismatches from the snapshots.

Unlike #24224 there is no pending state: the type-only check for merged namespaces now reads per-declaration flags from the redeclaration list instead of the symbol's live flags, so each declaration can update the flags right when it is lowered.
2026-07-07 16:48:19 +00:00
Dunqing 84eeb5570b fix(transformer): correct symbol flags for lowered enums (#24269)
Closes #24226 — the first commit is @camc314's fix from there.

Lowered enums kept their `RegularEnum`/`ConstEnum` symbol flags, so the flags no longer matched the emitted JavaScript:

- Top-level enums become `var` → `FunctionScopedVariable`.
- Exported and nested enums become `let` → `BlockScopedVariable`.
- Merged enums lowered to an assignment only lose their enum bits.

This removes over 1,000 stale symbol-flag mismatches from the snapshots.

Unlike #24226 there is no pending state: enum member inlining now identifies enums through `EnumData` (#24268) instead of `SymbolFlags`, so the flags can be updated right when the enum is lowered.
2026-07-07 16:48:19 +00:00
camc314 c3057dac59 fix(transformer): preserve generated class binding spans (#24220)
## Summary

- preserve outer and inner class binding spans during legacy decorator lowering
- add a span-aware traversal binding generator for transformed source-backed bindings
- update semantic coverage snapshots, removing stale `Span { start: 0, end: 0 }` mismatches
2026-07-07 06:53:38 +00:00
camc314 8260096253 fix(transformer): correct span for lowered namespace symbol (#24222)
## Summary

- preserve source spans when TypeScript namespace declarations are lowered to variable bindings
- update semantic coverage snapshots, removing stale `Span { start: 0, end: 0 }` mismatches
2026-07-07 02:27:08 +00:00
camc314 588d9971ac fix(semantic): mark TS PropertyDefinitions computed fields as type references (#24233)
## Summary

- resolve computed declare class-field keys through ValueAsType so their final references are Type
- allow declared keys to resolve type-only import bindings while preserving Read for ordinary runtime class fields
- add a red/green semantic snapshot and update conformance snapshots, yielding one additional TypeScript semantic pass and one additional transform-conformance pass
2026-07-06 16:28:50 +00:00
camc314 ce5f40f575 fix(transformer): declare temp for renamed anonymous class (#24230)
## Summary

- declare the class-properties temp when another transform names an anonymous class
- prevent static-field initialization from referencing an undeclared temp
- add legacy-decorator/class-properties conformance coverage

## Transformation

```mermaid
flowchart LR
    A["Anonymous class"] --> B["Class properties reserves _Class"]
    B --> C["Decorator names class _default"]
    C --> D["Declare var _Class"]
    D --> E["_Class = _default"]
    E --> F["Initialize static fields"]
```

```ts
export default class {
  @dec
  static foo = 0;
}
```

```js
var _Class;
export default class _default {}
_Class = _default;
babelHelpers.defineProperty(_Class, "foo", 0);
```
2026-07-06 15:05:12 +00:00
camc314 09bd6389c8 fix(transformer): scope anonymous decorated class bindings correctly (#24229)
## Summary

Generate bindings for anonymous decorated classes in the class’s enclosing scope.

## Problem

When an anonymous class element has a decorator, the transformer creates a
temporary `_default` binding while traversing the class body.

Previously, the binding was registered in the current class scope even though
the transformed declaration is emitted outside that class.

```text
Semantic data before fix          Transformed AST

Program                           Program
└─ Class scope                    └─ class/variable _default
   └─ binding: _default               ↑ emitted here
       ↑ recorded here
```

This caused the retained semantic data to disagree with semantics rebuilt from
the transformed AST:

```text
after transform: _default → class scope
rebuilt:         _default → enclosing scope
```

## Solution

Derive the class’s enclosing scope from its semantic scope and generate the
temporary binding there.

```text
Class scope
    │
    └── parent scope ── generate `_default` binding here
                            │
                            └── matches emitted declaration
```

This corrects binding ownership for anonymous default-export classes with
decorated members.
2026-07-06 14:44:59 +00:00
camc314 4b7fa9ca35 fix(transformer): preserve namespace strict mode flags (#24227)
## Summary

Preserve strict-mode metadata when lowering TypeScript namespaces into IIFE-style functions.

Previously, namespace transformation always replaced the original scope flags with `Function | StrictMode`, causing semantic mismatches for namespaces that were not strict. The transform now retains strictness only when present on the original namespace or enclosing scope.

Updated semantic and transform-conformance snapshots to remove the resolved scope-flag mismatches.
2026-07-06 14:00:03 +00:00
camc314 d8ead846c8 fix(transformer): correct scoping flags for lowered enums (#24225)
## Summary

TypeScript enum lowering reuses the enum body’s scope for the generated function expression. Update that scope with `ScopeFlags::Function` so transformed semantic data matches a freshly rebuilt AST.
2026-07-06 13:34:00 +00:00
camc314 e8b50ee716 fix(transformer): clean up semantics for stripped TypeScript syntax (#24180)
## Summary

Keep transformer semantic data in sync when TypeScript-only syntax is removed.

This change:

- removes resolved and unresolved references belonging only to stripped TypeScript syntax
- removes bindings for erased type-only and unused imports
- preserves runtime bindings when a type import is merged with a value declaration
- promotes the surviving declaration’s symbol flags, span, and redeclaration metadata
- adds coverage for type imports merged with runtime declarations
- refreshes semantic and transformer conformance snapshots

## Motivation

We are preparing to change the AST shape, including adding and rearranging scopes.

Currently, transformer semantic snapshots contain thousands of lines of mismatches caused by stale bindings and references from TypeScript syntax that has already been removed from the AST. That creates a large amount of unrelated snapshot noise.

With that noise present, snapshot changes caused by the upcoming AST work would be difficult to review: a real scope or semantic regression could easily be hidden among mechanical churn.

This PR fixes the underlying semantic inconsistencies first, giving us a much cleaner baseline for reviewing future AST and scope changes. It is not intended to change emitted JavaScript.
2026-07-06 08:11:46 +00:00
oxc-guard 9e72aa0312 chore(submodules): update submodule SHAs to latest commits (#23451)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `05bb032` → `de8e621`
- babel: `2688fbd` → `1fb0b77`
- TypeScript: `7539c04` → `7964e22`
- estree-conformance: `ca42c3d` → `6514ad7`

This PR is automatically generated by the [update_submodules workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).
2026-06-15 14:31:01 +00:00
Dunqing 6c0bdf0486 feat(transformer/react-refresh): support module.property.useHook() (#23190)
## Summary

Ports facebook/react#35318 to the React Refresh plugin: hook calls through a nested member expression such as `FancyHook.property.useNestedThing()` are now collected into the `$RefreshSig$` custom-hooks list, so components using them no longer lose state on every fast-refresh update. As in the Babel plugin, the binding is resolved through one extra static member level, and a force reset is emitted when the root object has no binding in scope. Deeper nesting (`A.B.C.useHook()`) remains uncollected, same as upstream; note that Babel additionally emits `forceReset = true` in that case while oxc does not — a pre-existing, intentional divergence from #6143 that this PR does not change.

closes #16571

## Example

Input:

```jsx
import FancyHook from 'fancy';

export default function App() {
  const foo = FancyHook.property.useNestedThing();
  return <h1>{foo}</h1>;
}
```

Signature before:

```js
_s(App, "useNestedThing{foo}");
```

Signature after:

```js
_s(App, "useNestedThing{foo}", false, function() {
  return [FancyHook.property.useNestedThing];
});
```

---

This PR was prepared with AI assistance; I reviewed, tested, and verified the changes.
2026-06-12 02:04:52 +00:00
camc314 2c28748328 fix(transformer/class): parent generated constructors to class scope (#23222)
## Summary
- Parent synthesized constructors for TypeScript class field lowering under the transformed class body scope instead of the current traversal scope.
- Update the transform conformance snapshot now that the generated constructor scope matches the rebuilt semantic scope.

## Details
When `useDefineForClassFields` lowering needs to synthesize a constructor, the traversal is still positioned in the scope that encloses the class. Static blocks created in the same transform already use `class_scope_id`, but generated constructors used `create_child_scope_of_current`, so their semantic parent could point outside the class.

This changes the generated constructor scope creation to use `create_child_scope(class_scope_id, ...)`, matching where the constructor actually lives in the class body.

## Example
```ts
class Cls {
  y = 1;
  [key] = 2;
}
```

Conceptually this transform creates a constructor for the hoisted field initializers:

```ts
class Cls {
  constructor() {
    this.y = 1;
    this[key] = 2;
  }
}
```

Before this change, the constructor AST was nested in `Cls`, but its semantic scope was parented to the enclosing scope:

```mermaid
flowchart TD
  Program["Program scope"] --> Class["Class body scope: Cls"]
  Program --> GeneratedCtor["Generated constructor scope"]
  Class --> StaticBlocks["Generated static blocks"]
```

After this change, the generated constructor scope is parented to the class body scope, matching the rebuilt semantic tree:

```mermaid
flowchart TD
  Program["Program scope"] --> Class["Class body scope: Cls"]
  Class --> GeneratedCtor["Generated constructor scope"]
  Class --> StaticBlocks["Generated static blocks"]
```

The same mismatch showed up in the accessor backing field fixture:

```ts
class Hello {
  private input = { foo };
  accessor util = this.input.foo();
}
```

AI assistance: OpenAI Codex was used to prepare this change.
2026-06-10 10:01:04 +00:00
camc314 d74964cda6 perf(parser): store class definite assertion offset (#23170)
- Store class property definite assignment assertion positions as `u32` offsets instead of carrying a full `Span`.
- Reconstruct the one-character diagnostic span only when emitting class definite-assignment diagnostics.
- aligns the behaviour for definite assertion errors with variables by reporting on the definite assertion rather than on the whole class member
2026-06-09 15:33:10 +00:00
camc314 00244d8e10 feat(parser): report definite property initializer errors (#23160)
Report TS1263 for TypeScript class properties that combine a definite assignment assertion with an initializer.
2026-06-09 14:42:42 +00:00
oxc-guard b4f9cb9d8c chore(submodules): update submodule SHAs to latest commits (#23118)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `7e115f4` → `05bb032`
- babel: `d9fe348` → `2688fbd`
- TypeScript: `e5509e2` → `7539c04`
- estree-conformance: `faf089d` → `ca42c3d`

This PR is automatically generated by the [update_submodules workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).
2026-06-08 14:49:36 +00:00
oxc-guard[bot] 50baca037c chore(submodules): update submodule SHAs to latest commits (#22721)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `78fc3f8` → `7e115f4`
- babel: `3d34928` → `d9fe348`
- TypeScript: `f350b52` → `e5509e2`
- estree-conformance: `576e7d8` → `faf089d`


This PR is automatically generated by the [update_submodules
workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
2026-05-25 22:16:39 +08:00
Kyle Cannon ee659b6785 feat(transformer/legacy-decorator): add strictNullChecks option for nullable-union design:type (#22266)
## Summary

Adds a `strictNullChecks` field to `DecoratorOptions` (default `true`)
that
controls whether `null` and `undefined` are elided from union
`design:type`
metadata. The default preserves tsc strict semantics (`T | null` →
`Object`);
setting `strictNullChecks: false` matches
babel-plugin-transform-typescript-metadata
and tsc with `--strictNullChecks=false`, emitting the underlying
primitive
(`T | null` → `String` when `T = string`).

Originally this PR unconditionally elided null/undefined. Per @Dunqing's
review, it's now opt-in so the default-strict behaviour is preserved.

## Motivation

When `strictNullChecks` is off, tsc emits the constructor of the
underlying
primitive. babel-plugin-transform-typescript-metadata always elides null
and
undefined regardless of tsconfig. Downstream consumers (NestJS Swagger,
class-validator, TypeORM, AutoMapper) treat `Object` as "no metadata"
and
silently drop fields, so users with `strictNullChecks: false` (common in
older codebases) get worse behaviour under OXC than under tsc/babel.

## Implementation

```rust
pub struct DecoratorOptions {
    pub legacy: bool,
    pub emit_decorator_metadata: bool,
    #[serde(default = "default_as_true")]
    pub strict_null_checks: bool, // default true
}
```

Threaded through `Decorator::new(options) →
LegacyDecorator::new(&options) →
LegacyDecoratorMetadata::new(&options)` (matching the established
`TypeScript::new(&TypeScriptOptions)` precedent), and gated in
`serialize_union_or_intersection_constituents`:

```rust
TSType::TSNullKeyword(_) | TSType::TSUndefinedKeyword(_)
    if !is_intersection && !self.strict_null_checks => continue,
```

Mirrored at the napi binding (`napi/transform/src/transformer.rs`) so
direct
Node consumers can flip it the day this lands. Consumers that wire
tsconfig
(rolldown, bundlers) can forward `compilerOptions.strictNullChecks` to
it.

## Behaviour matrix

| Source | `strictNullChecks: true` (default) | `strictNullChecks:
false` |
|---|---|---|
| `string \| null` | `Object` (matches tsc strict, current behaviour) |
`String` (matches babel, tsc non-strict) |
| `number \| undefined` | `Object` | `Number` |
| `boolean \| null \| undefined` | `Object` | `Boolean` |
| `null \| undefined` | `void 0` | `void 0` |
| `string \| number` | `Object` | `Object` (distinct primitives) |
| `string & null` | `Object` | `Object` (intersection unchanged) |

## Test plan

- [x] 7 unit tests in `tests/integrations/decorator_metadata.rs`
covering
`T | null` under both `strictNullChecks: true` (emits `Object`, current
  behaviour) and `strictNullChecks: false` (emits primitive), plus
`T | undefined`, `T | null | undefined`, the `null | undefined`
void-only
case, the `string | number` distinct-primitive case (still `Object`),
and
  the intersection-with-null regression
- [x] New conformance fixture `oxc/metadata/nullable-union/` with its
own
  `options.json` setting `strictNullChecks: false`
- [x] `cargo test -p oxc_transformer` passes (31 unit + 11 integration)
- [x] `cargo run -p oxc_transform_conformance` shows 0 regressions;
baseline
  fixtures still pass plus the new `nullable-union`
- [x] `cargo fmt -p oxc_transformer -p oxc_transform_napi` and
  `cargo clippy --tests --no-deps` clean

AI assistance was used in writing this patch and tests; the contributor
has
reviewed and tested locally.

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-05-25 13:42:41 +08:00
Kyle Cannon 69a6ba6353 fix(transformer/legacy-decorator): emit Array for ReadonlyArray<T> in decorator metadata (#22265)
## Summary

`ReadonlyArray<T>` is a TypeScript-only utility type with no runtime
value. The legacy decorator metadata emit currently wraps it in the
runtime guard `typeof (_ref = typeof X !== "undefined" && X) ===
"function" ? _ref : Object`, which always falls through to `Object`
because there is no runtime `ReadonlyArray` binding. tsc resolves this
through the checker and emits `Array`; OXC's existing handling of the
short form `readonly T[]` already emits `Array`, so the generic form is
the only divergent shape.

## The bug

```ts
class Entity {
  @D() shortForm!: readonly string[];           // tsc: Array, OXC: Array  (already correct)
  @D() genericForm!: ReadonlyArray<string>;     // tsc: Array, OXC: Object (bug)
  @D() nestedReadonly!: ReadonlyArray<ReadonlyArray<number>>; // same
}
```

The two surface forms describe the same type but produce different
runtime metadata. Downstream consumers (TypeORM `@Column` array
inference, NestJS Swagger schema, AutoMapper field detection) rely on
the `Array` constructor; `Object` causes silent degradation.

## Fix

In `serialize_type_reference_node`, recognize `ReadonlyArray` by name
when the symbol is unresolved or type-only. Emit `global_array(ctx)`
directly, bypassing the wrapper. A user-shadowed `class ReadonlyArray
{}` would have a value symbol and falls through to the normal
class-handling path, so the shadow case is preserved.

```rust
if ident.name == "ReadonlyArray"
    && symbol_id.is_none_or(|sid| ctx.scoping().symbol_flags(sid).is_type())
{
    return Self::global_array(ctx);
}
```

## Scope

Only `ReadonlyArray` is handled in this PR. `ReadonlyMap`,
`ReadonlySet`, `WeakMap`, `WeakSet`, and `Readonly<Array<T>>` peel are
left for a follow-up because tsc's spec emit for those is less
unambiguous and warrants case-by-case verification. The same
predicate-style approach extends naturally if reviewers want them folded
in.

## Test plan

- [x] 4 unit tests in `decorator_metadata_readonly_array.rs`: bare
ReadonlyArray, nested ReadonlyArray, regression on `readonly T[]` short
form, and a user-shadowed `class ReadonlyArray {}` regression
- [x] New conformance fixture `oxc/metadata/readonly-array/`
- [x] `cargo test -p oxc_transformer` passes (31 unit + 9 integration)
- [x] `cargo run -p oxc_transform_conformance` shows 229 OXC fixtures
still passing, +1 new fixture (`readonly-array`); 0 regressions
- [x] `cargo fmt -p oxc_transformer` and `cargo clippy -p
oxc_transformer --tests --no-deps` clean

AI assistance was used in writing this patch and tests; the contributor
has reviewed and tested locally.

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-05-21 18:19:35 +08:00
Dunqing e421ef0f84 fix(transformer/legacy-decorator): return runtime binding for design:type (#22640)
Closes #22227.
Closes #22228.

## Summary

- Switch `design:type` / `design:paramtypes` / `design:returntype` emit from `typeof X === "function" ? X : Object` to `typeof X === "undefined" ? Object : X`, matching SWC and Babel. Cross-file imported enums and namespace-imported enums (`NS.E`) now return the actual enum object instead of silently degrading to `Object` — consumers like type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator can introspect via `Object.values()`.
- Cases where the runtime binding is genuinely missing still fall back to `Object` (stale ambient declarations, erased type imports, unresolved references), so existing behavior is preserved everywhere else.
- See #14740 for the design rationale: matching SWC/Babel rather than tsc's primitive-type emit (which requires cross-file type information OXC doesn't have).
- Conformance: 5 existing fixtures updated for the new emit shape, 6 new fixtures cover the bug paths and predicate boundaries; no `Output mismatch` regressions.

## Example

Input:
```ts
import { StringEnum } from './enums';

class Source {
  @dec value!: StringEnum;
}
```

Before:
```js
// design:type === Object — StringEnum is an enum object, typeof !== "function"
babelHelpers.decorateMetadata("design:type",
  typeof (_ref = typeof StringEnum !== "undefined" && StringEnum) === "function"
    ? _ref : Object)
```

After:
```js
// design:type === StringEnum (the enum object — introspectable via Object.values)
babelHelpers.decorateMetadata("design:type",
  typeof StringEnum === "undefined" ? Object : StringEnum)
```

## Note on the SWC enum-metadata debate

[swc-project/swc#11033](https://github.com/swc-project/swc/issues/11033) argues SWC's emit is wrong for cross-file enums — the runtime returns the enum object (an `{...}`) rather than `Number` / `String` / `Object` as tsc would. This PR adopts the same emit shape SWC and Babel use, so it has the same property: cross-file enum metadata is the enum binding itself, not a primitive constructor.

OXC chose this side of the debate deliberately in #14740 — returning the enum object is what type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator depend on to introspect enum members. Matching tsc's primitive emit would require cross-file type information OXC doesn't have, and would unblock no real consumer. If TypeScript publishes a position on what non-type-checker transformers should emit, this can be revisited.

AI disclosure: drafted with Claude Code, reviewed manually.
2026-05-21 09:50:07 +00:00
oxc-guard[bot] ec7de07b08 chore(submodules): update submodule SHAs to latest commits (#22550)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `ccaac10` → `78fc3f8`
- babel: `4079bcd` → `3d34928`
- estree-conformance: `9c67f5e` → `576e7d8`


This PR is automatically generated by the [update_submodules
workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
2026-05-19 10:46:02 +08:00
Dunqing e6090e7ccc fix(transformer): keep enum IIFE when a non-inlinable value reference remains (#22501)
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).
2026-05-18 02:19:11 +00:00
Dunqing 931b7d6b97 fix(transformer): inline const enum members through type-cast wrappers (#22500)
Closes #22444.

`Phase.idle as Phase`, `Phase.idle satisfies Phase`, `Phase.idle!`, and `<Phase>Phase.idle` failed to inline under `optimizeConstEnums`. The const enum declaration was stripped while the member access was left as `Phase.idle`, producing a runtime `ReferenceError`.

## Cause

The enum inliner's `enter_expression` matched only bare `StaticMemberExpression`/`ComputedMemberExpression`. `annotations.rs` strips TS-only wrappers in its own `enter_expression`, but `walk_expression` only calls `enter_expression` once per node — after the strip the inner `Phase.idle` was never revisited.

## Fix

Peek through the wrappers via `Expression::get_inner_expression_mut()` (it already handles `TSAsExpression`, `TSSatisfiesExpression`, `TSNonNullExpression`, `TSTypeAssertion`, `TSInstantiationExpression`, `ParenthesizedExpression`). The outer `*expr = …` replacement then drops the wrapper along with the inlined value.

## Example

```ts
const enum Phase { idle, active }

class Foo {
  #foo = Phase.idle as Phase;
}
```

Before:
```js
class Foo {
  #foo = Phase.idle;  // dangling reference
}
```

After:
```js
class Foo {
  #foo = 0;
}
```

## Verification

New fixtures cover all wrapper shapes for const + regular enums:
- `babel-plugin-transform-typescript/const-enum-type-cast/`
- `babel-plugin-transform-typescript/optimize-enums/type-cast/`
2026-05-18 02:19:10 +00:00
camc314 c73c159e16 fix(transformer/async-to-generator): reparent parameter initializer scopes (#22507)
Fixes a semantic mismatch in `async_to_generator` for async methods with parameter default initializers.

Before this change, the transform reused the original method scope as the inner generator scope and moved parameter bindings to the wrapper scope. Scopes created inside parameter defaults were left parented under the generator, even though the transformed AST places those defaults with the wrapper parameters.

For example:

```js
class C {
  async method(value = function fallback() {}) {}
}
```

Conceptually this became:

```js
class C {
  method(value = function fallback() {}) {
    // `value` belongs to this wrapper scope.

    return asyncToGenerator(function* () {
      // Before: the scope for `fallback` was still parented here,
      // because this generator reused the original async method scope.
    })();
  }
}
```

A fresh semantic rebuild from the transformed AST sees it differently:

```js
class C {
  method(value = function fallback() {
    // After rebuild: `fallback` is created from the parameter default,
    // so its direct parent is the wrapper method scope.
  }) {
    return asyncToGenerator(function* () {
      // The generator should only own body scopes.
    })();
  }
}
```

This updates `BindingMover` so that when parameter bindings move to the wrapper, direct scopes created inside parameter-side expressions are reparented there too. It still stops at function/class scope boundaries, so nested body scopes are not traversed or moved.
2026-05-17 19:36:14 +00:00
camc314 ecfd3ca7c5 fix(transformer/async-to-generator): move only parameter bindings (#22503)
Previously, given:

```js
class C {
  async method(x = function y() {}) {}
}
```

The async-to-generator transform generates:

```js
class C {
  method(x = function y() {}) {
    return babelHelpers.asyncToGenerator(function* () {})();
  }
}
```

The method parameter stays on the outer wrapper method, while only the original body moves into the inner generator function.

Before this change, `BindingMover` walked the full parameter AST while moving those parameter bindings. That meant it also visited default initializer expressions, so bindings created inside the initializer, like `y` above, could be moved out of their own function/class scope and into the wrapper method scope.

After transform, rebuilt semantics would disagree with the transformed scope tree because the parameter binding was correct, but nested initializer bindings had the wrong owning scope.

This PR makes `BindingMover` only visit the binding positions for formal parameters, rest parameters, and the left side of assignment patterns. It skips initializer expressions so nested function/class bindings stay in the scopes created for those expressions.
2026-05-17 17:22:46 +00:00
oxc-guard e68f4fa1c3 chore(submodules): update submodule SHAs to latest commits (#22417)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `d0c1b45` → `ccaac10`
- babel: `6402dbb` → `4079bcd`
- estree-conformance: `d968f2c` → `9c67f5e`

This PR is automatically generated by the [update_submodules workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).
2026-05-14 10:36:57 +00:00
camc314 dda33deea8 fix(transformer/explicit-resource-management): align lexical binding scopes (#22320)
Part of #9699
2026-05-14 09:18:42 +00:00
camc314 8e79de8f86 fix(transformer): preserve for-await statement bodies (#22361) 2026-05-12 18:28:15 +00:00
leaysgur d3d0b18fde fix(traverse): handle ChainElement::TSNonNullExpression in GatherNodeParts (#22247)
Fixes #22246, transformer panics with `const x = a[b?.c!]?.["d"];` under es2020 target.

(I wasn't sure where to put the test, but I've confirmed that it works on my end for now.)
2026-05-11 09:37:46 +00:00
camc314 4e880de872 fix(transformer/object-rest-spread): declare temp vars for computed keys (#22284)
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.
2026-05-11 09:14:25 +00:00
Dunqing 5da9fda0d8 fix(transformer/explicit-resource-management): preserve class names (#22306)
Stacked on #22305.

## Why

`class C { static getSelf() { return C } }` lowered by the explicit resource management transform was emitting `var C = class {}` (anonymous), so when the outer `C` is reassigned (`C = 123`), `getSelf()` returns the reassigned value instead of the original class — the exact behaviour the named class expression's inner binding is supposed to protect against. Babel emits `var C = class C {}` for the same input.

## Fix

1. **Emit `var C = class C {}`** for both `class C {}` and `export default class C {}`. Adds a `preserve_class_expression_name` helper that mirrors the swap in `decorator::legacy::transform_class_declaration_with_class_decorators` — generate a fresh `Class`-flagged symbol in the class scope for the inner class id, swap `class_decl.id.symbol_id` (via `Cell::replace`) to the new symbol, and repurpose the original symbol as the outer `var` (flags → `FunctionScopedVariable`). Returns the outer `BoundIdentifier` and the original span so the caller can emit the outer declarator with the correct span.

2. **Carry the original span through both sides of the swap.** The inner symbol is created with the original `id.span` (via `Scoping::create_symbol(span, ...)` + `add_binding`); the outer var's `BindingIdentifier` is emitted with the same span (via `BoundIdentifier::create_spanned_binding_pattern`, added in #22305). This eliminates the `Symbol span mismatch` diffs in:
   - `transform-top-level/hoisting/input.mjs`
   - `transform-top-level/hoisting-default-class/input.mjs`
   - `export-class-name/input.js`

## Comparison with Babel

Babel reuses the *original `id` node* for the outer `var` and clones it for the inner class. This PR does the inverse — reuses the original *symbol id* for the outer `var` and generates a new symbol for the inner class. Given oxc's symbol-aware model, the inversion avoids rewiring every outer-scope reference (`const K = C`, `C = 123`, …): they already point at the original symbol.

## Remaining follow-up

`export-class-name/input.js` still has `Symbol reference IDs mismatch` for `C` — references inside the class body (`return C`) still resolve to the outer symbol in the transformer's bookkeeping. A fresh semantic rebuild over the printed AST correctly resolves them to the inner class symbol, so tools that re-run semantic (rolldown / oxc_minifier on a fresh build) are fine. Tools that trust the transformer's symbol table directly (in-place minification / mangler) will see the stale binding. Rewiring those references is a separate follow-up (`Reference.symbol_id` update + `delete_resolved_reference` / `add_resolved_reference`).

## Conformance impact

- Babel suite: **692 → 694 passing** (+2)
- `transform-top-level/hoisting/input.mjs` ✓
- `transform-top-level/hoisting-default-class/input.mjs` ✓
- `export-class-name/input.js`: 4 span mismatches eliminated; reference-rewiring follow-up remains.

## Test plan

- [x] `cargo run -p oxc_transform_conformance`
- [x] `cargo run -p oxc_transform_conformance -- --exec`
- [x] `cargo test -p oxc_transformer`
2026-05-11 08:34:24 +00:00
camc314 b5d970fe5d fix(transformer/explicit-resource-management): preserve class names (#22290)
related to https://github.com/oxc-project/oxc/issues/9699
2026-05-11 07:55:58 +00:00
Dunqing dd88726b8c fix(transformer/legacy-decorator): preserve accessor type annotation for emitDecoratorMetadata (#21966)
Fixes bug 7 of https://github.com/oxc-project/oxc/issues/21922.

## The bug

`@dec accessor x: T` always emitted `design:type = Object` regardless of `T`:

```ts
class Entity {
  @dec accessor name: string = "";   // tsc: design:type = String
  @dec accessor count: number = 0;   // tsc: design:type = Number
}
```

`Reflect.getMetadata("design:type", entity, "name")` returned `Object` instead of `String`. Frameworks reading `design:type` (NestJS Swagger, class-validator, TypeORM column metadata) silently degraded for every typed accessor field.

## Cause

`lower_accessor_properties` (in `enter_class`) rewrites
```ts
@dec accessor x: T = init;
```
into three members — a private storage field, a synthesized getter, and a synthesized setter — and transfers the decorators to the getter so the existing decorator pipeline picks them up. But it dropped the type annotation: the synth getter was constructed with `return_type: NONE`.

The metadata pass runs later and takes the getter branch:

```rust
if method.kind.is_get() {
    (self.serialize_type_annotation(method.value.return_type.as_ref(), ctx), None)
}
```

`return_type` is `None` because lowering discarded it, so `serialize_type_annotation` correctly fell through to `Object` for what it sees as an untyped getter.

The metadata pass's `enter_accessor_property` dispatch exists, but it never fires because the `AccessorProperty` no longer exists by the time traversal reaches the class body.

## Fix

Stop dropping the type annotation. In `lower_accessor_properties`:

```rust
let decorators = std::mem::replace(&mut accessor.decorators, ctx.ast.vec());
let type_annotation = accessor.type_annotation.take();
```

Thread `type_annotation` through `create_accessor_method` to the getter (setter passes `None`). To avoid a post-construction patch on the synthesized `MethodDefinition`, `create_class_method` now takes a `return_type: Option<ArenaBox<TSTypeAnnotation>>` parameter and threads it directly into the function's return-type slot. The other caller (`create_class_constructor_with_params`) passes `None`.

The lowered AST is now self-consistent — the synth getter for `accessor x: T` reads as a getter that returns `T`, which is what it semantically is, and the existing getter-metadata path serializes it correctly.

## Out of scope (architectural limit)

`type Uuid = string; @dec accessor x: Uuid` still emits `Object`, same as for non-accessor properties. Resolving type-alias references to their primitive constituent requires a name-based alias-resolution pass (the same one that `tsc`'s checker provides via `getTypeReferenceSerializationKind`). That's bug 1 of #21922 and a separate change from this fix; flagged here so reviewers don't mistake it for an omission.

## Test plan

- [x] Conformance fixture `oxc/metadata/accessor` covers primitives, untyped (correctly stays `Object`), `string[]` → `Array`, `static accessor`, and computed-key (`@dec accessor ["computed"]: number`)
- [x] `cargo test -p oxc_transformer` — 31 + 5 passed
- [x] `cargo run -p oxc_transform_conformance -- --filter legacy-decorators` — 73 pass / 23 fail (baseline 72/24, +1 new fixture, **0 regressions**)
- [x] `cargo run -p oxc_transform_conformance -- --exec` — accessor exec test verifies `Reflect.getMetadata` returns the correct constructor at runtime
- [x] `cargo fmt -p oxc_transformer`
2026-05-07 14:17:45 +00:00
Dunqing 0c7c01c63b fix(transformer/typescript): inline optional-chain enum member access (#21834)
## Summary

Closes #21813.
Closes #21828.

`c_num?.x` and `c_num?.['x']` were not handled by the enum-inlining pass in `enter_expression`, which only matched `Expression::StaticMemberExpression` and `Expression::ComputedMemberExpression`. When the es2020 optional-chain plugin lowered the chain first, the member access became:

```js
c_num === null || c_num === void 0 ? void 0 : c_num.x
```

Then the inline pass saw the inner `c_num.x` and replaced it with the constant + deleted *that one* reference — but the two references in the test condition stayed live. With `optimizeConstEnums: true`, the const-enum declaration is then unconditionally removed, leaving dangling references to a binding that no longer exists.

The same shape underlies the rolldown report ([rolldown/rolldown#9181](https://github.com/rolldown/rolldown/issues/9181)) where rolldown's cross-module enum inlining drops the import that `c_num` was bound to.

## Fix

Match `Expression::ChainExpression` whose top element is an optional `StaticMemberExpression` or `ComputedMemberExpression` on an enum identifier, and inline the entire chain to the constant **before** es2020 sees it. Reuses the existing `try_inline_enum_member` / `try_inline_computed_enum_member` helpers — they don't care about the `optional` flag.

## Fixtures

- `optimize-enums/optional-chain-issue-21813/` — the bug case + an `exec.ts` that runs the transformed output to verify runtime semantics
- `optimize-enums/optional-chain-value-kept/` — IIFE preserved when a bare `Foo` ref also exists
- `optimize-enums/optional-chain-deeper-access/` — `Foo?.x.toString()` / `Bar?.['y']?.toString()` exercise nested chains where the inner inline still fires via the existing arms
- `const-enum-optional-chain/` — const-enum path with `optimizeConstEnums: true`

## Known limitation

Deeper chains under es2020 lowering — `c_num?.x.y` or `c_num?.x()` where the optional access is below the chain element — still produce dangling refs because the chain element is non-optional or a `CallExpression` and this fix's match arm doesn't trigger. Without es2020 lowering they work (the inner `?.x` gets visited as a regular `Expression::StaticMemberExpression`), so the conformance fixtures pass. A more general fix would walk the chain to find the leftmost optional-on-enum access; out of scope for this PR.

## Test plan

- [x] `cargo run -p oxc_transform_conformance -- --filter optimize-enums/optional-chain` — all three pass
- [x] `cargo run -p oxc_transform_conformance -- --filter const-enum-optional-chain` — passes
- [x] `cargo run -p oxc_transform_conformance -- --exec --filter optimize-enums/optional-chain-issue-21813` — runtime check passes
- [x] `cargo test -p oxc_transformer` — green
- [x] `cargo clippy -p oxc_transformer -- -D warnings` — clean
- [x] CI Conformance — green (snapshots updated for `oxc.snap.md` + `oxc_exec.snap.md`)
2026-05-07 09:04:41 +00:00
oxc-guard[bot] 550052a096 chore(submodules): update submodule SHAs to latest commits (#22170)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `2b2ecea` → `d0c1b45`
- babel: `c543b03` → `6402dbb`
- TypeScript: `c7a0ae1` → `f350b52`
- estree-conformance: `e4104a1` → `d968f2c`


This PR is automatically generated by the [update_submodules
workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
2026-05-06 09:19:12 +01:00
Dunqing e3399ec3a6 fix(transformer/class-properties): preserve RHS in logical-assignment to static private field (#21950)
fixes #21874

`transform_static_assignment_expression` took `assign_expr.right` once at the top of the function (correct for the `=` branch), then took it a second time inside the `else` branch that handles compound operators. The second `take_in` returned a `NullLiteral` dummy, so `??=` / `&&=` / `||=` (and the binary ops) on a static private field where the receiver isn't a direct class reference — e.g. `this.#shared ??= 1` inside a static method — emitted `null` in place of the RHS:

```js
// before
class Singleton {
  static #shared;
  static shared() {
    this.#shared ??= 1;
    return this.#shared;
  }
}

// transformer output (broken):
_assertClassBrand(Singleton, this, _shared)._ ?? (_shared._ = _assertClassBrand(Singleton, this, null));
//                                                                                              ^^^^ should be 1
```

Removes the redundant inner `let class_ident = …; let value = assign_expr.right.take_in(…);` shadowing so the compound-operator branches use the already-captured outer `value` (the real RHS) when building the `_assertClassBrand(Class, object, value)` call. Output now matches Babel.

Adds a `private-static-logical-assignment` conformance fixture covering `??=`, `&&=`, and `||=`, plus an `exec.js` to verify runtime behavior.
2026-04-30 00:39:26 +00:00
Dunqing c59c1990bc fix(transformer/typescript): emit class fields for parameter properties (#21831)
## 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 #21600
Closes #21831
2026-04-30 00:39:25 +00:00
Dunqing 91e5bde420 fix(transformer/typescript): preserve computed-key static block when class has an empty constructor (#21562)
## Summary

When a TypeScript class used a computed-key static property together with a constructor that has no parameter-property assignments, the early return in `transform_class_fields` dropped the `computed_key_assignment_static_block` that initializes the temp var, leaving it declared but never assigned.

```ts
export class SampleClass {
  static [Symbol.toPrimitive] = "test"
  constructor() {}
}
```

previously emitted:

```js
let _Symbol$toPrimitive;
export class SampleClass {
  static {
    this[_Symbol$toPrimitive] = "test";
  }
  constructor() {}
}
```

Now emits (matching TypeScript's own output under `useDefineForClassFields: false`):

```js
let _Symbol$toPrimitive;
export class SampleClass {
  static {
    _Symbol$toPrimitive = Symbol.toPrimitive;
  }
  static {
    this[_Symbol$toPrimitive] = "test";
  }
  constructor() {}
}
```

## Test plan

- Added fixtures `computed-static-property-with-constructor` (exact repro) and `computed-static-property-with-constructor-super` (super() + multiple computed keys) under `babel-plugin-transform-typescript`.
- Verified output against `tsc` 5.7 with `useDefineForClassFields: false`.

Closes rolldown/rolldown#9121
2026-04-20 15:58:31 +08:00
Dunqing 56af2f4fe0 fix(transformer/async-to-generator): correct scope of inferred named FE in async-to-generator (#21458)
When the async-to-generator transform wraps an async arrow / anonymous async function expression whose name is inferred from its parent (e.g. `const foo = async () => {}`, `{ foo: async () => {} }`), it emits a named function expression as the caller:

```js
const foo = function () {
  var _ref = asyncToGenerator(function* () { /* body */ });
  return function foo() { return _ref.apply(this, arguments); };
}();
```

Per the JS spec, the inferred name `foo` binds only inside the named FE itself — not in the enclosing wrapper IIFE scope.

But the previous code had two bugs that together caused oxc to emit an AST that a fresh semantic analysis (e.g. Rolldown's minifier in Vite, or oxc's own minifier) would misread:

1. `create_function` set `FunctionType::FunctionDeclaration` whenever `id.is_some()`, even for callers using the result as a function expression. Any fresh semantic pass trusts `r#type` and treats the node as a declaration — which binds the name in the enclosing wrapper scope.
2. The inferred id's binding was registered in `wrapper_scope_id` rather than the function's own scope.

Combined, downstream minifiers resolve a body reference like `typeNext()` inside `const typeNext = async () => { typeNext(); }` to the inner caller instead of the outer `const typeNext`. The outer const then looks unused, gets inlined away, and the body reference fails at runtime with `ReferenceError: typeNext is not defined`.

## Fix

Pass `FunctionType` explicitly (via new `create_function_expression` and `create_function_declaration` helpers), and place the inferred id's binding in the caller function's own scope.

## Runtime verification

Ran input → oxc transform → oxc compress (same pipeline Rolldown/Vite uses):

**Pre-fix** output (matches the exact bug in the Vite issue — outer const inlined away):
```js
(function() {
  var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
  return function typeNext() { return _ref.apply(this, arguments); };
})()();
```
Runtime: `ReferenceError: typeNext is not defined` ✗

**Post-fix** output (outer const preserved; minifier correctly drops the now-unused inner FE name):
```js
const typeNext = function() {
  var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
  return function() { return _ref.apply(this, arguments); };
}();
typeNext();
```
Runtime: works correctly ✓

Closes #21125
Closes #21445
2026-04-15 14:06:08 +00:00