-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Fix block-scoped capturing by class elements inside iteration statements #28708
Conversation
src/compiler/types.ts
Outdated
@@ -5241,6 +5241,11 @@ namespace ts { | |||
writeFile: WriteFileCallback; | |||
} | |||
|
|||
export const enum LexicalEnvironmentScoping { |
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 think LexicalEnvironmentKind
would be more consistent with TypeScript's internal naming conventions.
src/compiler/transformer.ts
Outdated
createVariableDeclarationList(lexicalEnvironmentVariableDeclarations) | ||
createVariableDeclarationList( | ||
lexicalEnvironmentVariableDeclarations, | ||
lexicalEnvironmentScoping === LexicalEnvironmentScoping.Block ? NodeFlags.Let : undefined |
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.
Previously this was just a variable declaration environment, and not a block scope. I'm concerned that introducing let
declarations here will be a problem for down-level transformations to ES5 as they won't have been visited by the type checker and won't have the necessary NodeCheckFlags
to check for shadowing and per-iteration environments when transforming block-scoped variables down-level. This becomes even more complicated if this is part of the public API for TypeScript, as those things won't "just work" if someone builds a custom transformer.
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.
Thanks for pointing this out - latest changes should rely on the checker flag being set to determine if it should be a block-hoisted variable - so there's no API to say "hoist this variable into the block scope."
src/compiler/types.ts
Outdated
@@ -5249,7 +5254,7 @@ namespace ts { | |||
getCompilerOptions(): CompilerOptions; | |||
|
|||
/** Starts a new lexical environment. */ | |||
startLexicalEnvironment(): void; | |||
startLexicalEnvironment(scoping?: LexicalEnvironmentScoping): void; |
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'm not sure this should be part of the public API. It presents the possibility of a custom transformer introducing a let
-scoped variable we won't be able to properly down-level since it wasn't visited by the type checker.
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.
The new code changes don't expose a public API for starting a block scope and rely on the checker flags being set to determine which scope it should hoist a variable into.
src/compiler/checker.ts
Outdated
const container = getEnclosingBlockScopeContainer(node); | ||
let current = container; | ||
let containedInIterationStatement = false; | ||
while (current && !nodeStartsNewLexicalEnvironment(current)) { |
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.
We're doing something similar already in checkNestedBlockScopedBinding
. Is there overlap here that could be refactored into a separate method?
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.
Refactored this into getEnclosingIterationStatement
pinging @rbuckton for another review pass |
@rbuckton would you be able to take another look? We're excited about this change, thanks for your time |
@mheiber: I'll take another look this afternoon. |
57a142f
to
64a3d96
Compare
@rbuckton I've rebased and resolved the conflicts, should be good to go! |
src/compiler/checker.ts
Outdated
@@ -16726,6 +16726,11 @@ namespace ts { | |||
if (container.kind === SyntaxKind.PropertyDeclaration && hasModifier(container, ModifierFlags.Static)) { | |||
getNodeLinks(declaration).flags |= NodeCheckFlags.ClassWithConstructorReference; | |||
getNodeLinks(node).flags |= NodeCheckFlags.ConstructorReferenceInClass; | |||
// If the class expression is in a loop and the name of the class is used, |
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.
This is something we would normally handle in checkNestedBlockScopedBinding
, except it seems that function exits early if the target
is es2015
or later. Perhaps we should instead add a parameter to checkNestedBlockScopedBinding
to force it to execute when this condition is reached?
src/compiler/transformer.ts
Outdated
/** | ||
* Starts a block scope. Any existing block hoisted variables are pushed onto the stack and the related storage variables are reset. | ||
*/ | ||
function startBlockScope() { |
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.
If we are going to add these to TransformationContext
, we should enforce them by default in visitEachChild
(in visitor.ts), similar to how we enforce calls to startLexicalEnvironment
and endLexicalEnvironment
.
For example: in visitEachChild
, we have the following switch case for a MethodDeclaration
:
case SyntaxKind.MethodDeclaration:
return updateMethod(<MethodDeclaration>node,
...
visitParameterList((<MethodDeclaration>node).parameters, visitor, context, nodesVisitor),
visitNode((<MethodDeclaration>node).type, visitor, isTypeNode),
visitFunctionBody((<MethodDeclaration>node).body!, visitor, context));
If you look at visitParameterList
, it contains the following at the top of the function:
export function visitParameterList(nodes: NodeArray<ParameterDeclaration> | undefined, visitor: Visitor, context: TransformationContext, nodesVisitor = visitNodes) {
context.startLexicalEnvironment();
...
Conversely, visitFunctionBody
contains the following:
export function visitFunctionBody(node: ConciseBody | undefined, visitor: Visitor, context: TransformationContext): ConciseBody | undefined {
...
const declarations = context.endLexicalEnvironment();
if (some(declarations)) {
const block = convertToFunctionBody(updated);
const statements = mergeLexicalEnvironment(block.statements, declarations);
return updateBlock(block, statements);
}
return updated;
}
We should so something similar for any Block
.
src/compiler/transformer.ts
Outdated
@@ -247,6 +252,11 @@ namespace ts { | |||
function hoistVariableDeclaration(name: Identifier): void { | |||
Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); | |||
Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); | |||
// If the checker determined that this is a block scoped binding in a loop, we must emit a block-level variable declaration. |
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 would prefer if we had a separate function (i.e. addBlockVariableDeclaration
) to add block-scoped variables rather than having hoistVariableDeclaration
perform both. I'd rather avoid having to use the resolver for every temp variable we generate, and this would allow us to record net-new block-scoped temporary variables in other transformations.
NOTE: I suggest addBlockVariableDeclaration
rather than hoistBlockVariableDeclaration
above as "hoist" has a specific meaning that does not apply to block-scoped variables.
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.
Also, for block-scoped variables we should probably always enforce GeneratedIdentifierFlags.ReserveInNestedScopes
regardless of what was passed to createTempVariable
.
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.
How would you introduce a net-new block scoped variable correctly without the checker putting the appropriate flags on it? For example, the ES5 transformation relies on the checker flags to correctly determine whether there is a block scoped variable in a particular loop.
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.
My primary concern is performance. I'd rather avoid lookups against the resolver
for every generated identifier, if possible. The only case where this check seems like it is needed is here, so incurring the cost of the lookup for every hoisted variable is unnecessary overhead.
I would still recommend that we add addBlockVariableDeclaration
for this case, but we can mark it as /* @internal */
for now and revisit the API for custom transformer consumption later.
src/compiler/transformers/ts.ts
Outdated
@@ -549,6 +553,19 @@ namespace ts { | |||
} | |||
} | |||
|
|||
function visitBlock(node: Block): Block { |
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.
If you make the changes to visitEachChild
I mentioned above, this function would not be necessary.
src/compiler/transformers/ts.ts
Outdated
@@ -909,7 +926,13 @@ namespace ts { | |||
if (some(staticProperties) || some(pendingExpressions)) { | |||
const expressions: Expression[] = []; | |||
const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & NodeCheckFlags.ClassWithConstructorReference; | |||
const temp = createTempVariable(hoistVariableDeclaration, !!isClassWithConstructorReference); | |||
const temp = createTempVariable( | |||
name => { |
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.
If you make the changes to visitEachChild
I mentioned above, you shouldn't need this closure as you can pass addBlockVariableDeclaration
(or whatever we end up calling it) here and set the original node pointer after the fact.
As best I can tell from reading through the comments, this PR has a few outstanding tasks before it's ready to merge. @joeywatts do you still have time to look at this? It seems like a worthwhile bug to fix. |
@sandersn yes, I can try to fix this up in the next couple of days |
b351223
to
f48889f
Compare
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.
Can you make a few small changes and update this to the new NodeFactory API? Once that's done I can merge.
2bc147e
to
9c12240
Compare
1279c1e
to
8230cef
Compare
@rbuckton can you check whether the commits since your June review address your comments and then merge this? |
8230cef
to
6673dca
Compare
Rebased for you and got the tests all passing locally |
This is now merged in #43197 |
Fixes #27864.
For the following cases of class expressions/declarations in a loop: