feat(core): add undecorated classes migration schematic (#31650)
Introduces a new migration schematic that follows the given migration plan: https://hackmd.io/@alx/S1XKqMZeS. First case: The schematic detects decorated directives which inherit a constructor. The migration ensures that all base classes until the class with the explicit constructor are properly decorated with "@Directive()" or "@Component". In case one of these classes is not decorated, the schematic adds the abstract "@Directive()" decorator automatically. Second case: The schematic detects undecorated declarations and copies the inherited "@Directive()", "@Component" or "@Pipe" decorator to the undecorated derived class. This involves non-trivial import rewriting, identifier aliasing and AOT metadata serializing (as decorators are not always part of source files) PR Close #31650
This commit is contained in:
parent
5064dc75ac
commit
024c31da25
|
@ -15,5 +15,6 @@ npm_package(
|
|||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
"//packages/core/schematics/migrations/static-queries",
|
||||
"//packages/core/schematics/migrations/template-var-assignment",
|
||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
"version": "9-beta",
|
||||
"description": "Migrates usages of Renderer to Renderer2",
|
||||
"factory": "./migrations/renderer-to-renderer2/index"
|
||||
},
|
||||
"migration-v9-undecorated-classes-with-di": {
|
||||
"version": "9-beta",
|
||||
"description": "Decorates undecorated base classes of directives/components that use dependency injection. Copies metadata from base classes to derived directives/components/pipes that are not decorated.",
|
||||
"factory": "./migrations/undecorated-classes-with-di/index"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "undecorated-classes-with-di",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/migrations/undecorated-classes/google3:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/core",
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/core",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* @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 {AotCompiler, CompileStylesheetMetadata} from '@angular/compiler';
|
||||
import {createProgram, readConfiguration} from '@angular/compiler-cli';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/** Creates an NGC program that can be used to read and parse metadata for files. */
|
||||
export function createNgcProgram(
|
||||
createHost: (options: ts.CompilerOptions) => ts.CompilerHost, tsconfigPath: string | null,
|
||||
parseConfig: () => {
|
||||
rootNames: readonly string[],
|
||||
options: ts.CompilerOptions
|
||||
} = () => readConfiguration(tsconfigPath !)) {
|
||||
const {rootNames, options} = parseConfig();
|
||||
const host = createHost(options);
|
||||
const ngcProgram = createProgram({rootNames, options, host});
|
||||
const program = ngcProgram.getTsProgram();
|
||||
|
||||
// The "AngularCompilerProgram" does not expose the "AotCompiler" instance, nor does it
|
||||
// expose the logic that is necessary to analyze the determined modules. We work around
|
||||
// this by just accessing the necessary private properties using the bracket notation.
|
||||
const compiler: AotCompiler = (ngcProgram as any)['compiler'];
|
||||
const metadataResolver = compiler['_metadataResolver'];
|
||||
// Modify the "DirectiveNormalizer" to not normalize any referenced external stylesheets.
|
||||
// This is necessary because in CLI projects preprocessor files are commonly referenced
|
||||
// and we don't want to parse them in order to extract relative style references. This
|
||||
// breaks the analysis of the project because we instantiate a standalone AOT compiler
|
||||
// program which does not contain the custom logic by the Angular CLI Webpack compiler plugin.
|
||||
const directiveNormalizer = metadataResolver !['_directiveNormalizer'];
|
||||
directiveNormalizer['_normalizeStylesheet'] = function(metadata: CompileStylesheetMetadata) {
|
||||
return new CompileStylesheetMetadata(
|
||||
{styles: metadata.styles, styleUrls: [], moduleUrl: metadata.moduleUrl !});
|
||||
};
|
||||
|
||||
return {host, ngcProgram, program, compiler};
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* @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 {StaticSymbol} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Converts a directive metadata object into a TypeScript expression. Throws
|
||||
* if metadata cannot be cleanly converted.
|
||||
*/
|
||||
export function convertDirectiveMetadataToExpression(
|
||||
metadata: any, resolveSymbolImport: (symbol: StaticSymbol) => string | null,
|
||||
createImport: (moduleName: string, name: string) => ts.Expression,
|
||||
convertProperty?: (key: string, value: any) => ts.Expression | null): ts.Expression {
|
||||
if (typeof metadata === 'string') {
|
||||
return ts.createStringLiteral(metadata);
|
||||
} else if (Array.isArray(metadata)) {
|
||||
return ts.createArrayLiteral(metadata.map(
|
||||
el => convertDirectiveMetadataToExpression(
|
||||
el, resolveSymbolImport, createImport, convertProperty)));
|
||||
} else if (typeof metadata === 'number') {
|
||||
return ts.createNumericLiteral(metadata.toString());
|
||||
} else if (typeof metadata === 'boolean') {
|
||||
return metadata ? ts.createTrue() : ts.createFalse();
|
||||
} else if (typeof metadata === 'undefined') {
|
||||
return ts.createIdentifier('undefined');
|
||||
} else if (typeof metadata === 'bigint') {
|
||||
return ts.createBigIntLiteral(metadata.toString());
|
||||
} else if (typeof metadata === 'object') {
|
||||
// In case there is a static symbol object part of the metadata, try to resolve
|
||||
// the import expression of the symbol. If no import path could be resolved, an
|
||||
// error will be thrown as the symbol cannot be converted into TypeScript AST.
|
||||
if (metadata instanceof StaticSymbol) {
|
||||
const resolvedImport = resolveSymbolImport(metadata);
|
||||
if (resolvedImport === null) {
|
||||
throw new UnexpectedMetadataValueError();
|
||||
}
|
||||
return createImport(resolvedImport, metadata.name);
|
||||
}
|
||||
|
||||
const literalProperties: ts.PropertyAssignment[] = [];
|
||||
|
||||
for (const key of Object.keys(metadata)) {
|
||||
const metadataValue = metadata[key];
|
||||
let propertyValue: ts.Expression|null = null;
|
||||
|
||||
// Allows custom conversion of properties in an object. This is useful for special
|
||||
// cases where we don't want to store the enum values as integers, but rather use the
|
||||
// real enum symbol. e.g. instead of `2` we want to use `ViewEncapsulation.None`.
|
||||
if (convertProperty) {
|
||||
propertyValue = convertProperty(key, metadataValue);
|
||||
}
|
||||
|
||||
// In case the property value has not been assigned to an expression, we convert
|
||||
// the resolved metadata value into a TypeScript expression.
|
||||
if (propertyValue === null) {
|
||||
propertyValue = convertDirectiveMetadataToExpression(
|
||||
metadataValue, resolveSymbolImport, createImport, convertProperty);
|
||||
}
|
||||
|
||||
literalProperties.push(ts.createPropertyAssignment(key, propertyValue));
|
||||
}
|
||||
|
||||
return ts.createObjectLiteral(literalProperties, true);
|
||||
}
|
||||
|
||||
throw new UnexpectedMetadataValueError();
|
||||
}
|
||||
|
||||
/** Error that will be thrown if a unexpected value needs to be converted. */
|
||||
export class UnexpectedMetadataValueError extends Error {}
|
|
@ -0,0 +1,135 @@
|
|||
|
||||
/**
|
||||
* @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 {AotCompiler} from '@angular/compiler';
|
||||
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {NgDecorator} from '../../../utils/ng_decorators';
|
||||
import {unwrapExpression} from '../../../utils/typescript/functions';
|
||||
import {ImportManager} from '../import_manager';
|
||||
|
||||
import {ImportRewriteTransformerFactory, UnresolvedIdentifierError} from './import_rewrite_visitor';
|
||||
|
||||
/**
|
||||
* Class that can be used to copy decorators to a new location. The rewriter ensures that
|
||||
* identifiers and imports are rewritten to work in the new file location. Fields in a
|
||||
* decorator that cannot be cleanly copied will be copied with a comment explaining that
|
||||
* imports and identifiers need to be adjusted manually.
|
||||
*/
|
||||
export class DecoratorRewriter {
|
||||
previousSourceFile: ts.SourceFile|null = null;
|
||||
newSourceFile: ts.SourceFile|null = null;
|
||||
|
||||
newProperties: ts.ObjectLiteralElementLike[] = [];
|
||||
nonCopyableProperties: ts.ObjectLiteralElementLike[] = [];
|
||||
|
||||
private importRewriterFactory = new ImportRewriteTransformerFactory(
|
||||
this.importManager, this.typeChecker, this.compiler['_host']);
|
||||
|
||||
constructor(
|
||||
private importManager: ImportManager, private typeChecker: ts.TypeChecker,
|
||||
private evaluator: PartialEvaluator, private compiler: AotCompiler) {}
|
||||
|
||||
rewrite(ngDecorator: NgDecorator, newSourceFile: ts.SourceFile): ts.Decorator {
|
||||
const decorator = ngDecorator.node;
|
||||
|
||||
// Reset the previous state of the decorator rewriter.
|
||||
this.newProperties = [];
|
||||
this.nonCopyableProperties = [];
|
||||
this.newSourceFile = newSourceFile;
|
||||
this.previousSourceFile = decorator.getSourceFile();
|
||||
|
||||
// If the decorator will be added to the same source file it currently
|
||||
// exists in, we don't need to rewrite any paths or add new imports.
|
||||
if (this.previousSourceFile === newSourceFile) {
|
||||
return this._createDecorator(decorator.expression);
|
||||
}
|
||||
|
||||
const oldCallExpr = decorator.expression;
|
||||
|
||||
if (!oldCallExpr.arguments.length) {
|
||||
// Re-use the original decorator if there are no arguments and nothing needs
|
||||
// to be sanitized or rewritten.
|
||||
return this._createDecorator(decorator.expression);
|
||||
}
|
||||
|
||||
const metadata = unwrapExpression(oldCallExpr.arguments[0]);
|
||||
if (!ts.isObjectLiteralExpression(metadata)) {
|
||||
// Re-use the original decorator as there is no metadata that can be sanitized.
|
||||
return this._createDecorator(decorator.expression);
|
||||
}
|
||||
|
||||
metadata.properties.forEach(prop => {
|
||||
// We don't handle spread assignments, accessors or method declarations automatically
|
||||
// as it involves more advanced static analysis and these type of properties are not
|
||||
// picked up by ngc either.
|
||||
if (ts.isSpreadAssignment(prop) || ts.isAccessor(prop) || ts.isMethodDeclaration(prop)) {
|
||||
this.nonCopyableProperties.push(prop);
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedProp = this._sanitizeMetadataProperty(prop);
|
||||
if (sanitizedProp !== null) {
|
||||
this.newProperties.push(sanitizedProp);
|
||||
} else {
|
||||
this.nonCopyableProperties.push(prop);
|
||||
}
|
||||
});
|
||||
|
||||
// In case there is at least one non-copyable property, we add a leading comment to
|
||||
// the first property assignment in order to ask the developer to manually manage
|
||||
// imports and do path rewriting for these properties.
|
||||
if (this.nonCopyableProperties.length !== 0) {
|
||||
['The following fields were copied from the base class,',
|
||||
'but could not be updated automatically to work in the',
|
||||
'new file location. Please add any required imports for', 'the properties below:']
|
||||
.forEach(
|
||||
text => ts.addSyntheticLeadingComment(
|
||||
this.nonCopyableProperties[0], ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`,
|
||||
true));
|
||||
}
|
||||
|
||||
// Note that we don't update the decorator as we don't want to copy potential leading
|
||||
// comments of the decorator. This is necessary because otherwise comments from the
|
||||
// copied decorator end up describing the new class (which is not always correct).
|
||||
return this._createDecorator(ts.createCall(
|
||||
this.importManager.addImportToSourceFile(
|
||||
newSourceFile, ngDecorator.name, ngDecorator.moduleName),
|
||||
undefined, [ts.updateObjectLiteral(
|
||||
metadata, [...this.newProperties, ...this.nonCopyableProperties])]));
|
||||
}
|
||||
|
||||
/** Creates a new decorator with the given expression. */
|
||||
private _createDecorator(expr: ts.Expression): ts.Decorator {
|
||||
// Note that we don't update the decorator as we don't want to copy potential leading
|
||||
// comments of the decorator. This is necessary because otherwise comments from the
|
||||
// copied decorator end up describing the new class (which is not always correct).
|
||||
return ts.createDecorator(expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a metadata property by ensuring that all contained identifiers
|
||||
* are imported in the target source file.
|
||||
*/
|
||||
private _sanitizeMetadataProperty(prop: ts.ObjectLiteralElementLike): ts.ObjectLiteralElementLike
|
||||
|null {
|
||||
try {
|
||||
return ts
|
||||
.transform(prop, [ctx => this.importRewriterFactory.create(ctx, this.newSourceFile !)])
|
||||
.transformed[0];
|
||||
} catch (e) {
|
||||
// If the error is for an unresolved identifier, we want to return "null" because
|
||||
// such object literal elements could be added to the non-copyable properties.
|
||||
if (e instanceof UnresolvedIdentifierError) {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* @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 {AotCompilerHost} from '@angular/compiler';
|
||||
import {dirname, resolve} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Import, getImportOfIdentifier} from '../../../utils/typescript/imports';
|
||||
import {getValueSymbolOfDeclaration} from '../../../utils/typescript/symbol';
|
||||
import {ImportManager} from '../import_manager';
|
||||
|
||||
import {getPosixPath} from './path_format';
|
||||
import {ResolvedExport, getExportSymbolsOfFile} from './source_file_exports';
|
||||
|
||||
/**
|
||||
* Factory that creates a TypeScript transformer which ensures that
|
||||
* referenced identifiers are available at the target file location.
|
||||
*
|
||||
* Imports cannot be just added as sometimes identifiers collide in the
|
||||
* target source file and the identifier needs to be aliased.
|
||||
*/
|
||||
export class ImportRewriteTransformerFactory {
|
||||
private sourceFileExports = new Map<ts.SourceFile, ResolvedExport[]>();
|
||||
|
||||
constructor(
|
||||
private importManager: ImportManager, private typeChecker: ts.TypeChecker,
|
||||
private compilerHost: AotCompilerHost) {}
|
||||
|
||||
create<T extends ts.Node>(ctx: ts.TransformationContext, newSourceFile: ts.SourceFile):
|
||||
ts.Transformer<T> {
|
||||
const visitNode: ts.Visitor = (node: ts.Node) => {
|
||||
if (ts.isIdentifier(node)) {
|
||||
// Record the identifier reference and return the new identifier. The identifier
|
||||
// name can change if the generated import uses an namespaced import or aliased
|
||||
// import identifier (to avoid collisions).
|
||||
return this._recordIdentifierReference(node, newSourceFile);
|
||||
}
|
||||
|
||||
return ts.visitEachChild(node, visitNode, ctx);
|
||||
};
|
||||
|
||||
return (node: T) => ts.visitNode(node, visitNode);
|
||||
}
|
||||
|
||||
private _recordIdentifierReference(node: ts.Identifier, targetSourceFile: ts.SourceFile):
|
||||
ts.Node {
|
||||
// For object literal elements we don't want to check identifiers that describe the
|
||||
// property name. These identifiers do not refer to a value but rather to a property
|
||||
// name and therefore don't need to be imported. The exception is that for shorthand
|
||||
// property assignments the "name" identifier is both used as value and property name.
|
||||
if (ts.isObjectLiteralElementLike(node.parent) &&
|
||||
!ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const resolvedImport = getImportOfIdentifier(this.typeChecker, node);
|
||||
const sourceFile = node.getSourceFile();
|
||||
|
||||
if (resolvedImport) {
|
||||
const symbolName = resolvedImport.name;
|
||||
const moduleFileName =
|
||||
this.compilerHost.moduleNameToFileName(resolvedImport.importModule, sourceFile.fileName);
|
||||
|
||||
// In case the identifier refers to an export in the target source file, we need to use
|
||||
// the local identifier in the scope of the target source file. This is necessary because
|
||||
// the export could be aliased and the alias is not available to the target source file.
|
||||
if (moduleFileName && resolve(moduleFileName) === resolve(targetSourceFile.fileName)) {
|
||||
const resolvedExport =
|
||||
this._getSourceFileExports(targetSourceFile).find(e => e.exportName === symbolName);
|
||||
if (resolvedExport) {
|
||||
return resolvedExport.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
return this.importManager.addImportToSourceFile(
|
||||
targetSourceFile, symbolName,
|
||||
this._rewriteModuleImport(resolvedImport, targetSourceFile));
|
||||
} else {
|
||||
let symbol = getValueSymbolOfDeclaration(node, this.typeChecker);
|
||||
|
||||
if (symbol) {
|
||||
// If the symbol refers to a shorthand property assignment, we want to resolve the
|
||||
// value symbol of the shorthand property assignment. This is necessary because the
|
||||
// value symbol is ambiguous for shorthand property assignment identifiers as the
|
||||
// identifier resolves to both property name and property value.
|
||||
if (symbol.valueDeclaration && ts.isShorthandPropertyAssignment(symbol.valueDeclaration)) {
|
||||
symbol = this.typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration);
|
||||
}
|
||||
|
||||
const resolvedExport =
|
||||
this._getSourceFileExports(sourceFile).find(e => e.symbol === symbol);
|
||||
|
||||
if (resolvedExport) {
|
||||
return this.importManager.addImportToSourceFile(
|
||||
targetSourceFile, resolvedExport.exportName,
|
||||
getPosixPath(this.compilerHost.fileNameToModuleName(
|
||||
sourceFile.fileName, targetSourceFile.fileName)));
|
||||
}
|
||||
}
|
||||
|
||||
// The referenced identifier cannot be imported. In that case we throw an exception
|
||||
// which can be handled outside of the transformer.
|
||||
throw new UnresolvedIdentifierError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resolved exports of a given source file. Exports are cached
|
||||
* for subsequent calls.
|
||||
*/
|
||||
private _getSourceFileExports(sourceFile: ts.SourceFile): ResolvedExport[] {
|
||||
if (this.sourceFileExports.has(sourceFile)) {
|
||||
return this.sourceFileExports.get(sourceFile) !;
|
||||
}
|
||||
|
||||
const sourceFileExports = getExportSymbolsOfFile(sourceFile, this.typeChecker);
|
||||
this.sourceFileExports.set(sourceFile, sourceFileExports);
|
||||
return sourceFileExports;
|
||||
}
|
||||
|
||||
/** Rewrites a module import to be relative to the target file location. */
|
||||
private _rewriteModuleImport(resolvedImport: Import, newSourceFile: ts.SourceFile): string {
|
||||
if (!resolvedImport.importModule.startsWith('.')) {
|
||||
return resolvedImport.importModule;
|
||||
}
|
||||
|
||||
const importFilePath = resolvedImport.node.getSourceFile().fileName;
|
||||
const resolvedModulePath = resolve(dirname(importFilePath), resolvedImport.importModule);
|
||||
const relativeModuleName =
|
||||
this.compilerHost.fileNameToModuleName(resolvedModulePath, newSourceFile.fileName);
|
||||
|
||||
return getPosixPath(relativeModuleName);
|
||||
}
|
||||
}
|
||||
|
||||
/** Error that will be thrown if a given identifier cannot be resolved. */
|
||||
export class UnresolvedIdentifierError extends Error {}
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @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 {normalize} from 'path';
|
||||
|
||||
/** Normalizes the specified path to conform with the posix path format. */
|
||||
export function getPosixPath(pathString: string) {
|
||||
const normalized = normalize(pathString).replace(/\\/g, '/');
|
||||
if (!normalized.startsWith('.')) {
|
||||
return `./${normalized}`;
|
||||
}
|
||||
return normalized;
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @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 {getValueSymbolOfDeclaration} from '../../../utils/typescript/symbol';
|
||||
|
||||
export interface ResolvedExport {
|
||||
symbol: ts.Symbol;
|
||||
exportName: string;
|
||||
identifier: ts.Identifier;
|
||||
}
|
||||
|
||||
/** Computes the resolved exports of a given source file. */
|
||||
export function getExportSymbolsOfFile(
|
||||
sf: ts.SourceFile, typeChecker: ts.TypeChecker): ResolvedExport[] {
|
||||
const exports: {exportName: string, identifier: ts.Identifier}[] = [];
|
||||
const resolvedExports: ResolvedExport[] = [];
|
||||
|
||||
ts.forEachChild(sf, function visitNode(node) {
|
||||
if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) ||
|
||||
ts.isInterfaceDeclaration(node) &&
|
||||
(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0) {
|
||||
if (node.name) {
|
||||
exports.push({exportName: node.name.text, identifier: node.name});
|
||||
}
|
||||
} else if (ts.isVariableStatement(node)) {
|
||||
for (const decl of node.declarationList.declarations) {
|
||||
visitNode(decl);
|
||||
}
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) != 0 &&
|
||||
ts.isIdentifier(node.name)) {
|
||||
exports.push({exportName: node.name.text, identifier: node.name});
|
||||
}
|
||||
} else if (ts.isExportDeclaration(node)) {
|
||||
const {moduleSpecifier, exportClause} = node;
|
||||
if (!moduleSpecifier && exportClause) {
|
||||
exportClause.elements.forEach(el => exports.push({
|
||||
exportName: el.name.text,
|
||||
identifier: el.propertyName ? el.propertyName : el.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.forEach(({identifier, exportName}) => {
|
||||
const symbol = getValueSymbolOfDeclaration(identifier, typeChecker);
|
||||
if (symbol) {
|
||||
resolvedExports.push({symbol, identifier, exportName});
|
||||
}
|
||||
});
|
||||
|
||||
return resolvedExports;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @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 {getBaseTypeIdentifiers} from '../../utils/typescript/class_declaration';
|
||||
|
||||
/** Gets all base class declarations of the specified class declaration. */
|
||||
export function findBaseClassDeclarations(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker) {
|
||||
const result: {identifier: ts.Identifier, node: ts.ClassDeclaration}[] = [];
|
||||
let currentClass = node;
|
||||
|
||||
while (currentClass) {
|
||||
const baseTypes = getBaseTypeIdentifiers(currentClass);
|
||||
if (!baseTypes || baseTypes.length !== 1) {
|
||||
break;
|
||||
}
|
||||
const symbol = typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol();
|
||||
if (!symbol || !ts.isClassDeclaration(symbol.valueDeclaration)) {
|
||||
break;
|
||||
}
|
||||
result.push({identifier: baseTypes[0], node: symbol.valueDeclaration});
|
||||
currentClass = symbol.valueDeclaration;
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/**
|
||||
* @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 {dirname, resolve} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
/**
|
||||
* Import manager that can be used to add TypeScript imports to given source
|
||||
* files. The manager ensures that multiple transformations are applied properly
|
||||
* without shifted offsets and that similar existing import declarations are re-used.
|
||||
*/
|
||||
export class ImportManager {
|
||||
/** Map of import declarations that need to be updated to include the given symbols. */
|
||||
private updatedImports =
|
||||
new Map<ts.ImportDeclaration, {propertyName?: ts.Identifier, importName: ts.Identifier}[]>();
|
||||
/** Map of source-files and their previously used identifier names. */
|
||||
private usedIdentifierNames = new Map<ts.SourceFile, string[]>();
|
||||
/**
|
||||
* Array of previously resolved symbol imports. Cache can be re-used to return
|
||||
* the same identifier without checking the source-file again.
|
||||
*/
|
||||
private importCache: {
|
||||
sourceFile: ts.SourceFile,
|
||||
symbolName: string|null,
|
||||
moduleName: string,
|
||||
identifier: ts.Identifier
|
||||
}[] = [];
|
||||
|
||||
constructor(
|
||||
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder,
|
||||
private printer: ts.Printer) {}
|
||||
|
||||
/**
|
||||
* Adds an import to the given source-file and returns the TypeScript
|
||||
* identifier that can be used to access the newly imported symbol.
|
||||
*/
|
||||
addImportToSourceFile(
|
||||
sourceFile: ts.SourceFile, symbolName: string|null, moduleName: string,
|
||||
typeImport = false): ts.Expression {
|
||||
const sourceDir = dirname(sourceFile.fileName);
|
||||
let importStartIndex = 0;
|
||||
let existingImport: ts.ImportDeclaration|null = null;
|
||||
|
||||
// In case the given import has been already generated previously, we just return
|
||||
// the previous generated identifier in order to avoid duplicate generated imports.
|
||||
const cachedImport = this.importCache.find(
|
||||
c => c.sourceFile === sourceFile && c.symbolName === symbolName &&
|
||||
c.moduleName === moduleName);
|
||||
if (cachedImport) {
|
||||
return cachedImport.identifier;
|
||||
}
|
||||
|
||||
// Walk through all source-file top-level statements and search for import declarations
|
||||
// that already match the specified "moduleName" and can be updated to import the
|
||||
// given symbol. If no matching import can be found, the last import in the source-file
|
||||
// will be used as starting point for a new import that will be generated.
|
||||
for (let i = sourceFile.statements.length - 1; i >= 0; i--) {
|
||||
const statement = sourceFile.statements[i];
|
||||
|
||||
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) ||
|
||||
!statement.importClause) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (importStartIndex === 0) {
|
||||
importStartIndex = this._getEndPositionOfNode(statement);
|
||||
}
|
||||
|
||||
const moduleSpecifier = statement.moduleSpecifier.text;
|
||||
|
||||
if (moduleSpecifier.startsWith('.') &&
|
||||
resolve(sourceDir, moduleSpecifier) !== resolve(sourceDir, moduleName) ||
|
||||
moduleSpecifier !== moduleName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.importClause.namedBindings) {
|
||||
const namedBindings = statement.importClause.namedBindings;
|
||||
|
||||
// In case a "Type" symbol is imported, we can't use namespace imports
|
||||
// because these only export symbols available at runtime (no types)
|
||||
if (ts.isNamespaceImport(namedBindings) && !typeImport) {
|
||||
return ts.createPropertyAccess(
|
||||
ts.createIdentifier(namedBindings.name.text),
|
||||
ts.createIdentifier(symbolName || 'default'));
|
||||
} else if (ts.isNamedImports(namedBindings) && symbolName) {
|
||||
const existingElement = namedBindings.elements.find(
|
||||
e =>
|
||||
e.propertyName ? e.propertyName.text === symbolName : e.name.text === symbolName);
|
||||
|
||||
if (existingElement) {
|
||||
return ts.createIdentifier(existingElement.name.text);
|
||||
}
|
||||
|
||||
// In case the symbol could not be found in an existing import, we
|
||||
// keep track of the import declaration as it can be updated to include
|
||||
// the specified symbol name without having to create a new import.
|
||||
existingImport = statement;
|
||||
}
|
||||
} else if (statement.importClause.name && !symbolName) {
|
||||
return ts.createIdentifier(statement.importClause.name.text);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingImport) {
|
||||
const propertyIdentifier = ts.createIdentifier(symbolName !);
|
||||
const generatedUniqueIdentifier = this._getUniqueIdentifier(sourceFile, symbolName !);
|
||||
const needsGeneratedUniqueName = generatedUniqueIdentifier.text !== symbolName;
|
||||
const importName = needsGeneratedUniqueName ? generatedUniqueIdentifier : propertyIdentifier;
|
||||
|
||||
// Since it can happen that multiple classes need to be imported within the
|
||||
// specified source file and we want to add the identifiers to the existing
|
||||
// import declaration, we need to keep track of the updated import declarations.
|
||||
// We can't directly update the import declaration for each identifier as this
|
||||
// would throw off the recorder offsets. We need to keep track of the new identifiers
|
||||
// for the import and perform the import transformation as batches per source-file.
|
||||
this.updatedImports.set(
|
||||
existingImport, (this.updatedImports.get(existingImport) || []).concat({
|
||||
propertyName: needsGeneratedUniqueName ? propertyIdentifier : undefined,
|
||||
importName: importName,
|
||||
}));
|
||||
|
||||
// Keep track of all updated imports so that we don't generate duplicate
|
||||
// similar imports as these can't be statically analyzed in the source-file yet.
|
||||
this.importCache.push({sourceFile, moduleName, symbolName, identifier: importName});
|
||||
|
||||
return importName;
|
||||
}
|
||||
|
||||
let identifier: ts.Identifier|null = null;
|
||||
let newImport: ts.ImportDeclaration|null = null;
|
||||
|
||||
if (symbolName) {
|
||||
const propertyIdentifier = ts.createIdentifier(symbolName);
|
||||
const generatedUniqueIdentifier = this._getUniqueIdentifier(sourceFile, symbolName);
|
||||
const needsGeneratedUniqueName = generatedUniqueIdentifier.text !== symbolName;
|
||||
identifier = needsGeneratedUniqueName ? generatedUniqueIdentifier : propertyIdentifier;
|
||||
|
||||
newImport = ts.createImportDeclaration(
|
||||
undefined, undefined,
|
||||
ts.createImportClause(
|
||||
undefined,
|
||||
ts.createNamedImports([ts.createImportSpecifier(
|
||||
needsGeneratedUniqueName ? propertyIdentifier : undefined, identifier)])),
|
||||
ts.createStringLiteral(moduleName));
|
||||
} else {
|
||||
identifier = this._getUniqueIdentifier(sourceFile, 'defaultExport');
|
||||
newImport = ts.createImportDeclaration(
|
||||
undefined, undefined, ts.createImportClause(identifier, undefined),
|
||||
ts.createStringLiteral(moduleName));
|
||||
}
|
||||
|
||||
const newImportText = this.printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);
|
||||
// If the import is generated at the start of the source file, we want to add
|
||||
// a new-line after the import. Otherwise if the import is generated after an
|
||||
// existing import, we need to prepend a new-line so that the import is not on
|
||||
// the same line as the existing import anchor.
|
||||
this.getUpdateRecorder(sourceFile)
|
||||
.addNewImport(
|
||||
importStartIndex, importStartIndex === 0 ? `${newImportText}\n` : `\n${newImportText}`);
|
||||
|
||||
// Keep track of all generated imports so that we don't generate duplicate
|
||||
// similar imports as these can't be statically analyzed in the source-file yet.
|
||||
this.importCache.push({sourceFile, symbolName, moduleName, identifier});
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the collected import changes within the appropriate update recorders. The
|
||||
* updated imports can only be updated *once* per source-file because previous updates
|
||||
* could otherwise shift the source-file offsets.
|
||||
*/
|
||||
recordChanges() {
|
||||
this.updatedImports.forEach((expressions, importDecl) => {
|
||||
const sourceFile = importDecl.getSourceFile();
|
||||
const recorder = this.getUpdateRecorder(sourceFile);
|
||||
const namedBindings = importDecl.importClause !.namedBindings as ts.NamedImports;
|
||||
const newNamedBindings = ts.updateNamedImports(
|
||||
namedBindings,
|
||||
namedBindings.elements.concat(expressions.map(
|
||||
({propertyName, importName}) => ts.createImportSpecifier(propertyName, importName))));
|
||||
|
||||
const newNamedBindingsText =
|
||||
this.printer.printNode(ts.EmitHint.Unspecified, newNamedBindings, sourceFile);
|
||||
recorder.updateExistingImport(namedBindings, newNamedBindingsText);
|
||||
});
|
||||
}
|
||||
|
||||
/** Gets an unique identifier with a base name for the given source file. */
|
||||
private _getUniqueIdentifier(sourceFile: ts.SourceFile, baseName: string): ts.Identifier {
|
||||
if (this.isUniqueIdentifierName(sourceFile, baseName)) {
|
||||
this._recordUsedIdentifier(sourceFile, baseName);
|
||||
return ts.createIdentifier(baseName);
|
||||
}
|
||||
|
||||
let name = null;
|
||||
let counter = 1;
|
||||
do {
|
||||
name = `${baseName}_${counter++}`;
|
||||
} while (!this.isUniqueIdentifierName(sourceFile, name));
|
||||
|
||||
this._recordUsedIdentifier(sourceFile, name !);
|
||||
return ts.createIdentifier(name !);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified identifier name is used within the given
|
||||
* source file.
|
||||
*/
|
||||
private isUniqueIdentifierName(sourceFile: ts.SourceFile, name: string) {
|
||||
if (this.usedIdentifierNames.has(sourceFile) &&
|
||||
this.usedIdentifierNames.get(sourceFile) !.indexOf(name) !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walk through the source file and search for an identifier matching
|
||||
// the given name. In that case, it's not guaranteed that this name
|
||||
// is unique in the given declaration scope and we just return false.
|
||||
const nodeQueue: ts.Node[] = [sourceFile];
|
||||
while (nodeQueue.length) {
|
||||
const node = nodeQueue.shift() !;
|
||||
if (ts.isIdentifier(node) && node.text === name) {
|
||||
return false;
|
||||
}
|
||||
nodeQueue.push(...node.getChildren());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _recordUsedIdentifier(sourceFile: ts.SourceFile, identifierName: string) {
|
||||
this.usedIdentifierNames.set(
|
||||
sourceFile, (this.usedIdentifierNames.get(sourceFile) || []).concat(identifierName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the full end of a given node. By default the end position of a node is
|
||||
* before all trailing comments. This could mean that generated imports shift comments.
|
||||
*/
|
||||
private _getEndPositionOfNode(node: ts.Node) {
|
||||
const nodeEndPos = node.getEnd();
|
||||
const commentRanges = ts.getTrailingCommentRanges(node.getSourceFile().text, nodeEndPos);
|
||||
if (!commentRanges || !commentRanges.length) {
|
||||
return nodeEndPos;
|
||||
}
|
||||
return commentRanges[commentRanges.length - 1] !.end;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/**
|
||||
* @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 {logging} from '@angular-devkit/core';
|
||||
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {AotCompiler} from '@angular/compiler';
|
||||
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
|
||||
import {TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
|
||||
import {relative} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
|
||||
import {createNgcProgram} from './create_ngc_program';
|
||||
import {NgDeclarationCollector} from './ng_declaration_collector';
|
||||
import {UndecoratedClassesTransform} from './transform';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
const MIGRATION_RERUN_MESSAGE = 'Migration can be rerun with: "ng update @angular/core ' +
|
||||
'--from 8.0.0 --to 9.0.0 --migrate-only"';
|
||||
|
||||
const MIGRATION_AOT_FAILURE = 'This migration uses the Angular compiler internally and ' +
|
||||
'therefore projects that no longer build successfully after the update cannot run ' +
|
||||
'the migration. Please ensure there are no AOT compilation errors and rerun the migration.';
|
||||
|
||||
/** Entry point for the V9 "undecorated-classes-with-di" migration. */
|
||||
export default function(): Rule {
|
||||
return (tree: Tree, ctx: SchematicContext) => {
|
||||
const {buildPaths} = getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const failures: string[] = [];
|
||||
|
||||
ctx.logger.info('------ Undecorated classes with DI migration ------');
|
||||
|
||||
if (!buildPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot migrate undecorated derived classes and ' +
|
||||
'undecorated base classes which use DI.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of buildPaths) {
|
||||
failures.push(...runUndecoratedClassesMigration(tree, tsconfigPath, basePath, ctx.logger));
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
ctx.logger.info('Could not migrate all undecorated classes that use dependency');
|
||||
ctx.logger.info('injection. Please manually fix the following failures:');
|
||||
failures.forEach(message => ctx.logger.warn(`⮑ ${message}`));
|
||||
} else {
|
||||
ctx.logger.info('Successfully migrated all found undecorated classes');
|
||||
ctx.logger.info('that use dependency injection.');
|
||||
}
|
||||
|
||||
ctx.logger.info('----------------------------------------------');
|
||||
};
|
||||
}
|
||||
|
||||
function runUndecoratedClassesMigration(
|
||||
tree: Tree, tsconfigPath: string, basePath: string, logger: logging.LoggerApi): string[] {
|
||||
const failures: string[] = [];
|
||||
const programData = gracefullyCreateProgram(tree, basePath, tsconfigPath, logger);
|
||||
|
||||
// Gracefully exit if the program could not be created.
|
||||
if (programData === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {program, compiler} = programData;
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const partialEvaluator =
|
||||
new PartialEvaluator(new TypeScriptReflectionHost(typeChecker), typeChecker);
|
||||
const declarationCollector = new NgDeclarationCollector(typeChecker, partialEvaluator);
|
||||
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
|
||||
|
||||
// Analyze source files by detecting all directives, components and providers.
|
||||
rootSourceFiles.forEach(sourceFile => declarationCollector.visitNode(sourceFile));
|
||||
|
||||
const {decoratedDirectives, decoratedProviders, undecoratedDeclarations} = declarationCollector;
|
||||
const transform =
|
||||
new UndecoratedClassesTransform(typeChecker, compiler, partialEvaluator, getUpdateRecorder);
|
||||
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
|
||||
|
||||
// Run the migrations for decorated providers and both decorated and undecorated
|
||||
// directives. The transform failures are collected and converted into human-readable
|
||||
// failures which can be printed to the console.
|
||||
[...transform.migrateDecoratedDirectives(decoratedDirectives),
|
||||
...transform.migrateDecoratedProviders(decoratedProviders),
|
||||
...transform.migrateUndecoratedDeclarations(Array.from(undecoratedDeclarations))]
|
||||
.forEach(({node, message}) => {
|
||||
const nodeSourceFile = node.getSourceFile();
|
||||
const relativeFilePath = relative(basePath, nodeSourceFile.fileName);
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
|
||||
failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`);
|
||||
});
|
||||
|
||||
// Record the changes collected in the import manager and transformer.
|
||||
transform.recordChanges();
|
||||
|
||||
// Walk through each update recorder and commit the update. We need to commit the
|
||||
// updates in batches per source file as there can be only one recorder per source
|
||||
// file in order to avoid shifted character offsets.
|
||||
updateRecorders.forEach(recorder => recorder.commitUpdate());
|
||||
|
||||
return failures;
|
||||
|
||||
/** Gets the update recorder for the specified source file. */
|
||||
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
|
||||
if (updateRecorders.has(sourceFile)) {
|
||||
return updateRecorders.get(sourceFile) !;
|
||||
}
|
||||
const treeRecorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
const recorder: UpdateRecorder = {
|
||||
addClassComment(node: ts.ClassDeclaration, text: string) {
|
||||
treeRecorder.insertLeft(node.members.pos, `\n // ${text}\n`);
|
||||
},
|
||||
addClassDecorator(node: ts.ClassDeclaration, text: string) {
|
||||
// New imports should be inserted at the left while decorators should be inserted
|
||||
// at the right in order to ensure that imports are inserted before the decorator
|
||||
// if the start position of import and decorator is the source file start.
|
||||
treeRecorder.insertRight(node.getStart(), `${text}\n`);
|
||||
},
|
||||
addNewImport(start: number, importText: string) {
|
||||
// New imports should be inserted at the left while decorators should be inserted
|
||||
// at the right in order to ensure that imports are inserted before the decorator
|
||||
// if the start position of import and decorator is the source file start.
|
||||
treeRecorder.insertLeft(start, importText);
|
||||
},
|
||||
updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string) {
|
||||
treeRecorder.remove(namedBindings.getStart(), namedBindings.getWidth());
|
||||
treeRecorder.insertRight(namedBindings.getStart(), newNamedBindings);
|
||||
},
|
||||
commitUpdate() { tree.commitUpdate(treeRecorder); }
|
||||
};
|
||||
updateRecorders.set(sourceFile, recorder);
|
||||
return recorder;
|
||||
}
|
||||
}
|
||||
|
||||
function gracefullyCreateProgram(
|
||||
tree: Tree, basePath: string, tsconfigPath: string,
|
||||
logger: logging.LoggerApi): {compiler: AotCompiler, program: ts.Program}|null {
|
||||
try {
|
||||
const {ngcProgram, host, program, compiler} = createNgcProgram((options) => {
|
||||
const host = ts.createCompilerHost(options, true);
|
||||
|
||||
// We need to overwrite the host "readFile" method, as we want the TypeScript
|
||||
// program to be based on the file contents in the virtual file tree.
|
||||
host.readFile = fileName => {
|
||||
const buffer = tree.read(relative(basePath, fileName));
|
||||
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which
|
||||
// which breaks the CLI UpdateRecorder.
|
||||
// See: https://github.com/angular/angular/pull/30719
|
||||
return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined;
|
||||
};
|
||||
|
||||
return host;
|
||||
}, tsconfigPath);
|
||||
const syntacticDiagnostics = ngcProgram.getTsSyntacticDiagnostics();
|
||||
const structuralDiagnostics = ngcProgram.getNgStructuralDiagnostics();
|
||||
|
||||
// Syntactic TypeScript errors can throw off the query analysis and therefore we want
|
||||
// to notify the developer that we couldn't analyze parts of the project. Developers
|
||||
// can just re-run the migration after fixing these failures.
|
||||
if (syntacticDiagnostics.length) {
|
||||
logger.warn(
|
||||
`\nTypeScript project "${tsconfigPath}" has syntactical errors which could cause ` +
|
||||
`an incomplete migration. Please fix the following failures and rerun the migration:`);
|
||||
logger.error(ts.formatDiagnostics(syntacticDiagnostics, host));
|
||||
logger.info(MIGRATION_RERUN_MESSAGE);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (structuralDiagnostics.length) {
|
||||
throw new Error(ts.formatDiagnostics(<ts.Diagnostic[]>structuralDiagnostics, host));
|
||||
}
|
||||
|
||||
return {program, compiler};
|
||||
} catch (e) {
|
||||
logger.warn(`\n${MIGRATION_AOT_FAILURE}. The following project failed: ${tsconfigPath}\n`);
|
||||
logger.error(`${e.toString()}\n`);
|
||||
logger.info(MIGRATION_RERUN_MESSAGE);
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* @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 {Reference} from '@angular/compiler-cli/src/ngtsc/imports';
|
||||
import {PartialEvaluator, ResolvedValue} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {NgDecorator, getAngularDecorators} from '../../utils/ng_decorators';
|
||||
import {getPropertyNameText} from '../../utils/typescript/property_name';
|
||||
|
||||
|
||||
/**
|
||||
* Visitor that walks through specified TypeScript nodes and collects all defined
|
||||
* directives and provider classes. Directives are separated by decorated and
|
||||
* undecorated directives.
|
||||
*/
|
||||
export class NgDeclarationCollector {
|
||||
/** List of resolved directives which are decorated. */
|
||||
decoratedDirectives: ts.ClassDeclaration[] = [];
|
||||
|
||||
/** List of resolved providers which are decorated. */
|
||||
decoratedProviders: ts.ClassDeclaration[] = [];
|
||||
|
||||
/** Set of resolved Angular declarations which are not decorated. */
|
||||
undecoratedDeclarations = new Set<ts.ClassDeclaration>();
|
||||
|
||||
constructor(public typeChecker: ts.TypeChecker, private evaluator: PartialEvaluator) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
this._visitClassDeclaration(node);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, n => this.visitNode(n));
|
||||
}
|
||||
|
||||
private _visitClassDeclaration(node: ts.ClassDeclaration) {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
|
||||
const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule');
|
||||
|
||||
if (hasDirectiveDecorator(node, this.typeChecker, ngDecorators)) {
|
||||
this.decoratedDirectives.push(node);
|
||||
} else if (hasInjectableDecorator(node, this.typeChecker, ngDecorators)) {
|
||||
this.decoratedProviders.push(node);
|
||||
} else if (ngModuleDecorator) {
|
||||
this._visitNgModuleDecorator(ngModuleDecorator);
|
||||
}
|
||||
}
|
||||
|
||||
private _visitNgModuleDecorator(decorator: NgDecorator) {
|
||||
const decoratorCall = decorator.node.expression;
|
||||
const metadata = decoratorCall.arguments[0];
|
||||
|
||||
if (!metadata || !ts.isObjectLiteralExpression(metadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entryComponentsNode: ts.Expression|null = null;
|
||||
let declarationsNode: ts.Expression|null = null;
|
||||
|
||||
metadata.properties.forEach(p => {
|
||||
if (!ts.isPropertyAssignment(p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = getPropertyNameText(p.name);
|
||||
|
||||
if (name === 'entryComponents') {
|
||||
entryComponentsNode = p.initializer;
|
||||
} else if (name === 'declarations') {
|
||||
declarationsNode = p.initializer;
|
||||
}
|
||||
});
|
||||
|
||||
// In case the module specifies the "entryComponents" field, walk through all
|
||||
// resolved entry components and collect the referenced directives.
|
||||
if (entryComponentsNode) {
|
||||
flattenTypeList(this.evaluator.evaluate(entryComponentsNode)).forEach(ref => {
|
||||
if (ts.isClassDeclaration(ref.node) &&
|
||||
!hasNgDeclarationDecorator(ref.node, this.typeChecker)) {
|
||||
this.undecoratedDeclarations.add(ref.node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// In case the module specifies the "declarations" field, walk through all
|
||||
// resolved declarations and collect the referenced directives.
|
||||
if (declarationsNode) {
|
||||
flattenTypeList(this.evaluator.evaluate(declarationsNode)).forEach(ref => {
|
||||
if (ts.isClassDeclaration(ref.node) &&
|
||||
!hasNgDeclarationDecorator(ref.node, this.typeChecker)) {
|
||||
this.undecoratedDeclarations.add(ref.node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Flattens a list of type references. */
|
||||
function flattenTypeList(value: ResolvedValue): Reference[] {
|
||||
if (Array.isArray(value)) {
|
||||
return <Reference[]>value.reduce(
|
||||
(res: Reference[], v: ResolvedValue) => res.concat(flattenTypeList(v)), []);
|
||||
} else if (value instanceof Reference) {
|
||||
return [value];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Checks whether the given node has the "@Directive" or "@Component" decorator set. */
|
||||
export function hasDirectiveDecorator(
|
||||
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker, ngDecorators?: NgDecorator[]): boolean {
|
||||
return (ngDecorators || getNgClassDecorators(node, typeChecker))
|
||||
.some(({name}) => name === 'Directive' || name === 'Component');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Checks whether the given node has the "@Injectable" decorator set. */
|
||||
export function hasInjectableDecorator(
|
||||
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker, ngDecorators?: NgDecorator[]): boolean {
|
||||
return (ngDecorators || getNgClassDecorators(node, typeChecker))
|
||||
.some(({name}) => name === 'Injectable');
|
||||
}
|
||||
/** Whether the given node has an explicit decorator that describes an Angular declaration. */
|
||||
export function hasNgDeclarationDecorator(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker) {
|
||||
return getNgClassDecorators(node, typeChecker)
|
||||
.some(({name}) => name === 'Component' || name === 'Directive' || name === 'Pipe');
|
||||
}
|
||||
|
||||
/** Gets all Angular decorators of a given class declaration. */
|
||||
export function getNgClassDecorators(
|
||||
node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): NgDecorator[] {
|
||||
if (!node.decorators) {
|
||||
return [];
|
||||
}
|
||||
return getAngularDecorators(typeChecker, node.decorators);
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
/**
|
||||
* @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 {AotCompiler, AotCompilerHost, CompileMetadataResolver, StaticSymbol, StaticSymbolResolver, SummaryResolver} from '@angular/compiler';
|
||||
import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
|
||||
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {getAngularDecorators} from '../../utils/ng_decorators';
|
||||
import {hasExplicitConstructor} from '../../utils/typescript/class_declaration';
|
||||
import {getImportOfIdentifier} from '../../utils/typescript/imports';
|
||||
|
||||
import {UnexpectedMetadataValueError, convertDirectiveMetadataToExpression} from './decorator_rewrite/convert_directive_metadata';
|
||||
import {DecoratorRewriter} from './decorator_rewrite/decorator_rewriter';
|
||||
import {findBaseClassDeclarations} from './find_base_classes';
|
||||
import {ImportManager} from './import_manager';
|
||||
import {hasDirectiveDecorator, hasInjectableDecorator} from './ng_declaration_collector';
|
||||
import {UpdateRecorder} from './update_recorder';
|
||||
|
||||
|
||||
|
||||
/** Resolved metadata of a declaration. */
|
||||
interface DeclarationMetadata {
|
||||
metadata: any;
|
||||
type: 'Component'|'Directive'|'Pipe';
|
||||
}
|
||||
|
||||
export interface TransformFailure {
|
||||
node: ts.Node;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class UndecoratedClassesTransform {
|
||||
private printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
||||
private importManager = new ImportManager(this.getUpdateRecorder, this.printer);
|
||||
private decoratorRewriter =
|
||||
new DecoratorRewriter(this.importManager, this.typeChecker, this.evaluator, this.compiler);
|
||||
|
||||
private compilerHost: AotCompilerHost;
|
||||
private symbolResolver: StaticSymbolResolver;
|
||||
private metadataResolver: CompileMetadataResolver;
|
||||
|
||||
/** Set of class declarations which have been decorated with "@Directive". */
|
||||
private decoratedDirectives = new Set<ts.ClassDeclaration>();
|
||||
/** Set of class declarations which have been decorated with "@Injectable" */
|
||||
private decoratedProviders = new Set<ts.ClassDeclaration>();
|
||||
/**
|
||||
* Set of class declarations which have been analyzed and need to specify
|
||||
* an explicit constructor.
|
||||
*/
|
||||
private missingExplicitConstructorClasses = new Set<ts.ClassDeclaration>();
|
||||
|
||||
constructor(
|
||||
private typeChecker: ts.TypeChecker, private compiler: AotCompiler,
|
||||
private evaluator: PartialEvaluator,
|
||||
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {
|
||||
this.symbolResolver = compiler['_symbolResolver'];
|
||||
this.compilerHost = compiler['_host'];
|
||||
this.metadataResolver = compiler['_metadataResolver'];
|
||||
|
||||
// Unset the default error recorder so that the reflector will throw an exception
|
||||
// if metadata cannot be resolved.
|
||||
this.compiler.reflector['errorRecorder'] = undefined;
|
||||
|
||||
// Disables that static symbols are resolved through summaries from within the static
|
||||
// reflector. Summaries cannot be used for decorator serialization as decorators are
|
||||
// omitted in summaries and the decorator can't be reconstructed from the directive summary.
|
||||
this._disableSummaryResolution();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates decorated directives which can potentially inherit a constructor
|
||||
* from an undecorated base class. All base classes until the first one
|
||||
* with an explicit constructor will be decorated with the abstract "@Directive()"
|
||||
* decorator. See case 1 in the migration plan: https://hackmd.io/@alx/S1XKqMZeS
|
||||
*/
|
||||
migrateDecoratedDirectives(directives: ts.ClassDeclaration[]): TransformFailure[] {
|
||||
return directives.reduce(
|
||||
(failures, node) => failures.concat(this._migrateDirectiveBaseClass(node)),
|
||||
[] as TransformFailure[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates decorated providers which can potentially inherit a constructor
|
||||
* from an undecorated base class. All base classes until the first one
|
||||
* with an explicit constructor will be decorated with the "@Injectable()".
|
||||
*/
|
||||
migrateDecoratedProviders(providers: ts.ClassDeclaration[]): TransformFailure[] {
|
||||
return providers.reduce(
|
||||
(failures, node) => failures.concat(this._migrateProviderBaseClass(node)),
|
||||
[] as TransformFailure[]);
|
||||
}
|
||||
|
||||
private _migrateProviderBaseClass(node: ts.ClassDeclaration): TransformFailure[] {
|
||||
// In case the provider has an explicit constructor, we don't need to do anything
|
||||
// because the class is already decorated and does not inherit a constructor.
|
||||
if (hasExplicitConstructor(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const orderedBaseClasses = findBaseClassDeclarations(node, this.typeChecker);
|
||||
let lastDecoratedClass: ts.ClassDeclaration = node;
|
||||
|
||||
for (let {node: baseClass, identifier} of orderedBaseClasses) {
|
||||
const baseClassFile = baseClass.getSourceFile();
|
||||
|
||||
if (hasExplicitConstructor(baseClass)) {
|
||||
if (baseClassFile.isDeclarationFile) {
|
||||
const staticSymbol = this._getStaticSymbolOfIdentifier(identifier);
|
||||
|
||||
// If the base class is decorated through metadata files, we don't
|
||||
// need to add a comment to the derived class for the external base class.
|
||||
if (staticSymbol && this.metadataResolver.isInjectable(staticSymbol)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the base class is not decorated, we cannot decorate the base class and
|
||||
// need to a comment to the last decorated class.
|
||||
return this._addMissingExplicitConstructorTodo(lastDecoratedClass);
|
||||
}
|
||||
|
||||
this._addInjectableDecorator(baseClass);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the "@Injectable" decorator for all base classes in the inheritance chain
|
||||
// until the base class with the explicit constructor. The decorator will be only
|
||||
// added for base classes which can be modified.
|
||||
if (!baseClassFile.isDeclarationFile) {
|
||||
this._addInjectableDecorator(baseClass);
|
||||
lastDecoratedClass = baseClass;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private _migrateDirectiveBaseClass(node: ts.ClassDeclaration): TransformFailure[] {
|
||||
// In case the directive has an explicit constructor, we don't need to do
|
||||
// anything because the class is already decorated with "@Directive" or "@Component"
|
||||
if (hasExplicitConstructor(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const orderedBaseClasses = findBaseClassDeclarations(node, this.typeChecker);
|
||||
let lastDecoratedClass: ts.ClassDeclaration = node;
|
||||
|
||||
for (let {node: baseClass, identifier} of orderedBaseClasses) {
|
||||
const baseClassFile = baseClass.getSourceFile();
|
||||
|
||||
if (hasExplicitConstructor(baseClass)) {
|
||||
if (baseClassFile.isDeclarationFile) {
|
||||
// If the base class is decorated through metadata files, we don't
|
||||
// need to add a comment to the derived class for the external base class.
|
||||
if (this._hasDirectiveMetadata(identifier)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the base class is not decorated, we cannot decorate the base class and
|
||||
// need to a comment to the last decorated class.
|
||||
return this._addMissingExplicitConstructorTodo(lastDecoratedClass);
|
||||
}
|
||||
|
||||
this._addAbstractDirectiveDecorator(baseClass);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the abstract directive decorator for all base classes in the inheritance
|
||||
// chain until the base class with the explicit constructor. The decorator will
|
||||
// be only added for base classes which can be modified.
|
||||
if (!baseClassFile.isDeclarationFile) {
|
||||
this._addAbstractDirectiveDecorator(baseClass);
|
||||
lastDecoratedClass = baseClass;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the abstract "@Directive()" decorator to the given class in case there
|
||||
* is no existing directive decorator.
|
||||
*/
|
||||
private _addAbstractDirectiveDecorator(baseClass: ts.ClassDeclaration) {
|
||||
if (hasDirectiveDecorator(baseClass, this.typeChecker) ||
|
||||
this.decoratedDirectives.has(baseClass)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseClassFile = baseClass.getSourceFile();
|
||||
const recorder = this.getUpdateRecorder(baseClassFile);
|
||||
const directiveExpr =
|
||||
this.importManager.addImportToSourceFile(baseClassFile, 'Directive', '@angular/core');
|
||||
|
||||
const newDecorator = ts.createDecorator(ts.createCall(directiveExpr, undefined, []));
|
||||
const newDecoratorText =
|
||||
this.printer.printNode(ts.EmitHint.Unspecified, newDecorator, baseClassFile);
|
||||
|
||||
recorder.addClassDecorator(baseClass, newDecoratorText);
|
||||
this.decoratedDirectives.add(baseClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the abstract "@Injectable()" decorator to the given class in case there
|
||||
* is no existing directive decorator.
|
||||
*/
|
||||
private _addInjectableDecorator(baseClass: ts.ClassDeclaration) {
|
||||
if (hasInjectableDecorator(baseClass, this.typeChecker) ||
|
||||
this.decoratedProviders.has(baseClass)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseClassFile = baseClass.getSourceFile();
|
||||
const recorder = this.getUpdateRecorder(baseClassFile);
|
||||
const injectableExpr =
|
||||
this.importManager.addImportToSourceFile(baseClassFile, 'Injectable', '@angular/core');
|
||||
|
||||
const newDecorator = ts.createDecorator(ts.createCall(injectableExpr, undefined, []));
|
||||
const newDecoratorText =
|
||||
this.printer.printNode(ts.EmitHint.Unspecified, newDecorator, baseClassFile);
|
||||
|
||||
recorder.addClassDecorator(baseClass, newDecoratorText);
|
||||
this.decoratedProviders.add(baseClass);
|
||||
}
|
||||
|
||||
/** Adds a comment for adding an explicit constructor to the given class declaration. */
|
||||
private _addMissingExplicitConstructorTodo(node: ts.ClassDeclaration): TransformFailure[] {
|
||||
// In case a todo comment has been already inserted to the given class, we don't
|
||||
// want to add a comment or transform failure multiple times.
|
||||
if (this.missingExplicitConstructorClasses.has(node)) {
|
||||
return [];
|
||||
}
|
||||
this.missingExplicitConstructorClasses.add(node);
|
||||
const recorder = this.getUpdateRecorder(node.getSourceFile());
|
||||
recorder.addClassComment(node, 'TODO: add explicit constructor');
|
||||
return [{node: node, message: 'Class needs to declare an explicit constructor.'}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates undecorated directives which were referenced in NgModule declarations.
|
||||
* These directives inherit the metadata from a parent base class, but with Ivy
|
||||
* these classes need to explicitly have a decorator for locality. The migration
|
||||
* determines the inherited decorator and copies it to the undecorated declaration.
|
||||
*
|
||||
* Note that the migration serializes the metadata for external declarations
|
||||
* where the decorator is not part of the source file AST.
|
||||
*
|
||||
* See case 2 in the migration plan: https://hackmd.io/@alx/S1XKqMZeS
|
||||
*/
|
||||
migrateUndecoratedDeclarations(directives: ts.ClassDeclaration[]): TransformFailure[] {
|
||||
return directives.reduce(
|
||||
(failures, node) => failures.concat(this._migrateDerivedDeclaration(node)),
|
||||
[] as TransformFailure[]);
|
||||
}
|
||||
|
||||
private _migrateDerivedDeclaration(node: ts.ClassDeclaration): TransformFailure[] {
|
||||
const targetSourceFile = node.getSourceFile();
|
||||
const orderedBaseClasses = findBaseClassDeclarations(node, this.typeChecker);
|
||||
let newDecoratorText: string|null = null;
|
||||
|
||||
for (let {node: baseClass, identifier} of orderedBaseClasses) {
|
||||
// Before looking for decorators within the metadata or summary files, we
|
||||
// try to determine the directive decorator through the source file AST.
|
||||
if (baseClass.decorators) {
|
||||
const ngDecorator =
|
||||
getAngularDecorators(this.typeChecker, baseClass.decorators)
|
||||
.find(({name}) => name === 'Component' || name === 'Directive' || name === 'Pipe');
|
||||
|
||||
if (ngDecorator) {
|
||||
const newDecorator = this.decoratorRewriter.rewrite(ngDecorator, node.getSourceFile());
|
||||
newDecoratorText = this.printer.printNode(
|
||||
ts.EmitHint.Unspecified, newDecorator, ngDecorator.node.getSourceFile());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no metadata could be found within the source-file AST, try to find
|
||||
// decorator data through Angular metadata and summary files.
|
||||
const staticSymbol = this._getStaticSymbolOfIdentifier(identifier);
|
||||
|
||||
// Check if the static symbol resolves to a class declaration with
|
||||
// pipe or directive metadata.
|
||||
if (!staticSymbol ||
|
||||
!(this.metadataResolver.isPipe(staticSymbol) ||
|
||||
this.metadataResolver.isDirective(staticSymbol))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata = this._resolveDeclarationMetadata(staticSymbol);
|
||||
|
||||
// If no metadata could be resolved for the static symbol, print a failure message
|
||||
// and ask the developer to manually migrate the class. This case is rare because
|
||||
// usually decorator metadata is always present but just can't be read if a program
|
||||
// only has access to summaries (this is a special case in google3).
|
||||
if (!metadata) {
|
||||
return [{
|
||||
node,
|
||||
message: `Class cannot be migrated as the inherited metadata from ` +
|
||||
`${identifier.getText()} cannot be converted into a decorator. Please manually
|
||||
decorate the class.`,
|
||||
}];
|
||||
}
|
||||
|
||||
const newDecorator = this._constructDecoratorFromMetadata(metadata, targetSourceFile);
|
||||
if (!newDecorator) {
|
||||
const annotationType = metadata.type;
|
||||
return [{
|
||||
node,
|
||||
message: `Class cannot be migrated as the inherited @${annotationType} decorator ` +
|
||||
`cannot be copied. Please manually add a @${annotationType} decorator.`,
|
||||
}];
|
||||
}
|
||||
|
||||
// In case the decorator could be constructed from the resolved metadata, use
|
||||
// that decorator for the derived undecorated classes.
|
||||
newDecoratorText =
|
||||
this.printer.printNode(ts.EmitHint.Unspecified, newDecorator, targetSourceFile);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!newDecoratorText) {
|
||||
return [{
|
||||
node,
|
||||
message:
|
||||
'Class cannot be migrated as no directive/component/pipe metadata could be found. ' +
|
||||
'Please manually add a @Directive, @Component or @Pipe decorator.'
|
||||
}];
|
||||
}
|
||||
|
||||
this.getUpdateRecorder(targetSourceFile).addClassDecorator(node, newDecoratorText);
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Records all changes that were made in the import manager. */
|
||||
recordChanges() { this.importManager.recordChanges(); }
|
||||
|
||||
/**
|
||||
* Constructs a TypeScript decorator node from the specified declaration metadata. Returns
|
||||
* null if the metadata could not be simplified/resolved.
|
||||
*/
|
||||
private _constructDecoratorFromMetadata(
|
||||
directiveMetadata: DeclarationMetadata, targetSourceFile: ts.SourceFile): ts.Decorator|null {
|
||||
try {
|
||||
const decoratorExpr = convertDirectiveMetadataToExpression(
|
||||
directiveMetadata.metadata,
|
||||
staticSymbol =>
|
||||
this.compilerHost
|
||||
.fileNameToModuleName(staticSymbol.filePath, targetSourceFile.fileName)
|
||||
.replace(/\/index$/, ''),
|
||||
(moduleName: string, name: string) =>
|
||||
this.importManager.addImportToSourceFile(targetSourceFile, name, moduleName),
|
||||
(propertyName, value) => {
|
||||
// Only normalize properties called "changeDetection" and "encapsulation"
|
||||
// for "@Directive" and "@Component" annotations.
|
||||
if (directiveMetadata.type === 'Pipe') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Instead of using the number as value for the "changeDetection" and
|
||||
// "encapsulation" properties, we want to use the actual enum symbols.
|
||||
if (propertyName === 'changeDetection' && typeof value === 'number') {
|
||||
return ts.createPropertyAccess(
|
||||
this.importManager.addImportToSourceFile(
|
||||
targetSourceFile, 'ChangeDetectionStrategy', '@angular/core'),
|
||||
ChangeDetectionStrategy[value]);
|
||||
} else if (propertyName === 'encapsulation' && typeof value === 'number') {
|
||||
return ts.createPropertyAccess(
|
||||
this.importManager.addImportToSourceFile(
|
||||
targetSourceFile, 'ViewEncapsulation', '@angular/core'),
|
||||
ViewEncapsulation[value]);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return ts.createDecorator(ts.createCall(
|
||||
this.importManager.addImportToSourceFile(
|
||||
targetSourceFile, directiveMetadata.type, '@angular/core'),
|
||||
undefined, [decoratorExpr]));
|
||||
} catch (e) {
|
||||
if (e instanceof UnexpectedMetadataValueError) {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given identifier resolves to a class declaration that
|
||||
* has metadata for a directive.
|
||||
*/
|
||||
private _hasDirectiveMetadata(node: ts.Identifier): boolean {
|
||||
const symbol = this._getStaticSymbolOfIdentifier(node);
|
||||
|
||||
if (!symbol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.metadataResolver.isDirective(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the declaration metadata of a given static symbol. The metadata
|
||||
* is determined by resolving metadata for the static symbol.
|
||||
*/
|
||||
private _resolveDeclarationMetadata(symbol: StaticSymbol): null|DeclarationMetadata {
|
||||
try {
|
||||
// Note that this call can throw if the metadata is not computable. In that
|
||||
// case we are not able to serialize the metadata into a decorator and we return
|
||||
// null.
|
||||
const annotations = this.compiler.reflector.annotations(symbol).find(
|
||||
s => s.ngMetadataName === 'Component' || s.ngMetadataName === 'Directive' ||
|
||||
s.ngMetadataName === 'Pipe');
|
||||
|
||||
if (!annotations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {ngMetadataName, ...metadata} = annotations;
|
||||
|
||||
// Delete the "ngMetadataName" property as we don't want to generate
|
||||
// a property assignment in the new decorator for that internal property.
|
||||
delete metadata['ngMetadataName'];
|
||||
|
||||
return {type: ngMetadataName, metadata};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _getStaticSymbolOfIdentifier(node: ts.Identifier): StaticSymbol|null {
|
||||
const sourceFile = node.getSourceFile();
|
||||
const resolvedImport = getImportOfIdentifier(this.typeChecker, node);
|
||||
|
||||
if (!resolvedImport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moduleName =
|
||||
this.compilerHost.moduleNameToFileName(resolvedImport.importModule, sourceFile.fileName);
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the declaration symbol as symbols could be aliased due to
|
||||
// metadata re-exports.
|
||||
return this.compiler.reflector.findSymbolDeclaration(
|
||||
this.symbolResolver.getStaticSymbol(moduleName, resolvedImport.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables that static symbols are resolved through summaries. Summaries
|
||||
* cannot be used for decorator analysis as decorators are omitted in summaries.
|
||||
*/
|
||||
private _disableSummaryResolution() {
|
||||
// We never want to resolve symbols through summaries. Summaries never contain
|
||||
// decorators for class symbols and therefore summaries will cause every class
|
||||
// to be considered as undecorated. See reason for this in: "ToJsonSerializer".
|
||||
// In order to ensure that metadata is not retrieved through summaries, we
|
||||
// need to disable summary resolution, clear previous symbol caches. This way
|
||||
// future calls to "StaticReflector#annotations" are based on metadata files.
|
||||
this.symbolResolver['_resolveSymbolFromSummary'] = () => null;
|
||||
this.symbolResolver['resolvedSymbols'].clear();
|
||||
this.symbolResolver['resolvedFilePaths'].clear();
|
||||
this.compiler.reflector['annotationCache'].clear();
|
||||
|
||||
// Original summary resolver used by the AOT compiler.
|
||||
const summaryResolver = this.symbolResolver['summaryResolver'];
|
||||
|
||||
// Additionally we need to ensure that no files are treated as "library" files when
|
||||
// resolving metadata. This is necessary because by default the symbol resolver discards
|
||||
// class metadata for library files. See "StaticSymbolResolver#createResolvedSymbol".
|
||||
// Patching this function **only** for the static symbol resolver ensures that metadata
|
||||
// is not incorrectly omitted. Note that we only want to do this for the symbol resolver
|
||||
// because otherwise we could break the summary loading logic which is used to detect
|
||||
// if a static symbol is either a directive, component or pipe (see MetadataResolver).
|
||||
this.symbolResolver['summaryResolver'] = <SummaryResolver<StaticSymbol>>{
|
||||
fromSummaryFileName: summaryResolver.fromSummaryFileName.bind(summaryResolver),
|
||||
addSummary: summaryResolver.addSummary.bind(summaryResolver),
|
||||
getImportAs: summaryResolver.getImportAs.bind(summaryResolver),
|
||||
getKnownModuleName: summaryResolver.getKnownModuleName.bind(summaryResolver),
|
||||
resolveSummary: summaryResolver.resolveSummary.bind(summaryResolver),
|
||||
toSummaryFileName: summaryResolver.toSummaryFileName.bind(summaryResolver),
|
||||
getSymbolsOf: summaryResolver.getSymbolsOf.bind(summaryResolver),
|
||||
isLibraryFile: () => false,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* Update recorder interface that is used to transform source files in a non-colliding
|
||||
* way. Also this indirection makes it possible to re-use transformation logic with
|
||||
* different replacement tools (e.g. TSLint or CLI devkit).
|
||||
*/
|
||||
export interface UpdateRecorder {
|
||||
addNewImport(start: number, importText: string): void;
|
||||
updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string): void;
|
||||
addClassDecorator(node: ts.ClassDeclaration, text: string): void;
|
||||
addClassComment(node: ts.ClassDeclaration, text: string): void;
|
||||
commitUpdate(): void;
|
||||
}
|
|
@ -15,6 +15,7 @@ ts_library(
|
|||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
"//packages/core/schematics/migrations/static-queries",
|
||||
"//packages/core/schematics/migrations/template-var-assignment",
|
||||
"//packages/core/schematics/migrations/undecorated-classes-with-di",
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/core",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Template string function that can be used to dedent the resulting
|
||||
* string literal. The smallest common indentation will be omitted.
|
||||
*/
|
||||
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
let joinedString = '';
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
joinedString += `${strings[i]}${values[i]}`;
|
||||
}
|
||||
joinedString += strings[strings.length - 1];
|
||||
|
||||
const matches = joinedString.match(/^[ \t]*(?=\S)/gm);
|
||||
if (matches === null) {
|
||||
return joinedString;
|
||||
}
|
||||
|
||||
const minLineIndent = Math.min(...matches.map(el => el.length));
|
||||
const omitMinIndentRegex = new RegExp(`^[ \\t]{${minLineIndent}}`, 'gm');
|
||||
return minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,7 @@
|
|||
"types": [],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@angular/core": ["../"],
|
||||
"@angular/compiler": ["../../compiler"],
|
||||
"@angular/compiler/*": ["../../compiler/*"],
|
||||
"@angular/compiler-cli": ["../../compiler-cli"],
|
||||
|
|
|
@ -15,6 +15,7 @@ export type CallExpressionDecorator = ts.Decorator & {
|
|||
|
||||
export interface NgDecorator {
|
||||
name: string;
|
||||
moduleName: string;
|
||||
node: CallExpressionDecorator;
|
||||
importNode: ts.ImportDeclaration;
|
||||
}
|
||||
|
@ -30,6 +31,7 @@ export function getAngularDecorators(
|
|||
.map(({node, importData}) => ({
|
||||
node: node as CallExpressionDecorator,
|
||||
name: importData !.name,
|
||||
moduleName: importData !.importModule,
|
||||
importNode: importData !.node
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -30,3 +30,8 @@ export function findParentClassDeclaration(node: ts.Node): ts.ClassDeclaration|n
|
|||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Checks whether the given class declaration has an explicit constructor or not. */
|
||||
export function hasExplicitConstructor(node: ts.ClassDeclaration): boolean {
|
||||
return node.members.some(ts.isConstructorDeclaration);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @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';
|
||||
|
||||
export function getValueSymbolOfDeclaration(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol|
|
||||
undefined {
|
||||
let symbol = typeChecker.getSymbolAtLocation(node);
|
||||
|
||||
while (symbol && symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
|
||||
return symbol;
|
||||
}
|
Loading…
Reference in New Issue