feat(compiler): narrow types of expressions used in *ngIf (#20702)

Structural directives can now specify a type guard that describes
what types can be inferred for an input expression inside the
directive's template.

NgIf was modified to declare an input guard on ngIf.

After this change, `fullTemplateTypeCheck` will infer that
usage of `ngIf` expression inside it's template is truthy.

For example, if a component has a property `person?: Person`
and a template of `<div *ngIf="person"> {{person.name}} </div>`
the compiler will no longer report that `person` might be null or
undefined.

The template compiler will generate code similar to,

```
  if (NgIf.ngIfTypeGuard(instance.person)) {
    instance.person.name
  }
```

to validate the template's use of the interpolation expression.
Calling the type guard in this fashion allows TypeScript to infer
that `person` is non-null.

Fixes: #19756?

PR Close #20702
This commit is contained in:
Chuck Jazdzewski 2017-11-29 16:29:05 -08:00 committed by Jason Aden
parent e544742156
commit e7d9cb3e4c
19 changed files with 341 additions and 53 deletions

View File

@ -151,6 +151,8 @@ export class NgIf {
} }
} }
} }
public static ngIfTypeGuard: <T>(v: T|null|undefined|false) => v is T;
} }
/** /**

View File

@ -81,6 +81,141 @@ describe('ng type checker', () => {
}); });
}); });
describe('type narrowing', () => {
const a = (files: MockFiles, options: object = {}) => {
accept(files, {fullTemplateTypeCheck: true, ...options});
};
it('should narrow an *ngIf like directive', () => {
a({
'src/app.component.ts': '',
'src/lib.ts': '',
'src/app.module.ts': `
import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core';
export interface Person {
name: string;
}
@Component({
selector: 'comp',
template: '<div *myIf="person"> {{person.name}} </div>'
})
export class MainComp {
person?: Person;
}
export class MyIfContext {
public $implicit: any = null;
public myIf: any = null;
}
@Directive({selector: '[myIf]'})
export class MyIf {
constructor(templateRef: TemplateRef<MyIfContext>) {}
@Input()
set myIf(condition: any) {}
static myIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
}
@NgModule({
declarations: [MainComp, MyIf],
})
export class MainModule {}`
});
});
it('should narrow a renamed *ngIf like directive', () => {
a({
'src/app.component.ts': '',
'src/lib.ts': '',
'src/app.module.ts': `
import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core';
export interface Person {
name: string;
}
@Component({
selector: 'comp',
template: '<div *my-if="person"> {{person.name}} </div>'
})
export class MainComp {
person?: Person;
}
export class MyIfContext {
public $implicit: any = null;
public myIf: any = null;
}
@Directive({selector: '[my-if]'})
export class MyIf {
constructor(templateRef: TemplateRef<MyIfContext>) {}
@Input('my-if')
set myIf(condition: any) {}
static myIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
}
@NgModule({
declarations: [MainComp, MyIf],
})
export class MainModule {}`
});
});
it('should narrow a type in a nested *ngIf like directive', () => {
a({
'src/app.component.ts': '',
'src/lib.ts': '',
'src/app.module.ts': `
import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core';
export interface Address {
street: string;
}
export interface Person {
name: string;
address?: Address;
}
@Component({
selector: 'comp',
template: '<div *myIf="person"> {{person.name}} <span *myIf="person.address">{{person.address.street}}</span></div>'
})
export class MainComp {
person?: Person;
}
export class MyIfContext {
public $implicit: any = null;
public myIf: any = null;
}
@Directive({selector: '[myIf]'})
export class MyIf {
constructor(templateRef: TemplateRef<MyIfContext>) {}
@Input()
set myIf(condition: any) {}
static myIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
}
@NgModule({
declarations: [MainComp, MyIf],
})
export class MainModule {}`
});
});
});
describe('regressions ', () => { describe('regressions ', () => {
const a = (files: MockFiles, options: object = {}) => { const a = (files: MockFiles, options: object = {}) => {
accept(files, {fullTemplateTypeCheck: true, ...options}); accept(files, {fullTemplateTypeCheck: true, ...options});

View File

@ -1038,6 +1038,25 @@ describe('Collector', () => {
expect(metadata).toBeUndefined(); expect(metadata).toBeUndefined();
}); });
it('should collect type guards', () => {
const metadata = collectSource(`
import {Directive, Input, TemplateRef} from '@angular/core';
@Directive({selector: '[myIf]'})
export class MyIf {
constructor(private templateRef: TemplateRef) {}
@Input() myIf: any;
static typeGuard: <T>(v: T | null | undefined): v is T;
}
`);
expect((metadata.metadata.MyIf as any).statics.typeGuard)
.not.toBeUndefined('typeGuard was not collected');
});
it('should be able to collect an invalid access expression', () => { it('should be able to collect an invalid access expression', () => {
const source = createSource(` const source = createSource(`
import {Component} from '@angular/core'; import {Component} from '@angular/core';

View File

@ -271,7 +271,7 @@ export class AotCompiler {
const {template: parsedTemplate, pipes: usedPipes} = const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, moduleMeta, directives); this._parseTemplate(compMeta, moduleMeta, directives);
ctx.statements.push(...this._typeCheckCompiler.compileComponent( ctx.statements.push(...this._typeCheckCompiler.compileComponent(
componentId, compMeta, parsedTemplate, usedPipes, externalReferenceVars)); componentId, compMeta, parsedTemplate, usedPipes, externalReferenceVars, ctx));
} }
emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle { emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle {

View File

@ -29,6 +29,7 @@ const IGNORE = {
const USE_VALUE = 'useValue'; const USE_VALUE = 'useValue';
const PROVIDE = 'provide'; const PROVIDE = 'provide';
const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']); const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']);
const TYPEGUARD_POSTFIX = 'TypeGuard';
function shouldIgnore(value: any): boolean { function shouldIgnore(value: any): boolean {
return value && value.__symbolic == 'ignore'; return value && value.__symbolic == 'ignore';
@ -43,6 +44,7 @@ export class StaticReflector implements CompileReflector {
private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>(); private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>();
private parameterCache = new Map<StaticSymbol, any[]>(); private parameterCache = new Map<StaticSymbol, any[]>();
private methodCache = new Map<StaticSymbol, {[key: string]: boolean}>(); private methodCache = new Map<StaticSymbol, {[key: string]: boolean}>();
private staticCache = new Map<StaticSymbol, string[]>();
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>(); private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
private injectionToken: StaticSymbol; private injectionToken: StaticSymbol;
private opaqueToken: StaticSymbol; private opaqueToken: StaticSymbol;
@ -251,6 +253,18 @@ export class StaticReflector implements CompileReflector {
return methodNames; return methodNames;
} }
private _staticMembers(type: StaticSymbol): string[] {
let staticMembers = this.staticCache.get(type);
if (!staticMembers) {
const classMetadata = this.getTypeMetadata(type);
const staticMemberData = classMetadata['statics'] || {};
staticMembers = Object.keys(staticMemberData);
this.staticCache.set(type, staticMembers);
}
return staticMembers;
}
private findParentType(type: StaticSymbol, classMetadata: any): StaticSymbol|undefined { private findParentType(type: StaticSymbol, classMetadata: any): StaticSymbol|undefined {
const parentType = this.trySimplify(type, classMetadata['extends']); const parentType = this.trySimplify(type, classMetadata['extends']);
if (parentType instanceof StaticSymbol) { if (parentType instanceof StaticSymbol) {
@ -273,6 +287,21 @@ export class StaticReflector implements CompileReflector {
} }
} }
guards(type: any): {[key: string]: StaticSymbol} {
if (!(type instanceof StaticSymbol)) {
this.reportError(
new Error(`guards received ${JSON.stringify(type)} which is not a StaticSymbol`), type);
return {};
}
const staticMembers = this._staticMembers(type);
const result: {[key: string]: StaticSymbol} = {};
for (let name of staticMembers) {
result[name.substr(0, name.length - TYPEGUARD_POSTFIX.length)] =
this.getStaticSymbol(type.filePath, type.name, [name]);
}
return result;
}
private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void { private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args)); this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args));
} }

View File

@ -254,6 +254,7 @@ export interface CompileDirectiveSummary extends CompileTypeSummary {
providers: CompileProviderMetadata[]; providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[]; viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[]; queries: CompileQueryMetadata[];
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[]; viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[]; entryComponents: CompileEntryComponentMetadata[];
changeDetection: ChangeDetectionStrategy|null; changeDetection: ChangeDetectionStrategy|null;
@ -268,8 +269,8 @@ export interface CompileDirectiveSummary extends CompileTypeSummary {
*/ */
export class CompileDirectiveMetadata { export class CompileDirectiveMetadata {
static create({isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs, static create({isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs,
host, providers, viewProviders, queries, viewQueries, entryComponents, template, host, providers, viewProviders, queries, guards, viewQueries, entryComponents,
componentViewType, rendererType, componentFactory}: { template, componentViewType, rendererType, componentFactory}: {
isHost: boolean, isHost: boolean,
type: CompileTypeMetadata, type: CompileTypeMetadata,
isComponent: boolean, isComponent: boolean,
@ -282,6 +283,7 @@ export class CompileDirectiveMetadata {
providers: CompileProviderMetadata[], providers: CompileProviderMetadata[],
viewProviders: CompileProviderMetadata[], viewProviders: CompileProviderMetadata[],
queries: CompileQueryMetadata[], queries: CompileQueryMetadata[],
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[], viewQueries: CompileQueryMetadata[],
entryComponents: CompileEntryComponentMetadata[], entryComponents: CompileEntryComponentMetadata[],
template: CompileTemplateMetadata, template: CompileTemplateMetadata,
@ -336,6 +338,7 @@ export class CompileDirectiveMetadata {
providers, providers,
viewProviders, viewProviders,
queries, queries,
guards,
viewQueries, viewQueries,
entryComponents, entryComponents,
template, template,
@ -358,6 +361,7 @@ export class CompileDirectiveMetadata {
providers: CompileProviderMetadata[]; providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[]; viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[]; queries: CompileQueryMetadata[];
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[]; viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[]; entryComponents: CompileEntryComponentMetadata[];
@ -367,10 +371,27 @@ export class CompileDirectiveMetadata {
rendererType: StaticSymbol|object|null; rendererType: StaticSymbol|object|null;
componentFactory: StaticSymbol|object|null; componentFactory: StaticSymbol|object|null;
constructor({isHost, type, isComponent, selector, exportAs, constructor({isHost,
changeDetection, inputs, outputs, hostListeners, hostProperties, type,
hostAttributes, providers, viewProviders, queries, viewQueries, isComponent,
entryComponents, template, componentViewType, rendererType, componentFactory}: { selector,
exportAs,
changeDetection,
inputs,
outputs,
hostListeners,
hostProperties,
hostAttributes,
providers,
viewProviders,
queries,
guards,
viewQueries,
entryComponents,
template,
componentViewType,
rendererType,
componentFactory}: {
isHost: boolean, isHost: boolean,
type: CompileTypeMetadata, type: CompileTypeMetadata,
isComponent: boolean, isComponent: boolean,
@ -385,6 +406,7 @@ export class CompileDirectiveMetadata {
providers: CompileProviderMetadata[], providers: CompileProviderMetadata[],
viewProviders: CompileProviderMetadata[], viewProviders: CompileProviderMetadata[],
queries: CompileQueryMetadata[], queries: CompileQueryMetadata[],
guards: {[key: string]: any},
viewQueries: CompileQueryMetadata[], viewQueries: CompileQueryMetadata[],
entryComponents: CompileEntryComponentMetadata[], entryComponents: CompileEntryComponentMetadata[],
template: CompileTemplateMetadata|null, template: CompileTemplateMetadata|null,
@ -406,6 +428,7 @@ export class CompileDirectiveMetadata {
this.providers = _normalizeArray(providers); this.providers = _normalizeArray(providers);
this.viewProviders = _normalizeArray(viewProviders); this.viewProviders = _normalizeArray(viewProviders);
this.queries = _normalizeArray(queries); this.queries = _normalizeArray(queries);
this.guards = guards;
this.viewQueries = _normalizeArray(viewQueries); this.viewQueries = _normalizeArray(viewQueries);
this.entryComponents = _normalizeArray(entryComponents); this.entryComponents = _normalizeArray(entryComponents);
this.template = template; this.template = template;
@ -430,6 +453,7 @@ export class CompileDirectiveMetadata {
providers: this.providers, providers: this.providers,
viewProviders: this.viewProviders, viewProviders: this.viewProviders,
queries: this.queries, queries: this.queries,
guards: this.guards,
viewQueries: this.viewQueries, viewQueries: this.viewQueries,
entryComponents: this.entryComponents, entryComponents: this.entryComponents,
changeDetection: this.changeDetection, changeDetection: this.changeDetection,

View File

@ -17,6 +17,7 @@ export abstract class CompileReflector {
abstract annotations(typeOrFunc: /*Type*/ any): any[]; abstract annotations(typeOrFunc: /*Type*/ any): any[];
abstract propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]}; abstract propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]};
abstract hasLifecycleHook(type: any, lcProperty: string): boolean; abstract hasLifecycleHook(type: any, lcProperty: string): boolean;
abstract guards(typeOrFunc: /* Type */ any): {[key: string]: any};
abstract componentModuleUrl(type: /*Type*/ any, cmpMetadata: Component): string; abstract componentModuleUrl(type: /*Type*/ any, cmpMetadata: Component): string;
abstract resolveExternalReference(ref: o.ExternalReference): any; abstract resolveExternalReference(ref: o.ExternalReference): any;
} }

View File

@ -90,6 +90,14 @@ export class ConvertPropertyBindingResult {
constructor(public stmts: o.Statement[], public currValExpr: o.Expression) {} constructor(public stmts: o.Statement[], public currValExpr: o.Expression) {}
} }
export enum BindingForm {
// The general form of binding expression, supports all expressions.
General,
// Try to generate a simple binding (no temporaries or statements)
// otherise generate a general binding
TrySimple,
}
/** /**
* Converts the given expression AST into an executable output AST, assuming the expression * Converts the given expression AST into an executable output AST, assuming the expression
* is used in property binding. The expression has to be preprocessed via * is used in property binding. The expression has to be preprocessed via
@ -97,7 +105,8 @@ export class ConvertPropertyBindingResult {
*/ */
export function convertPropertyBinding( export function convertPropertyBinding(
localResolver: LocalResolver | null, implicitReceiver: o.Expression, localResolver: LocalResolver | null, implicitReceiver: o.Expression,
expressionWithoutBuiltins: cdAst.AST, bindingId: string): ConvertPropertyBindingResult { expressionWithoutBuiltins: cdAst.AST, bindingId: string,
form: BindingForm): ConvertPropertyBindingResult {
if (!localResolver) { if (!localResolver) {
localResolver = new DefaultLocalResolver(); localResolver = new DefaultLocalResolver();
} }
@ -110,6 +119,8 @@ export function convertPropertyBinding(
for (let i = 0; i < visitor.temporaryCount; i++) { for (let i = 0; i < visitor.temporaryCount; i++) {
stmts.push(temporaryDeclaration(bindingId, i)); stmts.push(temporaryDeclaration(bindingId, i));
} }
} else if (form == BindingForm.TrySimple) {
return new ConvertPropertyBindingResult([], outputExpr);
} }
stmts.push(currValExpr.set(outputExpr).toDeclStmt(null, [o.StmtModifier.Final])); stmts.push(currValExpr.set(outputExpr).toDeclStmt(null, [o.StmtModifier.Final]));

View File

@ -51,6 +51,7 @@ export interface Directive {
providers?: Provider[]; providers?: Provider[];
exportAs?: string; exportAs?: string;
queries?: {[key: string]: any}; queries?: {[key: string]: any};
guards?: {[key: string]: any};
} }
export const createDirective = export const createDirective =
makeMetadataFactory<Directive>('Directive', (dir: Directive = {}) => dir); makeMetadataFactory<Directive>('Directive', (dir: Directive = {}) => dir);

View File

@ -44,7 +44,8 @@ export class DirectiveResolver {
const metadata = findLast(typeMetadata, isDirectiveMetadata); const metadata = findLast(typeMetadata, isDirectiveMetadata);
if (metadata) { if (metadata) {
const propertyMetadata = this._reflector.propMetadata(type); const propertyMetadata = this._reflector.propMetadata(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, type); const guards = this._reflector.guards(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, guards, type);
} }
} }
@ -56,12 +57,12 @@ export class DirectiveResolver {
} }
private _mergeWithPropertyMetadata( private _mergeWithPropertyMetadata(
dm: Directive, propertyMetadata: {[key: string]: any[]}, directiveType: Type): Directive { dm: Directive, propertyMetadata: {[key: string]: any[]}, guards: {[key: string]: any},
directiveType: Type): Directive {
const inputs: string[] = []; const inputs: string[] = [];
const outputs: string[] = []; const outputs: string[] = [];
const host: {[key: string]: string} = {}; const host: {[key: string]: string} = {};
const queries: {[key: string]: any} = {}; const queries: {[key: string]: any} = {};
Object.keys(propertyMetadata).forEach((propName: string) => { Object.keys(propertyMetadata).forEach((propName: string) => {
const input = findLast(propertyMetadata[propName], (a) => createInput.isTypeOf(a)); const input = findLast(propertyMetadata[propName], (a) => createInput.isTypeOf(a));
if (input) { if (input) {
@ -105,18 +106,20 @@ export class DirectiveResolver {
queries[propName] = query; queries[propName] = query;
} }
}); });
return this._merge(dm, inputs, outputs, host, queries, directiveType); return this._merge(dm, inputs, outputs, host, queries, guards, directiveType);
} }
private _extractPublicName(def: string) { return splitAtColon(def, [null !, def])[1].trim(); } private _extractPublicName(def: string) { return splitAtColon(def, [null !, def])[1].trim(); }
private _dedupeBindings(bindings: string[]): string[] { private _dedupeBindings(bindings: string[]): string[] {
const names = new Set<string>(); const names = new Set<string>();
const publicNames = new Set<string>();
const reversedResult: string[] = []; const reversedResult: string[] = [];
// go last to first to allow later entries to overwrite previous entries // go last to first to allow later entries to overwrite previous entries
for (let i = bindings.length - 1; i >= 0; i--) { for (let i = bindings.length - 1; i >= 0; i--) {
const binding = bindings[i]; const binding = bindings[i];
const name = this._extractPublicName(binding); const name = this._extractPublicName(binding);
publicNames.add(name);
if (!names.has(name)) { if (!names.has(name)) {
names.add(name); names.add(name);
reversedResult.push(binding); reversedResult.push(binding);
@ -127,14 +130,13 @@ export class DirectiveResolver {
private _merge( private _merge(
directive: Directive, inputs: string[], outputs: string[], host: {[key: string]: string}, directive: Directive, inputs: string[], outputs: string[], host: {[key: string]: string},
queries: {[key: string]: any}, directiveType: Type): Directive { queries: {[key: string]: any}, guards: {[key: string]: any}, directiveType: Type): Directive {
const mergedInputs = const mergedInputs =
this._dedupeBindings(directive.inputs ? directive.inputs.concat(inputs) : inputs); this._dedupeBindings(directive.inputs ? directive.inputs.concat(inputs) : inputs);
const mergedOutputs = const mergedOutputs =
this._dedupeBindings(directive.outputs ? directive.outputs.concat(outputs) : outputs); this._dedupeBindings(directive.outputs ? directive.outputs.concat(outputs) : outputs);
const mergedHost = directive.host ? {...directive.host, ...host} : host; const mergedHost = directive.host ? {...directive.host, ...host} : host;
const mergedQueries = directive.queries ? {...directive.queries, ...queries} : queries; const mergedQueries = directive.queries ? {...directive.queries, ...queries} : queries;
if (createComponent.isTypeOf(directive)) { if (createComponent.isTypeOf(directive)) {
const comp = directive as Component; const comp = directive as Component;
return createComponent({ return createComponent({
@ -166,7 +168,7 @@ export class DirectiveResolver {
host: mergedHost, host: mergedHost,
exportAs: directive.exportAs, exportAs: directive.exportAs,
queries: mergedQueries, queries: mergedQueries,
providers: directive.providers providers: directive.providers, guards
}); });
} }
} }

View File

@ -208,6 +208,7 @@ export class CompileMetadataResolver {
providers: [], providers: [],
viewProviders: [], viewProviders: [],
queries: [], queries: [],
guards: {},
viewQueries: [], viewQueries: [],
componentViewType: hostViewType, componentViewType: hostViewType,
rendererType: rendererType:
@ -240,6 +241,7 @@ export class CompileMetadataResolver {
providers: metadata.providers, providers: metadata.providers,
viewProviders: metadata.viewProviders, viewProviders: metadata.viewProviders,
queries: metadata.queries, queries: metadata.queries,
guards: metadata.guards,
viewQueries: metadata.viewQueries, viewQueries: metadata.viewQueries,
entryComponents: metadata.entryComponents, entryComponents: metadata.entryComponents,
componentViewType: metadata.componentViewType, componentViewType: metadata.componentViewType,
@ -383,6 +385,7 @@ export class CompileMetadataResolver {
providers: providers || [], providers: providers || [],
viewProviders: viewProviders || [], viewProviders: viewProviders || [],
queries: queries || [], queries: queries || [],
guards: dirMeta.guards || {},
viewQueries: viewQueries || [], viewQueries: viewQueries || [],
entryComponents: entryComponentMetadata, entryComponents: entryComponentMetadata,
componentViewType: nonNormalizedTemplateMetadata ? this.getComponentViewClass(directiveType) : componentViewType: nonNormalizedTemplateMetadata ? this.getComponentViewClass(directiveType) :

View File

@ -10,13 +10,15 @@ import {AotCompilerOptions} from '../aot/compiler_options';
import {StaticReflector} from '../aot/static_reflector'; import {StaticReflector} from '../aot/static_reflector';
import {StaticSymbol} from '../aot/static_symbol'; import {StaticSymbol} from '../aot/static_symbol';
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompilePipeSummary} from '../compile_metadata'; import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompilePipeSummary} from '../compile_metadata';
import {BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {BindingForm, BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter';
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast'; import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
import {Identifiers} from '../identifiers'; import {Identifiers} from '../identifiers';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
import {convertValueToOutputAst} from '../output/value_util'; import {convertValueToOutputAst} from '../output/value_util';
import {ParseSourceSpan} from '../parse_util'; import {ParseSourceSpan} from '../parse_util';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {OutputContext} from '../util';
/** /**
* Generates code that is used to type check templates. * Generates code that is used to type check templates.
@ -34,27 +36,33 @@ export class TypeCheckCompiler {
*/ */
compileComponent( compileComponent(
componentId: string, component: CompileDirectiveMetadata, template: TemplateAst[], componentId: string, component: CompileDirectiveMetadata, template: TemplateAst[],
usedPipes: CompilePipeSummary[], usedPipes: CompilePipeSummary[], externalReferenceVars: Map<StaticSymbol, string>,
externalReferenceVars: Map<StaticSymbol, string>): o.Statement[] { ctx: OutputContext): o.Statement[] {
const pipes = new Map<string, StaticSymbol>(); const pipes = new Map<string, StaticSymbol>();
usedPipes.forEach(p => pipes.set(p.name, p.type.reference)); usedPipes.forEach(p => pipes.set(p.name, p.type.reference));
let embeddedViewCount = 0; let embeddedViewCount = 0;
const viewBuilderFactory = (parent: ViewBuilder | null): ViewBuilder => { const viewBuilderFactory =
const embeddedViewIndex = embeddedViewCount++; (parent: ViewBuilder | null, guards: GuardExpression[]): ViewBuilder => {
return new ViewBuilder( const embeddedViewIndex = embeddedViewCount++;
this.options, this.reflector, externalReferenceVars, parent, component.type.reference, return new ViewBuilder(
component.isHost, embeddedViewIndex, pipes, viewBuilderFactory); this.options, this.reflector, externalReferenceVars, parent, component.type.reference,
}; component.isHost, embeddedViewIndex, pipes, guards, ctx, viewBuilderFactory);
};
const visitor = viewBuilderFactory(null); const visitor = viewBuilderFactory(null, []);
visitor.visitAll([], template); visitor.visitAll([], template);
return visitor.build(componentId); return visitor.build(componentId);
} }
} }
interface GuardExpression {
guard: StaticSymbol;
expression: Expression;
}
interface ViewBuilderFactory { interface ViewBuilderFactory {
(parent: ViewBuilder): ViewBuilder; (parent: ViewBuilder, guards: GuardExpression[]): ViewBuilder;
} }
// Note: This is used as key in Map and should therefore be // Note: This is used as key in Map and should therefore be
@ -94,6 +102,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
private externalReferenceVars: Map<StaticSymbol, string>, private parent: ViewBuilder|null, private externalReferenceVars: Map<StaticSymbol, string>, private parent: ViewBuilder|null,
private component: StaticSymbol, private isHostComponent: boolean, private component: StaticSymbol, private isHostComponent: boolean,
private embeddedViewIndex: number, private pipes: Map<string, StaticSymbol>, private embeddedViewIndex: number, private pipes: Map<string, StaticSymbol>,
private guards: GuardExpression[], private ctx: OutputContext,
private viewBuilderFactory: ViewBuilderFactory) {} private viewBuilderFactory: ViewBuilderFactory) {}
private getOutputVar(type: o.BuiltinTypeName|StaticSymbol): string { private getOutputVar(type: o.BuiltinTypeName|StaticSymbol): string {
@ -112,6 +121,20 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
return varName; return varName;
} }
private getTypeGuardExpressions(ast: EmbeddedTemplateAst): GuardExpression[] {
const result = [...this.guards];
for (let directive of ast.directives) {
for (let input of directive.inputs) {
const guard = directive.directive.guards[input.directiveName];
if (guard) {
result.push(
{guard, expression: {context: this.component, value: input.value} as Expression});
}
}
}
return result;
}
visitAll(variables: VariableAst[], astNodes: TemplateAst[]) { visitAll(variables: VariableAst[], astNodes: TemplateAst[]) {
this.variables = variables; this.variables = variables;
templateVisitAll(this, astNodes); templateVisitAll(this, astNodes);
@ -119,7 +142,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
build(componentId: string, targetStatements: o.Statement[] = []): o.Statement[] { build(componentId: string, targetStatements: o.Statement[] = []): o.Statement[] {
this.children.forEach((child) => child.build(componentId, targetStatements)); this.children.forEach((child) => child.build(componentId, targetStatements));
const viewStmts: o.Statement[] = let viewStmts: o.Statement[] =
[o.variable(DYNAMIC_VAR_NAME).set(o.NULL_EXPR).toDeclStmt(o.DYNAMIC_TYPE)]; [o.variable(DYNAMIC_VAR_NAME).set(o.NULL_EXPR).toDeclStmt(o.DYNAMIC_TYPE)];
let bindingCount = 0; let bindingCount = 0;
this.updates.forEach((expression) => { this.updates.forEach((expression) => {
@ -127,7 +150,8 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
const bindingId = `${bindingCount++}`; const bindingId = `${bindingCount++}`;
const nameResolver = context === this.component ? this : defaultResolver; const nameResolver = context === this.component ? this : defaultResolver;
const {stmts, currValExpr} = convertPropertyBinding( const {stmts, currValExpr} = convertPropertyBinding(
nameResolver, o.variable(this.getOutputVar(context)), value, bindingId); nameResolver, o.variable(this.getOutputVar(context)), value, bindingId,
BindingForm.General);
stmts.push(new o.ExpressionStatement(currValExpr)); stmts.push(new o.ExpressionStatement(currValExpr));
viewStmts.push(...stmts.map( viewStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan))); (stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
@ -142,6 +166,27 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan))); (stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
}); });
if (this.guards.length) {
let guardExpression: o.Expression|undefined = undefined;
for (const guard of this.guards) {
const {context, value} = this.preprocessUpdateExpression(guard.expression);
const bindingId = `${bindingCount++}`;
const nameResolver = context === this.component ? this : defaultResolver;
// We only support support simple expressions and ignore others as they
// are unlikely to affect type narrowing.
const {stmts, currValExpr} = convertPropertyBinding(
nameResolver, o.variable(this.getOutputVar(context)), value, bindingId,
BindingForm.TrySimple);
if (stmts.length == 0) {
const callGuard = this.ctx.importExpr(guard.guard).callFn([currValExpr]);
guardExpression = guardExpression ? guardExpression.and(callGuard) : callGuard;
}
}
if (guardExpression) {
viewStmts = [new o.IfStmt(guardExpression, viewStmts)];
}
}
const viewName = `_View_${componentId}_${this.embeddedViewIndex}`; const viewName = `_View_${componentId}_${this.embeddedViewIndex}`;
const viewFactory = new o.DeclareFunctionStmt(viewName, [], viewStmts); const viewFactory = new o.DeclareFunctionStmt(viewName, [], viewStmts);
targetStatements.push(viewFactory); targetStatements.push(viewFactory);
@ -163,7 +208,12 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
// for the context in any embedded view. // for the context in any embedded view.
// We keep this behaivor behind a flag for now. // We keep this behaivor behind a flag for now.
if (this.options.fullTemplateTypeCheck) { if (this.options.fullTemplateTypeCheck) {
const childVisitor = this.viewBuilderFactory(this); // Find any applicable type guards. For example, NgIf has a type guard on ngIf
// (see NgIf.ngIfTypeGuard) that can be used to indicate that a template is only
// stamped out if ngIf is truthy so any bindings in the template can assume that,
// if a nullable type is used for ngIf, that expression is not null or undefined.
const guards = this.getTypeGuardExpressions(ast);
const childVisitor = this.viewBuilderFactory(this, guards);
this.children.push(childVisitor); this.children.push(childVisitor);
childVisitor.visitAll(ast.variables, ast.children); childVisitor.visitAll(ast.variables, ast.children);
} }

View File

@ -8,7 +8,7 @@
import {CompileDirectiveMetadata, CompilePipeSummary, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; import {CompileDirectiveMetadata, CompilePipeSummary, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata';
import {CompileReflector} from '../compile_reflector'; import {CompileReflector} from '../compile_reflector';
import {BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {BindingForm, BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter';
import {ArgumentType, BindingFlags, ChangeDetectionStrategy, NodeFlags, QueryBindingType, QueryValueType, ViewFlags} from '../core'; import {ArgumentType, BindingFlags, ChangeDetectionStrategy, NodeFlags, QueryBindingType, QueryValueType, ViewFlags} from '../core';
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast'; import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
import {Identifiers} from '../identifiers'; import {Identifiers} from '../identifiers';
@ -859,7 +859,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
const bindingId = `${updateBindingCount++}`; const bindingId = `${updateBindingCount++}`;
const nameResolver = context === COMP_VAR ? self : null; const nameResolver = context === COMP_VAR ? self : null;
const {stmts, currValExpr} = const {stmts, currValExpr} =
convertPropertyBinding(nameResolver, context, value, bindingId); convertPropertyBinding(nameResolver, context, value, bindingId, BindingForm.General);
updateStmts.push(...stmts.map( updateStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan))); (stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
return o.applySourceSpanToExpressionIfNeeded(currValExpr, sourceSpan); return o.applySourceSpanToExpressionIfNeeded(currValExpr, sourceSpan);

View File

@ -126,6 +126,7 @@ export function main() {
outputs: [], outputs: [],
host: {}, host: {},
queries: {}, queries: {},
guards: {},
exportAs: undefined, exportAs: undefined,
providers: undefined providers: undefined
})); }));
@ -154,6 +155,7 @@ export function main() {
outputs: [], outputs: [],
host: {}, host: {},
queries: {}, queries: {},
guards: {},
exportAs: undefined, exportAs: undefined,
providers: undefined providers: undefined
})); }));
@ -164,6 +166,7 @@ export function main() {
outputs: [], outputs: [],
host: {}, host: {},
queries: {}, queries: {},
guards: {},
exportAs: undefined, exportAs: undefined,
providers: undefined providers: undefined
})); }));

View File

@ -38,28 +38,29 @@ function createTypeMeta({reference, diDeps}: {reference: any, diDeps?: any[]}):
return {reference: reference, diDeps: diDeps || [], lifecycleHooks: []}; return {reference: reference, diDeps: diDeps || [], lifecycleHooks: []};
} }
function compileDirectiveMetadataCreate({isHost, type, isComponent, selector, exportAs, function compileDirectiveMetadataCreate(
changeDetection, inputs, outputs, host, providers, {isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs, host,
viewProviders, queries, viewQueries, entryComponents, providers, viewProviders, queries, guards, viewQueries, entryComponents, template,
template, componentViewType, rendererType}: { componentViewType, rendererType}: {
isHost?: boolean, isHost?: boolean,
type?: CompileTypeMetadata, type?: CompileTypeMetadata,
isComponent?: boolean, isComponent?: boolean,
selector?: string | null, selector?: string | null,
exportAs?: string | null, exportAs?: string | null,
changeDetection?: ChangeDetectionStrategy | null, changeDetection?: ChangeDetectionStrategy | null,
inputs?: string[], inputs?: string[],
outputs?: string[], outputs?: string[],
host?: {[key: string]: string}, host?: {[key: string]: string},
providers?: CompileProviderMetadata[] | null, providers?: CompileProviderMetadata[] | null,
viewProviders?: CompileProviderMetadata[] | null, viewProviders?: CompileProviderMetadata[] | null,
queries?: CompileQueryMetadata[] | null, queries?: CompileQueryMetadata[] | null,
viewQueries?: CompileQueryMetadata[], guards?: {[key: string]: any},
entryComponents?: CompileEntryComponentMetadata[], viewQueries?: CompileQueryMetadata[],
template?: CompileTemplateMetadata, entryComponents?: CompileEntryComponentMetadata[],
componentViewType?: StaticSymbol | ProxyClass | null, template?: CompileTemplateMetadata,
rendererType?: StaticSymbol | RendererType2 | null, componentViewType?: StaticSymbol | ProxyClass | null,
}) { rendererType?: StaticSymbol | RendererType2 | null,
}) {
return CompileDirectiveMetadata.create({ return CompileDirectiveMetadata.create({
isHost: !!isHost, isHost: !!isHost,
type: noUndefined(type) !, type: noUndefined(type) !,
@ -73,6 +74,7 @@ function compileDirectiveMetadataCreate({isHost, type, isComponent, selector, ex
providers: providers || [], providers: providers || [],
viewProviders: viewProviders || [], viewProviders: viewProviders || [],
queries: queries || [], queries: queries || [],
guards: guards || {},
viewQueries: viewQueries || [], viewQueries: viewQueries || [],
entryComponents: entryComponents || [], entryComponents: entryComponents || [],
template: noUndefined(template) !, template: noUndefined(template) !,
@ -390,6 +392,7 @@ export function main() {
providers: [], providers: [],
viewProviders: [], viewProviders: [],
queries: [], queries: [],
guards: {},
viewQueries: [], viewQueries: [],
entryComponents: [], entryComponents: [],
componentViewType: null, componentViewType: null,

View File

@ -13,6 +13,7 @@ export interface PlatformReflectionCapabilities {
isReflectionEnabled(): boolean; isReflectionEnabled(): boolean;
factory(type: Type<any>): Function; factory(type: Type<any>): Function;
hasLifecycleHook(type: any, lcProperty: string): boolean; hasLifecycleHook(type: any, lcProperty: string): boolean;
guards(type: any): {[key: string]: any};
/** /**
* Return a list of annotations/types for constructor parameters * Return a list of annotations/types for constructor parameters

View File

@ -207,6 +207,8 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities {
return type instanceof Type && lcProperty in type.prototype; return type instanceof Type && lcProperty in type.prototype;
} }
guards(type: any): {[key: string]: any} { return {}; }
getter(name: string): GetterFn { return <GetterFn>new Function('o', 'return o.' + name + ';'); } getter(name: string): GetterFn { return <GetterFn>new Function('o', 'return o.' + name + ';'); }
setter(name: string): SetterFn { setter(name: string): SetterFn {

View File

@ -42,6 +42,7 @@ export class JitReflector implements CompileReflector {
hasLifecycleHook(type: any, lcProperty: string): boolean { hasLifecycleHook(type: any, lcProperty: string): boolean {
return this.reflectionCapabilities.hasLifecycleHook(type, lcProperty); return this.reflectionCapabilities.hasLifecycleHook(type, lcProperty);
} }
guards(type: any): {[key: string]: any} { return this.reflectionCapabilities.guards(type); }
resolveExternalReference(ref: ExternalReference): any { resolveExternalReference(ref: ExternalReference): any {
return builtinExternalReferences.get(ref) || ref.runtime; return builtinExternalReferences.get(ref) || ref.runtime;
} }

View File

@ -276,6 +276,7 @@ export declare class NgIf {
ngIfElse: TemplateRef<NgIfContext>; ngIfElse: TemplateRef<NgIfContext>;
ngIfThen: TemplateRef<NgIfContext>; ngIfThen: TemplateRef<NgIfContext>;
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>); constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>);
static ngIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
} }
/** @stable */ /** @stable */