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

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

View File

@ -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: [],
};
}