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