diff --git a/modules/@angular/compiler-cli/src/compiler_host.ts b/modules/@angular/compiler-cli/src/compiler_host.ts index a332c657a9f18c..50ebaab6cf3a94 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 b9d2620c26ebae..0a1f6d3e60783e 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 e4792d4c1e163d..3d9bce1cd22049 100644 --- a/modules/@angular/compiler/src/aot/static_reflector.ts +++ b/modules/@angular/compiler/src/aot/static_reflector.ts @@ -70,8 +70,9 @@ 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; @@ -99,29 +100,45 @@ export class StaticReflector implements ReflectorReader { public annotations(type: StaticSymbol): any[] { let annotations = this.annotationCache.get(type); if (!annotations) { + annotations = []; const classMetadata = this.getTypeMetadata(type); + if (classMetadata['extends']) { + const parentAnnotations = this.annotations(this.simplify(type, classMetadata['extends'])); + annotations.push(...parentAnnotations); + } if (classMetadata['decorators']) { - annotations = this.simplify(type, classMetadata['decorators']); - } else { - annotations = []; + const ownAnnotations: any[] = this.simplify(type, classMetadata['decorators']); + annotations.push(...ownAnnotations); } this.annotationCache.set(type, annotations.filter(ann => !!ann)); } return annotations; } - public propMetadata(type: StaticSymbol): {[key: string]: any} { + public propMetadata(type: StaticSymbol): {[key: string]: any[]} { let propMetadata = this.propertyCache.get(type); if (!propMetadata) { - const classMetadata = this.getTypeMetadata(type); - const members = classMetadata ? classMetadata['members'] : {}; - propMetadata = mapStringMap(members, (propData, propName) => { + const classMetadata = this.getTypeMetadata(type) || {}; + propMetadata = {}; + if (classMetadata['extends']) { + const parentPropMetadata = this.propMetadata(this.simplify(type, classMetadata['extends'])); + Object.keys(parentPropMetadata).forEach((parentProp) => { + propMetadata[parentProp] = parentPropMetadata[parentProp]; + }); + } + + const members = classMetadata['members'] || {}; + Object.keys(members).forEach((propName) => { + const propData = members[propName]; const prop = (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 +172,8 @@ export class StaticReflector implements ReflectorReader { } parameters.push(nestedResult); }); + } else if (classMetadata['extends']) { + parameters = this.parameters(this.simplify(type, classMetadata['extends'])); } if (!parameters) { parameters = []; @@ -168,23 +187,47 @@ export class StaticReflector implements ReflectorReader { } } + private _methodNames(type: any): {[key: string]: boolean} { + let methodNames = this.methodCache.get(type); + if (!methodNames) { + const classMetadata = this.getTypeMetadata(type) || {}; + methodNames = {}; + if (classMetadata['extends']) { + const parentMethodNames = this._methodNames(this.simplify(type, classMetadata['extends'])); + Object.keys(parentMethodNames).forEach((parentProp) => { + methodNames[parentProp] = parentMethodNames[parentProp]; + }); + } + + const members = classMetadata['members'] || {}; + Object.keys(members).forEach((propName) => { + const propData = members[propName]; + const isMethod = (propData).some(a => a['__symbolic'] == 'method'); + methodNames[propName] = methodNames[propName] || isMethod; + }); + this.methodCache.set(type, methodNames); + } + return methodNames; + } + hasLifecycleHook(type: any, lcProperty: string): boolean { if (!(type instanceof StaticSymbol)) { throw new Error( `hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`); } - const classMetadata = this.getTypeMetadata(type); - const members = classMetadata ? classMetadata['members'] : null; - const member: any[] = - members && members.hasOwnProperty(lcProperty) ? members[lcProperty] : null; - return member ? member.some(a => a['__symbolic'] == 'method') : false; + try { + return !!this._methodNames(type)[lcProperty]; + } catch (e) { + console.log(`Failed on type ${JSON.stringify(type)} with error ${e}`); + throw e; + } } - private registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void { + registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void { this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args)); } - private registerFunction(type: StaticSymbol, fn: any): void { + registerFunction(type: StaticSymbol, fn: any): void { this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args)); } diff --git a/modules/@angular/compiler/src/directive_resolver.ts b/modules/@angular/compiler/src/directive_resolver.ts index f3ba7b07b9704a..11cb7415c802e1 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,43 +59,48 @@ export class DirectiveResolver { const queries: {[key: string]: any} = {}; Object.keys(propertyMetadata).forEach((propName: string) => { - - propertyMetadata[propName].forEach(a => { - if (a instanceof Input) { - if (a.bindingPropertyName) { - inputs.push(`${propName}: ${a.bindingPropertyName}`); - } else { - inputs.push(propName); - } - } else if (a instanceof Output) { - const output: Output = a; - if (output.bindingPropertyName) { - outputs.push(`${propName}: ${output.bindingPropertyName}`); - } else { - outputs.push(propName); - } - } else if (a instanceof HostBinding) { - const hostBinding: HostBinding = a; - if (hostBinding.hostPropertyName) { - const startWith = hostBinding.hostPropertyName[0]; - if (startWith === '(') { - throw new Error(`@HostBinding can not bind to events. Use @HostListener instead.`); - } else if (startWith === '[') { - throw new Error( - `@HostBinding parameter should be a property name, 'class.', or 'attr.'.`); - } - host[`[${hostBinding.hostPropertyName}]`] = propName; - } else { - host[`[${propName}]`] = propName; + const input = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Input); + if (input) { + if (input.bindingPropertyName) { + inputs.push(`${propName}: ${input.bindingPropertyName}`); + } else { + inputs.push(propName); + } + } + const output = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Output); + if (output) { + if (output.bindingPropertyName) { + outputs.push(`${propName}: ${output.bindingPropertyName}`); + } else { + outputs.push(propName); + } + } + const hostBinding = + ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof HostBinding); + if (hostBinding) { + if (hostBinding.hostPropertyName) { + const startWith = hostBinding.hostPropertyName[0]; + if (startWith === '(') { + throw new Error(`@HostBinding can not bind to events. Use @HostListener instead.`); + } else if (startWith === '[') { + throw new Error( + `@HostBinding parameter should be a property name, 'class.', or 'attr.'.`); } - } else if (a instanceof HostListener) { - const hostListener: HostListener = a; - const args = hostListener.args || []; - host[`(${hostListener.eventName})`] = `${propName}(${args.join(',')})`; - } else if (a instanceof Query) { - queries[propName] = a; + host[`[${hostBinding.hostPropertyName}]`] = propName; + } else { + host[`[${propName}]`] = propName; } - }); + } + const hostListener = + ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof HostListener); + if (hostListener) { + const args = hostListener.args || []; + host[`(${hostListener.eventName})`] = `${propName}(${args.join(',')})`; + } + const query = ListWrapper.findLast(propertyMetadata[propName], (a) => a instanceof Query); + if (query) { + queries[propName] = query; + } }); return this._merge(dm, inputs, outputs, host, queries, directiveType); } diff --git a/modules/@angular/compiler/src/ng_module_resolver.ts b/modules/@angular/compiler/src/ng_module_resolver.ts index fce0a8a83e387f..e74128c6daa4ad 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 47a2d860b14db3..61df4ca6ee4d8c 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/src/pipe_resolver_spec.ts b/modules/@angular/compiler/src/pipe_resolver_spec.ts new file mode 100644 index 00000000000000..3d9003ff863934 --- /dev/null +++ b/modules/@angular/compiler/src/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/compiler/test/aot/static_reflector_spec.ts b/modules/@angular/compiler/test/aot/static_reflector_spec.ts index 91dd008e188cbb..7e441082de10cd 100644 --- a/modules/@angular/compiler/test/aot/static_reflector_spec.ts +++ b/modules/@angular/compiler/test/aot/static_reflector_spec.ts @@ -21,10 +21,12 @@ describe('StaticReflector', () => { let host: StaticReflectorHost; let reflector: StaticReflector; - beforeEach(() => { - host = new MockStaticReflectorHost(); + function init(testData: {[key: string]: any} = DEFAULT_TEST_DATA) { + host = new MockStaticReflectorHost(testData); reflector = new StaticReflector(host); - }); + } + + beforeEach(() => init()); function simplify(context: StaticSymbol, value: any) { return reflector.simplify(context, value); @@ -517,11 +519,175 @@ 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); + reflector.registerDecoratorOrConstructor( + reflector.getStaticSymbol('/tmp/src/decorator.ts', 'ClassDecorator'), ClassDecorator); + reflector.registerDecoratorOrConstructor( + reflector.getStaticSymbol('/tmp/src/decorator.ts', 'ParamDecorator'), ParamDecorator); + reflector.registerDecoratorOrConstructor( + reflector.getStaticSymbol('/tmp/src/decorator.ts', 'PropDecorator'), 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 d061c16c17cef3..baa52ea4724a40 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; @@ -150,11 +147,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', () => { @@ -179,6 +187,26 @@ export function main() { .toThrowError( `Input 'a' defined multiple times in 'SomeDirectiveWithDuplicateRenamedInputs'`); }); + + 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']); + }); }); describe('outputs', () => { @@ -203,6 +231,26 @@ export function main() { .toThrowError( `Output event 'a' defined multiple times in 'SomeDirectiveWithDuplicateRenamedOutputs'`); }); + + 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']); + }); }); describe('host', () => { @@ -233,6 +281,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 +347,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 639ed7ec9b6b6d..e99d544c080ded 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/core/src/di/metadata.ts b/modules/@angular/core/src/di/metadata.ts index c7073c19c6740c..2a0cad479c67c5 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 620812166d3a94..aaa9759d0ce249 100644 --- a/modules/@angular/core/src/reflection/reflection_capabilities.ts +++ b/modules/@angular/core/src/reflection/reflection_capabilities.ts @@ -12,6 +12,11 @@ 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! + */ +const DELEGATE_CTOR = /^function\s+\w+\(\)\s*{\s*\w+\.apply\(this,\s*arguments\);\s*}$/; + export class ReflectionCapabilities implements PlatformReflectionCapabilities { private _reflect: any; @@ -49,15 +54,27 @@ 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 a bug in + // TypeScript 2.x that also sets 'design:paramtypes' to [] + // if a class inherits from another class but has no ctor declared itself. + // TODO: link or find the bug. + 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); + } + } + + 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[propName]) { + decorators.push(...propMetadata[propName]); + } + decorators.push(...ownPropMetadata[propName]); + propMetadata[propName] = decorators; + }); } - return {}; + 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 a39f46c41c76e5..19090552294a67 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 410cda3643054a..84b11c60529980 100644 --- a/modules/@angular/core/test/reflection/reflector_spec.ts +++ b/modules/@angular/core/test/reflection/reflector_spec.ts @@ -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,307 @@ export function main() { expect(func(obj, ['value'])).toEqual('value'); }); }); + + 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 ee87790ae199a6..7272b0504216fd 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 e3e1c8d907b6b3..d07629f54a69a7 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 (var 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 3852f63cfcd466..6299d220500db1 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. diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index 5097c553736368..b136e131796da6 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 9dbb4e45607715..1fdc9b980a3f83 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 [