## Summary
- propagate `DirectEval` when async-to-generator lowering inserts wrapper scopes
- update the Test262 semantic snapshot for the newly passing cases
## Known edge case
A direct eval nested in a function-valued default parameter of an async method can still leave a stale flag when that parameter scope is reparented. This should be addressed by the new function body scope logic
Fixes semantic scope mismatches when async function declarations are lowered inside sloppy block scopes.
Before this change, `async_to_generator` kept the original async function binding, and the generated helper binding, in the current block scope. After transform those declarations are no longer async:
```js
with (obj) {
async function ref() {}
}
```
becomes roughly:
```js
with (obj) {
function ref() {
return _ref.apply(this, arguments);
}
function _ref() {
_ref = babelHelpers.asyncToGenerator(function* () {});
return _ref.apply(this, arguments);
}
}
```
A fresh semantic rebuild treats those lowered plain function declarations with Annex B hoisting rules, so in sloppy block scopes their bindings can belong to the enclosing var scope instead of the block scope. The transformed semantic tree was still using the pre-lowered block placement, which caused rebuild mismatches in Test262 coverage.
This PR makes `async_to_generator` choose the same binding scope that a rebuilt semantic tree would use for the emitted plain function declarations, while preserving the existing behavior for TypeScript, strict mode, var scopes, and cases where the hoist scope already has a binding.
## Summary
Fixes semantic scope metadata for private method helper functions that
are lifted out of classes nested inside static field initializers.
The class-properties transform converts a private method into a helper
function declaration and emits that helper outside the class. For a
nested class inside a static field initializer, `current_scope_id()`
points at the nested class scope, but the helper is actually emitted
into the surrounding hoist scope. That made semantic rebuild disagree
with the transformed AST.
## Before
```js
class Outer {
static value = class Inner {
static #method() {}
};
}
```
The helper function was reparented as if it still lived under the nested
class scope:
```mermaid
flowchart TD
Program["enclosing hoist scope"] --> Outer["Outer class scope"]
Outer --> Field["static field initializer"]
Field --> Inner["Inner class scope"]
Inner --> Helper["lifted helper function scope"]
Helper -.-> Problem["incorrect parent + strict-mode source"]
```
In code, the old logic always used the current scope and current scope
flags:
```rust
let new_parent_id = ctx.current_scope_id();
ctx.scoping_mut().change_scope_parent_id(scope_id, Some(new_parent_id));
let make_sloppy_mode = !ctx.current_scope_flags().is_strict_mode();
```
That is correct for normal private methods, but not for private methods
inside a class expression that appears in a static field initializer.
## After
When the transform is running under a static `PropertyDefinitionValue`,
the helper is parented to the current hoist scope instead:
```rust
let new_parent_id = if Self::is_inside_static_property_initializer(ctx) {
ctx.current_hoist_scope_id()
} else {
ctx.current_scope_id()
};
ctx.scoping_mut().change_scope_parent_id(scope_id, Some(new_parent_id));
let make_sloppy_mode = !ctx.scoping().scope_flags(new_parent_id).is_strict_mode();
```
The predicate checks the field is actually static:
```rust
matches!(ancestor, Ancestor::PropertyDefinitionValue(property) if *property.r#static())
```
The resulting scope shape now matches where the helper is emitted:
```mermaid
flowchart TD
Program["enclosing hoist scope"] --> Helper["lifted helper function scope"]
Program --> Outer["Outer class scope"]
Outer --> Field["static field initializer"]
Field --> Inner["Inner class scope"]
```
The strict/sloppy adjustment also reads from the selected parent scope,
so the helper's flags match the output location rather than the original
nested class context.
## Validation
- `just fmt`
- `cargo test -p oxc_transformer`
- `just coverage` was run before review. It aborted later with a stack
overflow / signal 6 in `target/coverage/oxc_coverage`; before the abort,
the targeted `semantic_test262` failures were removed from the generated
snapshot output.
AI assistance: Codex was used to cherry-pick, review, test, and draft
this PR description.
Fixes private method transforms so scopes inherited from a transformed private method are converted back to sloppy mode when the surrounding scope is sloppy.
Previously we only cleared `StrictMode` on the private method function scope itself. That left child scopes, such as async/generator wrapper scopes, marked as strict even though the transformed output is no longer inside a class method. This caused semantic rebuild mismatches across Babel and Test262 coverage.
Nested classes are still left strict, since class bodies are always strict mode.
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.
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
- Skip annexB assignmenttargettype tests in test262 conformance suite
- These tests require call expressions to be valid assignment targets in non-strict mode (e.g., `f() = 1`, `f()++`, `for (f() in [1]) {}`)
- This is an Annex B "web reality" feature that oxc intentionally does not support
## Test plan
- [x] `cargo coverage -- parser` shows 100% pass rate (46556/46556)
- [x] All affected snapshots updated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Fix parsing of `for (using of = null;;)` as a using declaration where `of` is the binding identifier
- Previously, the parser incorrectly treated `of` as the keyword starting a for-of loop
The fix uses a lookahead to check if the token after `of` is `=`, `;`, or `:`, matching TypeScript's parser behavior.
## Test plan
- `cargo coverage -- parser` - conformance tests pass
- `cargo test -p oxc_parser` - unit tests pass
🤖 Generated with [Claude Code](https://claude.ai/code)