Skip to content

Commit 6ec7a42

Browse files
crisbetodylhunn
authored andcommitted
fix(compiler-cli): avoid conflicts with built-in global variables in for loop blocks (#53319)
Currently we generate the following TCB for a `@for` loop: ```ts // @for (item of items; track item) {...} for (const item of this.items) { var _t1 = item; // Do things with `_t1` } ``` This is problematic if the item name is the same as a global variable (e.g. `document`), because when the TCB has references to that variable (e.g. `document.createElement`), it'll find the loop initializer instead of the global variable. These changes fix the issue by generating the following instead: ```ts for (const _t1 of this.items) { // Do things with `_t1` } ``` Fixes #53293. PR Close #53319
1 parent 187ba05 commit 6ec7a42

File tree

3 files changed

+58
-20
lines changed

3 files changed

+58
-20
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,8 +1444,13 @@ class TcbForOfOp extends TcbOp {
14441444

14451445
override execute(): null {
14461446
const loopScope = Scope.forNodes(this.tcb, this.scope, this.block, this.block.children, null);
1447+
const initializerId = loopScope.resolve(this.block.item);
1448+
if (!ts.isIdentifier(initializerId)) {
1449+
throw new Error(
1450+
`Could not resolve for loop variable ${this.block.item.name} to an identifier`);
1451+
}
14471452
const initializer = ts.factory.createVariableDeclarationList(
1448-
[ts.factory.createVariableDeclaration(this.block.item.name)], ts.NodeFlags.Const);
1453+
[ts.factory.createVariableDeclaration(initializerId)], ts.NodeFlags.Const);
14491454
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
14501455
// Add a non-null expression to allow such values to be assigned.
14511456
const expression = ts.factory.createNonNullExpression(
@@ -1561,9 +1566,10 @@ class Scope {
15611566

15621567
/**
15631568
* Map of variables declared on the template that created this `Scope` (represented by
1564-
* `TmplAstVariable` nodes) to the index of their `TcbVariableOp`s in the `opQueue`.
1569+
* `TmplAstVariable` nodes) to the index of their `TcbVariableOp`s in the `opQueue`, or to
1570+
* pre-resolved variable identifiers.
15651571
*/
1566-
private varMap = new Map<TmplAstVariable, number>();
1572+
private varMap = new Map<TmplAstVariable, number|ts.Identifier>();
15671573

15681574
/**
15691575
* Statements for this template.
@@ -1635,10 +1641,11 @@ class Scope {
16351641
tcb, scope, tcbExpression(expression, tcb, scope), expressionAlias));
16361642
}
16371643
} else if (scopedNode instanceof TmplAstForLoopBlock) {
1638-
this.registerVariable(
1639-
scope, scopedNode.item,
1640-
new TcbBlockVariableOp(
1641-
tcb, scope, ts.factory.createIdentifier(scopedNode.item.name), scopedNode.item));
1644+
// Register the variable for the loop so it can be resolved by
1645+
// children. It'll be declared once the loop is created.
1646+
const loopInitializer = tcb.allocateId();
1647+
addParseSpanInfo(loopInitializer, scopedNode.item.sourceSpan);
1648+
scope.varMap.set(scopedNode.item, loopInitializer);
16421649

16431650
for (const [name, variable] of Object.entries(scopedNode.contextVariables)) {
16441651
if (!this.forLoopContextVariableTypes.has(name)) {
@@ -1770,7 +1777,8 @@ class Scope {
17701777
} else if (ref instanceof TmplAstVariable && this.varMap.has(ref)) {
17711778
// Resolving a context variable for this template.
17721779
// Execute the `TcbVariableOp` associated with the `TmplAstVariable`.
1773-
return this.resolveOp(this.varMap.get(ref)!);
1780+
const opIndexOrNode = this.varMap.get(ref)!;
1781+
return typeof opIndexOrNode === 'number' ? this.resolveOp(opIndexOrNode) : opIndexOrNode;
17741782
} else if (
17751783
ref instanceof TmplAstTemplate && directive === undefined &&
17761784
this.templateCtxOpMap.has(ref)) {

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,7 +1545,7 @@ describe('type check blocks', () => {
15451545
`;
15461546

15471547
const result = tcb(TEMPLATE);
1548-
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
1548+
expect(result).toContain('for (const _t1 of ((this).items)!) {');
15491549
expect(result).toContain('"" + ((this).main(_t1))');
15501550
expect(result).toContain('"" + ((this).empty())');
15511551
});
@@ -1558,7 +1558,7 @@ describe('type check blocks', () => {
15581558
`;
15591559

15601560
const result = tcb(TEMPLATE);
1561-
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
1561+
expect(result).toContain('for (const _t1 of ((this).items)!) {');
15621562
expect(result).toContain('var _t2: number = null!;');
15631563
expect(result).toContain('var _t3: boolean = null! as boolean;');
15641564
expect(result).toContain('var _t4: boolean = null! as boolean;');
@@ -1576,7 +1576,7 @@ describe('type check blocks', () => {
15761576
`;
15771577

15781578
const result = tcb(TEMPLATE);
1579-
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
1579+
expect(result).toContain('for (const _t1 of ((this).items)!) {');
15801580
expect(result).toContain('var _t2: number = null!;');
15811581
expect(result).toContain('var _t3: boolean = null! as boolean;');
15821582
expect(result).toContain('var _t4: boolean = null! as boolean;');
@@ -1592,7 +1592,7 @@ describe('type check blocks', () => {
15921592
`;
15931593

15941594
const result = tcb(TEMPLATE);
1595-
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
1595+
expect(result).toContain('for (const _t1 of ((this).items)!) {');
15961596
expect(result).toContain('var _t2: number = null!;');
15971597
expect(result).toContain('"" + (((this).$index)) + (_t2)');
15981598
});
@@ -1609,20 +1609,16 @@ describe('type check blocks', () => {
16091609
`;
16101610

16111611
const result = tcb(TEMPLATE);
1612-
expect(result).toContain(
1613-
'for (const item of ((this).items)!) { var _t1 = item; var _t2: number = null!;');
1612+
expect(result).toContain('for (const _t1 of ((this).items)!) { var _t2: number = null!;');
16141613
expect(result).toContain('"" + (_t1) + (_t2)');
1615-
expect(result).toContain(
1616-
'for (const inner of ((_t1).items)!) { var _t8 = inner; var _t9: number = null!;');
1614+
expect(result).toContain('for (const _t8 of ((_t1).items)!) { var _t9: number = null!;');
16171615
expect(result).toContain('"" + (_t1) + (_t2) + (_t8) + (_t9)');
16181616
});
16191617

16201618
it('should generate the tracking expression of a for loop', () => {
16211619
const result = tcb(`@for (item of items; track trackingFn($index, item, prop)) {}`);
1622-
1623-
expect(result).toContain(
1624-
'for (const item of ((this).items)!) { var _t1: number = null!; var _t2 = item;');
1625-
expect(result).toContain('(this).trackingFn(_t1, _t2, ((this).prop));');
1620+
expect(result).toContain('for (const _t1 of ((this).items)!) { var _t2: number = null!;');
1621+
expect(result).toContain('(this).trackingFn(_t2, _t1, ((this).prop));');
16261622
});
16271623
});
16281624
});

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5019,6 +5019,40 @@ suppress
50195019
`Type 'number' must have a '[Symbol.iterator]()' method that returns an iterator.`
50205020
]);
50215021
});
5022+
5023+
it('should check for loop variables with the same name as built-in globals', () => {
5024+
// strictTemplates are necessary so the event listener is checked.
5025+
env.tsconfig({strictTemplates: true});
5026+
env.write('test.ts', `
5027+
import {Component, Directive, Input} from '@angular/core';
5028+
5029+
@Directive({
5030+
standalone: true,
5031+
selector: '[dir]'
5032+
})
5033+
export class Dir {
5034+
@Input('dir') value!: string;
5035+
}
5036+
5037+
@Component({
5038+
standalone: true,
5039+
imports: [Dir],
5040+
template: \`
5041+
@for (document of documents; track document) {
5042+
<button [dir]="document" (click)="$event.stopPropagation()"></button>
5043+
}
5044+
\`,
5045+
})
5046+
export class Main {
5047+
documents = [1, 2, 3];
5048+
}
5049+
`);
5050+
5051+
const diags = env.driveDiagnostics();
5052+
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
5053+
`Type 'number' is not assignable to type 'string'.`,
5054+
]);
5055+
});
50225056
});
50235057
});
50245058
});

0 commit comments

Comments
 (0)