-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Fixes var declaration shadowing in async functions #21215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
136c4d0
c4fddba
2ba29d8
e655446
afaa139
5b45db7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,9 +12,9 @@ namespace ts { | |
|
||
export function transformES2017(context: TransformationContext) { | ||
const { | ||
startLexicalEnvironment, | ||
resumeLexicalEnvironment, | ||
endLexicalEnvironment | ||
endLexicalEnvironment, | ||
hoistVariableDeclaration | ||
} = context; | ||
|
||
const resolver = context.getEmitResolver(); | ||
|
@@ -33,6 +33,8 @@ namespace ts { | |
*/ | ||
let enclosingSuperContainerFlags: NodeCheckFlags = 0; | ||
|
||
let enclosingFunctionParameterNames: UnderscoreEscapedMap<true>; | ||
|
||
// Save the previous transformation hooks. | ||
const previousOnEmitNode = context.onEmitNode; | ||
const previousOnSubstituteNode = context.onSubstituteNode; | ||
|
@@ -83,6 +85,108 @@ namespace ts { | |
} | ||
} | ||
|
||
function asyncBodyVisitor(node: Node): VisitResult<Node> { | ||
if (isNodeWithPossibleHoistedDeclaration(node)) { | ||
switch (node.kind) { | ||
case SyntaxKind.VariableStatement: | ||
return visitVariableStatementInAsyncBody(node); | ||
case SyntaxKind.ForStatement: | ||
return visitForStatementInAsyncBody(node); | ||
case SyntaxKind.ForInStatement: | ||
return visitForInStatementInAsyncBody(node); | ||
case SyntaxKind.ForOfStatement: | ||
return visitForOfStatementInAsyncBody(node); | ||
case SyntaxKind.CatchClause: | ||
return visitCatchClauseInAsyncBody(node); | ||
case SyntaxKind.Block: | ||
case SyntaxKind.SwitchStatement: | ||
case SyntaxKind.CaseBlock: | ||
case SyntaxKind.CaseClause: | ||
case SyntaxKind.DefaultClause: | ||
case SyntaxKind.TryStatement: | ||
case SyntaxKind.DoStatement: | ||
case SyntaxKind.WhileStatement: | ||
case SyntaxKind.IfStatement: | ||
case SyntaxKind.WithStatement: | ||
case SyntaxKind.LabeledStatement: | ||
return visitEachChild(node, asyncBodyVisitor, context); | ||
default: | ||
return Debug.assertNever(node, "Unhandled node."); | ||
} | ||
} | ||
return visitor(node); | ||
} | ||
|
||
function visitCatchClauseInAsyncBody(node: CatchClause) { | ||
const catchClauseNames = createUnderscoreEscapedMap<true>(); | ||
recordDeclarationName(node.variableDeclaration, catchClauseNames); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. variableDeclaration may be undefined There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, by the time we get to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a test case in e655446 to verify this case. |
||
|
||
// names declared in a catch variable are block scoped | ||
let catchClauseUnshadowedNames: UnderscoreEscapedMap<true>; | ||
catchClauseNames.forEach((_, escapedName) => { | ||
if (enclosingFunctionParameterNames.has(escapedName)) { | ||
if (!catchClauseUnshadowedNames) { | ||
catchClauseUnshadowedNames = cloneMap(enclosingFunctionParameterNames); | ||
} | ||
catchClauseUnshadowedNames.delete(escapedName); | ||
} | ||
}); | ||
|
||
if (catchClauseUnshadowedNames) { | ||
const savedEnclosingFunctionParameterNames = enclosingFunctionParameterNames; | ||
enclosingFunctionParameterNames = catchClauseUnshadowedNames; | ||
const result = visitEachChild(node, asyncBodyVisitor, context); | ||
enclosingFunctionParameterNames = savedEnclosingFunctionParameterNames; | ||
return result; | ||
} | ||
else { | ||
return visitEachChild(node, asyncBodyVisitor, context); | ||
} | ||
} | ||
|
||
function visitVariableStatementInAsyncBody(node: VariableStatement) { | ||
if (isVariableDeclarationListWithCollidingName(node.declarationList)) { | ||
const expression = visitVariableDeclarationListWithCollidingNames(node.declarationList, /*hasReceiver*/ false); | ||
return expression ? createStatement(expression) : undefined; | ||
} | ||
return visitEachChild(node, visitor, context); | ||
} | ||
|
||
function visitForInStatementInAsyncBody(node: ForInStatement) { | ||
return updateForIn( | ||
node, | ||
isVariableDeclarationListWithCollidingName(node.initializer) | ||
? visitVariableDeclarationListWithCollidingNames(node.initializer, /*hasReceiver*/ true) | ||
: visitNode(node.initializer, visitor, isForInitializer), | ||
visitNode(node.expression, visitor, isExpression), | ||
visitNode(node.statement, asyncBodyVisitor, isStatement, liftToBlock) | ||
); | ||
} | ||
|
||
function visitForOfStatementInAsyncBody(node: ForOfStatement) { | ||
return updateForOf( | ||
node, | ||
visitNode(node.awaitModifier, visitor, isToken), | ||
isVariableDeclarationListWithCollidingName(node.initializer) | ||
? visitVariableDeclarationListWithCollidingNames(node.initializer, /*hasReceiver*/ true) | ||
: visitNode(node.initializer, visitor, isForInitializer), | ||
visitNode(node.expression, visitor, isExpression), | ||
visitNode(node.statement, asyncBodyVisitor, isStatement, liftToBlock) | ||
); | ||
} | ||
|
||
function visitForStatementInAsyncBody(node: ForStatement) { | ||
return updateFor( | ||
node, | ||
isVariableDeclarationListWithCollidingName(node.initializer) | ||
? visitVariableDeclarationListWithCollidingNames(node.initializer, /*hasReceiver*/ false) | ||
: visitNode(node.initializer, visitor, isForInitializer), | ||
visitNode(node.condition, visitor, isExpression), | ||
visitNode(node.incrementor, visitor, isExpression), | ||
visitNode((<ForStatement>node).statement, asyncBodyVisitor, isStatement, liftToBlock) | ||
); | ||
} | ||
|
||
/** | ||
* Visits an AwaitExpression node. | ||
* | ||
|
@@ -197,6 +301,82 @@ namespace ts { | |
); | ||
} | ||
|
||
function recordDeclarationName({ name }: ParameterDeclaration | VariableDeclaration | BindingElement, names: UnderscoreEscapedMap<true>) { | ||
if (isIdentifier(name)) { | ||
names.set(name.escapedText, true); | ||
} | ||
else { | ||
for (const element of name.elements) { | ||
if (!isOmittedExpression(element)) { | ||
recordDeclarationName(element, names); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function isVariableDeclarationListWithCollidingName(node: ForInitializer): node is VariableDeclarationList { | ||
return node | ||
&& isVariableDeclarationList(node) | ||
&& !(node.flags & NodeFlags.BlockScoped) | ||
&& forEach(node.declarations, collidesWithParameterName); | ||
} | ||
|
||
function visitVariableDeclarationListWithCollidingNames(node: VariableDeclarationList, hasReceiver: boolean) { | ||
hoistVariableDeclarationList(node); | ||
|
||
const variables = getInitializedVariables(node); | ||
if (variables.length === 0) { | ||
if (hasReceiver) { | ||
return visitNode(convertToAssignmentElementTarget(node.declarations[0].name), visitor, isExpression); | ||
} | ||
return undefined; | ||
} | ||
|
||
return inlineExpressions(map(variables, transformInitializedVariable)); | ||
} | ||
|
||
function hoistVariableDeclarationList(node: VariableDeclarationList) { | ||
forEach(node.declarations, hoistVariable); | ||
} | ||
|
||
function hoistVariable({ name }: VariableDeclaration | BindingElement) { | ||
if (isIdentifier(name)) { | ||
hoistVariableDeclaration(name); | ||
} | ||
else { | ||
for (const element of name.elements) { | ||
if (!isOmittedExpression(element)) { | ||
hoistVariable(element); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function transformInitializedVariable(node: VariableDeclaration) { | ||
const converted = setSourceMapRange( | ||
createAssignment( | ||
convertToAssignmentElementTarget(node.name), | ||
node.initializer | ||
), | ||
node | ||
); | ||
return visitNode(converted, visitor, isExpression); | ||
} | ||
|
||
function collidesWithParameterName({ name }: VariableDeclaration | BindingElement): boolean { | ||
if (isIdentifier(name)) { | ||
return enclosingFunctionParameterNames.has(name.escapedText); | ||
} | ||
else { | ||
for (const element of name.elements) { | ||
if (!isOmittedExpression(element) && collidesWithParameterName(element)) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function transformAsyncFunctionBody(node: MethodDeclaration | AccessorDeclaration | FunctionDeclaration | FunctionExpression): FunctionBody; | ||
function transformAsyncFunctionBody(node: ArrowFunction): ConciseBody; | ||
function transformAsyncFunctionBody(node: FunctionLikeDeclaration): ConciseBody { | ||
|
@@ -214,6 +394,13 @@ namespace ts { | |
// passed to `__awaiter` is executed inside of the callback to the | ||
// promise constructor. | ||
|
||
const savedEnclosingFunctionParameterNames = enclosingFunctionParameterNames; | ||
enclosingFunctionParameterNames = createUnderscoreEscapedMap<true>(); | ||
for (const parameter of node.parameters) { | ||
recordDeclarationName(parameter, enclosingFunctionParameterNames); | ||
} | ||
|
||
let result: ConciseBody; | ||
if (!isArrowFunction) { | ||
const statements: Statement[] = []; | ||
const statementOffset = addPrologue(statements, (<Block>node.body).statements, /*ensureUseStrict*/ false, visitor); | ||
|
@@ -223,7 +410,7 @@ namespace ts { | |
context, | ||
hasLexicalArguments, | ||
promiseConstructor, | ||
transformFunctionBodyWorker(<Block>node.body, statementOffset) | ||
transformAsyncFunctionBodyWorker(<Block>node.body, statementOffset) | ||
) | ||
) | ||
); | ||
|
@@ -246,35 +433,36 @@ namespace ts { | |
} | ||
} | ||
|
||
return block; | ||
result = block; | ||
} | ||
else { | ||
const expression = createAwaiterHelper( | ||
context, | ||
hasLexicalArguments, | ||
promiseConstructor, | ||
transformFunctionBodyWorker(node.body) | ||
transformAsyncFunctionBodyWorker(node.body) | ||
); | ||
|
||
const declarations = endLexicalEnvironment(); | ||
if (some(declarations)) { | ||
const block = convertToFunctionBody(expression); | ||
return updateBlock(block, setTextRange(createNodeArray(concatenate(block.statements, declarations)), block.statements)); | ||
result = updateBlock(block, setTextRange(createNodeArray(concatenate(block.statements, declarations)), block.statements)); | ||
} | ||
else { | ||
result = expression; | ||
} | ||
|
||
return expression; | ||
} | ||
|
||
enclosingFunctionParameterNames = savedEnclosingFunctionParameterNames; | ||
return result; | ||
} | ||
|
||
function transformFunctionBodyWorker(body: ConciseBody, start?: number) { | ||
function transformAsyncFunctionBodyWorker(body: ConciseBody, start?: number) { | ||
if (isBlock(body)) { | ||
return updateBlock(body, visitLexicalEnvironment(body.statements, visitor, context, start)); | ||
return updateBlock(body, visitNodes(body.statements, asyncBodyVisitor, isStatement, start)); | ||
} | ||
else { | ||
startLexicalEnvironment(); | ||
const visited = convertToFunctionBody(visitNode(body, visitor, isConciseBody)); | ||
const declarations = endLexicalEnvironment(); | ||
return updateBlock(visited, setTextRange(createNodeArray(concatenate(visited.statements, declarations)), visited.statements)); | ||
return convertToFunctionBody(visitNode(body, asyncBodyVisitor, isConciseBody)); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a canonical source we can reference in a comment here for the list-of-things-which-may-contain-or-whose-children-may-contain-hoisted-declarations? It seems nonobvious which elements of the statement and declaration list need to be explicitly visited here.
Also: Does
LabeledStatement
need to be handled? egThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added
isNodeWithPossibleVarDeclaration
in 2ba29d8 as a way to verify this list.