mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
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:
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:
|
||||
|
||||
|
||||
+80
@@ -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"]);
|
||||
})();
|
||||
+13
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["transform-explicit-resource-management"]
|
||||
}
|
||||
+26
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user