Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/common/src/directives/ng_if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ export class NgIf {
}
}
}

public static ngIfTypeGuard: <T>(v: T|null|undefined|false) => v is T;
}

/**
Expand Down
135 changes: 135 additions & 0 deletions packages/compiler-cli/test/diagnostics/check_types_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ', () => {
const a = (files: MockFiles, options: object = {}) => {
accept(files, {fullTemplateTypeCheck: true, ...options});
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler-cli/test/metadata/collector_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,25 @@ describe('Collector', () => {
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', () => {
const source = createSource(`
import {Component} from '@angular/core';
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/aot/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export class AotCompiler {
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, moduleMeta, directives);
ctx.statements.push(...this._typeCheckCompiler.compileComponent(
componentId, compMeta, parsedTemplate, usedPipes, externalReferenceVars));
componentId, compMeta, parsedTemplate, usedPipes, externalReferenceVars, ctx));
}

emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle {
Expand Down
29 changes: 29 additions & 0 deletions packages/compiler/src/aot/static_reflector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const IGNORE = {
const USE_VALUE = 'useValue';
const PROVIDE = 'provide';
const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']);
const TYPEGUARD_POSTFIX = 'TypeGuard';

function shouldIgnore(value: any): boolean {
return value && value.__symbolic == 'ignore';
Expand All @@ -43,6 +44,7 @@ export class StaticReflector implements CompileReflector {
private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>();
private parameterCache = new Map<StaticSymbol, any[]>();
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 injectionToken: StaticSymbol;
private opaqueToken: StaticSymbol;
Expand Down Expand Up @@ -251,6 +253,18 @@ export class StaticReflector implements CompileReflector {
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 {
const parentType = this.trySimplify(type, classMetadata['extends']);
if (parentType instanceof StaticSymbol) {
Expand All @@ -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 {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args));
}
Expand Down
36 changes: 30 additions & 6 deletions packages/compiler/src/compile_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export interface CompileDirectiveSummary extends CompileTypeSummary {
providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[];
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[];
changeDetection: ChangeDetectionStrategy|null;
Expand All @@ -268,8 +269,8 @@ export interface CompileDirectiveSummary extends CompileTypeSummary {
*/
export class CompileDirectiveMetadata {
static create({isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs,
host, providers, viewProviders, queries, viewQueries, entryComponents, template,
componentViewType, rendererType, componentFactory}: {
host, providers, viewProviders, queries, guards, viewQueries, entryComponents,
template, componentViewType, rendererType, componentFactory}: {
isHost: boolean,
type: CompileTypeMetadata,
isComponent: boolean,
Expand All @@ -282,6 +283,7 @@ export class CompileDirectiveMetadata {
providers: CompileProviderMetadata[],
viewProviders: CompileProviderMetadata[],
queries: CompileQueryMetadata[],
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[],
entryComponents: CompileEntryComponentMetadata[],
template: CompileTemplateMetadata,
Expand Down Expand Up @@ -336,6 +338,7 @@ export class CompileDirectiveMetadata {
providers,
viewProviders,
queries,
guards,
viewQueries,
entryComponents,
template,
Expand All @@ -358,6 +361,7 @@ export class CompileDirectiveMetadata {
providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[];
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[];

Expand All @@ -367,10 +371,27 @@ export class CompileDirectiveMetadata {
rendererType: StaticSymbol|object|null;
componentFactory: StaticSymbol|object|null;

constructor({isHost, type, isComponent, selector, exportAs,
changeDetection, inputs, outputs, hostListeners, hostProperties,
hostAttributes, providers, viewProviders, queries, viewQueries,
entryComponents, template, componentViewType, rendererType, componentFactory}: {
constructor({isHost,
type,
isComponent,
selector,
exportAs,
changeDetection,
inputs,
outputs,
hostListeners,
hostProperties,
hostAttributes,
providers,
viewProviders,
queries,
guards,
viewQueries,
entryComponents,
template,
componentViewType,
rendererType,
componentFactory}: {
isHost: boolean,
type: CompileTypeMetadata,
isComponent: boolean,
Expand All @@ -385,6 +406,7 @@ export class CompileDirectiveMetadata {
providers: CompileProviderMetadata[],
viewProviders: CompileProviderMetadata[],
queries: CompileQueryMetadata[],
guards: {[key: string]: any},
viewQueries: CompileQueryMetadata[],
entryComponents: CompileEntryComponentMetadata[],
template: CompileTemplateMetadata|null,
Expand All @@ -406,6 +428,7 @@ export class CompileDirectiveMetadata {
this.providers = _normalizeArray(providers);
this.viewProviders = _normalizeArray(viewProviders);
this.queries = _normalizeArray(queries);
this.guards = guards;
this.viewQueries = _normalizeArray(viewQueries);
this.entryComponents = _normalizeArray(entryComponents);
this.template = template;
Expand All @@ -430,6 +453,7 @@ export class CompileDirectiveMetadata {
providers: this.providers,
viewProviders: this.viewProviders,
queries: this.queries,
guards: this.guards,
viewQueries: this.viewQueries,
entryComponents: this.entryComponents,
changeDetection: this.changeDetection,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/compile_reflector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export abstract class CompileReflector {
abstract annotations(typeOrFunc: /*Type*/ any): any[];
abstract propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]};
abstract hasLifecycleHook(type: any, lcProperty: string): boolean;
abstract guards(typeOrFunc: /* Type */ any): {[key: string]: any};
abstract componentModuleUrl(type: /*Type*/ any, cmpMetadata: Component): string;
abstract resolveExternalReference(ref: o.ExternalReference): any;
}
13 changes: 12 additions & 1 deletion packages/compiler/src/compiler_util/expression_converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,23 @@ export class ConvertPropertyBindingResult {
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
* is used in property binding. The expression has to be preprocessed via
* `convertPropertyBindingBuiltins`.
*/
export function convertPropertyBinding(
localResolver: LocalResolver | null, implicitReceiver: o.Expression,
expressionWithoutBuiltins: cdAst.AST, bindingId: string): ConvertPropertyBindingResult {
expressionWithoutBuiltins: cdAst.AST, bindingId: string,
form: BindingForm): ConvertPropertyBindingResult {
if (!localResolver) {
localResolver = new DefaultLocalResolver();
}
Expand All @@ -110,6 +119,8 @@ export function convertPropertyBinding(
for (let i = 0; i < visitor.temporaryCount; 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]));
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface Directive {
providers?: Provider[];
exportAs?: string;
queries?: {[key: string]: any};
guards?: {[key: string]: any};
}
export const createDirective =
makeMetadataFactory<Directive>('Directive', (dir: Directive = {}) => dir);
Expand Down
Loading