From 0c8917b3483d1fcaedd63acbc6fcceccac196e01 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 30 Aug 2023 11:51:39 +0200 Subject: [PATCH] refactor(compiler): type check contents of control flow blocks (#51570) Adds type checking for the contents of `if`, `switch` and `for` blocks. **Note:** this is just an initial implementation to get some basic type checking working and to figure out the testing setup. We'll need special TCB structures for this syntax so that we can support type narrowing. PR Close #51570 --- .../ngtsc/typecheck/src/type_check_block.ts | 16 ++- .../typecheck/test/type_check_block_spec.ts | 57 ++++++++++ .../GOLDEN_PARTIAL.js | 41 ++++++- .../basic_for.ts | 1 + .../for_aliased_template_variables.ts | 8 ++ .../for_data_slots.ts | 1 + .../for_template_variables.ts | 9 ++ .../for_template_variables_listener.ts | 7 ++ .../for_track_by_field.ts | 1 + .../for_track_by_index.ts | 1 + .../for_variables_expression.ts | 3 + .../for_with_empty.ts | 1 + .../if_nested_alias.ts | 4 + .../if_nested_alias_listeners.ts | 6 +- .../if_with_alias.ts | 2 + .../nested_for.ts | 4 + .../nested_for_template_variables.ts | 5 + .../test/ngtsc/template_typecheck_spec.ts | 101 ++++++++++++++++++ 18 files changed, 264 insertions(+), 4 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 11adcedde58..5c6c83be97d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstElement, TmplAstIcu, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TransplantedType} from '@angular/compiler'; +import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstElement, TmplAstForLoopBlock, TmplAstIcu, TmplAstIfBlock, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TransplantedType} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../imports'; @@ -1510,6 +1510,20 @@ class Scope { node.placeholder !== null && this.appendChildren(node.placeholder); node.loading !== null && this.appendChildren(node.loading); node.error !== null && this.appendChildren(node.error); + } else if (node instanceof TmplAstIfBlock) { + // TODO(crisbeto): type check the branch expression. + for (const branch of node.branches) { + this.appendChildren(branch); + } + } else if (node instanceof TmplAstSwitchBlock) { + // TODO(crisbeto): type check switch condition + for (const currentCase of node.cases) { + this.appendChildren(currentCase); + } + } else if (node instanceof TmplAstForLoopBlock) { + // TODO(crisbeto): type check loop expression, context variables and trackBy + this.appendChildren(node); + node.empty && this.appendChildren(node.empty); } else if (node instanceof TmplAstBoundText) { this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node)); } else if (node instanceof TmplAstIcu) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 1ec36f21cd9..6df995b21dc 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -1379,4 +1379,61 @@ describe('type check blocks', () => { '"" + ((this).main()); "" + ((this).placeholder()); "" + ((this).loading()); "" + ((this).error());'); }); }); + + // TODO(crisbeto): tests for the bindings of conditionals and context variables. + describe('conditional blocks', () => { + // TODO(crisbeto): temporary utility while conditional blocks are disabled by default + function conditionalTcb(template: string): string { + return tcb( + template, undefined, undefined, undefined, + {enabledBlockTypes: new Set(['if', 'switch'])}); + } + + it('should generate bindings inside if block', () => { + const TEMPLATE = ` + {#if expr} + {{main()}} + {:else if expr1}{{one()}} + {:else if expr2}{{two()}} + {:else}{{other()}} + {/if} + `; + + expect(conditionalTcb(TEMPLATE)) + .toContain( + '"" + ((this).main()); "" + ((this).one()); "" + ((this).two()); "" + ((this).other());'); + }); + + it('should generate bindings inside switch block', () => { + const TEMPLATE = ` + {#switch expr} + {:case 1}{{one()}} + {:case 2}{{two()}} + {:default}{{default()}} + {/switch} + `; + + expect(conditionalTcb(TEMPLATE)) + .toContain('"" + ((this).one()); "" + ((this).two()); "" + ((this).default());'); + }); + }); + + // TODO(crisbeto): tests for the for loop expression and context variables + describe('for loop blocks', () => { + // TODO(crisbeto): temporary utility while for loop blocks are disabled by default + function loopTcb(template: string): string { + return tcb(template, undefined, undefined, undefined, {enabledBlockTypes: new Set(['for'])}); + } + + it('should generate bindings inside for loop blocks', () => { + const TEMPLATE = ` + {#for item of items; track item} + {{main()}} + {:empty}{{empty()}} + {/for} + `; + + expect(loopTcb(TEMPLATE)).toContain('"" + ((this).main()); "" + ((this).empty());'); + }); + }); }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js index b8522b17a82..5c11dff9797 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js @@ -530,6 +530,7 @@ import * as i0 from "@angular/core"; export declare class MyApp { message: string; value: () => number; + alias: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -579,6 +580,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE import * as i0 from "@angular/core"; export declare class MyApp { value: () => number; + root: any; + inner: any; + innermost: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -592,7 +596,7 @@ export class MyApp { constructor() { this.value = () => 1; } - log(_) { } + log(..._) { } } MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` @@ -633,7 +637,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE import * as i0 from "@angular/core"; export declare class MyApp { value: () => number; - log(_: any): void; + log(..._: any[]): void; + root: any; + inner: any; + innermost: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -677,6 +684,7 @@ export declare class MyApp { items: { name: string; }[]; + item: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -726,6 +734,7 @@ export declare class MyApp { items: { name: string; }[]; + item: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -769,6 +778,7 @@ export declare class MyApp { items: { name: string; }[]; + item: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -812,6 +822,7 @@ export declare class MyApp { items: { name: string; }[]; + item: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -866,6 +877,8 @@ export declare class MyApp { name: string; subItems: string[]; }[]; + item: any; + subitem: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -921,6 +934,13 @@ import * as i0 from "@angular/core"; export declare class MyApp { message: string; items: never[]; + item: any; + $index: any; + $first: any; + $last: any; + $even: any; + $odd: any; + $count: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -976,6 +996,12 @@ import * as i0 from "@angular/core"; export declare class MyApp { message: string; items: never[]; + idx: any; + f: any; + l: any; + ev: any; + o: any; + co: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -1036,6 +1062,9 @@ export declare class MyApp { name: string; subItems: string[]; }[]; + item: any; + outerCount: any; + $count: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -1083,6 +1112,11 @@ export declare class MyApp { message: string; items: never[]; log(..._: any[]): void; + item: any; + ev: any; + $index: any; + $first: any; + $count: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -1112,6 +1146,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE import * as i0 from "@angular/core"; export declare class MyApp { items: never[]; + item: any; + $odd: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -1151,6 +1187,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE import * as i0 from "@angular/core"; export declare class MyApp { items: string[]; + item: any; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for.ts index 3fc1b7fe644..2da0a4c6ccb 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for.ts @@ -11,4 +11,5 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; items = [{name: 'one'}, {name: 'two'}, {name: 'three'}]; + item: any; // TODO(crisbeto): remove this once template type checking is full implemented. } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables.ts index 883ed33025a..02e472ddc74 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables.ts @@ -18,4 +18,12 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; items = []; + + // TODO(crisbeto): remove this once template type checking is full implemented. + idx: any; + f: any; + l: any; + ev: any; + o: any; + co: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots.ts index 9c74bfa9493..2fba79a5291 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots.ts @@ -11,4 +11,5 @@ import {Component} from '@angular/core'; }) export class MyApp { items = ['one', 'two', 'three']; + item: any; // TODO(crisbeto): remove this once template type checking is full implemented. } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables.ts index d37012244e0..dfdd321a6dc 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables.ts @@ -18,4 +18,13 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; items = []; + + // TODO(crisbeto): remove this once template type checking is full implemented. + item: any; + $index: any; + $first: any; + $last: any; + $even: any; + $odd: any; + $count: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener.ts index 40cfafe48ca..2cb348c30be 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener.ts @@ -14,4 +14,11 @@ export class MyApp { message = 'hello'; items = []; log(..._: any[]) {} + + // TODO(crisbeto): remove this once template type checking is full implemented. + item: any; + ev: any; + $index: any; + $first: any; + $count: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field.ts index 4035eee0fa7..de495f43d20 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field.ts @@ -11,4 +11,5 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; items = [{name: 'one'}, {name: 'two'}, {name: 'three'}]; + item: any; // TODO(crisbeto): remove this once template type checking is full implemented. } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index.ts index d0aec2de26d..c3ee7be427e 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index.ts @@ -11,4 +11,5 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; items = [{name: 'one'}, {name: 'two'}, {name: 'three'}]; + item: any; // TODO(crisbeto): remove this once template type checking is full implemented. } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_variables_expression.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_variables_expression.ts index 92ad8ce2e44..d7889837bf4 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_variables_expression.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_variables_expression.ts @@ -5,4 +5,7 @@ import {Component} from '@angular/core'; }) export class MyApp { items = []; + // TODO(crisbeto): remove this once template type checking is full implemented. + item: any; + $odd: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty.ts index 2f72f30252a..39e8afb45a9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty.ts @@ -14,4 +14,5 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; items = [{name: 'one'}, {name: 'two'}, {name: 'three'}]; + item: any; // TODO(crisbeto): remove this once template type checking is full implemented. } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias.ts index 9b2ebdfc617..49b98e127cd 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias.ts @@ -15,4 +15,8 @@ import {Component} from '@angular/core'; }) export class MyApp { value = () => 1; + // TODO(crisbeto): remove this once template type checking is full implemented. + root: any; + inner: any; + innermost: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners.ts index c6f0805753d..db86f9bc616 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_nested_alias_listeners.ts @@ -17,5 +17,9 @@ import {Component} from '@angular/core'; }) export class MyApp { value = () => 1; - log(_: any) {} + log(..._: any[]) {} + // TODO(crisbeto): remove this once template type checking is full implemented. + root: any; + inner: any; + innermost: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_with_alias.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_with_alias.ts index efba07e4d0a..ef1f6947a8a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_with_alias.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/if_with_alias.ts @@ -11,4 +11,6 @@ import {Component} from '@angular/core'; export class MyApp { message = 'hello'; value = () => 1; + // TODO(crisbeto): remove this once template type checking is full implemented. + alias: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for.ts index af65fe384fa..19ba9b5408a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for.ts @@ -18,4 +18,8 @@ export class MyApp { {name: 'two', subItems: ['sub one', 'sub two', 'sub three']}, {name: 'three', subItems: ['sub one', 'sub two', 'sub three']}, ]; + + // TODO(crisbeto): remove this once template type checking is full implemented. + item: any; + subitem: any; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables.ts index 3cef397ad90..ae74cdc0ff8 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables.ts @@ -21,4 +21,9 @@ export class MyApp { {name: 'two', subItems: ['sub one', 'sub two', 'sub three']}, {name: 'three', subItems: ['sub one', 'sub two', 'sub three']}, ]; + + // TODO(crisbeto): remove this once template type checking is full implemented. + item: any; + outerCount: any; + $count: any; } diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 4a909bfdd44..77e7f81ad1c 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -3656,5 +3656,106 @@ suppress ]); }); }); + + describe('conditional blocks', () => { + beforeEach(() => { + env.tsconfig({_enabledBlockTypes: ['if', 'switch']}); + }); + + // TODO(crisbeto): test to check the bindings of the branches. + // TODO(crisbeto): test for an `if` block with an `as` assignment. + // TODO(crisbeto): test for type narrowing. + it('should check bindings inside if blocks', () => { + env.write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + {#if expr} + {{does_not_exist_main}} + {:else if expr1}{{does_not_exist_one}} + {:else if expr2}{{does_not_exist_two}} + {:else}{{does_not_exist_else}} + {/if} + \`, + standalone: true, + }) + export class Main { + expr = false; + expr1 = false; + expr2 = false; + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([ + `Property 'does_not_exist_main' does not exist on type 'Main'.`, + `Property 'does_not_exist_one' does not exist on type 'Main'.`, + `Property 'does_not_exist_two' does not exist on type 'Main'.`, + `Property 'does_not_exist_else' does not exist on type 'Main'.`, + ]); + }); + + // TODO(crisbeto): test to check the bindings of the cases. + // TODO(crisbeto): test for type narrowing. + it('should check bindings inside switch blocks', () => { + env.write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + {#switch expr} + {:case 1}{{does_not_exist_one}} + {:case 2}{{does_not_exist_two}} + {:default}{{does_not_exist_default}} + {/switch} + \`, + standalone: true, + }) + export class Main { + expr: any; + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([ + `Property 'does_not_exist_one' does not exist on type 'Main'.`, + `Property 'does_not_exist_two' does not exist on type 'Main'.`, + `Property 'does_not_exist_default' does not exist on type 'Main'.`, + ]); + }); + }); + + // TODO(crisbeto): test for the loop expression binding + // TODO(crisbeto): test for the track expression. + // TODO(crisbeto): test for the context variables ($index, $odd etc). + describe('for loop blocks', () => { + beforeEach(() => { + env.tsconfig({_enabledBlockTypes: ['for']}); + }); + + it('should check bindings inside of for loop blocks', () => { + env.write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + {#for item of items; track item} + {{does_not_exist_main}} + {:empty}{{does_not_exist_empty}} + {/for} + \`, + standalone: true, + }) + export class Main {} + `); + + const diags = env.driveDiagnostics(); + expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([ + `Property 'does_not_exist_main' does not exist on type 'Main'.`, + `Property 'does_not_exist_empty' does not exist on type 'Main'.`, + ]); + }); + }); }); });