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:
@ -6,12 +6,14 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {ErrorCode, makeDiagnostic} from '../../../src/ngtsc/diagnostics';
|
||||
|
||||
import {Reference} from '../../../src/ngtsc/imports';
|
||||
import {ClassDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {isRelativePath} from '../utils';
|
||||
|
||||
import {Migration, MigrationHost} from './migration';
|
||||
import {createDirectiveDecorator, hasConstructor, hasDirectiveDecorator, isClassDeclaration} from './utils';
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that the parents of directives and components that have no constructor are also decorated
|
||||
* as a `Directive`.
|
||||
@ -59,36 +61,49 @@ export class UndecoratedParentMigration implements Migration {
|
||||
}
|
||||
|
||||
// Only interested in `clazz` if it inherits from a base class.
|
||||
const baseClassExpr = host.reflectionHost.getBaseClassExpression(clazz);
|
||||
if (baseClassExpr === null) {
|
||||
return null;
|
||||
}
|
||||
let baseClazzRef = determineBaseClass(clazz, host);
|
||||
while (baseClazzRef !== null) {
|
||||
const baseClazz = baseClazzRef.node;
|
||||
|
||||
if (!ts.isIdentifier(baseClassExpr)) {
|
||||
return makeDiagnostic(
|
||||
ErrorCode.NGCC_MIGRATION_EXTERNAL_BASE_CLASS, baseClassExpr,
|
||||
`${clazz.name.text} class has a dynamic base class ${baseClassExpr.getText()}, so it is not possible to migrate.`);
|
||||
}
|
||||
// Do not proceed if the base class already has a decorator, or is not in scope of the
|
||||
// entry-point that is currently being compiled.
|
||||
if (hasDirectiveDecorator(host, baseClazz) || !host.isInScope(baseClazz)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const baseClazz = host.reflectionHost.getDeclarationOfIdentifier(baseClassExpr) !.node;
|
||||
if (baseClazz === null || !isClassDeclaration(baseClazz)) {
|
||||
return null;
|
||||
}
|
||||
// Inject an `@Directive()` decorator for the base class.
|
||||
host.injectSyntheticDecorator(baseClazz, createDirectiveDecorator(baseClazz));
|
||||
|
||||
// Only interested in this base class if it doesn't have a `Directive` or `Component` decorator.
|
||||
if (hasDirectiveDecorator(host, baseClazz)) {
|
||||
return null;
|
||||
}
|
||||
// If the base class has a constructor, there's no need to continue walking up the
|
||||
// inheritance chain. The injected decorator ensures that a factory is generated that does
|
||||
// not delegate to the base class.
|
||||
if (hasConstructor(host, baseClazz)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const importInfo = host.reflectionHost.getImportOfIdentifier(baseClassExpr);
|
||||
if (importInfo !== null && !isRelativePath(importInfo.from)) {
|
||||
return makeDiagnostic(
|
||||
ErrorCode.NGCC_MIGRATION_EXTERNAL_BASE_CLASS, baseClassExpr,
|
||||
'The base class was imported from an external entry-point so we cannot add a directive to it.');
|
||||
// Continue with another level of class inheritance.
|
||||
baseClazzRef = determineBaseClass(baseClazz, host);
|
||||
}
|
||||
|
||||
host.injectSyntheticDecorator(baseClazz, createDirectiveDecorator(baseClazz));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a reference to the base class, or `null` if the class has no base class or if it could
|
||||
* not be statically determined.
|
||||
*/
|
||||
function determineBaseClass(
|
||||
clazz: ClassDeclaration, host: MigrationHost): Reference<ClassDeclaration>|null {
|
||||
const baseClassExpr = host.reflectionHost.getBaseClassExpression(clazz);
|
||||
if (baseClassExpr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseClass = host.evaluator.evaluate(baseClassExpr);
|
||||
if (!(baseClass instanceof Reference) || !isClassDeclaration(baseClass.node as ts.Declaration)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return baseClass as Reference<ClassDeclaration>;
|
||||
}
|
||||
|
@ -19,7 +19,8 @@ export function isClassDeclaration(clazz: ts.Declaration): clazz is ClassDeclara
|
||||
* Returns true if the `clazz` is decorated as a `Directive` or `Component`.
|
||||
*/
|
||||
export function hasDirectiveDecorator(host: MigrationHost, clazz: ClassDeclaration): boolean {
|
||||
return host.metadata.getDirectiveMetadata(new Reference(clazz)) !== null;
|
||||
const ref = new Reference(clazz);
|
||||
return host.metadata.getDirectiveMetadata(ref) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,18 +34,13 @@ export function hasConstructor(host: MigrationHost, clazz: ClassDeclaration): bo
|
||||
* Create an empty `Directive` decorator that will be associated with the `clazz`.
|
||||
*/
|
||||
export function createDirectiveDecorator(clazz: ClassDeclaration): Decorator {
|
||||
const selectorArg = ts.createObjectLiteral([
|
||||
// TODO: At the moment ngtsc does not accept a directive with no selector
|
||||
ts.createPropertyAssignment('selector', ts.createStringLiteral('NGCC_DUMMY')),
|
||||
]);
|
||||
|
||||
return {
|
||||
name: 'Directive',
|
||||
identifier: null,
|
||||
import: {name: 'Directive', from: '@angular/core'},
|
||||
node: null,
|
||||
synthesizedFor: clazz.name,
|
||||
args: [reifySourceFile(selectorArg)],
|
||||
args: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user