feat(ivy): ngcc - implement UndecoratedParentMigration
(#31544)
Implementing the "undecorated parent" migration described in https://hackmd.io/sfb3Ju2MTmKHSUiX_dLWGg#Design PR Close #31544
This commit is contained in:

committed by
Misko Hevery

parent
4d93d2406f
commit
59c3700c8c
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {ErrorCode, makeDiagnostic} from '../../../src/ngtsc/diagnostics';
|
||||
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`.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* export class BasePlain {
|
||||
* constructor(private vcr: ViewContainerRef) {}
|
||||
* }
|
||||
*
|
||||
* @Directive({selector: '[blah]'})
|
||||
* export class DerivedDir extends BasePlain {}
|
||||
* ```
|
||||
*
|
||||
* When compiling `DerivedDir` which extends the undecorated `BasePlain` class, the compiler needs
|
||||
* to generate an `ngDirectiveDef` for `DerivedDir`. In particular, it needs to generate a factory
|
||||
* function that creates instances of `DerivedDir`.
|
||||
*
|
||||
* As `DerivedDir` has no constructor, the factory function for `DerivedDir` must delegate to the
|
||||
* factory function for `BasePlain`. But for this to work, `BasePlain` must have a factory function,
|
||||
* itself.
|
||||
*
|
||||
* This migration adds a `Directive` decorator to such undecorated parent classes, to ensure that
|
||||
* the compiler will create the necessary factory function.
|
||||
*
|
||||
* The resulting code looks like:
|
||||
*
|
||||
* ```
|
||||
* @Directive()
|
||||
* export class BasePlain {
|
||||
* constructor(private vcr: ViewContainerRef) {}
|
||||
* }
|
||||
*
|
||||
* @Directive({selector: '[blah]'})
|
||||
* export class DerivedDir extends BasePlain {}
|
||||
* ```
|
||||
*/
|
||||
export class UndecoratedParentMigration implements Migration {
|
||||
apply(clazz: ClassDeclaration, host: MigrationHost): ts.Diagnostic|null {
|
||||
// Only interested in `clazz` if it is a `Component` or a `Directive`,
|
||||
// and it has no constructor of its own.
|
||||
if (!hasDirectiveDecorator(host, clazz) || hasConstructor(host, clazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only interested in `clazz` if it inherits from a base class.
|
||||
const baseClassExpr = host.reflectionHost.getBaseClassExpression(clazz);
|
||||
if (baseClassExpr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseClazz = host.reflectionHost.getDeclarationOfIdentifier(baseClassExpr) !.node;
|
||||
if (!isClassDeclaration(baseClazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only interested in this base class if it doesn't have a `Directive` or `Component` decorator.
|
||||
if (hasDirectiveDecorator(host, baseClazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
host.injectSyntheticDecorator(baseClazz, createDirectiveDecorator(baseClazz));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
63
packages/compiler-cli/ngcc/src/migrations/utils.ts
Normal file
63
packages/compiler-cli/ngcc/src/migrations/utils.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {Reference} from '../../../src/ngtsc/imports';
|
||||
import {ClassDeclaration, Decorator, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {MigrationHost} from './migration';
|
||||
|
||||
export function isClassDeclaration(clazz: ts.Declaration): clazz is ClassDeclaration {
|
||||
return isNamedClassDeclaration(clazz) || isNamedFunctionDeclaration(clazz) ||
|
||||
isNamedVariableDeclaration(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the `clazz` has its own constructor function.
|
||||
*/
|
||||
export function hasConstructor(host: MigrationHost, clazz: ClassDeclaration): boolean {
|
||||
return host.reflectionHost.getConstructorParameters(clazz) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')),
|
||||
]);
|
||||
const decoratorType = ts.createIdentifier('Directive');
|
||||
const decoratorNode = ts.createObjectLiteral([
|
||||
ts.createPropertyAssignment('type', decoratorType),
|
||||
ts.createPropertyAssignment('args', ts.createArrayLiteral([selectorArg])),
|
||||
]);
|
||||
|
||||
setParentPointers(clazz.getSourceFile(), decoratorNode);
|
||||
|
||||
return {
|
||||
name: 'Directive',
|
||||
identifier: decoratorType,
|
||||
import: {name: 'Directive', from: '@angular/core'},
|
||||
node: decoratorNode,
|
||||
args: [selectorArg],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a tree of AST nodes have their parents wired up.
|
||||
*/
|
||||
export function setParentPointers(parent: ts.Node, child: ts.Node): void {
|
||||
child.parent = parent;
|
||||
ts.forEachChild(child, grandchild => setParentPointers(child, grandchild));
|
||||
}
|
Reference in New Issue
Block a user