feat(core): properly support inheritance

## Inheritance Semantics:

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

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

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

## Example

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

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

  ngOnInit() {}
}

class ChildDirective extends ParentDirective {}
```

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

View File

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

View File

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

View File

@ -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 [