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", | ||||
|  | ||||
							
								
								
									
										28
									
								
								packages/core/schematics/test/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								packages/core/schematics/test/helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
| } | ||||
|  | ||||
							
								
								
									
										20
									
								
								packages/core/schematics/utils/typescript/symbol.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/core/schematics/utils/typescript/symbol.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user