Revert "fix(compiler): wrap @for collection expression before appending non-null assertion"

This reverts commit ed8f16c078.

This caused a breakage in TGP because the changes (correctly) produced new
diagnostics on ` @for (a of (x | async) || []; track a) {` x is `any`. This
results in `<any> | async` which produces `unknown | null` and then the `@if`
finally narrows this down to `{}` which isn't iteratable.

(cherry picked from commit 37ca679192)
This commit is contained in:
Andrew Scott
2026-09-11 09:14:25 -07:00
parent 23e5d3090a
commit 8a837f6c34
3 changed files with 10 additions and 152 deletions
@@ -2348,7 +2348,7 @@ describe('type check blocks', () => {
`;
const result = tcb(TEMPLATE);
expect(result).toContain('for (const _t1 of (((this).items))!) {');
expect(result).toContain('for (const _t1 of ((this).items)!) {');
expect(result).toContain('"" + ((this).main(_t1))');
expect(result).toContain('"" + ((this).empty())');
});
@@ -2361,7 +2361,7 @@ describe('type check blocks', () => {
`;
const result = tcb(TEMPLATE);
expect(result).toContain('for (const _t1 of (((this).items))!) {');
expect(result).toContain('for (const _t1 of ((this).items)!) {');
expect(result).toContain('var _t2 = null! as number;');
expect(result).toContain('var _t3 = null! as boolean;');
expect(result).toContain('var _t4 = null! as boolean;');
@@ -2379,7 +2379,7 @@ describe('type check blocks', () => {
`;
const result = tcb(TEMPLATE);
expect(result).toContain('for (const _t1 of (((this).items))!) {');
expect(result).toContain('for (const _t1 of ((this).items)!) {');
expect(result).toContain('var _t2 = null! as number;');
expect(result).toContain('var _t3 = null! as boolean;');
expect(result).toContain('var _t4 = null! as boolean;');
@@ -2395,7 +2395,7 @@ describe('type check blocks', () => {
`;
const result = tcb(TEMPLATE);
expect(result).toContain('for (const _t1 of (((this).items))!) {');
expect(result).toContain('for (const _t1 of ((this).items)!) {');
expect(result).toContain('var _t2 = null! as number;');
expect(result).toContain('var _t3 = null! as number;');
expect(result).toContain('"" + (_t2) + (_t3)');
@@ -2413,15 +2413,15 @@ describe('type check blocks', () => {
`;
const result = tcb(TEMPLATE);
expect(result).toContain('for (const _t1 of (((this).items))!) { var _t2 = null! as number;');
expect(result).toContain('for (const _t1 of ((this).items)!) { var _t2 = null! as number;');
expect(result).toContain('"" + (_t1) + (_t2)');
expect(result).toContain('for (const _t3 of (((_t1).items))!) { var _t4 = null! as number;');
expect(result).toContain('for (const _t3 of ((_t1).items)!) { var _t4 = null! as number;');
expect(result).toContain('"" + (_t1) + ((_t2)) + (_t3) + (_t4)');
});
it('should generate the tracking expression of a for loop', () => {
const result = tcb(`@for (item of items; track trackingFn($index, item, prop)) {}`);
expect(result).toContain('for (const _t1 of (((this).items))!) { var _t2 = null! as number;');
expect(result).toContain('for (const _t1 of ((this).items)!) { var _t2 = null! as number;');
expect(result).toContain('(this).trackingFn(_t2, _t1, ((this).prop));');
});
@@ -2435,23 +2435,10 @@ describe('type check blocks', () => {
`;
const result = tcb(TEMPLATE, undefined, {checkControlFlowBodies: false});
expect(result).toContain('for (const _t1 of (((this).items))!) {');
expect(result).toContain('for (const _t1 of ((this).items)!) {');
expect(result).not.toContain('.main');
expect(result).not.toContain('.empty');
});
it('should wrap compound expressions in parentheses before appending non-null assertion', () => {
const TEMPLATE = `
@for (item of items && items.slice(0, 10); track item) {
{{item}}
}
`;
const result = tcb(TEMPLATE);
expect(result).toContain(
'for (const _t1 of ((((this).items)) && ((((this).items)).slice(0, 10)))!) {',
);
});
});
describe('let declarations', () => {
@@ -7120,134 +7120,6 @@ suppress
]);
});
it('should report diagnostics within sub-expressions of compound @for loop expressions', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@for (item of items && does_not_exist; track item) {
{{item}}
}
\`,
})
export class Main {
items = [1, 2, 3];
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
"Property 'does_not_exist' does not exist on type 'Main'.",
]);
});
it('should report diagnostics on invalid arguments in compound @for loop expressions', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@for (item of items && items.slice('not_a_number'); track item) {
{{item}}
}
\`,
})
export class Main {
items = [1, 2, 3];
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
"Argument of type 'string' is not assignable to parameter of type 'number'.",
]);
});
it('should report diagnostics on ternary expressions in @for loop expressions', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@for (item of condition ? not_found : items; track item) {
{{item}}
}
\`,
})
export class Main {
condition = true;
items = [1, 2, 3];
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
"Property 'not_found' does not exist on type 'Main'.",
]);
});
it('should report diagnostics on nested property reads in @for loop expressions', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@for (item of nested.does_not_exist; track item) {
{{item}}
}
\`,
})
export class Main {
nested = {a: 1};
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
"Property 'does_not_exist' does not exist on type '{ a: number; }'.",
]);
});
it('should report diagnostics when calling functions with invalid arguments in @for loop expressions', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`
@for (item of getItems('invalid'); track item) {
{{item}}
}
\`,
})
export class Main {
getItems(count: number): string[] {
return [];
}
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
"Argument of type 'string' is not assignable to parameter of type 'number'.",
]);
});
it('should check for loop variables with the same name as built-in globals', () => {
// strictTemplates are necessary so the event listener is checked.
env.tsconfig({strictTemplates: true});
@@ -45,9 +45,8 @@ export class TcbForOfOp extends TcbOp {
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
// Add a non-null expression to allow such values to be assigned.
const expr = tcbExpression(this.block.expression, this.tcb, this.scope).wrapForTypeChecker();
const expression = new TcbExpr(`${expr.print()}!`).addParseSpanInfo(
this.block.expression.sourceSpan,
const expression = new TcbExpr(
`${tcbExpression(this.block.expression, this.tcb, this.scope).print()}!`,
);
let statements: TcbExpr[];