|
| 1 | + |
| 2 | +/** |
| 3 | + * @license |
| 4 | + * Copyright Google Inc. All Rights Reserved. |
| 5 | + * |
| 6 | + * Use of this source code is governed by an MIT-style license that can be |
| 7 | + * found in the LICENSE file at https://angular.io/license |
| 8 | + */ |
| 9 | +import {AotCompiler} from '@angular/compiler'; |
| 10 | +import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator'; |
| 11 | +import * as ts from 'typescript'; |
| 12 | + |
| 13 | +import {NgDecorator} from '../../../utils/ng_decorators'; |
| 14 | +import {unwrapExpression} from '../../../utils/typescript/functions'; |
| 15 | +import {ImportManager} from '../import_manager'; |
| 16 | + |
| 17 | +import {ImportRewriteTransformerFactory, UnresolvedIdentifierError} from './import_rewrite_visitor'; |
| 18 | + |
| 19 | +/** |
| 20 | + * Class that can be used to copy decorators to a new location. The rewriter ensures that |
| 21 | + * identifiers and imports are rewritten to work in the new file location. Fields in a |
| 22 | + * decorator that cannot be cleanly copied will be copied with a comment explaining that |
| 23 | + * imports and identifiers need to be adjusted manually. |
| 24 | + */ |
| 25 | +export class DecoratorRewriter { |
| 26 | + previousSourceFile: ts.SourceFile|null = null; |
| 27 | + newSourceFile: ts.SourceFile|null = null; |
| 28 | + |
| 29 | + newProperties: ts.ObjectLiteralElementLike[] = []; |
| 30 | + nonCopyableProperties: ts.ObjectLiteralElementLike[] = []; |
| 31 | + |
| 32 | + private importRewriterFactory = new ImportRewriteTransformerFactory( |
| 33 | + this.importManager, this.typeChecker, this.compiler['_host']); |
| 34 | + |
| 35 | + constructor( |
| 36 | + private importManager: ImportManager, private typeChecker: ts.TypeChecker, |
| 37 | + private evaluator: PartialEvaluator, private compiler: AotCompiler) {} |
| 38 | + |
| 39 | + rewrite(ngDecorator: NgDecorator, newSourceFile: ts.SourceFile): ts.Decorator { |
| 40 | + const decorator = ngDecorator.node; |
| 41 | + |
| 42 | + // Reset the previous state of the decorator rewriter. |
| 43 | + this.newProperties = []; |
| 44 | + this.nonCopyableProperties = []; |
| 45 | + this.newSourceFile = newSourceFile; |
| 46 | + this.previousSourceFile = decorator.getSourceFile(); |
| 47 | + |
| 48 | + // If the decorator will be added to the same source file it currently |
| 49 | + // exists in, we don't need to rewrite any paths or add new imports. |
| 50 | + if (this.previousSourceFile === newSourceFile) { |
| 51 | + return this._createDecorator(decorator.expression); |
| 52 | + } |
| 53 | + |
| 54 | + const oldCallExpr = decorator.expression; |
| 55 | + |
| 56 | + if (!oldCallExpr.arguments.length) { |
| 57 | + // Re-use the original decorator if there are no arguments and nothing needs |
| 58 | + // to be sanitized or rewritten. |
| 59 | + return this._createDecorator(decorator.expression); |
| 60 | + } |
| 61 | + |
| 62 | + const metadata = unwrapExpression(oldCallExpr.arguments[0]); |
| 63 | + if (!ts.isObjectLiteralExpression(metadata)) { |
| 64 | + // Re-use the original decorator as there is no metadata that can be sanitized. |
| 65 | + return this._createDecorator(decorator.expression); |
| 66 | + } |
| 67 | + |
| 68 | + metadata.properties.forEach(prop => { |
| 69 | + // We don't handle spread assignments, accessors or method declarations automatically |
| 70 | + // as it involves more advanced static analysis and these type of properties are not |
| 71 | + // picked up by ngc either. |
| 72 | + if (ts.isSpreadAssignment(prop) || ts.isAccessor(prop) || ts.isMethodDeclaration(prop)) { |
| 73 | + this.nonCopyableProperties.push(prop); |
| 74 | + return; |
| 75 | + } |
| 76 | + |
| 77 | + const sanitizedProp = this._sanitizeMetadataProperty(prop); |
| 78 | + if (sanitizedProp !== null) { |
| 79 | + this.newProperties.push(sanitizedProp); |
| 80 | + } else { |
| 81 | + this.nonCopyableProperties.push(prop); |
| 82 | + } |
| 83 | + }); |
| 84 | + |
| 85 | + // In case there is at least one non-copyable property, we add a leading comment to |
| 86 | + // the first property assignment in order to ask the developer to manually manage |
| 87 | + // imports and do path rewriting for these properties. |
| 88 | + if (this.nonCopyableProperties.length !== 0) { |
| 89 | + ['The following fields were copied from the base class,', |
| 90 | + 'but could not be updated automatically to work in the', |
| 91 | + 'new file location. Please add any required imports for', 'the properties below:'] |
| 92 | + .forEach( |
| 93 | + text => ts.addSyntheticLeadingComment( |
| 94 | + this.nonCopyableProperties[0], ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, |
| 95 | + true)); |
| 96 | + } |
| 97 | + |
| 98 | + // Note that we don't update the decorator as we don't want to copy potential leading |
| 99 | + // comments of the decorator. This is necessary because otherwise comments from the |
| 100 | + // copied decorator end up describing the new class (which is not always correct). |
| 101 | + return this._createDecorator(ts.createCall( |
| 102 | + this.importManager.addImportToSourceFile( |
| 103 | + newSourceFile, ngDecorator.name, ngDecorator.moduleName), |
| 104 | + undefined, [ts.updateObjectLiteral( |
| 105 | + metadata, [...this.newProperties, ...this.nonCopyableProperties])])); |
| 106 | + } |
| 107 | + |
| 108 | + /** Creates a new decorator with the given expression. */ |
| 109 | + private _createDecorator(expr: ts.Expression): ts.Decorator { |
| 110 | + // Note that we don't update the decorator as we don't want to copy potential leading |
| 111 | + // comments of the decorator. This is necessary because otherwise comments from the |
| 112 | + // copied decorator end up describing the new class (which is not always correct). |
| 113 | + return ts.createDecorator(expr); |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Sanitizes a metadata property by ensuring that all contained identifiers |
| 118 | + * are imported in the target source file. |
| 119 | + */ |
| 120 | + private _sanitizeMetadataProperty(prop: ts.ObjectLiteralElementLike): ts.ObjectLiteralElementLike |
| 121 | + |null { |
| 122 | + try { |
| 123 | + return ts |
| 124 | + .transform(prop, [ctx => this.importRewriterFactory.create(ctx, this.newSourceFile !)]) |
| 125 | + .transformed[0]; |
| 126 | + } catch (e) { |
| 127 | + // If the error is for an unresolved identifier, we want to return "null" because |
| 128 | + // such object literal elements could be added to the non-copyable properties. |
| 129 | + if (e instanceof UnresolvedIdentifierError) { |
| 130 | + return null; |
| 131 | + } |
| 132 | + throw e; |
| 133 | + } |
| 134 | + } |
| 135 | +} |
0 commit comments