diff --git a/packages/babel-plugin-proposal-explicit-resource-management/src/index.ts b/packages/babel-plugin-proposal-explicit-resource-management/src/index.ts index 01db068d15ba..ca58f2c6c860 100644 --- a/packages/babel-plugin-proposal-explicit-resource-management/src/index.ts +++ b/packages/babel-plugin-proposal-explicit-resource-management/src/index.ts @@ -1,7 +1,7 @@ import { declare } from "@babel/helper-plugin-utils"; import syntacExplicitResourceManagement from "@babel/plugin-syntax-explicit-resource-management"; -import { types as t, template } from "@babel/core"; -import type { NodePath } from "@babel/traverse"; +import { types as t, template, traverse, type PluginPass } from "@babel/core"; +import type { NodePath, Visitor } from "@babel/traverse"; export default declare(api => { // TOOD: assert version 7.22.0 @@ -20,178 +20,204 @@ export default declare(api => { ); } - return { - name: "proposal-explicit-resource-management", - inherits: syntacExplicitResourceManagement, + const transformUsingDeclarationsVisitor: Visitor = { + ForOfStatement(path: NodePath) { + const { left } = path.node; + if (!isUsingDeclaration(left)) return; + + const { id } = left.declarations[0]; + const tmpId = path.scope.generateUidIdentifierBasedOnNode(id); + left.declarations[0].id = tmpId; + left.kind = "const"; + + path.ensureBlock(); + path.node.body.body.unshift( + t.variableDeclaration("using", [ + t.variableDeclarator(id, t.cloneNode(tmpId)), + ]), + ); + }, + BlockStatement(path, state) { + let stackId: t.Identifier | null = null; + let needsAwait = false; - visitor: { - ForOfStatement(path: NodePath) { - const { left } = path.node; - if (!isUsingDeclaration(left)) return; - - const { id } = left.declarations[0]; - const tmpId = path.scope.generateUidIdentifierBasedOnNode(id); - left.declarations[0].id = tmpId; - left.kind = "const"; - - path.ensureBlock(); - path.node.body.body.unshift( - t.variableDeclaration("using", [ - t.variableDeclarator(id, t.cloneNode(tmpId)), - ]), - ); - }, - BlockStatement(path, state) { - let stackId: t.Identifier | null = null; - let needsAwait = false; - - for (const node of path.node.body) { - if (!isUsingDeclaration(node)) continue; - stackId ??= path.scope.generateUidIdentifier("stack"); - const isAwaitUsing = - node.kind === "await using" || TOP_LEVEL_AWAIT_USING.has(node); - needsAwait ||= isAwaitUsing; - - if ( - !TOP_LEVEL_AWAIT_USING.delete(node) && - !TOP_LEVEL_USING.delete(node) - ) { - node.kind = "const"; - } - node.declarations.forEach(decl => { - const args = [t.cloneNode(stackId), decl.init]; - if (isAwaitUsing) args.push(t.booleanLiteral(true)); - decl.init = t.callExpression(state.addHelper("using"), args); - }); - } - if (!stackId) return; - - const errorId = path.scope.generateUidIdentifier("error"); - const hasErrorId = path.scope.generateUidIdentifier("hasError"); - - let disposeCall: t.Expression = t.callExpression( - state.addHelper("dispose"), - [ - t.cloneNode(stackId), - t.cloneNode(errorId), - t.cloneNode(hasErrorId), - // Pass SuppressedError so that it can be used with "pure" - // polyfills that do not compile the contents of runtime - // helpers. - template.expression.ast` - typeof SuppressedError !== "undefined" && SuppressedError - `, - ], - ); - if (needsAwait) disposeCall = t.awaitExpression(disposeCall); - - const replacement = template.statement.ast` - try { - var ${stackId} = []; - ${path.node.body} - } catch (_) { - var ${errorId} = _; - var ${hasErrorId} = true; - } finally { - ${disposeCall} - } - `; + for (const node of path.node.body) { + if (!isUsingDeclaration(node)) continue; + stackId ??= path.scope.generateUidIdentifier("stack"); + const isAwaitUsing = + node.kind === "await using" || TOP_LEVEL_AWAIT_USING.has(node); + needsAwait ||= isAwaitUsing; - const { parentPath } = path; if ( - parentPath.isFunction() || - parentPath.isTryStatement() || - parentPath.isCatchClause() || - parentPath.isStaticBlock() + !TOP_LEVEL_AWAIT_USING.delete(node) && + !TOP_LEVEL_USING.delete(node) ) { - path.replaceWith(t.blockStatement([replacement])); - } else { - path.replaceWith(replacement); + node.kind = "const"; + } + node.declarations.forEach(decl => { + const args = [t.cloneNode(stackId), decl.init]; + if (isAwaitUsing) args.push(t.booleanLiteral(true)); + decl.init = t.callExpression(state.addHelper("using"), args); + }); + } + if (!stackId) return; + + const errorId = path.scope.generateUidIdentifier("error"); + const hasErrorId = path.scope.generateUidIdentifier("hasError"); + + let disposeCall: t.Expression = t.callExpression( + state.addHelper("dispose"), + [ + t.cloneNode(stackId), + t.cloneNode(errorId), + t.cloneNode(hasErrorId), + // Pass SuppressedError so that it can be used with "pure" + // polyfills that do not compile the contents of runtime + // helpers. + template.expression.ast` + typeof SuppressedError !== "undefined" && SuppressedError + `, + ], + ); + if (needsAwait) disposeCall = t.awaitExpression(disposeCall); + + const replacement = template.statement.ast` + try { + var ${stackId} = []; + ${path.node.body} + } catch (_) { + var ${errorId} = _; + var ${hasErrorId} = true; + } finally { + ${disposeCall} } + `; + + const { parentPath } = path; + if ( + parentPath.isFunction() || + parentPath.isTryStatement() || + parentPath.isCatchClause() || + parentPath.isStaticBlock() + ) { + path.replaceWith(t.blockStatement([replacement])); + } else { + path.replaceWith(replacement); + } + }, + }; + + const transformUsingDeclarationsVisitorSkipFn: Visitor = + traverse.visitors.merge([ + transformUsingDeclarationsVisitor, + { + Function(path) { + path.skip(); + }, }, - // To transform top-level using declarations, we must wrap the - // module body in a block after hoisting all the exports and imports. - // This might cause some variables to be `undefined` rather than TDZ. - Program(path) { - if (path.node.sourceType !== "module") return; - if (!path.node.body.some(isUsingDeclaration)) return; - - const innerBlockBody = []; - for (const stmt of path.get("body")) { - if (stmt.isFunctionDeclaration() || stmt.isImportDeclaration()) { - continue; - } + ]); + + return { + name: "proposal-explicit-resource-management", + inherits: syntacExplicitResourceManagement, - let { node } = stmt; - let shouldRemove = true; - - if (stmt.isExportDefaultDeclaration()) { - let { declaration } = stmt.node; - let varId; - if (t.isClassDeclaration(declaration)) { - varId = declaration.id; - declaration.id = null; - declaration = t.toExpression(declaration); - } else if (!t.isExpression(declaration)) { + visitor: traverse.visitors.merge([ + transformUsingDeclarationsVisitor, + { + // To transform top-level using declarations, we must wrap the + // module body in a block after hoisting all the exports and imports. + // This might cause some variables to be `undefined` rather than TDZ. + Program(path) { + if (path.node.sourceType !== "module") return; + if (!path.node.body.some(isUsingDeclaration)) return; + + const innerBlockBody = []; + for (const stmt of path.get("body")) { + if (stmt.isFunctionDeclaration() || stmt.isImportDeclaration()) { continue; } - varId ??= path.scope.generateUidIdentifier("_default"); - innerBlockBody.push( - t.variableDeclaration("var", [ - t.variableDeclarator(varId, declaration), - ]), - ); - stmt.replaceWith( - t.exportNamedDeclaration(null, [ - t.exportSpecifier(t.cloneNode(varId), t.identifier("default")), - ]), - ); - continue; - } + let { node } = stmt; + let shouldRemove = true; + + if (stmt.isExportDefaultDeclaration()) { + let { declaration } = stmt.node; + let varId; + if (t.isClassDeclaration(declaration)) { + varId = declaration.id; + declaration.id = null; + declaration = t.toExpression(declaration); + } else if (!t.isExpression(declaration)) { + continue; + } + + varId ??= path.scope.generateUidIdentifier("_default"); + innerBlockBody.push( + t.variableDeclaration("var", [ + t.variableDeclarator(varId, declaration), + ]), + ); + stmt.replaceWith( + t.exportNamedDeclaration(null, [ + t.exportSpecifier( + t.cloneNode(varId), + t.identifier("default"), + ), + ]), + ); + continue; + } - if (stmt.isExportNamedDeclaration()) { - node = stmt.node.declaration; - if (!node || t.isFunction(node)) continue; + if (stmt.isExportNamedDeclaration()) { + node = stmt.node.declaration; + if (!node || t.isFunction(node)) continue; - stmt.replaceWith( - t.exportNamedDeclaration( - null, - Object.keys(t.getOuterBindingIdentifiers(node, false)).map(id => - t.exportSpecifier(t.identifier(id), t.identifier(id)), + stmt.replaceWith( + t.exportNamedDeclaration( + null, + Object.keys(t.getOuterBindingIdentifiers(node, false)).map( + id => t.exportSpecifier(t.identifier(id), t.identifier(id)), + ), ), - ), - ); - shouldRemove = false; - } else if (stmt.isExportDeclaration()) { - continue; - } + ); + shouldRemove = false; + } else if (stmt.isExportDeclaration()) { + continue; + } - if (t.isClassDeclaration(node)) { - const { id } = node; - node.id = null; - innerBlockBody.push( - t.variableDeclaration("var", [ - t.variableDeclarator(id, t.toExpression(node)), - ]), - ); - } else if (t.isVariableDeclaration(node)) { - if (node.kind === "using") { - TOP_LEVEL_USING.add(stmt.node); - } else if (node.kind === "await using") { - TOP_LEVEL_AWAIT_USING.add(stmt.node); + if (t.isClassDeclaration(node)) { + const { id } = node; + node.id = null; + innerBlockBody.push( + t.variableDeclaration("var", [ + t.variableDeclarator(id, t.toExpression(node)), + ]), + ); + } else if (t.isVariableDeclaration(node)) { + if (node.kind === "using") { + TOP_LEVEL_USING.add(stmt.node); + } else if (node.kind === "await using") { + TOP_LEVEL_AWAIT_USING.add(stmt.node); + } + node.kind = "var"; + innerBlockBody.push(node); + } else { + innerBlockBody.push(stmt.node); } - node.kind = "var"; - innerBlockBody.push(node); - } else { - innerBlockBody.push(stmt.node); - } - if (shouldRemove) stmt.remove(); - } + if (shouldRemove) stmt.remove(); + } - path.pushContainer("body", t.blockStatement(innerBlockBody)); + path.pushContainer("body", t.blockStatement(innerBlockBody)); + }, + // We must transform `await using` in async functions before that + // async-to-generator will transform `await` expressions into `yield` + Function(path, state) { + if (path.node.async) { + path.traverse(transformUsingDeclarationsVisitorSkipFn, state); + } + }, }, - }, + ]), }; }); diff --git a/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/input.js b/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/input.js new file mode 100644 index 000000000000..059dd346b95c --- /dev/null +++ b/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/input.js @@ -0,0 +1,7 @@ +async function fn() { + await 0; + { + await using x = y; + await 1; + } +} diff --git a/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/options.json b/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/options.json new file mode 100644 index 000000000000..8a8d8ecc0cd8 --- /dev/null +++ b/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/options.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + "proposal-explicit-resource-management", + "transform-async-to-generator" + ] +} diff --git a/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/output.js b/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/output.js new file mode 100644 index 000000000000..822bf19d4df1 --- /dev/null +++ b/packages/babel-plugin-proposal-explicit-resource-management/test/fixtures/integration/async-to-generator/output.js @@ -0,0 +1,19 @@ +function fn() { + return _fn.apply(this, arguments); +} +function _fn() { + _fn = babelHelpers.asyncToGenerator(function* () { + yield 0; + try { + var _stack = []; + const x = babelHelpers.using(_stack, y, true); + yield 1; + } catch (_) { + var _error = _; + var _hasError = true; + } finally { + yield babelHelpers.dispose(_stack, _error, _hasError, typeof SuppressedError !== "undefined" && SuppressedError); + } + }); + return _fn.apply(this, arguments); +}