-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Fix block-scoped capturing by class elements inside iteration statements #28708
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
Conversation
src/compiler/types.ts
Outdated
| 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
|
|
||
| /** 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
| 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
| 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
| } | ||
| } | ||
|
|
||
| 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
| 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
rbuckton
left a comment
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: