## Summary
A single minify pass produced output where a second pass added `/* @__PURE__ */` / `/* #__NO_SIDE_EFFECTS__ */` annotations the first pass had missed.
Examples (line 2 is what one pass produces; line 3 is what two passes produce, which is what idempotency requires from one pass):
```js
new RegExp('^…$' + '$') // input
new RegExp("^…$") // single pass (before fix)
/* @__PURE__ */ new RegExp("^…$") // expected
```
```js
var ab = new ArrayBuffer(1); var dv = new DataView(ab); foo(dv) // input
var dv = new DataView(/* @__PURE__ */ new ArrayBuffer(1)); foo(dv) // single pass (before fix)
var dv = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(1)); foo(dv) // expected
```
## Root cause
`Normalize::set_pure_or_no_side_effects_to_new_expr` / `set_no_side_effects_to_call_expr` set the flags by inspecting the callee identity and the *current* arg shape (e.g. `new RegExp(<string literal>)` → pure, `new DataView(<already-pure NewExpression>)` → pure). But `Normalize` is a one-shot pass that runs once *before* the peephole fixed-point loop:
```rust
// crates/oxc_minifier/src/compressor.rs
Normalize::new(normalize_options).build(program, &mut ctx);
Self::run_in_loop(max_iterations, program, &mut ctx)
```
The loop then folds string concats and inlines single-use variables, turning opaque args into the literal / already-pure shapes `Normalize` would have matched on — but only after `Normalize` has finished. The flag is stale by the time codegen runs.
## Fix
Wire the two flag-setters back into `PeepholeOptimizations`'s `exit_call_expression` / `exit_new_expression` so they re-evaluate each loop iteration:
- DCE-only mode: the exit hooks already early-return without doing other substitutions; the flag-setter runs before the early return.
- Full minify mode: the flag-setter runs *after* `substitute_*`, so it sees the post-flatten arg shape.
Both flag-setters already guard with `if pure { return; }` — they're monotone, so cascading is well-behaved and the loop converges normally.
Promoted `set_pure_or_no_side_effects_to_new_expr` and `set_no_side_effects_to_call_expr` to `pub(crate)` so peephole can call them.
## Found by
monitor-oxc's DCE runner: 104 files in the npm-top-3000 corpus failed the idempotency check on this pattern (sentry, vite, svgo, js-yaml, html-minifier-terser, libphonenumber-js, loupe, basic-ftp, is-data-view, stack-utils, …).
## Tests
- `dead_code_elimination::pure_comment_re_evaluated_after_string_concat_fold`
- `dead_code_elimination::pure_comment_re_evaluated_after_variable_inline`
- `inline_single_use_variable::pure_comment_for_pure_global_constructors_after_inline` (this one goes through full minify; `test_options` checks idempotency, so the test fails without the non-DCE arm of the fix)
## Verification
- `cargo test -p oxc_minifier` → 504 passed (was 501; +3 new), 0 failed
- `cargo test -p oxc_codegen` → 105 passed, 0 failed
- `just minsize` → snapshot unchanged across all 12 fixtures
- `just allocs` → +3 allocs / +6 max-allocs on pdf.mjs (negligible)
- End-to-end via monitor-oxc DCE runner: 104 → 0 PURE-annotation idempotency failures on the 116,429-file corpus. The 9 remaining DCE failures are unrelated bugs (arrow-alternate comment attachment ×8, `__webpack_exports__` unused-variable fixed-point ×1) and need separate fixes.
## Summary
In unambiguous (`.js` auto-detect) sources, `export var x = await + 1;` reported a spurious `Duplicated export 'x'` error. The parser first parsed the statement with `await` as an identifier, then re-parsed it under `Context::Await` to patch the AST — but the second pass re-invoked `visit_export_named_declaration`, recording the binding `x` a second time in the module record.
`export` is unambiguously module syntax per ECMA-262 §16.2.3 (`ExportDeclaration` is only a `ModuleItem`, never a Script production). So commit to the Module goal — enable `Context::Await` — as soon as the next top-level token is `Kind::Export`. The statement then parses correctly on the first attempt; no reparse and no duplicate record.
`Kind::Import` is intentionally **not** eager-switched. TypeScript's `import name = ns.path` is a script-compatible namespace alias (see `topLevelAwait.2.ts`), so treating `import` as a module starter would break that case.
The only TS snapshot churn is +16 lines of additional error messages for already-failing tests (`topLevelAwaitErrors.5.ts`, `topLevelAwaitErrors.6.ts` — `export ... await`). Summary counts unchanged.
Closes#22520Fixes#22158
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.
## Summary
`parse_extends_clause` kept iterating after `parse_lhs_expression_or_higher` set a fatal error, producing a `TSInterfaceHeritage` with an inverted span (`start > end`). `parse_class` then passed that span to `classes_can_only_extend_single_class`, which panicked in `miette` when converting the span to a label (`debug_assert!(self.start <= self.end)` in `Span::size`). Release builds silently produced garbage diagnostics.
Repro from the issue:
```js
class extends,{
```
Closes#22515🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
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.
Previously, given:
```
for (var { x, ...rest } of iterable) {}
```
The destructuring pattern appears in the `for` head, but `var` bindings are not owned by that scope - `x` and `rest` are hoisted into the current hoist scope.
After transform:
```
for (var _ref of iterable) {
var x = _ref.x;
var rest = _objectWithoutProperties(_ref, _excluded);
}
```
The bindings moved into the loop scope, but since we were passing the `for` loop's scope, `move_binding_by_symbol_id` was a no-op as the var decls were not defined in the `for` loop's scope.
This PR get's the declared scope of the symbol and uses it rather than assuming that the var is scoped to the function
`for await` lowering emits several temporary bindings as `var`, but the transformer was registering those generated symbols in the current block scope instead of the nearest hoist scope.
For example, this input:
```js
async function* f(iter) {
{
for await (const value of iter) {
value;
}
}
}
```
is lowered into code shaped like this:
```js
async function* f(iter) {
{
var _iteratorAbruptCompletion = false;
var _didIteratorError = false;
var _iteratorError;
try {
for (
var _iterator = _asyncIterator(iter), _step;
// ^^^^^^^^^ ^^^^^
// These are `var` declarations, so semantically they belong to
// the function/hoist scope, not the inner block scope.
_iteratorAbruptCompletion = !(_step = await _iterator.next()).done;
_iteratorAbruptCompletion = false
) {
const value = _step.value;
value;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
}
}
}
```
Before this PR, the AST transform generated the right JavaScript, but its semantic metadata recorded `_iterator`, `_step`, `_didIteratorError`, `_iteratorError`, and `_iteratorAbruptCompletion` in the current block scope:
That made the transformed semantic data disagree with semantic data rebuilt from the emitted AST.
The transform rewrites cases like:
```js
for ({ ...rest } of [{}]) {}
```
to use a generated `var` binding in the loop head:
```js
for (var _ref of [{}]) {
// ...
}
```
Previously, the generated `_ref` binding was being created in the loop statment scope (`current_scope_id`), this is incorrect because it is being emitted as `var`. The rebuilt semantic correctly shows it being hoisted into the current var-hoist scope.
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.
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`
Fixes parser recovery for class computed property names whose expression starts with an array literal, such as `class C { [[]]() {} }`.
The TypeScript index-signature lookahead previously treated a nested `[` after the opening computed-name bracket as an unambiguous index signature. That caused valid class computed keys containing array literals to be parsed through the wrong branch and rejected.
This changes the lookahead so only `...` remains an early unambiguous index-signature signal, allowing nested arrays and other assignment expressions to parse as computed property names.
fixes: #22157
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.
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [rust](https://redirect.github.com/rust-lang/rust) | minor | `1.94.1` → `1.95.0` |
---
### Release Notes
<details>
<summary>rust-lang/rust (rust)</summary>
### [`v1.95.0`](https://redirect.github.com/rust-lang/rust/compare/1.94.1...1.95.0)
[Compare Source](https://redirect.github.com/rust-lang/rust/compare/1.94.1...1.95.0)
</details>
---
### Configuration
📅 **Schedule**: (in timezone Asia/Shanghai)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/oxc-project/oxc).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjMuOCIsInVwZGF0ZWRJblZlciI6IjQzLjEyMy44IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
## Summary
Depends on #20602.
Closes: https://github.com/oxc-project/oxc/issues/6073Closes: #11844
- Inline const enum member accesses (`Foo.Bar` / `Foo["Bar"]`) to literal values when `optimize_const_enums` is enabled
- Remove non-exported const enum declarations when all members are evaluable
- Use `exit_statements` for enum removal so inlining happens before removal
- Delete references when inlining enum member accesses for correct semantic data
- Handle `IdentifierReferenceRename` for sibling enum member references in merged declarations
## Test plan
- [x] Transform conformance tests pass (`cargo run -p oxc_transform_conformance`)
- [x] `cargo test -p oxc_transformer` passes
- [x] Babel conformance snapshot updated
## Summary
- Use raw property/identifier names instead of `get_var_name_from_node` (which strips leading underscores) when generating private backing field names for accessor properties
- For non-computed keys, use `PropertyKey::name()` which preserves leading underscores
- For computed keys, match on `Expression::Identifier` to get the raw name directly
- No counters, maps, or helper functions needed — collisions are avoided at the source
This prevents `accessor prop` and `accessor _prop` from producing the same `#_prop_accessor_storage` name, which caused a runtime `TypeError: Cannot initialize the same private elements twice on an object`.
Closes#20946
Supersedes #21023
## Test plan
- [x] Added test fixture covering both non-computed (`prop`/`_prop`) and computed (`[prop]`/`[_prop]`) accessor name collisions
- [x] All existing accessor tests pass unchanged
- [x] `cargo test -p oxc_transformer` passes
- [x] Full `cargo run -p oxc_transform_conformance` passes
- [x] `cargo clippy` clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`illegal_modifier_error` contained a large amount of repetitious code, and was hard to read. Convert it to a shorter version which makes the logic clearer, reusing the lookup table introduced in #20748.
This change also reduces the size of generated code considerably and makes it faster, but #20748 moved this code onto a cold path, so that doesn't really matter. The main benefit is greater readability.
One test case produces a slightly different error after this change.
```ts
class D extends B {
constructor(override readonly public foo: string) {}
}
```
```diff
- TS(1029): 'public' modifier must precede 'override' modifier.
+ TS(1029): 'public' modifier must precede 'readonly' modifier.
```
Both errors are correct, and which gets flagged was arbitrary before - it wasn't dependent on the order of modifiers in the source code, only the order of `if` branches in `illegal_modifier_error`. So IMO this change isn't important - it's just a *different* arbitrary order now.
This PR replaces the mangler-level fix in #20705 with a semantic-level fix that is simpler and allows better name reuse.
## Summary
In sloppy mode JavaScript, function declarations inside blocks (`if`, `try`, `switch`, `{}`) have **Annex B.3.2.1** semantics: they create an implicit `var`-like binding in the enclosing function scope at block exit. The semantic analysis was not modeling this hoisting, which caused the mangler to assign conflicting names to block-scoped functions and outer `var` bindings — changing program behavior at runtime.
### How the fix works
Hoist plain (non-async, non-generator) function declarations from block scopes to the enclosing var scope during binding, using the **same pattern as `var` hoisting** (binder.rs lines 46-98):
1. `move_binding(block_scope, var_scope, name)` — moves the binding
2. `set_symbol_scope_id(symbol_id, var_scope)` — updates the symbol's scope
3. `hoisting_variables.insert(block_scope, name, symbol_id)` — keeps a hoisting entry in the block scope for redeclaration checks
This makes the mangler handle Annex B functions naturally alongside other var-scope bindings, with **zero mangler changes needed**. Name reuse from sibling scopes is preserved (unlike the mangler-level fix which was overly conservative).
### Guards
- `is_declaration` — function expressions are bound in their own (var) scope
- `!self.r#async && !self.generator` — Annex B only applies to plain functions
- `!builder.source_type.is_typescript()` — Annex B is JavaScript-only
- `!scope_flags.is_var()` — already in a var scope, no hoisting needed
- `!scope_flags.is_strict_mode()` — no Annex B in strict mode / modules
Fixes#20610. Fixes#14316.
Downstream: [vitejs/vite#22009](https://github.com/vitejs/vite/issues/22009), [rolldown/rolldown#8791](https://github.com/rolldown/rolldown/issues/8791).
## Test plan
- 13 sloppy-mode mangler snapshot test cases (all pass without mangler changes)
- 975 linter tests pass
- 40 semantic unit tests pass
- Parser conformance: improved (babel negative 1687→1689, test262 negative stays 100%)
- Semantic conformance: ~10 new mismatches in `semantic_test262` — these are transformer rebuild differences where the transformed JS output correctly gets Annex B hoisting that the original TS parse didn't have
## Summary
- Fix `design:type` metadata for getter/setter decorators without type annotations emitting `void 0` instead of `Object`
- Use `serialize_type_annotation` (which defaults to `Object`) instead of `serialize_return_type_of_node` (which defaults to `void 0`) for getter `design:type`
- Verified output matches TypeScript's behavior
Closes#20368
## Test plan
- [x] Added untyped getter/setter test cases to `getter-setter-method` fixture
- [x] Verified output matches TypeScript compiler output
- [x] All conformance tests pass with no regressions
- [x] Clippy clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Closes#20133
- Lower decorated `accessor` properties to private backing field + getter/setter pair when `experimentalDecorators` is enabled, matching TypeScript's `classFields.ts` `transformAutoAccessor`
- Only accessors with decorators are lowered — undecorated accessors are left as-is
- Support computed accessor keys (e.g. `@dec accessor [expr]`) via temp var to ensure the key expression is evaluated only once
- Accessor lowering runs in `enter_class` (Decorator plugin) so es2022 class-properties can further transform the generated private fields
- Decorators are transferred from the accessor to the generated getter, so the legacy decorator transform emits `_decorate()` calls
### Transform example
```ts
// Input
class C {
@dec accessor prop: string = "hello";
@dec accessor [expr]: any;
}
```
```js
// Output (without class-properties)
var _a = expr;
class C {
#_prop_accessor_storage = "hello";
get prop() { return this.#_prop_accessor_storage; }
set prop(value) { this.#_prop_accessor_storage = value; }
#_a;
get [_a]() { return this.#_a; }
set [_a](value) { this.#_a = value; }
}
babelHelpers.decorate([dec], C.prototype, "prop", null);
babelHelpers.decorate([dec], C.prototype, _a, null);
```
## Test plan
- [x] New test `oxc/accessor` — decorated accessors without class-properties plugin (instance, static, initialized, computed key)
- [x] New test `oxc/accessor-with-class-properties` — full pipeline with class-properties plugin (private fields transformed to WeakMap/brand patterns)
- [x] New test `decoratorOnClassAccessorProperty1` — instance, static, and initialized accessor properties with decorators
- [x] Existing `decoratorOnClassProperty13` output updated for accessor lowering
- [x] Undecorated accessors are not lowered (`oxc-13284.js` coverage test passes)
- [x] `cargo clippy`, `just fmt` clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This PR has two fixes. The second fix exposed a pre-existing bug that
needed to be fixed first.
- 24a72d7bc: `create_read_write_target` was used for a simple `=`
assignment when creating a temp var for decorated computed property
keys. Should be `create_write_target`.
- f0affc6f4: `transform_class_on_exit` was removing all no-initializer
properties unconditionally. But the legacy decorator embeds an
assignment in the computed key (`[FIELD_NAME]` → `[_a =
FIELD_NAME]`), so removing the property also removes the assignment,
leaving `_a` undefined in `_decorate()`. Keep properties whose computed
keys have side effects, matching TypeScript's
behavior.
Fixes#20418
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
## Summary
- Report a syntax error for `new super()` per the ECMAScript spec grammar
- `MemberExpression` includes `SuperProperty` (`super.x`, `super[x]`) but NOT bare `super`, so `new super()` is invalid
- `new super.method()` and `new super[x]()` remain valid
Closes#20306🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tighten up parsing of TS snapshot files for ESTree conformance tests. Previously if the snapshot files were malformed and didn't match expected format, those tests would be silently skipped. Instead, panic if snapshot files are malformed.
Refactor of ESTree conformance tests.
These conformance tests check both AST and tokens for each category (Test262, Acorn JSX, TS). The functions that test AST and tokens for each category have a lot of shared logic.
Move that shared logic into a separate function for each category, so the main test functions for AST and tokens contain only the logic that's specific to them. This makes the tests less verbose and easier to understand.
ESTree conformance tests for tokens were doing unnecessary work. Converting all spans in the AST from UTF-8 to UTF-16 is unnecessary, since the AST is never looked at in these tests.
Pure refactor. Re-order testing functions in ESTree conformance tests so each pair of AST test and tokens test functions are together. Does not alter any code, just re-order it.
## Summary
- Replace bubble-up reference resolution with V8-style walk-up algorithm
- References are collected in a flat `Vec<(Ident, ReferenceId)>` during traversal instead of per-scope hashmaps
- After full AST visit, all references are resolved in a single pass by walking up the scope chain from each reference's scope
- `leave_scope()` no longer does any reference resolution work — only `direct_eval` flag propagation remains
- Early resolution via checkpoint mechanism preserved for function parameters and catch parameters (prevents binding to body variables that share the same scope)
- Deduplicated walk-up logic into a single `#[inline(always)]` helper shared by both normal and early resolution paths
- Uses `to_vec+truncate` instead of `split_off` to preserve `Vec` capacity and avoid reallocation
- Removes `smallvec` and `oxc_data_structures` dependencies from `oxc_semantic`
### Why this is faster
| Operation | Bubble-up (old) | Walk-up (new) |
|-----------|-----------------|---------------|
| Per reference creation | Hashmap insert | Vec push (O(1) amortized) |
| Per scope exit | Drain + merge hashmap | Nothing |
| Per reference resolution | Implicit via merging | Walk scope chain, O(depth) lookups |
| Total hashmap mutations | O(refs × depth) inserts+drains | 0 during traversal |
Hashmap lookups (read-only) are cheaper than hashmap drain+insert (mutating, allocating). The flat Vec also eliminates creating/recycling per-depth hashmaps entirely.
### Snapshot changes
3 jest linter rule snapshots have diagnostic reordering (same diagnostics, different emission order) due to references being processed in insertion order rather than scope-by-scope.
🤖 Generated with [Claude Code](https://claude.com/claude-code)