Implement variable renaming for using declaration shadowing in for-of loops

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-06-24 16:54:57 +00:00
parent 34e17b3bb8
commit 237579e33f
2 changed files with 199 additions and 112 deletions

View File

@ -1,67 +1,69 @@
import {
addEmitHelpers,
addRange,
append,
arrayFrom,
BindingElement,
Block,
Bundle,
CaseOrDefaultClause,
chainBundle,
ClassDeclaration,
Debug,
EmitFlags,
ExportAssignment,
ExportSpecifier,
Expression,
firstOrUndefined,
ForOfStatement,
ForStatement,
GeneratedIdentifierFlags,
getEmitFlags,
hasSyntacticModifier,
Identifier,
IdentifierNameMap,
isArray,
isBindingPattern,
isBlock,
isCaseClause,
isCustomPrologue,
isExpression,
isGeneratedIdentifier,
isIdentifier,
isLocalName,
isNamedEvaluation,
isOmittedExpression,
isPrologueDirective,
isSourceFile,
isStatement,
isVariableDeclarationList,
isVariableStatement,
ModifierFlags,
Node,
NodeFlags,
setCommentRange,
setEmitFlags,
setOriginalNode,
setSourceMapRange,
setTextRange,
skipOuterExpressions,
SourceFile,
Statement,
SwitchStatement,
SyntaxKind,
TransformationContext,
TransformFlags,
transformNamedEvaluation,
VariableDeclaration,
VariableDeclarationList,
VariableStatement,
visitArray,
visitEachChild,
visitNode,
visitNodes,
VisitResult,
import {
addEmitHelpers,
addRange,
append,
arrayFrom,
BindingElement,
Block,
Bundle,
CaseOrDefaultClause,
chainBundle,
ClassDeclaration,
Debug,
EmitFlags,
ExportAssignment,
ExportSpecifier,
Expression,
firstOrUndefined,
forEachChild,
ForOfStatement,
ForStatement,
GeneratedIdentifierFlags,
getEmitFlags,
hasSyntacticModifier,
Identifier,
IdentifierNameMap,
isArray,
isBindingPattern,
isBlock,
isCaseClause,
isCustomPrologue,
isExpression,
isGeneratedIdentifier,
isIdentifier,
isLocalName,
isNamedEvaluation,
isOmittedExpression,
isPrologueDirective,
isSourceFile,
isStatement,
isVariableDeclaration,
isVariableDeclarationList,
isVariableStatement,
ModifierFlags,
Node,
NodeFlags,
setCommentRange,
setEmitFlags,
setOriginalNode,
setSourceMapRange,
setTextRange,
skipOuterExpressions,
SourceFile,
Statement,
SwitchStatement,
SyntaxKind,
TransformationContext,
TransformFlags,
transformNamedEvaluation,
VariableDeclaration,
VariableDeclarationList,
VariableStatement,
visitArray,
visitEachChild,
visitNode,
visitNodes,
VisitResult,
} from "../_namespaces/ts.js";
const enum UsingKind {
@ -289,54 +291,121 @@ export function transformESNext(context: TransformationContext): (x: SourceFile
);
}
return visitEachChild(node, visitor, context);
}
function visitForOfStatement(node: ForOfStatement) {
if (isUsingVariableDeclarationList(node.initializer)) {
// given:
//
// for (using x of y) { ... }
//
// produces a shallow transformation to:
//
// for (const x_1 of y) {
// using x = x;
// ...
// }
//
// before handing the shallow transformation back to the visitor for an in-depth transformation.
const forInitializer = node.initializer;
const forDecl = firstOrUndefined(forInitializer.declarations) || factory.createVariableDeclaration(factory.createTempVariable(/*recordTempVariable*/ undefined));
const isAwaitUsing = getUsingKindOfVariableDeclarationList(forInitializer) === UsingKind.Async;
const temp = factory.getGeneratedNameForNode(forDecl.name);
const usingVar = factory.updateVariableDeclaration(forDecl, forDecl.name, /*exclamationToken*/ undefined, /*type*/ undefined, temp);
const usingVarList = factory.createVariableDeclarationList([usingVar], isAwaitUsing ? NodeFlags.AwaitUsing : NodeFlags.Using);
const usingVarStatement = factory.createVariableStatement(/*modifiers*/ undefined, usingVarList);
return visitNode(
factory.updateForOfStatement(
node,
node.awaitModifier,
factory.createVariableDeclarationList([
factory.createVariableDeclaration(temp),
], NodeFlags.Const),
node.expression,
isBlock(node.statement) ?
factory.updateBlock(node.statement, [
usingVarStatement,
...node.statement.statements,
]) :
factory.createBlock([
usingVarStatement,
node.statement,
], /*multiLine*/ true),
),
visitor,
isStatement,
);
}
return visitEachChild(node, visitor, context);
return visitEachChild(node, visitor, context);
}
/**
* Collects all variable declarations that shadow a given identifier name in a statement.
*/
function collectShadowingVariables(statement: Statement, shadowedName: string): VariableDeclaration[] {
const shadowingVars: VariableDeclaration[] = [];
function visit(node: Node): void {
if (isVariableStatement(node)) {
for (const declaration of node.declarationList.declarations) {
if (isIdentifier(declaration.name) && declaration.name.escapedText === shadowedName) {
shadowingVars.push(declaration);
}
}
}
forEachChild(node, visit);
}
visit(statement);
return shadowingVars;
}
/**
* Creates a visitor that renames shadowing variables to avoid conflicts.
*/
function createShadowingVariableRenamer(shadowedName: string): (node: Node) => VisitResult<Node> {
const renamingMap = new Map<string, Identifier>();
return function renameShadowingVariables(node: Node): VisitResult<Node> {
if (isVariableDeclaration(node) && isIdentifier(node.name) && node.name.escapedText === shadowedName) {
// Create a unique name for this shadowing variable
const uniqueName = factory.createUniqueName(shadowedName as string, GeneratedIdentifierFlags.Optimistic);
renamingMap.set(node.name.escapedText as string, uniqueName);
return factory.updateVariableDeclaration(
node,
uniqueName,
node.exclamationToken,
node.type,
visitNode(node.initializer, renameShadowingVariables, isExpression)
);
}
if (isIdentifier(node)) {
const renamed = renamingMap.get(node.escapedText as string);
if (renamed) {
return renamed;
}
}
return visitEachChild(node, renameShadowingVariables, context);
};
}
function visitForOfStatement(node: ForOfStatement) {
if (isUsingVariableDeclarationList(node.initializer)) {
// given:
//
// for (using x of y) { ... }
//
// produces a shallow transformation to:
//
// for (const x_1 of y) {
// using x = x;
// ...
// }
//
// before handing the shallow transformation back to the visitor for an in-depth transformation.
const forInitializer = node.initializer;
const forDecl = firstOrUndefined(forInitializer.declarations) || factory.createVariableDeclaration(factory.createTempVariable(/*recordTempVariable*/ undefined));
const isAwaitUsing = getUsingKindOfVariableDeclarationList(forInitializer) === UsingKind.Async;
const temp = factory.getGeneratedNameForNode(forDecl.name);
const usingVar = factory.updateVariableDeclaration(forDecl, forDecl.name, /*exclamationToken*/ undefined, /*type*/ undefined, temp);
const usingVarList = factory.createVariableDeclarationList([usingVar], isAwaitUsing ? NodeFlags.AwaitUsing : NodeFlags.Using);
const usingVarStatement = factory.createVariableStatement(/*modifiers*/ undefined, usingVarList);
// Check if the loop body contains shadowing variables and rename them if necessary
const shadowedName = isIdentifier(forDecl.name) ? forDecl.name.escapedText as string : undefined;
let transformedStatement = node.statement;
if (shadowedName) {
const shadowingVars = collectShadowingVariables(node.statement, shadowedName);
if (shadowingVars.length > 0) {
// Apply the renaming visitor to the loop body
const renamer = createShadowingVariableRenamer(shadowedName);
transformedStatement = visitNode(node.statement, renamer, isStatement);
}
}
return visitNode(
factory.updateForOfStatement(
node,
node.awaitModifier,
factory.createVariableDeclarationList([
factory.createVariableDeclaration(temp),
], NodeFlags.Const),
node.expression,
isBlock(transformedStatement) ?
factory.updateBlock(transformedStatement, [
usingVarStatement,
...transformedStatement.statements,
]) :
factory.createBlock([
usingVarStatement,
transformedStatement,
], /*multiLine*/ true),
),
visitor,
isStatement,
);
}
return visitEachChild(node, visitor, context);
}
function visitCaseOrDefaultClause(node: CaseOrDefaultClause, envBinding: Identifier) {

View File

@ -0,0 +1,18 @@
// @target: esnext,es2022,es2017,es2015,es5
// @module: esnext
// @lib: esnext
// @noTypesAndSymbols: true
class Foo {}
for (using foo of []) {
const foo = new Foo();
}
for (using bar of []) {
let bar = "test";
}
for (using baz of []) {
var baz = 42;
}