mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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
This commit is contained in:
parent
98d98f2c94
commit
0c8917b348
18 changed files with 264 additions and 4 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -530,6 +530,7 @@ import * as i0 from "@angular/core";
|
|||
export declare class MyApp {
|
||||
message: string;
|
||||
value: () => number;
|
||||
alias: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -677,6 +684,7 @@ export declare class MyApp {
|
|||
items: {
|
||||
name: string;
|
||||
}[];
|
||||
item: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -726,6 +734,7 @@ export declare class MyApp {
|
|||
items: {
|
||||
name: string;
|
||||
}[];
|
||||
item: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -769,6 +778,7 @@ export declare class MyApp {
|
|||
items: {
|
||||
name: string;
|
||||
}[];
|
||||
item: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -812,6 +822,7 @@ export declare class MyApp {
|
|||
items: {
|
||||
name: string;
|
||||
}[];
|
||||
item: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -866,6 +877,8 @@ export declare class MyApp {
|
|||
name: string;
|
||||
subItems: string[];
|
||||
}[];
|
||||
item: any;
|
||||
subitem: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -1036,6 +1062,9 @@ export declare class MyApp {
|
|||
name: string;
|
||||
subItems: string[];
|
||||
}[];
|
||||
item: any;
|
||||
outerCount: any;
|
||||
$count: any;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
@ -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<MyApp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, false, never>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'.`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue