diff --git a/modules/@angular/compiler-cli/src/compiler_host.ts b/modules/@angular/compiler-cli/src/compiler_host.ts index a332c657a9..50ebaab6cf 100644 --- a/modules/@angular/compiler-cli/src/compiler_host.ts +++ b/modules/@angular/compiler-cli/src/compiler_host.ts @@ -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; diff --git a/modules/@angular/compiler-cli/test/aot_host_spec.ts b/modules/@angular/compiler-cli/test/aot_host_spec.ts index b9d2620c26..0a1f6d3e60 100644 --- a/modules/@angular/compiler-cli/test/aot_host_spec.ts +++ b/modules/@angular/compiler-cli/test/aot_host_spec.ts @@ -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'}} + } } ]); }); @@ -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"}}}`, } diff --git a/modules/@angular/compiler/src/aot/static_reflector.ts b/modules/@angular/compiler/src/aot/static_reflector.ts index a4a7b45b7d..7bd24cf144 100644 --- a/modules/@angular/compiler/src/aot/static_reflector.ts +++ b/modules/@angular/compiler/src/aot/static_reflector.ts @@ -70,16 +70,24 @@ export class StaticSymbolCache { export class StaticReflector implements ReflectorReader { private declarationCache = new Map(); private annotationCache = new Map(); - private propertyCache = new Map(); + private propertyCache = new Map(); private parameterCache = new Map(); + private methodCache = new Map(); private metadataCache = new Map(); private conversionMap = new Map 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 = (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 = (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(); @@ -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) { diff --git a/modules/@angular/compiler/src/directive_resolver.ts b/modules/@angular/compiler/src/directive_resolver.ts index f3ba7b07b9..2db755dfe3 100644 --- a/modules/@angular/compiler/src/directive_resolver.ts +++ b/modules/@angular/compiler/src/directive_resolver.ts @@ -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, 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.', or 'attr.'.`); - } - 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.', or 'attr.'.`); + } + 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(); + 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): 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; diff --git a/modules/@angular/compiler/src/ng_module_resolver.ts b/modules/@angular/compiler/src/ng_module_resolver.ts index fce0a8a83e..e74128c6da 100644 --- a/modules/@angular/compiler/src/ng_module_resolver.ts +++ b/modules/@angular/compiler/src/ng_module_resolver.ts @@ -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, 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; diff --git a/modules/@angular/compiler/src/pipe_resolver.ts b/modules/@angular/compiler/src/pipe_resolver.ts index 47a2d860b1..61df4ca6ee 100644 --- a/modules/@angular/compiler/src/pipe_resolver.ts +++ b/modules/@angular/compiler/src/pipe_resolver.ts @@ -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, 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; } diff --git a/modules/@angular/compiler/test/aot/static_reflector_spec.ts b/modules/@angular/compiler/test/aot/static_reflector_spec.ts index 91dd008e18..50c73db506 100644 --- a/modules/@angular/compiler/test/aot/static_reflector_spec.ts +++ b/modules/@angular/compiler/test/aot/static_reflector_spec.ts @@ -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[] = (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[] = (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; - } - } -} diff --git a/modules/@angular/compiler/test/directive_resolver_spec.ts b/modules/@angular/compiler/test/directive_resolver_spec.ts index d061c16c17..8da2116e1e 100644 --- a/modules/@angular/compiler/test/directive_resolver_spec.ts +++ b/modules/@angular/compiler/test/directive_resolver_spec.ts @@ -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.', or 'attr.'.`); }); + + 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', () => { diff --git a/modules/@angular/compiler/test/ng_module_resolver_spec.ts b/modules/@angular/compiler/test/ng_module_resolver_spec.ts index 639ed7ec9b..e99d544c08 100644 --- a/modules/@angular/compiler/test/ng_module_resolver_spec.ts +++ b/modules/@angular/compiler/test/ng_module_resolver_spec.ts @@ -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'})); + }); + }); } diff --git a/modules/@angular/compiler/test/pipe_resolver_spec.ts b/modules/@angular/compiler/test/pipe_resolver_spec.ts new file mode 100644 index 0000000000..3d9003ff86 --- /dev/null +++ b/modules/@angular/compiler/test/pipe_resolver_spec.ts @@ -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'})); + }); + + }); +} diff --git a/modules/@angular/core/src/di/metadata.ts b/modules/@angular/core/src/di/metadata.ts index c7073c19c6..2a0cad479c 100644 --- a/modules/@angular/core/src/di/metadata.ts +++ b/modules/@angular/core/src/di/metadata.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {makeParamDecorator} from '../util/decorators'; +import {makeDecorator, makeParamDecorator} from '../util/decorators'; + /** * Type of the Inject decorator / constructor function. @@ -150,7 +151,7 @@ export interface Injectable {} * @stable * @Annotation */ -export const Injectable: InjectableDecorator = makeParamDecorator('Injectable', []); +export const Injectable: InjectableDecorator = makeDecorator('Injectable', []); /** * Type of the Self decorator / constructor function. diff --git a/modules/@angular/core/src/reflection/reflection_capabilities.ts b/modules/@angular/core/src/reflection/reflection_capabilities.ts index 620812166d..f1a26cdc26 100644 --- a/modules/@angular/core/src/reflection/reflection_capabilities.ts +++ b/modules/@angular/core/src/reflection/reflection_capabilities.ts @@ -12,6 +12,12 @@ import {Type} from '../type'; import {PlatformReflectionCapabilities} from './platform_reflection_capabilities'; import {GetterFn, MethodFn, SetterFn} from './types'; +/** + * Attention: This regex has to hold even if the code is minified! + */ +export const DELEGATE_CTOR = + /^function\s+\S+\(\)\s*{\s*("use strict";)?\s*(return\s+)?\S+\.apply\(this,\s*arguments\)/; + export class ReflectionCapabilities implements PlatformReflectionCapabilities { private _reflect: any; @@ -49,15 +55,26 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { return result; } - parameters(type: Type): any[][] { + private _ownParameters(type: Type, parentCtor: any): any[][] { + // If we have no decorators, we only have function.length as metadata. + // In that case, to detect whether a child class declared an own constructor or not, + // we need to look inside of that constructor to check whether it is + // just calling the parent. + // This also helps to work around for https://github.com/Microsoft/TypeScript/issues/12439 + // that sets 'design:paramtypes' to [] + // if a class inherits from another class but has no ctor declared itself. + if (DELEGATE_CTOR.exec(type.toString())) { + return null; + } + // Prefer the direct API. - if ((type).parameters) { + if ((type).parameters && (type).parameters !== parentCtor.parameters) { return (type).parameters; } // API of tsickle for lowering decorators to properties on the class. const tsickleCtorParams = (type).ctorParameters; - if (tsickleCtorParams) { + if (tsickleCtorParams && tsickleCtorParams !== parentCtor.ctorParameters) { // Newer tsickle uses a function closure // Retain the non-function case for compatibility with older tsickle const ctorParameters = @@ -70,20 +87,35 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API for metadata created by invoking the decorators. - if (isPresent(this._reflect) && isPresent(this._reflect.getMetadata)) { - const paramAnnotations = this._reflect.getMetadata('parameters', type); - const paramTypes = this._reflect.getMetadata('design:paramtypes', type); + if (isPresent(this._reflect) && isPresent(this._reflect.getOwnMetadata)) { + const paramAnnotations = this._reflect.getOwnMetadata('parameters', type); + const paramTypes = this._reflect.getOwnMetadata('design:paramtypes', type); if (paramTypes || paramAnnotations) { return this._zipTypesAndAnnotations(paramTypes, paramAnnotations); } } - // The array has to be filled with `undefined` because holes would be skipped by `some` + + // If a class has no decorators, at least create metadata + // based on function.length. + // Note: We know that this is a real constructor as we checked + // the content of the constructor above. return new Array((type.length)).fill(undefined); } - annotations(typeOrFunc: Type): any[] { + parameters(type: Type): any[][] { + // Note: only report metadata if we have at least one class decorator + // to stay in sync with the static reflector. + const parentCtor = Object.getPrototypeOf(type.prototype).constructor; + let parameters = this._ownParameters(type, parentCtor); + if (!parameters && parentCtor !== Object) { + parameters = this.parameters(parentCtor); + } + return parameters || []; + } + + private _ownAnnotations(typeOrFunc: Type, parentCtor: any): any[] { // Prefer the direct API. - if ((typeOrFunc).annotations) { + if ((typeOrFunc).annotations && (typeOrFunc).annotations !== parentCtor.annotations) { let annotations = (typeOrFunc).annotations; if (typeof annotations === 'function' && annotations.annotations) { annotations = annotations.annotations; @@ -92,21 +124,27 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API of tsickle for lowering decorators to properties on the class. - if ((typeOrFunc).decorators) { + if ((typeOrFunc).decorators && (typeOrFunc).decorators !== parentCtor.decorators) { return convertTsickleDecoratorIntoMetadata((typeOrFunc).decorators); } // API for metadata created by invoking the decorators. - if (this._reflect && this._reflect.getMetadata) { - const annotations = this._reflect.getMetadata('annotations', typeOrFunc); - if (annotations) return annotations; + if (this._reflect && this._reflect.getOwnMetadata) { + return this._reflect.getOwnMetadata('annotations', typeOrFunc); } - return []; } - propMetadata(typeOrFunc: any): {[key: string]: any[]} { + annotations(typeOrFunc: Type): any[] { + const parentCtor = Object.getPrototypeOf(typeOrFunc.prototype).constructor; + const ownAnnotations = this._ownAnnotations(typeOrFunc, parentCtor) || []; + const parentAnnotations = parentCtor !== Object ? this.annotations(parentCtor) : []; + return parentAnnotations.concat(ownAnnotations); + } + + private _ownPropMetadata(typeOrFunc: any, parentCtor: any): {[key: string]: any[]} { // Prefer the direct API. - if ((typeOrFunc).propMetadata) { + if ((typeOrFunc).propMetadata && + (typeOrFunc).propMetadata !== parentCtor.propMetadata) { let propMetadata = (typeOrFunc).propMetadata; if (typeof propMetadata === 'function' && propMetadata.propMetadata) { propMetadata = propMetadata.propMetadata; @@ -115,7 +153,8 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API of tsickle for lowering decorators to properties on the class. - if ((typeOrFunc).propDecorators) { + if ((typeOrFunc).propDecorators && + (typeOrFunc).propDecorators !== parentCtor.propDecorators) { const propDecorators = (typeOrFunc).propDecorators; const propMetadata = <{[key: string]: any[]}>{}; Object.keys(propDecorators).forEach(prop => { @@ -125,11 +164,32 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API for metadata created by invoking the decorators. - if (this._reflect && this._reflect.getMetadata) { - const propMetadata = this._reflect.getMetadata('propMetadata', typeOrFunc); - if (propMetadata) return propMetadata; + if (this._reflect && this._reflect.getOwnMetadata) { + return this._reflect.getOwnMetadata('propMetadata', typeOrFunc); } - return {}; + } + + propMetadata(typeOrFunc: any): {[key: string]: any[]} { + const parentCtor = Object.getPrototypeOf(typeOrFunc.prototype).constructor; + const propMetadata: {[key: string]: any[]} = {}; + if (parentCtor !== Object) { + const parentPropMetadata = this.propMetadata(parentCtor); + Object.keys(parentPropMetadata).forEach((propName) => { + propMetadata[propName] = parentPropMetadata[propName]; + }); + } + const ownPropMetadata = this._ownPropMetadata(typeOrFunc, parentCtor); + if (ownPropMetadata) { + Object.keys(ownPropMetadata).forEach((propName) => { + const decorators: any[] = []; + if (propMetadata.hasOwnProperty(propName)) { + decorators.push(...propMetadata[propName]); + } + decorators.push(...ownPropMetadata[propName]); + propMetadata[propName] = decorators; + }); + } + return propMetadata; } hasLifecycleHook(type: any, lcProperty: string): boolean { diff --git a/modules/@angular/core/src/util/decorators.ts b/modules/@angular/core/src/util/decorators.ts index a39f46c41c..1909055229 100644 --- a/modules/@angular/core/src/util/decorators.ts +++ b/modules/@angular/core/src/util/decorators.ts @@ -262,7 +262,7 @@ export function makeDecorator( const metaCtor = makeMetadataCtor([props]); function DecoratorFactory(objOrType: any): (cls: any) => any { - if (!(Reflect && Reflect.getMetadata)) { + if (!(Reflect && Reflect.getOwnMetadata)) { throw 'reflect-metadata shim is required when using class decorators'; } @@ -327,7 +327,7 @@ export function makeParamDecorator( return ParamDecorator; function ParamDecorator(cls: any, unusedKey: any, index: number): any { - const parameters: any[][] = Reflect.getMetadata('parameters', cls) || []; + const parameters: any[][] = Reflect.getOwnMetadata('parameters', cls) || []; // there might be gaps if some in between parameters do not have annotations. // we pad with nulls. diff --git a/modules/@angular/core/test/reflection/reflector_spec.ts b/modules/@angular/core/test/reflection/reflector_spec.ts index 410cda3643..bcb605525b 100644 --- a/modules/@angular/core/test/reflection/reflector_spec.ts +++ b/modules/@angular/core/test/reflection/reflector_spec.ts @@ -7,7 +7,7 @@ */ import {Reflector} from '@angular/core/src/reflection/reflection'; -import {ReflectionCapabilities} from '@angular/core/src/reflection/reflection_capabilities'; +import {DELEGATE_CTOR, ReflectionCapabilities} from '@angular/core/src/reflection/reflection_capabilities'; import {makeDecorator, makeParamDecorator, makePropDecorator} from '@angular/core/src/util/decorators'; interface ClassDecoratorFactory { @@ -107,7 +107,6 @@ export function main() { class ForwardDep {} expect(reflector.parameters(Forward)).toEqual([[ForwardDep]]); }); - }); describe('propMetadata', () => { @@ -117,6 +116,15 @@ export function main() { expect(p['c']).toEqual([new PropDecorator('p3')]); expect(p['someMethod']).toEqual([new PropDecorator('p4')]); }); + + it('should also return metadata if the class has no decorator', () => { + class Test { + @PropDecorator('test') + prop: any; + } + + expect(reflector.propMetadata(Test)).toEqual({'prop': [new PropDecorator('test')]}); + }); }); describe('annotations', () => { @@ -154,5 +162,321 @@ export function main() { expect(func(obj, ['value'])).toEqual('value'); }); }); + + describe('ctor inheritance detection', () => { + it('should use the right regex', () => { + class Parent {} + + class ChildNoCtor extends Parent {} + class ChildWithCtor extends Parent { + constructor() { super(); } + } + + expect(DELEGATE_CTOR.exec(ChildNoCtor.toString())).toBeTruthy(); + expect(DELEGATE_CTOR.exec(ChildWithCtor.toString())).toBeFalsy(); + }); + }); + + describe('inheritance with decorators', () => { + it('should inherit annotations', () => { + + @ClassDecorator({value: 'parent'}) + class Parent { + } + + @ClassDecorator({value: 'child'}) + class Child extends Parent { + } + + class ChildNoDecorators extends Parent {} + + // Check that metadata for Parent was not changed! + expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({value: 'parent'})]); + + expect(reflector.annotations(Child)).toEqual([ + new ClassDecorator({value: 'parent'}), new ClassDecorator({value: 'child'}) + ]); + + expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator( + {value: 'parent'})]); + }); + + it('should inherit parameters', () => { + class A {} + class B {} + class C {} + + // Note: We need the class decorator as well, + // as otherwise TS won't capture the ctor arguments! + @ClassDecorator({value: 'parent'}) + class Parent { + constructor(@ParamDecorator('a') a: A, @ParamDecorator('b') b: B) {} + } + + class Child extends Parent {} + + // Note: We need the class decorator as well, + // as otherwise TS won't capture the ctor arguments! + @ClassDecorator({value: 'child'}) + class ChildWithCtor extends Parent { + constructor(@ParamDecorator('c') c: C) { super(null, null); } + } + + class ChildWithCtorNoDecorator extends Parent { + constructor(a: any, b: any, c: any) { super(null, null); } + } + + // Check that metadata for Parent was not changed! + expect(reflector.parameters(Parent)).toEqual([ + [A, new ParamDecorator('a')], [B, new ParamDecorator('b')] + ]); + + expect(reflector.parameters(Child)).toEqual([ + [A, new ParamDecorator('a')], [B, new ParamDecorator('b')] + ]); + + expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]); + + // If we have no decorator, we don't get metadata about the ctor params. + // But we should still get an array of the right length based on function.length. + expect(reflector.parameters(ChildWithCtorNoDecorator)).toEqual([ + undefined, undefined, undefined + ]); + }); + + it('should inherit property metadata', () => { + class A {} + class B {} + class C {} + + class Parent { + @PropDecorator('a') + a: A; + @PropDecorator('b1') + b: B; + } + + class Child extends Parent { + @PropDecorator('b2') + b: B; + @PropDecorator('c') + c: C; + } + + // Check that metadata for Parent was not changed! + expect(reflector.propMetadata(Parent)).toEqual({ + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1')], + }); + + expect(reflector.propMetadata(Child)).toEqual({ + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1'), new PropDecorator('b2')], + 'c': [new PropDecorator('c')] + }); + }); + + it('should inherit lifecycle hooks', () => { + class Parent { + hook1() {} + hook2() {} + } + + class Child extends Parent { + hook2() {} + hook3() {} + } + + function hooks(symbol: any, names: string[]): boolean[] { + return names.map(name => reflector.hasLifecycleHook(symbol, name)); + } + + // Check that metadata for Parent was not changed! + expect(hooks(Parent, ['hook1', 'hook2', 'hook3'])).toEqual([true, true, false]); + + expect(hooks(Child, ['hook1', 'hook2', 'hook3'])).toEqual([true, true, true]); + }); + + }); + + describe('inheritance with tsickle', () => { + it('should inherit annotations', () => { + + class Parent { + static decorators = [{type: ClassDecorator, args: [{value: 'parent'}]}]; + } + + class Child extends Parent { + static decorators = [{type: ClassDecorator, args: [{value: 'child'}]}]; + } + + class ChildNoDecorators extends Parent {} + + // Check that metadata for Parent was not changed! + expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({value: 'parent'})]); + + expect(reflector.annotations(Child)).toEqual([ + new ClassDecorator({value: 'parent'}), new ClassDecorator({value: 'child'}) + ]); + + expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator( + {value: 'parent'})]); + }); + + it('should inherit parameters', () => { + class A {} + class B {} + class C {} + + class Parent { + static ctorParameters = () => + [{type: A, decorators: [{type: ParamDecorator, args: ['a']}]}, + {type: B, decorators: [{type: ParamDecorator, args: ['b']}]}, + ]; + } + + class Child extends Parent {} + + class ChildWithCtor extends Parent { + static ctorParameters = + () => [{type: C, decorators: [{type: ParamDecorator, args: ['c']}]}, ]; + constructor() { super(); } + } + + // Check that metadata for Parent was not changed! + expect(reflector.parameters(Parent)).toEqual([ + [A, new ParamDecorator('a')], [B, new ParamDecorator('b')] + ]); + + expect(reflector.parameters(Child)).toEqual([ + [A, new ParamDecorator('a')], [B, new ParamDecorator('b')] + ]); + + expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]); + }); + + it('should inherit property metadata', () => { + class A {} + class B {} + class C {} + + class Parent { + static propDecorators: any = { + 'a': [{type: PropDecorator, args: ['a']}], + 'b': [{type: PropDecorator, args: ['b1']}], + }; + } + + class Child extends Parent { + static propDecorators: any = { + 'b': [{type: PropDecorator, args: ['b2']}], + 'c': [{type: PropDecorator, args: ['c']}], + }; + } + + // Check that metadata for Parent was not changed! + expect(reflector.propMetadata(Parent)).toEqual({ + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1')], + }); + + expect(reflector.propMetadata(Child)).toEqual({ + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1'), new PropDecorator('b2')], + 'c': [new PropDecorator('c')] + }); + }); + + }); + + describe('inheritance with es5 API', () => { + it('should inherit annotations', () => { + + class Parent { + static annotations = [new ClassDecorator({value: 'parent'})]; + } + + class Child extends Parent { + static annotations = [new ClassDecorator({value: 'child'})]; + } + + class ChildNoDecorators extends Parent {} + + // Check that metadata for Parent was not changed! + expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({value: 'parent'})]); + + expect(reflector.annotations(Child)).toEqual([ + new ClassDecorator({value: 'parent'}), new ClassDecorator({value: 'child'}) + ]); + + expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator( + {value: 'parent'})]); + }); + + it('should inherit parameters', () => { + class A {} + class B {} + class C {} + + class Parent { + static parameters = [ + [A, new ParamDecorator('a')], + [B, new ParamDecorator('b')], + ]; + } + + class Child extends Parent {} + + class ChildWithCtor extends Parent { + static parameters = [ + [C, new ParamDecorator('c')], + ]; + constructor() { super(); } + } + + // Check that metadata for Parent was not changed! + expect(reflector.parameters(Parent)).toEqual([ + [A, new ParamDecorator('a')], [B, new ParamDecorator('b')] + ]); + + expect(reflector.parameters(Child)).toEqual([ + [A, new ParamDecorator('a')], [B, new ParamDecorator('b')] + ]); + + expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]); + }); + + it('should inherit property metadata', () => { + class A {} + class B {} + class C {} + + class Parent { + static propMetadata: any = { + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1')], + }; + } + + class Child extends Parent { + static propMetadata: any = { + 'b': [new PropDecorator('b2')], + 'c': [new PropDecorator('c')], + }; + } + + // Check that metadata for Parent was not changed! + expect(reflector.propMetadata(Parent)).toEqual({ + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1')], + }); + + expect(reflector.propMetadata(Child)).toEqual({ + 'a': [new PropDecorator('a')], + 'b': [new PropDecorator('b1'), new PropDecorator('b2')], + 'c': [new PropDecorator('c')] + }); + }); + }); }); } diff --git a/modules/@angular/core/test/util/decorators_spec.ts b/modules/@angular/core/test/util/decorators_spec.ts index ee87790ae1..7272b05042 100644 --- a/modules/@angular/core/test/util/decorators_spec.ts +++ b/modules/@angular/core/test/util/decorators_spec.ts @@ -53,7 +53,7 @@ export function main() { it('should invoke as decorator', () => { function Type() {} TestDecorator({marker: 'WORKS'})(Type); - const annotations = Reflect.getMetadata('annotations', Type); + const annotations = Reflect.getOwnMetadata('annotations', Type); expect(annotations[0].marker).toEqual('WORKS'); }); diff --git a/modules/@angular/facade/src/collection.ts b/modules/@angular/facade/src/collection.ts index e3e1c8d907..6b5a7a1c03 100644 --- a/modules/@angular/facade/src/collection.ts +++ b/modules/@angular/facade/src/collection.ts @@ -52,6 +52,15 @@ export class StringMapWrapper { export interface Predicate { (value: T, index?: number, array?: T[]): boolean; } export class ListWrapper { + static findLast(arr: T[], condition: (value: T) => boolean): T { + for (let i = arr.length - 1; i >= 0; i--) { + if (condition(arr[i])) { + return arr[i]; + } + } + return null; + } + static removeAll(list: T[], items: T[]) { for (let i = 0; i < items.length; ++i) { const index = list.indexOf(items[i]); diff --git a/tools/@angular/tsc-wrapped/src/collector.ts b/tools/@angular/tsc-wrapped/src/collector.ts index 3852f63cfc..62e4deaaa0 100644 --- a/tools/@angular/tsc-wrapped/src/collector.ts +++ b/tools/@angular/tsc-wrapped/src/collector.ts @@ -93,6 +93,15 @@ export class MetadataCollector { } } + // Add class parents + if (classDeclaration.heritageClauses) { + classDeclaration.heritageClauses.forEach((hc) => { + if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) { + hc.types.forEach(type => result.extends = referenceFrom(type.expression)); + } + }); + } + // Add class decorators if (classDeclaration.decorators) { result.decorators = getDecorators(classDeclaration.decorators); @@ -196,8 +205,7 @@ export class MetadataCollector { result.statics = statics; } - return result.decorators || members || statics ? recordEntry(result, classDeclaration) : - undefined; + return recordEntry(result, classDeclaration); } // Predeclare classes and functions @@ -257,11 +265,7 @@ export class MetadataCollector { const className = classDeclaration.name.text; if (node.flags & ts.NodeFlags.Export) { if (!metadata) metadata = {}; - if (classDeclaration.decorators) { - metadata[className] = classMetadataOf(classDeclaration); - } else { - metadata[className] = {__symbolic: 'class'}; - } + metadata[className] = classMetadataOf(classDeclaration); } } // Otherwise don't record metadata for the class. @@ -469,14 +473,15 @@ function validateMetadata( } } - function validateMember(member: MemberMetadata) { + function validateMember(classData: ClassMetadata, member: MemberMetadata) { if (member.decorators) { member.decorators.forEach(validateExpression); } if (isMethodMetadata(member) && member.parameterDecorators) { member.parameterDecorators.forEach(validateExpression); } - if (isConstructorMetadata(member) && member.parameters) { + // Only validate parameters of classes for which we know that are used with our DI + if (classData.decorators && isConstructorMetadata(member) && member.parameters) { member.parameters.forEach(validateExpression); } } @@ -487,7 +492,7 @@ function validateMetadata( } if (classData.members) { Object.getOwnPropertyNames(classData.members) - .forEach(name => classData.members[name].forEach(validateMember)); + .forEach(name => classData.members[name].forEach((m) => validateMember(classData, m))); } } diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index 5097c55373..b136e13179 100644 --- a/tools/@angular/tsc-wrapped/src/schema.ts +++ b/tools/@angular/tsc-wrapped/src/schema.ts @@ -36,6 +36,7 @@ export interface ModuleExportMetadata { export interface ClassMetadata { __symbolic: 'class'; + extends?: MetadataSymbolicExpression|MetadataError; decorators?: (MetadataSymbolicExpression|MetadataError)[]; members?: MetadataMap; statics?: {[name: string]: MetadataValue | FunctionMetadata}; diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index 9dbb4e4560..1fdc9b980a 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -44,6 +44,9 @@ describe('Collector', () => { 'static-method-call.ts', 'static-method-with-if.ts', 'static-method-with-default.ts', + 'class-inheritance.ts', + 'class-inheritance-parent.ts', + 'class-inheritance-declarations.d.ts' ]); service = ts.createLanguageService(host, documentRegistry); program = service.getProgram(); @@ -616,6 +619,32 @@ describe('Collector', () => { }); }); + describe('inheritance', () => { + it('should record `extends` clauses for declared classes', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts')); + expect(metadata.metadata['DeclaredChildClass']) + .toEqual({__symbolic: 'class', extends: {__symbolic: 'reference', name: 'ParentClass'}}); + }); + + it('should record `extends` clauses for classes in the same file', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts')); + expect(metadata.metadata['ChildClassSameFile']) + .toEqual({__symbolic: 'class', extends: {__symbolic: 'reference', name: 'ParentClass'}}); + }); + + it('should record `extends` clauses for classes in a different file', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts')); + expect(metadata.metadata['ChildClassOtherFile']).toEqual({ + __symbolic: 'class', + extends: { + __symbolic: 'reference', + module: './class-inheritance-parent', + name: 'ParentClassFromOtherFile' + } + }); + }); + }); + function override(fileName: string, content: string) { host.overrideFile(fileName, content); host.addFile(fileName); @@ -844,6 +873,20 @@ const FILES: Directory = { export abstract class AbstractClass {} export declare class DeclaredClass {} `, + 'class-inheritance-parent.ts': ` + export class ParentClassFromOtherFile {} + `, + 'class-inheritance.ts': ` + import {ParentClassFromOtherFile} from './class-inheritance-parent'; + + export class ParentClass {} + + export declare class DeclaredChildClass extends ParentClass {} + + export class ChildClassSameFile extends ParentClass {} + + export class ChildClassOtherFile extends ParentClassFromOtherFile {} + `, 'exported-functions.ts': ` export function one(a: string, b: string, c: string) { return {a: a, b: b, c: c}; @@ -877,9 +920,6 @@ const FILES: Directory = { export const constValue = 100; `, 'static-method.ts': ` - import {Injectable} from 'angular2/core'; - - @Injectable() export class MyModule { static with(comp: any): any[] { return [ @@ -890,9 +930,6 @@ const FILES: Directory = { } `, 'static-method-with-default.ts': ` - import {Injectable} from 'angular2/core'; - - @Injectable() export class MyModule { static with(comp: any, foo: boolean = true, bar: boolean = false): any[] { return [ @@ -913,9 +950,6 @@ const FILES: Directory = { export class Foo { } `, 'static-field.ts': ` - import {Injectable} from 'angular2/core'; - - @Injectable() export class MyModule { static VALUE = 'Some string'; } @@ -930,9 +964,6 @@ const FILES: Directory = { export class Foo { } `, 'static-method-with-if.ts': ` - import {Injectable} from 'angular2/core'; - - @Injectable() export class MyModule { static with(cond: boolean): any[] { return [