Skip to content

Commit

Permalink
feat(core): property support inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
tbosch committed Nov 22, 2016
1 parent 491d5a2 commit 222f9a7
Show file tree
Hide file tree
Showing 19 changed files with 965 additions and 142 deletions.
2 changes: 1 addition & 1 deletion modules/@angular/compiler-cli/src/compiler_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class CompilerHost implements AotCompilerHost {
if (!v2Metadata && v1Metadata) {
// patch up v1 to v2 by merging the metadata with metadata collected from the d.ts file
// as the only difference between the versions is whether all exports are contained in
// the metadata
// the metadata and the `extends` clause.
v2Metadata = {'__symbolic': 'module', 'version': 2, 'metadata': {}};
if (v1Metadata.exports) {
v2Metadata.exports = v1Metadata.exports;
Expand Down
13 changes: 11 additions & 2 deletions modules/@angular/compiler-cli/test/aot_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,11 @@ describe('CompilerHost', () => {
{__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}}, {
__symbolic: 'module',
version: 2,
metadata: {foo: {__symbolic: 'class'}, bar: {__symbolic: 'class'}}
metadata: {
foo: {__symbolic: 'class'},
Bar: {__symbolic: 'class', members: {ngOnInit: [{__symbolic: 'method'}]}},
BarChild: {__symbolic: 'class', extends: {__symbolic: 'reference', name: 'Bar'}}
}
}
]);
});
Expand Down Expand Up @@ -198,7 +202,12 @@ const FILES: Entry = {
}
},
'metadata_versions': {
'v1.d.ts': 'export declare class bar {}',
'v1.d.ts': `
export declare class Bar {
ngOnInit() {}
}
export declare class BarChild extends Bar {}
`,
'v1.metadata.json':
`{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`,
}
Expand Down
79 changes: 61 additions & 18 deletions modules/@angular/compiler/src/aot/static_reflector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ export class StaticSymbolCache {
export class StaticReflector implements ReflectorReader {
private declarationCache = new Map<string, StaticSymbol>();
private annotationCache = new Map<StaticSymbol, any[]>();
private propertyCache = new Map<StaticSymbol, {[key: string]: any}>();
private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>();
private parameterCache = new Map<StaticSymbol, any[]>();
private methodCache = new Map<StaticSymbol, {[key: string]: boolean}>();
private metadataCache = new Map<string, {[key: string]: any}>();
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
private opaqueToken: StaticSymbol;
Expand Down Expand Up @@ -99,29 +100,45 @@ export class StaticReflector implements ReflectorReader {
public annotations(type: StaticSymbol): any[] {
let annotations = this.annotationCache.get(type);
if (!annotations) {
annotations = [];
const classMetadata = this.getTypeMetadata(type);
if (classMetadata['extends']) {
const parentAnnotations = this.annotations(this.simplify(type, classMetadata['extends']));
annotations.push(...parentAnnotations);
}
if (classMetadata['decorators']) {
annotations = this.simplify(type, classMetadata['decorators']);
} else {
annotations = [];
const ownAnnotations: any[] = this.simplify(type, classMetadata['decorators']);
annotations.push(...ownAnnotations);
}
this.annotationCache.set(type, annotations.filter(ann => !!ann));
}
return annotations;
}

public propMetadata(type: StaticSymbol): {[key: string]: any} {
public propMetadata(type: StaticSymbol): {[key: string]: any[]} {
let propMetadata = this.propertyCache.get(type);
if (!propMetadata) {
const classMetadata = this.getTypeMetadata(type);
const members = classMetadata ? classMetadata['members'] : {};
propMetadata = mapStringMap(members, (propData, propName) => {
const classMetadata = this.getTypeMetadata(type) || {};
propMetadata = {};
if (classMetadata['extends']) {
const parentPropMetadata = this.propMetadata(this.simplify(type, classMetadata['extends']));
Object.keys(parentPropMetadata).forEach((parentProp) => {
propMetadata[parentProp] = parentPropMetadata[parentProp];
});
}

const members = classMetadata['members'] || {};
Object.keys(members).forEach((propName) => {
const propData = members[propName];
const prop = (<any[]>propData)
.find(a => a['__symbolic'] == 'property' || a['__symbolic'] == 'method');
const decorators: any[] = [];
if (propMetadata[propName]) {
decorators.push(...propMetadata[propName]);
}
propMetadata[propName] = decorators;
if (prop && prop['decorators']) {
return this.simplify(type, prop['decorators']);
} else {
return [];
decorators.push(...this.simplify(type, prop['decorators']));
}
});
this.propertyCache.set(type, propMetadata);
Expand Down Expand Up @@ -155,6 +172,8 @@ export class StaticReflector implements ReflectorReader {
}
parameters.push(nestedResult);
});
} else if (classMetadata['extends']) {
parameters = this.parameters(this.simplify(type, classMetadata['extends']));
}
if (!parameters) {
parameters = [];
Expand All @@ -168,23 +187,47 @@ export class StaticReflector implements ReflectorReader {
}
}

private _methodNames(type: any): {[key: string]: boolean} {
let methodNames = this.methodCache.get(type);
if (!methodNames) {
const classMetadata = this.getTypeMetadata(type) || {};
methodNames = {};
if (classMetadata['extends']) {
const parentMethodNames = this._methodNames(this.simplify(type, classMetadata['extends']));
Object.keys(parentMethodNames).forEach((parentProp) => {
methodNames[parentProp] = parentMethodNames[parentProp];
});
}

const members = classMetadata['members'] || {};
Object.keys(members).forEach((propName) => {
const propData = members[propName];
const isMethod = (<any[]>propData).some(a => a['__symbolic'] == 'method');
methodNames[propName] = methodNames[propName] || isMethod;
});
this.methodCache.set(type, methodNames);
}
return methodNames;
}

hasLifecycleHook(type: any, lcProperty: string): boolean {
if (!(type instanceof StaticSymbol)) {
throw new Error(
`hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`);
}
const classMetadata = this.getTypeMetadata(type);
const members = classMetadata ? classMetadata['members'] : null;
const member: any[] =
members && members.hasOwnProperty(lcProperty) ? members[lcProperty] : null;
return member ? member.some(a => a['__symbolic'] == 'method') : false;
try {
return !!this._methodNames(type)[lcProperty];
} catch (e) {
console.log(`Failed on type ${JSON.stringify(type)} with error ${e}`);
throw e;
}
}

private registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args));
}

private registerFunction(type: StaticSymbol, fn: any): void {
registerFunction(type: StaticSymbol, fn: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args));
}

Expand Down
80 changes: 43 additions & 37 deletions modules/@angular/compiler/src/directive_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

import {Component, Directive, HostBinding, HostListener, Injectable, Input, Output, Query, Type, resolveForwardRef} from '@angular/core';

import {StringMapWrapper} from './facade/collection';
import {ListWrapper, StringMapWrapper} from './facade/collection';
import {stringify} from './facade/lang';
import {ReflectorReader, reflector} from './private_import_core';
import {splitAtColon} from './util';


/*
* Resolve a `Type` for {@link Directive}.
*
Expand All @@ -35,7 +36,7 @@ export class DirectiveResolver {
resolve(type: Type<any>, throwIfNotFound = true): Directive {
const typeMetadata = this._reflector.annotations(resolveForwardRef(type));
if (typeMetadata) {
const metadata = typeMetadata.find(isDirectiveMetadata);
const metadata = ListWrapper.findLast(typeMetadata, isDirectiveMetadata);
if (metadata) {
const propertyMetadata = this._reflector.propMetadata(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, type);
Expand All @@ -58,43 +59,48 @@ export class DirectiveResolver {
const queries: {[key: string]: any} = {};

Object.keys(propertyMetadata).forEach((propName: string) => {

propertyMetadata[propName].forEach(a => {
if (a instanceof Input) {
if (a.bindingPropertyName) {
inputs.push(`${propName}: ${a.bindingPropertyName}`);
} else {
inputs.push(propName);
}
} else if (a instanceof Output) {
const output: Output = a;
if (output.bindingPropertyName) {
outputs.push(`${propName}: ${output.bindingPropertyName}`);
} else {
outputs.push(propName);
}
} else if (a instanceof HostBinding) {
const hostBinding: HostBinding = a;
if (hostBinding.hostPropertyName) {
const startWith = hostBinding.hostPropertyName[0];
if (startWith === '(') {
throw new Error(`@HostBinding can not bind to events. Use @HostListener instead.`);
} else if (startWith === '[') {
throw new Error(
`@HostBinding parameter should be a property name, 'class.<name>', or 'attr.<name>'.`);
}
host[`[${hostBinding.hostPropertyName}]`] = propName;
} else {
host[`[${propName}]`] = propName;
const input = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Input);
if (input) {
if (input.bindingPropertyName) {
inputs.push(`${propName}: ${input.bindingPropertyName}`);
} else {
inputs.push(propName);
}
}
const output = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Output);
if (output) {
if (output.bindingPropertyName) {
outputs.push(`${propName}: ${output.bindingPropertyName}`);
} else {
outputs.push(propName);
}
}
const hostBinding =
ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof HostBinding);
if (hostBinding) {
if (hostBinding.hostPropertyName) {
const startWith = hostBinding.hostPropertyName[0];
if (startWith === '(') {
throw new Error(`@HostBinding can not bind to events. Use @HostListener instead.`);
} else if (startWith === '[') {
throw new Error(
`@HostBinding parameter should be a property name, 'class.<name>', or 'attr.<name>'.`);
}
} else if (a instanceof HostListener) {
const hostListener: HostListener = a;
const args = hostListener.args || [];
host[`(${hostListener.eventName})`] = `${propName}(${args.join(',')})`;
} else if (a instanceof Query) {
queries[propName] = a;
host[`[${hostBinding.hostPropertyName}]`] = propName;
} else {
host[`[${propName}]`] = propName;
}
});
}
const hostListener =
ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof HostListener);
if (hostListener) {
const args = hostListener.args || [];
host[`(${hostListener.eventName})`] = `${propName}(${args.join(',')})`;
}
const query = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Query);
if (query) {
queries[propName] = query;
}
});
return this._merge(dm, inputs, outputs, host, queries, directiveType);
}
Expand Down
4 changes: 3 additions & 1 deletion modules/@angular/compiler/src/ng_module_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {Injectable, NgModule, Type} from '@angular/core';

import {ListWrapper} from './facade/collection';
import {isPresent, stringify} from './facade/lang';
import {ReflectorReader, reflector} from './private_import_core';

Expand All @@ -25,7 +26,8 @@ export class NgModuleResolver {
isNgModule(type: any) { return this._reflector.annotations(type).some(_isNgModuleMetadata); }

resolve(type: Type<any>, throwIfNotFound = true): NgModule {
const ngModuleMeta: NgModule = this._reflector.annotations(type).find(_isNgModuleMetadata);
const ngModuleMeta: NgModule =
ListWrapper.findLast(this._reflector.annotations(type), _isNgModuleMetadata);

if (isPresent(ngModuleMeta)) {
return ngModuleMeta;
Expand Down
3 changes: 2 additions & 1 deletion modules/@angular/compiler/src/pipe_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {Injectable, Pipe, Type, resolveForwardRef} from '@angular/core';

import {ListWrapper} from './facade/collection';
import {isPresent, stringify} from './facade/lang';
import {ReflectorReader, reflector} from './private_import_core';

Expand Down Expand Up @@ -37,7 +38,7 @@ export class PipeResolver {
resolve(type: Type<any>, throwIfNotFound = true): Pipe {
const metas = this._reflector.annotations(resolveForwardRef(type));
if (isPresent(metas)) {
const annotation = metas.find(_isPipeMetadata);
const annotation = ListWrapper.findLast(metas, _isPipeMetadata);
if (isPresent(annotation)) {
return annotation;
}
Expand Down
52 changes: 52 additions & 0 deletions modules/@angular/compiler/src/pipe_resolver_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
import {Pipe} from '@angular/core/src/metadata';
import {stringify} from '../src/facade/lang';

@Pipe({name: 'somePipe', pure: true})
class SomePipe {
}

class SimpleClass {}

export function main() {
describe('PipeResolver', () => {
let resolver: PipeResolver;

beforeEach(() => { resolver = new PipeResolver(); });

it('should read out the metadata from the class', () => {
const moduleMetadata = resolver.resolve(SomePipe);
expect(moduleMetadata).toEqual(new Pipe({name: 'somePipe', pure: true}));
});

it('should throw when simple class has no pipe decorator', () => {
expect(() => resolver.resolve(SimpleClass))
.toThrowError(`No Pipe decorator found on ${stringify(SimpleClass)}`);
});

it('should support inheriting the metadata', function() {
@Pipe({name: 'p'})
class Parent {
}

class ChildNoDecorator extends Parent {}

@Pipe({name: 'c'})
class ChildWithDecorator extends Parent {
}

expect(resolver.resolve(ChildNoDecorator)).toEqual(new Pipe({name: 'p'}));

expect(resolver.resolve(ChildWithDecorator)).toEqual(new Pipe({name: 'c'}));
});

});
}
Loading

0 comments on commit 222f9a7

Please sign in to comment.