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:
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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};
|
||||
|
@ -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 [
|
||||
|
Reference in New Issue
Block a user