refactor(ngcc): rework undecorated parent migration (#33362)

Previously, the (currently disabled) undecorated parent migration in
ngcc would produce errors when a base class could not be determined
statically or when a class extends from a class in another package. This
is not ideal, as it would cause the library to fail compilation without
a workaround, whereas those problems are not guaranteed to cause issues.

Additionally, inheritance chains were not handled. This commit reworks
the migration to address these limitations.

PR Close #33362
This commit is contained in:
JoostK
2019-10-20 23:28:00 +02:00
committed by Andrew Kushnir
parent 3858b26211
commit 2e5e1dd5f5
4 changed files with 135 additions and 53 deletions

View File

@ -26,19 +26,20 @@ runInEachFileSystem(() => {
});
it('should ignore undecorated classes', () => {
const {program, analysis} = setUpAndAnalyzeProgram([{
const {program, analysis, errors} = setUpAndAnalyzeProgram([{
name: INDEX_FILENAME,
contents: `
export class DerivedClass extends BaseClass {}
export class BaseClass {}
`
}]);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(INDEX_FILENAME) !);
expect(file).toBeUndefined();
});
it('should ignore an undecorated base class if the derived class has a constructor', () => {
const {program, analysis} = setUpAndAnalyzeProgram([{
const {program, analysis, errors} = setUpAndAnalyzeProgram([{
name: INDEX_FILENAME,
contents: `
import {Directive, ViewContainerRef} from '@angular/core';
@ -54,6 +55,7 @@ runInEachFileSystem(() => {
export class BaseClass {}
`
}]);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(INDEX_FILENAME) !) !;
expect(file.compiledClasses.find(c => c.name === 'DerivedClass')).toBeDefined();
expect(file.compiledClasses.find(c => c.name === 'BaseClass')).toBeUndefined();
@ -61,7 +63,7 @@ runInEachFileSystem(() => {
it('should add a decorator to an undecorated base class if the derived class is a Directive with no constructor',
() => {
const {program, analysis} = setUpAndAnalyzeProgram([{
const {program, analysis, errors} = setUpAndAnalyzeProgram([{
name: INDEX_FILENAME,
contents: `
import {Directive, ViewContainerRef} from '@angular/core';
@ -78,20 +80,96 @@ runInEachFileSystem(() => {
];
`
}]);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(INDEX_FILENAME) !) !;
expect(file.compiledClasses.find(c => c.name === 'DerivedClass')).toBeDefined();
const baseClass = file.compiledClasses.find(c => c.name === 'BaseClass') !;
expect(baseClass.decorators !.length).toEqual(1);
const decorator = baseClass.decorators ![0];
expect(decorator.name).toEqual('Directive');
expect(decorator.identifier).toBeNull('The decorator must be synthesized');
expect(decorator.import).toEqual({from: '@angular/core', name: 'Directive'});
expect(decorator.args !.length).toEqual(1);
expect(decorator.args !.length).toEqual(0);
});
it('should not add a decorator to a base class that is already decorated', () => {
const {program, analysis, errors} = setUpAndAnalyzeProgram([{
name: INDEX_FILENAME,
contents: `
import {Directive, ViewContainerRef} from '@angular/core';
export class DerivedClass extends BaseClass {
}
DerivedClass.decorators = [
{ type: Directive, args: [{ selector: '[dir]' }] }
];
export class BaseClass {
constructor(private vcr: ViewContainerRef) {}
}
BaseClass.decorators = [
{ type: Directive, args: [] }
];
BaseClass.ctorParameters = () => [
{ type: ViewContainerRef, }
];
`
}]);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(INDEX_FILENAME) !) !;
expect(file.compiledClasses.find(c => c.name === 'DerivedClass')).toBeDefined();
const baseClass = file.compiledClasses.find(c => c.name === 'BaseClass') !;
expect(baseClass.decorators !.length).toEqual(1);
const decorator = baseClass.decorators ![0];
expect(decorator.name).toEqual('Directive');
expect(decorator.identifier).not.toBeNull('The decorator must not be synthesized');
});
it('should add decorators to all classes in an inheritance chain until a constructor is found',
() => {
const {program, analysis, errors} = setUpAndAnalyzeProgram([{
name: INDEX_FILENAME,
contents: `
import {Directive, ViewContainerRef} from '@angular/core';
export class DerivedClass extends IntermediateClass {
}
DerivedClass.decorators = [
{ type: Directive, args: [{ selector: '[dir]' }] }
];
export class IntermediateClass extends BaseClass {}
export class BaseClass extends RealBaseClass {
constructor(private vcr: ViewContainerRef) {}
}
BaseClass.ctorParameters = () => [
{ type: ViewContainerRef, }
];
export class RealBaseClass {}
`
}]);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(INDEX_FILENAME) !) !;
expect(file.compiledClasses.find(c => c.name === 'DerivedClass')).toBeDefined();
expect(file.compiledClasses.find(c => c.name === 'RealBaseClass')).toBeUndefined();
const intermediateClass = file.compiledClasses.find(c => c.name === 'IntermediateClass') !;
expect(intermediateClass.decorators !.length).toEqual(1);
const intermediateDecorator = intermediateClass.decorators ![0];
expect(intermediateDecorator.name).toEqual('Directive');
expect(intermediateDecorator.identifier).toBeNull('The decorator must be synthesized');
expect(intermediateDecorator.import).toEqual({from: '@angular/core', name: 'Directive'});
expect(intermediateDecorator.args !.length).toEqual(0);
const baseClass = file.compiledClasses.find(c => c.name === 'BaseClass') !;
expect(baseClass.decorators !.length).toEqual(1);
const baseDecorator = baseClass.decorators ![0];
expect(baseDecorator.name).toEqual('Directive');
expect(baseDecorator.identifier).toBeNull('The decorator must be synthesized');
expect(baseDecorator.import).toEqual({from: '@angular/core', name: 'Directive'});
expect(baseDecorator.args !.length).toEqual(0);
});
it('should handle the base class being in a different file (same package) as the derived class',
() => {
const BASE_FILENAME = _('/node_modules/test-package/base.js');
const {program, analysis} = setUpAndAnalyzeProgram([
const {program, analysis, errors} = setUpAndAnalyzeProgram([
{
name: INDEX_FILENAME,
contents: `
@ -116,18 +194,20 @@ runInEachFileSystem(() => {
`
}
]);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(BASE_FILENAME) !) !;
const baseClass = file.compiledClasses.find(c => c.name === 'BaseClass') !;
expect(baseClass.decorators !.length).toEqual(1);
const decorator = baseClass.decorators ![0];
expect(decorator.name).toEqual('Directive');
expect(decorator.identifier).toBeNull('The decorator must be synthesized');
expect(decorator.import).toEqual({from: '@angular/core', name: 'Directive'});
expect(decorator.args !.length).toEqual(1);
expect(decorator.args !.length).toEqual(0);
});
it('should error if the base class being is a different package from the derived class', () => {
it('should skip the base class if it is in a different package from the derived class', () => {
const BASE_FILENAME = _('/node_modules/other-package/index.js');
const {errors} = setUpAndAnalyzeProgram([
const {program, analysis, errors} = setUpAndAnalyzeProgram([
{
name: INDEX_FILENAME,
contents: `
@ -152,7 +232,9 @@ runInEachFileSystem(() => {
`
}
]);
expect(errors.length).toEqual(1);
expect(errors).toEqual([]);
const file = analysis.get(program.getSourceFile(BASE_FILENAME) !);
expect(file).toBeUndefined();
});
});