feat(core): properly support inheritance

## Inheritance Semantics:

Decorators:
1) list the decorators of the class and its parents in the ancestor first order
2) only use the last decorator of each kind (e.g. @Component / ...)

Constructor parameters:
If a class inherits from a parent class and does not declare
a constructor, it inherits the parent class constructor,
and with it the parameter metadata of that parent class.

Lifecycle hooks:
Follow the normal class inheritance model,
i.e. lifecycle hooks of parent classes will be called
even if the method is not overwritten in the child class.

## Example

E.g. the following is a valid use of inheritance and it will
also inherit all metadata:

```
@Directive({selector: 'someDir'})
class ParentDirective {
  constructor(someDep: SomeDep) {}

  ngOnInit() {}
}

class ChildDirective extends ParentDirective {}
```

Closes #11606
Closes #12892
This commit is contained in:
Tobias Bosch
2016-11-18 15:17:44 -08:00
committed by vsavkin
parent 4a09251921
commit f5c8e0989d
19 changed files with 1102 additions and 260 deletions

View File

@ -70,16 +70,24 @@ 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;
constructor(
private host: StaticReflectorHost,
private staticSymbolCache: StaticSymbolCache = new StaticSymbolCache()) {
private staticSymbolCache: StaticSymbolCache = new StaticSymbolCache(),
knownMetadataClasses: {name: string, filePath: string, ctor: any}[] = [],
knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = []) {
this.initializeConversionMap();
knownMetadataClasses.forEach(
(kc) => this._registerDecoratorOrConstructor(
this.getStaticSymbol(kc.filePath, kc.name), kc.ctor));
knownMetadataFunctions.forEach(
(kf) => this._registerFunction(this.getStaticSymbol(kf.filePath, kf.name), kf.fn));
}
importUri(typeOrFunc: StaticSymbol): string {
@ -99,29 +107,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);
@ -155,6 +179,8 @@ export class StaticReflector implements ReflectorReader {
}
parameters.push(nestedResult);
});
} else if (classMetadata['extends']) {
parameters = this.parameters(this.simplify(type, classMetadata['extends']));
}
if (!parameters) {
parameters = [];
@ -168,23 +194,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.error(`Failed on type ${JSON.stringify(type)} with error ${e}`);
throw e;
}
}
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));
}
private registerFunction(type: StaticSymbol, fn: any): void {
private _registerFunction(type: StaticSymbol, fn: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args));
}
@ -193,50 +243,51 @@ export class StaticReflector implements ReflectorReader {
ANGULAR_IMPORT_LOCATIONS;
this.opaqueToken = this.findDeclaration(diOpaqueToken, 'OpaqueToken');
this.registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Host'), Host);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Host'), Host);
this._registerDecoratorOrConstructor(
this.findDeclaration(diDecorators, 'Injectable'), Injectable);
this.registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Self'), Self);
this.registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'SkipSelf'), SkipSelf);
this.registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Inject'), Inject);
this.registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Optional'), Optional);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Self'), Self);
this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'SkipSelf'), SkipSelf);
this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Inject'), Inject);
this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Optional'), Optional);
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'Attribute'), Attribute);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'ContentChild'), ContentChild);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'ContentChildren'), ContentChildren);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'ViewChild'), ViewChild);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'ViewChildren'), ViewChildren);
this.registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Input'), Input);
this.registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Output'), Output);
this.registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Pipe'), Pipe);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Input'), Input);
this._registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Output'), Output);
this._registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Pipe'), Pipe);
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'HostBinding'), HostBinding);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'HostListener'), HostListener);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'Directive'), Directive);
this.registerDecoratorOrConstructor(
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'Component'), Component);
this.registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'NgModule'), NgModule);
this._registerDecoratorOrConstructor(
this.findDeclaration(coreDecorators, 'NgModule'), NgModule);
// Note: Some metadata classes can be used directly with Provider.deps.
this.registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Host'), Host);
this.registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Self'), Self);
this.registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'SkipSelf'), SkipSelf);
this.registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Optional'), Optional);
this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Host'), Host);
this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Self'), Self);
this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'SkipSelf'), SkipSelf);
this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Optional'), Optional);
this.registerFunction(this.findDeclaration(animationMetadata, 'trigger'), trigger);
this.registerFunction(this.findDeclaration(animationMetadata, 'state'), state);
this.registerFunction(this.findDeclaration(animationMetadata, 'transition'), transition);
this.registerFunction(this.findDeclaration(animationMetadata, 'style'), style);
this.registerFunction(this.findDeclaration(animationMetadata, 'animate'), animate);
this.registerFunction(this.findDeclaration(animationMetadata, 'keyframes'), keyframes);
this.registerFunction(this.findDeclaration(animationMetadata, 'sequence'), sequence);
this.registerFunction(this.findDeclaration(animationMetadata, 'group'), group);
this._registerFunction(this.findDeclaration(animationMetadata, 'trigger'), trigger);
this._registerFunction(this.findDeclaration(animationMetadata, 'state'), state);
this._registerFunction(this.findDeclaration(animationMetadata, 'transition'), transition);
this._registerFunction(this.findDeclaration(animationMetadata, 'style'), style);
this._registerFunction(this.findDeclaration(animationMetadata, 'animate'), animate);
this._registerFunction(this.findDeclaration(animationMetadata, 'keyframes'), keyframes);
this._registerFunction(this.findDeclaration(animationMetadata, 'sequence'), sequence);
this._registerFunction(this.findDeclaration(animationMetadata, 'group'), group);
}
/**
@ -333,7 +384,7 @@ export class StaticReflector implements ReflectorReader {
/** @internal */
public simplify(context: StaticSymbol, value: any): any {
const _this = this;
const self = this;
let scope = BindingScope.empty;
const calling = new Map<StaticSymbol, boolean>();
@ -342,15 +393,15 @@ export class StaticReflector implements ReflectorReader {
let staticSymbol: StaticSymbol;
if (expression['module']) {
staticSymbol =
_this.findDeclaration(expression['module'], expression['name'], context.filePath);
self.findDeclaration(expression['module'], expression['name'], context.filePath);
} else {
staticSymbol = _this.getStaticSymbol(context.filePath, expression['name']);
staticSymbol = self.getStaticSymbol(context.filePath, expression['name']);
}
return staticSymbol;
}
function resolveReferenceValue(staticSymbol: StaticSymbol): any {
const moduleMetadata = _this.getModuleMetadata(staticSymbol.filePath);
const moduleMetadata = self.getModuleMetadata(staticSymbol.filePath);
const declarationValue =
moduleMetadata ? moduleMetadata['metadata'][staticSymbol.name] : null;
return declarationValue;
@ -360,7 +411,7 @@ export class StaticReflector implements ReflectorReader {
if (value && value.__symbolic === 'new' && value.expression) {
const target = value.expression;
if (target.__symbolic == 'reference') {
return sameSymbol(resolveReference(context, target), _this.opaqueToken);
return sameSymbol(resolveReference(context, target), self.opaqueToken);
}
}
return false;
@ -553,7 +604,7 @@ export class StaticReflector implements ReflectorReader {
const members = selectTarget.members ?
(selectTarget.members as string[]).concat(member) :
[member];
return _this.getStaticSymbol(selectTarget.filePath, selectTarget.name, members);
return self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members);
}
}
const member = simplify(expression['member']);
@ -589,11 +640,11 @@ export class StaticReflector implements ReflectorReader {
let target = expression['expression'];
if (target['module']) {
staticSymbol =
_this.findDeclaration(target['module'], target['name'], context.filePath);
self.findDeclaration(target['module'], target['name'], context.filePath);
} else {
staticSymbol = _this.getStaticSymbol(context.filePath, target['name']);
staticSymbol = self.getStaticSymbol(context.filePath, target['name']);
}
let converter = _this.conversionMap.get(staticSymbol);
let converter = self.conversionMap.get(staticSymbol);
if (converter) {
let args: any[] = expression['arguments'];
if (!args) {

View File

@ -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}.
*
@ -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);
@ -58,85 +59,76 @@ 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;
}
} 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;
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>'.`);
}
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);
}
private _extractPublicName(def: string) { return splitAtColon(def, [null, def])[1].trim(); }
private _dedupeBindings(bindings: string[]): string[] {
const names = new Set<string>();
const reversedResult: string[] = [];
// go last to first to allow later entries to overwrite previous entries
for (let i = bindings.length - 1; i >= 0; i--) {
const binding = bindings[i];
const name = this._extractPublicName(binding);
if (!names.has(name)) {
names.add(name);
reversedResult.push(binding);
}
}
return reversedResult.reverse();
}
private _merge(
directive: Directive, inputs: string[], outputs: string[], host: {[key: string]: string},
queries: {[key: string]: any}, directiveType: Type<any>): Directive {
const mergedInputs: string[] = inputs;
if (directive.inputs) {
const inputNames: string[] =
directive.inputs.map((def: string): string => this._extractPublicName(def));
inputs.forEach((inputDef: string) => {
const publicName = this._extractPublicName(inputDef);
if (inputNames.indexOf(publicName) > -1) {
throw new Error(
`Input '${publicName}' defined multiple times in '${stringify(directiveType)}'`);
}
});
mergedInputs.unshift(...directive.inputs);
}
const mergedOutputs: string[] = outputs;
if (directive.outputs) {
const outputNames: string[] =
directive.outputs.map((def: string): string => this._extractPublicName(def));
outputs.forEach((outputDef: string) => {
const publicName = this._extractPublicName(outputDef);
if (outputNames.indexOf(publicName) > -1) {
throw new Error(
`Output event '${publicName}' defined multiple times in '${stringify(directiveType)}'`);
}
});
mergedOutputs.unshift(...directive.outputs);
}
const mergedInputs =
this._dedupeBindings(directive.inputs ? directive.inputs.concat(inputs) : inputs);
const mergedOutputs =
this._dedupeBindings(directive.outputs ? directive.outputs.concat(outputs) : outputs);
const mergedHost = directive.host ? StringMapWrapper.merge(directive.host, host) : host;
const mergedQueries =
directive.queries ? StringMapWrapper.merge(directive.queries, queries) : queries;

View File

@ -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';
@ -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;

View File

@ -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';
@ -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;
}

