fix(transformer/explicit-resource-management): transform classic for loop using declarations (#27148)

## Summary

- Lower `using` and `await using` in classic `for` initializers so resources are disposed when the whole loop exits, including abrupt completion and errors in later initializers.
- Keep labels inside the disposal wrapper so labeled `continue` still targets the loop.
- Add transform and runtime fixtures covering disposal order and initializer failures.
This commit is contained in:
camc314 committed 2026-09-28 15:10:33 +00:00
1 parent 80045f5f13
commit 72d62a195e
7 files changed
+210 -4

No files matched your search

@@ -41,7 +41,7 @@ use oxc_allocator::{Address, ArenaBox, ArenaVec, GetAddress, ReplaceWith, TakeIn
use oxc_ast::ast::*;
use oxc_ecmascript::BoundNames;
use oxc_semantic::{NodeId, ScopeFlags, ScopeId, SymbolFlags, SymbolId};
use oxc_span::{SPAN, Span};
use oxc_span::{GetSpan, SPAN, Span};
use oxc_str::static_ident;
use oxc_traverse::{BoundIdentifier, Traverse};
@@ -306,6 +306,9 @@ impl<'a> Traverse<'a, TransformState<'a>> for ExplicitResourceManagement<'a> {
match stmt {
Statement::BlockStatement(_) => self.transform_block_statement(stmt, ctx),
Statement::SwitchStatement(_) => self.transform_switch_statement(stmt, ctx),
Statement::ForStatement(_) | Statement::LabeledStatement(_) => {
Self::transform_for_statement(stmt, ctx);
}
_ => {}
}
}
@@ -626,6 +629,87 @@ impl<'a> ExplicitResourceManagement<'a> {
}
}
/// Dispose a classic `for` loop's initializers after the entire loop completes.
/// Keep any labels inside the `try` so labeled `continue` statements still target the loop.
fn transform_for_statement(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let mut loop_stmt = &mut *stmt;
while let Statement::LabeledStatement(labeled) = loop_stmt {
loop_stmt = &mut labeled.body;
}
let Statement::ForStatement(for_stmt) = loop_stmt else { return };
let Some(ForStatementInit::VariableDeclaration(decl)) = &mut for_stmt.init else {
return;
};
let is_await_using = match decl.kind {
VariableDeclarationKind::Using => false,
VariableDeclarationKind::AwaitUsing => true,
_ => return,
};
let using_ctx = ctx.generate_uid(
"usingCtx",
ctx.current_hoist_scope_id(),
SymbolFlags::FunctionScopedVariable,
);
decl.kind = VariableDeclarationKind::Const;
for declarator in &mut decl.declarations {
if let Some(init) = declarator.init.take() {
declarator.init = Some(Expression::new_call_expression(
SPAN,
Expression::new_static_member_expression(
SPAN,
using_ctx.create_read_expression(ctx),
IdentifierName::new(
SPAN,
if is_await_using { static_ident!("a") } else { static_ident!("u") },
ctx,
),
false,
ctx,
),
None,
[Argument::from(init)],
false,
ctx,
));
}
}
let span = stmt.span();
let parent_scope_id = ctx.current_scope_id();
let body_scope_id = ctx.insert_scope_below_statement(stmt, ScopeFlags::empty());
let callee = helper_load(Helper::UsingCtx, ctx);
let context_stmt = Statement::new_variable_declaration(
SPAN,
VariableDeclarationKind::Var,
[VariableDeclarator::new(
SPAN,
using_ctx.create_binding_pattern(ctx),
None,
Some(Expression::new_call_expression(SPAN, callee, None, [], false, ctx)),
false,
ctx,
)],
false,
ctx,
);
stmt.replace_with(|loop_stmt| {
Self::create_try_stmt(
BlockStatement::boxed_with_scope_id(
SPAN,
[context_stmt, loop_stmt],
body_scope_id,
ctx,
),
&using_ctx,
parent_scope_id,
is_await_using,
span,
ctx,
)
});
}
/// Transform block statement.
///
/// Input:
@@ -1,6 +1,6 @@
commit: fd665901
Passed: 281/409
Passed: 282/410
# All Passed:
* babel-plugin-transform-class-static-block
@@ -21,7 +21,7 @@ Passed: 281/409
* plugin-tagged-template-transform
# babel-plugin-transform-explicit-resource-management (4/5)
# babel-plugin-transform-explicit-resource-management (5/6)
* export-class-name/input.js
Symbol reference IDs mismatch for "C":
after transform: SymbolId(1): [ReferenceId(1), ReferenceId(2), ReferenceId(3), ReferenceId(7)]
@@ -2,7 +2,7 @@ commit: fd665901
node: v26.5.0
Passed: 22 of 24 (91.67%)
Passed: 23 of 25 (92.00%)
Failures:
@@ -0,0 +1,80 @@
function disposable(events, name) {
return {
[Symbol.dispose]() {
events.push(name);
},
};
}
function asyncDisposable(events, name) {
return {
[Symbol.asyncDispose]: async () => {
events.push(name);
},
};
}
const events = [];
let iteration = 0;
outer: for (
using first = disposable(events, "first"), second = disposable(events, "second");
iteration < 2;
iteration++
) {
expect(events).toEqual([]);
if (iteration === 0) continue outer;
break outer;
}
expect(events).toEqual(["second", "first"]);
const failure = new Error("initializer failed");
const caught = [];
let sawFailure = false;
try {
for (
using first = disposable(caught, "disposed"),
second = (() => {
throw failure;
})();
false;
0
) {}
} catch (error) {
expect(error).toBe(failure);
sawFailure = true;
}
expect(sawFailure).toBe(true);
expect(caught).toEqual(["disposed"]);
return (async () => {
const asyncEvents = [];
for (
await using first = asyncDisposable(asyncEvents, "first"),
second = asyncDisposable(asyncEvents, "second");
true;
0
) {
expect(asyncEvents).toEqual([]);
break;
}
expect(asyncEvents).toEqual(["second", "first"]);
const asyncFailure = new Error("async initializer failed");
const asyncCaught = [];
let sawAsyncFailure = false;
try {
for (
await using first = asyncDisposable(asyncCaught, "disposed"),
second = (() => {
throw asyncFailure;
})();
false;
0
) {}
} catch (error) {
expect(error).toBe(asyncFailure);
sawAsyncFailure = true;
}
expect(sawAsyncFailure).toBe(true);
expect(asyncCaught).toEqual(["disposed"]);
})();
@@ -0,0 +1,13 @@
function run() {
outer: inner: for (using first = getFirst(), second = getSecond(); keepGoing(); advance()) {
if (skip()) continue outer;
if (stop()) break inner;
use(first, second);
}
}
async function runAsync() {
for (await using first = getFirst(), second = getSecond(); keepGoing(); advance()) {
use(first, second);
}
}
@@ -0,0 +1,3 @@
{
"plugins": ["transform-explicit-resource-management"]
}
@@ -0,0 +1,26 @@
function run() {
try {
var _usingCtx = babelHelpers.usingCtx();
outer: inner: for (const first = _usingCtx.u(getFirst()), second = _usingCtx.u(getSecond()); keepGoing(); advance()) {
if (skip()) continue outer;
if (stop()) break inner;
use(first, second);
}
} catch (_) {
_usingCtx.e = _;
} finally {
_usingCtx.d();
}
}
async function runAsync() {
try {
var _usingCtx2 = babelHelpers.usingCtx();
for (const first = _usingCtx2.a(getFirst()), second = _usingCtx2.a(getSecond()); keepGoing(); advance()) {
use(first, second);
}
} catch (_) {
_usingCtx2.e = _;
} finally {
await _usingCtx2.d();
}
}