View File

@ -21,10 +21,14 @@ describe('StaticReflector', () => {
let host: StaticReflectorHost;
let reflector: StaticReflector;
beforeEach(() => {
host = new MockStaticReflectorHost();
reflector = new StaticReflector(host);
});
function init(
testData: {[key: string]: any} = DEFAULT_TEST_DATA,
decorators: {name: string, filePath: string, ctor: any}[] = []) {
host = new MockStaticReflectorHost(testData);
reflector = new StaticReflector(host, undefined, decorators);
}
beforeEach(() => init());
function simplify(context: StaticSymbol, value: any) {
return reflector.simplify(context, value);
@ -517,11 +521,173 @@ describe('StaticReflector', () => {
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
});
describe('inheritance', () => {
class ClassDecorator {
constructor(public value: any) {}
}
class ParamDecorator {
constructor(public value: any) {}
}
class PropDecorator {
constructor(public value: any) {}
}
function initWithDecorator(testData: {[key: string]: any}) {
testData['/tmp/src/decorator.ts'] = `
export function ClassDecorator(): any {}
export function ParamDecorator(): any {}
export function PropDecorator(): any {}
`;
init(testData, [
{filePath: '/tmp/src/decorator.ts', name: 'ClassDecorator', ctor: ClassDecorator},
{filePath: '/tmp/src/decorator.ts', name: 'ParamDecorator', ctor: ParamDecorator},
{filePath: '/tmp/src/decorator.ts', name: 'PropDecorator', ctor: PropDecorator}
]);
}
it('should inherit annotations', () => {
initWithDecorator({
'/tmp/src/main.ts': `
import {ClassDecorator} from './decorator';
@ClassDecorator('parent')
export class Parent {}
@ClassDecorator('child')
export class Child extends Parent {}
export class ChildNoDecorators extends Parent {}
`
});
// Check that metadata for Parent was not changed!
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent')))
.toEqual([new ClassDecorator('parent')]);
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
.toEqual([new ClassDecorator('parent'), new ClassDecorator('child')]);
expect(
reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildNoDecorators')))
.toEqual([new ClassDecorator('parent')]);
});
it('should inherit parameters', () => {
initWithDecorator({
'/tmp/src/main.ts': `
import {ParamDecorator} from './decorator';
export class A {}
export class B {}
export class C {}
export class Parent {
constructor(@ParamDecorator('a') a: A, @ParamDecorator('b') b: B) {}
}
export class Child extends Parent {}
export class ChildWithCtor extends Parent {
constructor(@ParamDecorator('c') c: C) {}
}
`
});
// Check that metadata for Parent was not changed!
expect(reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent')))
.toEqual([
[reflector.getStaticSymbol('/tmp/src/main.ts', 'A'), new ParamDecorator('a')],
[reflector.getStaticSymbol('/tmp/src/main.ts', 'B'), new ParamDecorator('b')]
]);
expect(reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child'))).toEqual([
[reflector.getStaticSymbol('/tmp/src/main.ts', 'A'), new ParamDecorator('a')],
[reflector.getStaticSymbol('/tmp/src/main.ts', 'B'), new ParamDecorator('b')]
]);
expect(reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildWithCtor')))
.toEqual([[reflector.getStaticSymbol('/tmp/src/main.ts', 'C'), new ParamDecorator('c')]]);
});
it('should inherit property metadata', () => {
initWithDecorator({
'/tmp/src/main.ts': `
import {PropDecorator} from './decorator';
export class A {}
export class B {}
export class C {}
export class Parent {
@PropDecorator('a')
a: A;
@PropDecorator('b1')
b: B;
}
export class Child extends Parent {
@PropDecorator('b2')
b: B;
@PropDecorator('c')
c: C;
}
`
});
// Check that metadata for Parent was not changed!
expect(reflector.propMetadata(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent')))
.toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1')],
});
expect(reflector.propMetadata(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
.toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1'), new PropDecorator('b2')],
'c': [new PropDecorator('c')]
});
});
it('should inherit lifecycle hooks', () => {
initWithDecorator({
'/tmp/src/main.ts': `
export class Parent {
hook1() {}
hook2() {}
}
export class Child extends Parent {
hook2() {}
hook3() {}
}
`
});
function hooks(symbol: StaticSymbol, names: string[]): boolean[] {
return names.map(name => reflector.hasLifecycleHook(symbol, name));
}
// Check that metadata for Parent was not changed!
expect(hooks(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent'), [
'hook1', 'hook2', 'hook3'
])).toEqual([true, true, false]);
expect(hooks(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child'), [
'hook1', 'hook2', 'hook3'
])).toEqual([true, true, true]);
});
});
});
class MockStaticReflectorHost implements StaticReflectorHost {
private collector = new MetadataCollector();
constructor(private data: {[key: string]: any}) {}
// In tests, assume that symbols are not re-exported
moduleNameToFileName(modulePath: string, containingFile?: string): string {
function splitPath(path: string): string[] { return path.split(/\/|\\/g); }
@ -568,7 +734,28 @@ class MockStaticReflectorHost implements StaticReflectorHost {
getMetadataFor(moduleId: string): any { return this._getMetadataFor(moduleId); }
private _getMetadataFor(moduleId: string): any {
const data: {[key: string]: any} = {
if (this.data[moduleId] && moduleId.match(TS_EXT)) {
const text = this.data[moduleId];
if (typeof text === 'string') {
const sf = ts.createSourceFile(
moduleId, this.data[moduleId], ts.ScriptTarget.ES5, /* setParentNodes */ true);
const diagnostics: ts.Diagnostic[] = (<any>sf).parseDiagnostics;
if (diagnostics && diagnostics.length) {
throw Error(`Error encountered during parse of file ${moduleId}`);
}
return [this.collector.getMetadata(sf)];
}
}
const result = this.data[moduleId];
if (result) {
return Array.isArray(result) ? result : [result];
} else {
return null;
}
}
}
const DEFAULT_TEST_DATA: {[key: string]: any} = {
'/tmp/@angular/common/src/forms-deprecated/directives.d.ts': [{
'__symbolic': 'module',
'version': 2,
@ -1162,25 +1349,3 @@ class MockStaticReflectorHost implements StaticReflectorHost {
exports: [{from: './originNone'}, {from: './origin30'}]
}
};
if (data[moduleId] && moduleId.match(TS_EXT)) {
const text = data[moduleId];
if (typeof text === 'string') {
const sf = ts.createSourceFile(
moduleId, data[moduleId], ts.ScriptTarget.ES5, /* setParentNodes */ true);
const diagnostics: ts.Diagnostic[] = (<any>sf).parseDiagnostics;
if (diagnostics && diagnostics.length) {
throw Error(`Error encountered during parse of file ${moduleId}`);
}
return [this.collector.getMetadata(sf)];
}
}
const result = data[moduleId];
if (result) {
return Array.isArray(result) ? result : [result];
} else {
return null;
}
}
}

View File

@ -8,15 +8,12 @@
import {DirectiveResolver} from '@angular/compiler/src/directive_resolver';
import {Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Input, Output, ViewChild, ViewChildren} from '@angular/core/src/metadata';
import {reflector} from '@angular/core/src/reflection/reflection';
@Directive({selector: 'someDirective'})
class SomeDirective {
}
@Directive({selector: 'someChildDirective'})
class SomeChildDirective extends SomeDirective {
}
@Directive({selector: 'someDirective', inputs: ['c']})
class SomeDirectiveWithInputs {
@Input() a: any;
@ -31,28 +28,6 @@ class SomeDirectiveWithOutputs {
c: any;
}
@Directive({selector: 'someDirective', outputs: ['a']})
class SomeDirectiveWithDuplicateOutputs {
@Output() a: any;
}
@Directive({selector: 'someDirective', outputs: ['localA: a']})
class SomeDirectiveWithDuplicateRenamedOutputs {
@Output() a: any;
localA: any;
}
@Directive({selector: 'someDirective', inputs: ['a']})
class SomeDirectiveWithDuplicateInputs {
@Input() a: any;
}
@Directive({selector: 'someDirective', inputs: ['localA: a']})
class SomeDirectiveWithDuplicateRenamedInputs {
@Input() a: any;
localA: any;
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithSetterProps {
@Input('renamed')
@ -150,11 +125,22 @@ export function main() {
}).toThrowError('No Directive annotation found on SomeDirectiveWithoutMetadata');
});
it('should not read parent class Directive metadata', function() {
const directiveMetadata = resolver.resolve(SomeChildDirective);
expect(directiveMetadata)
.toEqual(new Directive(
{selector: 'someChildDirective', inputs: [], outputs: [], host: {}, queries: {}}));
it('should support inheriting the Directive metadata', function() {
@Directive({selector: 'p'})
class Parent {
}
class ChildNoDecorator extends Parent {}
@Directive({selector: 'c'})
class ChildWithDecorator extends Parent {
}
expect(resolver.resolve(ChildNoDecorator))
.toEqual(new Directive({selector: 'p', inputs: [], outputs: [], host: {}, queries: {}}));
expect(resolver.resolve(ChildWithDecorator))
.toEqual(new Directive({selector: 'c', inputs: [], outputs: [], host: {}, queries: {}}));
});
describe('inputs', () => {
@ -168,16 +154,52 @@ export function main() {
expect(directiveMetadata.inputs).toEqual(['a: renamed']);
});
it('should throw if duplicate inputs', () => {
expect(() => {
resolver.resolve(SomeDirectiveWithDuplicateInputs);
}).toThrowError(`Input 'a' defined multiple times in 'SomeDirectiveWithDuplicateInputs'`);
it('should remove duplicate inputs', () => {
@Directive({selector: 'someDirective', inputs: ['a', 'a']})
class SomeDirectiveWithDuplicateInputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
expect(directiveMetadata.inputs).toEqual(['a']);
});
it('should throw if duplicate inputs (with rename)', () => {
expect(() => { resolver.resolve(SomeDirectiveWithDuplicateRenamedInputs); })
.toThrowError(
`Input 'a' defined multiple times in 'SomeDirectiveWithDuplicateRenamedInputs'`);
it('should use the last input if duplicate inputs (with rename)', () => {
@Directive({selector: 'someDirective', inputs: ['a', 'localA: a']})
class SomeDirectiveWithDuplicateInputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
expect(directiveMetadata.inputs).toEqual(['localA: a']);
});
it('should prefer @Input over @Directive.inputs', () => {
@Directive({selector: 'someDirective', inputs: ['a']})
class SomeDirectiveWithDuplicateInputs {
@Input('a')
propA: any;
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs);
expect(directiveMetadata.inputs).toEqual(['propA: a']);
});
it('should support inheriting inputs', () => {
@Directive({selector: 'p'})
class Parent {
@Input()
p1: any;
@Input('p21')
p2: any;
}
class Child extends Parent {
@Input('p22')
p2: any;
@Input()
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.inputs).toEqual(['p1', 'p2: p22', 'p3']);
});
});
@ -192,16 +214,52 @@ export function main() {
expect(directiveMetadata.outputs).toEqual(['a: renamed']);
});
it('should throw if duplicate outputs', () => {
expect(() => { resolver.resolve(SomeDirectiveWithDuplicateOutputs); })
.toThrowError(
`Output event 'a' defined multiple times in 'SomeDirectiveWithDuplicateOutputs'`);
it('should remove duplicate outputs', () => {
@Directive({selector: 'someDirective', outputs: ['a', 'a']})
class SomeDirectiveWithDuplicateOutputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
expect(directiveMetadata.outputs).toEqual(['a']);
});
it('should throw if duplicate outputs (with rename)', () => {
expect(() => { resolver.resolve(SomeDirectiveWithDuplicateRenamedOutputs); })
.toThrowError(
`Output event 'a' defined multiple times in 'SomeDirectiveWithDuplicateRenamedOutputs'`);
it('should use the last output if duplicate outputs (with rename)', () => {
@Directive({selector: 'someDirective', outputs: ['a', 'localA: a']})
class SomeDirectiveWithDuplicateOutputs {
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
expect(directiveMetadata.outputs).toEqual(['localA: a']);
});
it('should prefer @Output over @Directive.outputs', () => {
@Directive({selector: 'someDirective', outputs: ['a']})
class SomeDirectiveWithDuplicateOutputs {
@Output('a')
propA: any;
}
const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs);
expect(directiveMetadata.outputs).toEqual(['propA: a']);
});
it('should support inheriting outputs', () => {
@Directive({selector: 'p'})
class Parent {
@Output()
p1: any;
@Output('p21')
p2: any;
}
class Child extends Parent {
@Output('p22')
p2: any;
@Output()
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.outputs).toEqual(['p1', 'p2: p22', 'p3']);
});
});
@ -233,6 +291,46 @@ export function main() {
.toThrowError(
`@HostBinding parameter should be a property name, 'class.<name>', or 'attr.<name>'.`);
});
it('should support inheriting host bindings', () => {
@Directive({selector: 'p'})
class Parent {
@HostBinding()
p1: any;
@HostBinding('p21')
p2: any;
}
class Child extends Parent {
@HostBinding('p22')
p2: any;
@HostBinding()
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host).toEqual({'[p1]': 'p1', '[p22]': 'p2', '[p3]': 'p3'});
});
it('should support inheriting host listeners', () => {
@Directive({selector: 'p'})
class Parent {
@HostListener('p1')
p1() {}
@HostListener('p21')
p2() {}
}
class Child extends Parent {
@HostListener('p22')
p2() {}
@HostListener('p3')
p3() {}
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.host).toEqual({'(p1)': 'p1()', '(p22)': 'p2()', '(p3)': 'p3()'});
});
});
describe('queries', () => {
@ -259,6 +357,30 @@ export function main() {
expect(directiveMetadata.queries)
.toEqual({'c': new ViewChild('c'), 'a': new ViewChild('a')});
});
it('should support inheriting queries', () => {
@Directive({selector: 'p'})
class Parent {
@ContentChild('p1')
p1: any;
@ContentChild('p21')
p2: any;
}
class Child extends Parent {
@ContentChild('p22')
p2: any;
@ContentChild('p3')
p3: any;
}
const directiveMetadata = resolver.resolve(Child);
expect(directiveMetadata.queries).toEqual({
'p1': new ContentChild('p1'),
'p2': new ContentChild('p22'),
'p3': new ContentChild('p3')
});
});
});
describe('Component', () => {

View File

@ -45,9 +45,26 @@ export function main() {
}));
});
it('should throw when simple class has no component decorator', () => {
it('should throw when simple class has no NgModule decorator', () => {
expect(() => resolver.resolve(SimpleClass))
.toThrowError(`No NgModule metadata found for '${stringify(SimpleClass)}'.`);
});
it('should support inheriting the metadata', function() {
@NgModule({id: 'p'})
class Parent {
}
class ChildNoDecorator extends Parent {}
@NgModule({id: 'c'})
class ChildWithDecorator extends Parent {
}
expect(resolver.resolve(ChildNoDecorator)).toEqual(new NgModule({id: 'p'}));
expect(resolver.resolve(ChildWithDecorator)).toEqual(new NgModule({id: 'c'}));
});
});
}

View File

@ -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'}));
});
});
}