feat(core): add automatic migration from Renderer to Renderer2 (#30936)
Adds a schematic and tslint rule that automatically migrate the consumer from `Renderer` to `Renderer2`. Supports:
* Renaming imports.
* Renaming property and method argument types.
* Casting to `Renderer`.
* Mapping all of the methods from the `Renderer` to `Renderer2`.
Note that some of the `Renderer` methods don't map cleanly between renderers. In these cases the migration adds a helper function at the bottom of the file which ensures that we generate valid code with the same return value as before. E.g. here's what the migration for `createText` looks like.
Before:
```
class SomeComponent {
  createAndAddText() {
    const node = this._renderer.createText(this._element.nativeElement, 'hello');
    node.textContent += ' world';
  }
}
```
After:
```
class SomeComponent {
  createAndAddText() {
    const node = __rendererCreateTextHelper(this._renderer, this._element.nativeElement, 'hello');
    node.textContent += ' world';
  }
}
function __rendererCreateTextHelper(renderer: any, parent: any, value: any) {
  const node = renderer.createText(value);
  if (parent) {
    renderer.appendChild(parent, node);
  }
  return node;
}
```
This PR resolves FW-1344.
PR Close #30936
			
			
This commit is contained in:
		
							parent
							
								
									9515f171b4
								
							
						
					
					
						commit
						c0955975f4
					
				| @ -12,6 +12,7 @@ npm_package( | |||||||
|     deps = [ |     deps = [ | ||||||
|         "//packages/core/schematics/migrations/injectable-pipe", |         "//packages/core/schematics/migrations/injectable-pipe", | ||||||
|         "//packages/core/schematics/migrations/move-document", |         "//packages/core/schematics/migrations/move-document", | ||||||
|  |         "//packages/core/schematics/migrations/renderer-to-renderer2", | ||||||
|         "//packages/core/schematics/migrations/static-queries", |         "//packages/core/schematics/migrations/static-queries", | ||||||
|         "//packages/core/schematics/migrations/template-var-assignment", |         "//packages/core/schematics/migrations/template-var-assignment", | ||||||
|     ], |     ], | ||||||
|  | |||||||
| @ -14,6 +14,11 @@ | |||||||
|       "version": "8-beta", |       "version": "8-beta", | ||||||
|       "description": "Warns developers if values are assigned to template variables", |       "description": "Warns developers if values are assigned to template variables", | ||||||
|       "factory": "./migrations/template-var-assignment/index" |       "factory": "./migrations/template-var-assignment/index" | ||||||
|  |     }, | ||||||
|  |     "migration-v9-renderer-to-renderer2": { | ||||||
|  |       "version": "9-beta", | ||||||
|  |       "description": "Migrates usages of Renderer to Renderer2", | ||||||
|  |       "factory": "./migrations/renderer-to-renderer2/index" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,18 @@ | |||||||
|  | load("//tools:defaults.bzl", "ts_library") | ||||||
|  | 
 | ||||||
|  | ts_library( | ||||||
|  |     name = "renderer-to-renderer2", | ||||||
|  |     srcs = glob(["**/*.ts"]), | ||||||
|  |     tsconfig = "//packages/core/schematics:tsconfig.json", | ||||||
|  |     visibility = [ | ||||||
|  |         "//packages/core/schematics:__pkg__", | ||||||
|  |         "//packages/core/schematics/migrations/renderer-to-renderer2/google3:__pkg__", | ||||||
|  |         "//packages/core/schematics/test:__pkg__", | ||||||
|  |     ], | ||||||
|  |     deps = [ | ||||||
|  |         "//packages/core/schematics/utils", | ||||||
|  |         "@npm//@angular-devkit/schematics", | ||||||
|  |         "@npm//@types/node", | ||||||
|  |         "@npm//typescript", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | ## Renderer -> Renderer2 migration | ||||||
|  | 
 | ||||||
|  | Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports | ||||||
|  | and renaming types. Tries to either map method calls directly from one renderer to the other, or | ||||||
|  | if that's not possible, inserts custom helper functions at the bottom of the file. | ||||||
|  | 
 | ||||||
|  | #### Before | ||||||
|  | ```ts | ||||||
|  | import { Renderer, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | @Component({}) | ||||||
|  | export class MyComponent { | ||||||
|  |   constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} | ||||||
|  | 
 | ||||||
|  |   changeColor() { | ||||||
|  |     this._renderer.setElementStyle(this._element.nativeElement, 'color', 'purple'); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### After | ||||||
|  | ```ts | ||||||
|  | import { Renderer2, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | @Component({}) | ||||||
|  | export class MyComponent { | ||||||
|  |   constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} | ||||||
|  | 
 | ||||||
|  |   changeColor() { | ||||||
|  |     this._renderer.setStyle(this._element.nativeElement, 'color', 'purple'); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
| @ -0,0 +1,13 @@ | |||||||
|  | load("//tools:defaults.bzl", "ts_library") | ||||||
|  | 
 | ||||||
|  | ts_library( | ||||||
|  |     name = "google3", | ||||||
|  |     srcs = glob(["**/*.ts"]), | ||||||
|  |     tsconfig = "//packages/core/schematics:tsconfig.json", | ||||||
|  |     visibility = ["//packages/core/schematics/test:__pkg__"], | ||||||
|  |     deps = [ | ||||||
|  |         "//packages/core/schematics/migrations/renderer-to-renderer2", | ||||||
|  |         "@npm//tslint", | ||||||
|  |         "@npm//typescript", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
| @ -0,0 +1,139 @@ | |||||||
|  | /** | ||||||
|  |  * @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 {Replacement, RuleFailure, Rules} from 'tslint'; | ||||||
|  | import * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {HelperFunction, getHelper} from '../helpers'; | ||||||
|  | import {migrateExpression, replaceImport} from '../migration'; | ||||||
|  | import {findCoreImport, findRendererReferences} from '../util'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works: | ||||||
|  |  * https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw
 | ||||||
|  |  */ | ||||||
|  | export class Rule extends Rules.TypedRule { | ||||||
|  |   applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { | ||||||
|  |     const typeChecker = program.getTypeChecker(); | ||||||
|  |     const printer = ts.createPrinter(); | ||||||
|  |     const failures: RuleFailure[] = []; | ||||||
|  |     const rendererImport = findCoreImport(sourceFile, 'Renderer'); | ||||||
|  | 
 | ||||||
|  |     // If there are no imports for the `Renderer`, we can exit early.
 | ||||||
|  |     if (!rendererImport) { | ||||||
|  |       return failures; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const {typedNodes, methodCalls, forwardRefs} = | ||||||
|  |         findRendererReferences(sourceFile, typeChecker, rendererImport); | ||||||
|  |     const helpersToAdd = new Set<HelperFunction>(); | ||||||
|  | 
 | ||||||
|  |     failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer)); | ||||||
|  |     typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile))); | ||||||
|  |     forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile))); | ||||||
|  | 
 | ||||||
|  |     methodCalls.forEach(call => { | ||||||
|  |       const {failure, requiredHelpers} = | ||||||
|  |           this._getMethodCallFailure(call, sourceFile, typeChecker, printer); | ||||||
|  | 
 | ||||||
|  |       failures.push(failure); | ||||||
|  | 
 | ||||||
|  |       if (requiredHelpers) { | ||||||
|  |         requiredHelpers.forEach(helperName => helpersToAdd.add(helperName)); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them.
 | ||||||
|  |     // The safest way to do so is to declare helper functions similar to the ones emitted by TS
 | ||||||
|  |     // which encapsulate the extra "glue" logic. We should only emit these functions once per
 | ||||||
|  |     // file and only if they're needed.
 | ||||||
|  |     if (helpersToAdd.size) { | ||||||
|  |       failures.push(this._getHelpersFailure(helpersToAdd, sourceFile, printer)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return failures; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Gets a failure for an import of the Renderer. */ | ||||||
|  |   private _getNamedImportsFailure( | ||||||
|  |       node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure { | ||||||
|  |     const replacementText = printer.printNode( | ||||||
|  |         ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile); | ||||||
|  | 
 | ||||||
|  |     return new RuleFailure( | ||||||
|  |         sourceFile, node.getStart(), node.getEnd(), | ||||||
|  |         'Imports of deprecated Renderer are not allowed. Please use Renderer2 instead.', | ||||||
|  |         this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Gets a failure for a typed node (e.g. function parameter or property). */ | ||||||
|  |   private _getTypedNodeFailure( | ||||||
|  |       node: ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression, | ||||||
|  |       sourceFile: ts.SourceFile): RuleFailure { | ||||||
|  |     const type = node.type !; | ||||||
|  | 
 | ||||||
|  |     return new RuleFailure( | ||||||
|  |         sourceFile, type.getStart(), type.getEnd(), | ||||||
|  |         'References to deprecated Renderer are not allowed. Please use Renderer2 instead.', | ||||||
|  |         this.ruleName, new Replacement(type.getStart(), type.getWidth(), 'Renderer2')); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Gets a failure for an identifier node. */ | ||||||
|  |   private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure { | ||||||
|  |     return new RuleFailure( | ||||||
|  |         sourceFile, node.getStart(), node.getEnd(), | ||||||
|  |         'References to deprecated Renderer are not allowed. Please use Renderer2 instead.', | ||||||
|  |         this.ruleName, new Replacement(node.getStart(), node.getWidth(), 'Renderer2')); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Gets a failure for a Renderer method call. */ | ||||||
|  |   private _getMethodCallFailure( | ||||||
|  |       call: ts.CallExpression, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, | ||||||
|  |       printer: ts.Printer): {failure: RuleFailure, requiredHelpers?: HelperFunction[]} { | ||||||
|  |     const {node, requiredHelpers} = migrateExpression(call, typeChecker); | ||||||
|  |     let fix: Replacement|undefined; | ||||||
|  | 
 | ||||||
|  |     if (node) { | ||||||
|  |       // If we migrated the node to a new expression, replace only the call expression.
 | ||||||
|  |       fix = new Replacement( | ||||||
|  |           call.getStart(), call.getWidth(), | ||||||
|  |           printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)); | ||||||
|  |     } else if (call.parent && ts.isExpressionStatement(call.parent)) { | ||||||
|  |       // Otherwise if the call is inside an expression statement, drop the entire statement.
 | ||||||
|  |       // This takes care of any trailing semicolons. We only need to drop nodes for cases like
 | ||||||
|  |       // `setBindingDebugInfo` which have been noop for a while so they can be removed safely.
 | ||||||
|  |       fix = new Replacement(call.parent.getStart(), call.parent.getWidth(), ''); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       failure: new RuleFailure( | ||||||
|  |           sourceFile, call.getStart(), call.getEnd(), 'Calls to Renderer methods are not allowed', | ||||||
|  |           this.ruleName, fix), | ||||||
|  |       requiredHelpers | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Gets a failure that inserts the required helper functions at the bottom of the file. */ | ||||||
|  |   private _getHelpersFailure( | ||||||
|  |       helpersToAdd: Set<HelperFunction>, sourceFile: ts.SourceFile, | ||||||
|  |       printer: ts.Printer): RuleFailure { | ||||||
|  |     const helpers: Replacement[] = []; | ||||||
|  |     const endOfFile = sourceFile.endOfFileToken; | ||||||
|  | 
 | ||||||
|  |     helpersToAdd.forEach(helperName => { | ||||||
|  |       helpers.push(new Replacement( | ||||||
|  |           endOfFile.getStart(), endOfFile.getWidth(), getHelper(helperName, sourceFile, printer))); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Add a failure at the end of the file which we can use as an anchor to insert the helpers.
 | ||||||
|  |     return new RuleFailure( | ||||||
|  |         sourceFile, endOfFile.getStart(), endOfFile.getStart() + 1, | ||||||
|  |         'File should contain Renderer helper functions. Run tslint with --fix to generate them.', | ||||||
|  |         this.ruleName, helpers); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,403 @@ | |||||||
|  | /** | ||||||
|  |  * @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'; | ||||||
|  | 
 | ||||||
|  | /** Names of the helper functions that are supported for this migration. */ | ||||||
|  | export const enum HelperFunction { | ||||||
|  |   any = 'AnyDuringRendererMigration', | ||||||
|  |   createElement = '__ngRendererCreateElementHelper', | ||||||
|  |   createText = '__ngRendererCreateTextHelper', | ||||||
|  |   createTemplateAnchor = '__ngRendererCreateTemplateAnchorHelper', | ||||||
|  |   projectNodes = '__ngRendererProjectNodesHelper', | ||||||
|  |   animate = '__ngRendererAnimateHelper', | ||||||
|  |   destroyView = '__ngRendererDestroyViewHelper', | ||||||
|  |   detachView = '__ngRendererDetachViewHelper', | ||||||
|  |   attachViewAfter = '__ngRendererAttachViewAfterHelper', | ||||||
|  |   splitNamespace = '__ngRendererSplitNamespaceHelper', | ||||||
|  |   setElementAttribute = '__ngRendererSetElementAttributeHelper' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Gets the string representation of a helper function. */ | ||||||
|  | export function getHelper( | ||||||
|  |     name: HelperFunction, sourceFile: ts.SourceFile, printer: ts.Printer): string { | ||||||
|  |   const helperDeclaration = getHelperDeclaration(name); | ||||||
|  |   return '\n' + printer.printNode(ts.EmitHint.Unspecified, helperDeclaration, sourceFile) + '\n'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a function declaration for the specified helper name. */ | ||||||
|  | function getHelperDeclaration(name: HelperFunction): ts.Node { | ||||||
|  |   switch (name) { | ||||||
|  |     case HelperFunction.any: | ||||||
|  |       return createAnyTypeHelper(); | ||||||
|  |     case HelperFunction.createElement: | ||||||
|  |       return getCreateElementHelper(); | ||||||
|  |     case HelperFunction.createText: | ||||||
|  |       return getCreateTextHelper(); | ||||||
|  |     case HelperFunction.createTemplateAnchor: | ||||||
|  |       return getCreateTemplateAnchorHelper(); | ||||||
|  |     case HelperFunction.projectNodes: | ||||||
|  |       return getProjectNodesHelper(); | ||||||
|  |     case HelperFunction.animate: | ||||||
|  |       return getAnimateHelper(); | ||||||
|  |     case HelperFunction.destroyView: | ||||||
|  |       return getDestroyViewHelper(); | ||||||
|  |     case HelperFunction.detachView: | ||||||
|  |       return getDetachViewHelper(); | ||||||
|  |     case HelperFunction.attachViewAfter: | ||||||
|  |       return getAttachViewAfterHelper(); | ||||||
|  |     case HelperFunction.setElementAttribute: | ||||||
|  |       return getSetElementAttributeHelper(); | ||||||
|  |     case HelperFunction.splitNamespace: | ||||||
|  |       return getSplitNamespaceHelper(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   throw new Error(`Unsupported helper called "${name}".`); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for a custom `any` type during the migration. */ | ||||||
|  | function createAnyTypeHelper(): ts.TypeAliasDeclaration { | ||||||
|  |   // type AnyDuringRendererMigration = any;
 | ||||||
|  |   return ts.createTypeAliasDeclaration( | ||||||
|  |       [], [], HelperFunction.any, [], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a function parameter that is typed as `any`. */ | ||||||
|  | function getAnyTypedParameter( | ||||||
|  |     parameterName: string | ts.Identifier, isRequired = true): ts.ParameterDeclaration { | ||||||
|  |   // Declare the parameter as `any` so we don't have to add extra logic to ensure that the
 | ||||||
|  |   // generated code will pass type checking. Use our custom `any` type so people have an incentive
 | ||||||
|  |   // to clean it up afterwards and to avoid potentially introducing lint warnings in G3.
 | ||||||
|  |   const type = ts.createTypeReferenceNode(HelperFunction.any, []); | ||||||
|  |   return ts.createParameter( | ||||||
|  |       [], [], undefined, parameterName, | ||||||
|  |       isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken), type); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `createElement`. */ | ||||||
|  | function getCreateElementHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const parent = ts.createIdentifier('parent'); | ||||||
|  |   const namespaceAndName = ts.createIdentifier('namespaceAndName'); | ||||||
|  |   const name = ts.createIdentifier('name'); | ||||||
|  |   const namespace = ts.createIdentifier('namespace'); | ||||||
|  | 
 | ||||||
|  |   // [namespace, name] = splitNamespace(namespaceAndName);
 | ||||||
|  |   const namespaceAndNameVariable = ts.createVariableDeclaration( | ||||||
|  |       ts.createArrayBindingPattern( | ||||||
|  |           [namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))), | ||||||
|  |       undefined, | ||||||
|  |       ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName])); | ||||||
|  | 
 | ||||||
|  |   // `renderer.createElement(name, namespace)`.
 | ||||||
|  |   const creationCall = | ||||||
|  |       ts.createCall(ts.createPropertyAccess(renderer, 'createElement'), [], [name, namespace]); | ||||||
|  | 
 | ||||||
|  |   return getCreationHelper( | ||||||
|  |       HelperFunction.createElement, creationCall, renderer, parent, [namespaceAndName], | ||||||
|  |       [ts.createVariableStatement( | ||||||
|  |           undefined, | ||||||
|  |           ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const))]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `createText`. */ | ||||||
|  | function getCreateTextHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const parent = ts.createIdentifier('parent'); | ||||||
|  |   const value = ts.createIdentifier('value'); | ||||||
|  | 
 | ||||||
|  |   // `renderer.createText(value)`.
 | ||||||
|  |   const creationCall = ts.createCall(ts.createPropertyAccess(renderer, 'createText'), [], [value]); | ||||||
|  | 
 | ||||||
|  |   return getCreationHelper(HelperFunction.createText, creationCall, renderer, parent, [value]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `createTemplateAnchor`. */ | ||||||
|  | function getCreateTemplateAnchorHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const parent = ts.createIdentifier('parent'); | ||||||
|  | 
 | ||||||
|  |   // `renderer.createComment('')`.
 | ||||||
|  |   const creationCall = ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'createComment'), [], [ts.createStringLiteral('')]); | ||||||
|  | 
 | ||||||
|  |   return getCreationHelper(HelperFunction.createTemplateAnchor, creationCall, renderer, parent); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Gets the function declaration for a creation helper. This is reused between `createElement`, | ||||||
|  |  * `createText` and `createTemplateAnchor` which follow a very similar pattern. | ||||||
|  |  * @param functionName Function that the helper should have. | ||||||
|  |  * @param creationCall Expression that is used to create a node inside the function. | ||||||
|  |  * @param rendererParameter Parameter for the `renderer`. | ||||||
|  |  * @param parentParameter Parameter for the `parent` inside the function. | ||||||
|  |  * @param extraParameters Extra parameters to be added to the end. | ||||||
|  |  * @param precedingVariables Extra variables to be added before the one that creates the `node`. | ||||||
|  |  */ | ||||||
|  | function getCreationHelper( | ||||||
|  |     functionName: HelperFunction, creationCall: ts.CallExpression, renderer: ts.Identifier, | ||||||
|  |     parent: ts.Identifier, extraParameters: ts.Identifier[] = [], | ||||||
|  |     precedingVariables: ts.VariableStatement[] = []): ts.FunctionDeclaration { | ||||||
|  |   const node = ts.createIdentifier('node'); | ||||||
|  | 
 | ||||||
|  |   // `const node = {{creationCall}}`.
 | ||||||
|  |   const nodeVariableStatement = ts.createVariableStatement( | ||||||
|  |       undefined, | ||||||
|  |       ts.createVariableDeclarationList( | ||||||
|  |           [ts.createVariableDeclaration(node, undefined, creationCall)], ts.NodeFlags.Const)); | ||||||
|  | 
 | ||||||
|  |   // `if (parent) { renderer.appendChild(parent, node) }`.
 | ||||||
|  |   const guardedAppendChildCall = ts.createIf( | ||||||
|  |       parent, ts.createBlock( | ||||||
|  |                   [ts.createExpressionStatement(ts.createCall( | ||||||
|  |                       ts.createPropertyAccess(renderer, 'appendChild'), [], [parent, node]))], | ||||||
|  |                   true)); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, functionName, [], | ||||||
|  |       [renderer, parent, ...extraParameters].map(name => getAnyTypedParameter(name)), undefined, | ||||||
|  |       ts.createBlock( | ||||||
|  |           [ | ||||||
|  |             ...precedingVariables, nodeVariableStatement, guardedAppendChildCall, | ||||||
|  |             ts.createReturn(node) | ||||||
|  |           ], | ||||||
|  |           true)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `projectNodes`. */ | ||||||
|  | function getProjectNodesHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const parent = ts.createIdentifier('parent'); | ||||||
|  |   const nodes = ts.createIdentifier('nodes'); | ||||||
|  |   const incrementor = ts.createIdentifier('i'); | ||||||
|  | 
 | ||||||
|  |   // for (let i = 0; i < nodes.length; i++) {
 | ||||||
|  |   //   renderer.appendChild(parent, nodes[i]);
 | ||||||
|  |   // }
 | ||||||
|  |   const loopInitializer = ts.createVariableDeclarationList( | ||||||
|  |       [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], | ||||||
|  |       ts.NodeFlags.Let); | ||||||
|  |   const loopCondition = ts.createBinary( | ||||||
|  |       incrementor, ts.SyntaxKind.LessThanToken, | ||||||
|  |       ts.createPropertyAccess(nodes, ts.createIdentifier('length'))); | ||||||
|  |   const appendStatement = ts.createExpressionStatement(ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'appendChild'), [], | ||||||
|  |       [parent, ts.createElementAccess(nodes, incrementor)])); | ||||||
|  |   const loop = ts.createFor( | ||||||
|  |       loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), | ||||||
|  |       ts.createBlock([appendStatement])); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.projectNodes, [], | ||||||
|  |       [renderer, parent, nodes].map(name => getAnyTypedParameter(name)), undefined, | ||||||
|  |       ts.createBlock([loop], true)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `animate`. */ | ||||||
|  | function getAnimateHelper(): ts.FunctionDeclaration { | ||||||
|  |   // throw new Error('...');
 | ||||||
|  |   const throwStatement = ts.createThrow(ts.createNew( | ||||||
|  |       ts.createIdentifier('Error'), [], | ||||||
|  |       [ts.createStringLiteral('Renderer.animate is no longer supported!')])); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.animate, [], [], undefined, | ||||||
|  |       ts.createBlock([throwStatement], true)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `destroyView`. */ | ||||||
|  | function getDestroyViewHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const allNodes = ts.createIdentifier('allNodes'); | ||||||
|  |   const incrementor = ts.createIdentifier('i'); | ||||||
|  | 
 | ||||||
|  |   // for (let i = 0; i < allNodes.length; i++) {
 | ||||||
|  |   //   renderer.destroyNode(allNodes[i]);
 | ||||||
|  |   // }
 | ||||||
|  |   const loopInitializer = ts.createVariableDeclarationList( | ||||||
|  |       [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], | ||||||
|  |       ts.NodeFlags.Let); | ||||||
|  |   const loopCondition = ts.createBinary( | ||||||
|  |       incrementor, ts.SyntaxKind.LessThanToken, | ||||||
|  |       ts.createPropertyAccess(allNodes, ts.createIdentifier('length'))); | ||||||
|  |   const destroyStatement = ts.createExpressionStatement(ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'destroyNode'), [], | ||||||
|  |       [ts.createElementAccess(allNodes, incrementor)])); | ||||||
|  |   const loop = ts.createFor( | ||||||
|  |       loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), | ||||||
|  |       ts.createBlock([destroyStatement])); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.destroyView, [], | ||||||
|  |       [renderer, allNodes].map(name => getAnyTypedParameter(name)), undefined, | ||||||
|  |       ts.createBlock([loop], true)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `detachView`. */ | ||||||
|  | function getDetachViewHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const rootNodes = ts.createIdentifier('rootNodes'); | ||||||
|  |   const incrementor = ts.createIdentifier('i'); | ||||||
|  |   const node = ts.createIdentifier('node'); | ||||||
|  | 
 | ||||||
|  |   // for (let i = 0; i < rootNodes.length; i++) {
 | ||||||
|  |   //   const node = rootNodes[i];
 | ||||||
|  |   //   renderer.removeChild(renderer.parentNode(node), node);
 | ||||||
|  |   // }
 | ||||||
|  |   const loopInitializer = ts.createVariableDeclarationList( | ||||||
|  |       [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], | ||||||
|  |       ts.NodeFlags.Let); | ||||||
|  |   const loopCondition = ts.createBinary( | ||||||
|  |       incrementor, ts.SyntaxKind.LessThanToken, | ||||||
|  |       ts.createPropertyAccess(rootNodes, ts.createIdentifier('length'))); | ||||||
|  | 
 | ||||||
|  |   // const node = rootNodes[i];
 | ||||||
|  |   const nodeVariableStatement = ts.createVariableStatement( | ||||||
|  |       undefined, ts.createVariableDeclarationList( | ||||||
|  |                      [ts.createVariableDeclaration( | ||||||
|  |                          node, undefined, ts.createElementAccess(rootNodes, incrementor))], | ||||||
|  |                      ts.NodeFlags.Const)); | ||||||
|  |   // renderer.removeChild(renderer.parentNode(node), node);
 | ||||||
|  |   const removeCall = ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'removeChild'), [], | ||||||
|  |       [ts.createCall(ts.createPropertyAccess(renderer, 'parentNode'), [], [node]), node]); | ||||||
|  | 
 | ||||||
|  |   const loop = ts.createFor( | ||||||
|  |       loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), | ||||||
|  |       ts.createBlock([nodeVariableStatement, ts.createExpressionStatement(removeCall)])); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.detachView, [], | ||||||
|  |       [renderer, rootNodes].map(name => getAnyTypedParameter(name)), undefined, | ||||||
|  |       ts.createBlock([loop], true)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `attachViewAfter` */ | ||||||
|  | function getAttachViewAfterHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const node = ts.createIdentifier('node'); | ||||||
|  |   const rootNodes = ts.createIdentifier('rootNodes'); | ||||||
|  |   const parent = ts.createIdentifier('parent'); | ||||||
|  |   const nextSibling = ts.createIdentifier('nextSibling'); | ||||||
|  |   const incrementor = ts.createIdentifier('i'); | ||||||
|  |   const createConstWithMethodCallInitializer = (constName: ts.Identifier, methodToCall: string) => { | ||||||
|  |     return ts.createVariableStatement( | ||||||
|  |         undefined, | ||||||
|  |         ts.createVariableDeclarationList( | ||||||
|  |             [ts.createVariableDeclaration( | ||||||
|  |                 constName, undefined, | ||||||
|  |                 ts.createCall(ts.createPropertyAccess(renderer, methodToCall), [], [node]))], | ||||||
|  |             ts.NodeFlags.Const)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // const parent = renderer.parentNode(node);
 | ||||||
|  |   const parentVariableStatement = createConstWithMethodCallInitializer(parent, 'parentNode'); | ||||||
|  | 
 | ||||||
|  |   // const nextSibling = renderer.nextSibling(node);
 | ||||||
|  |   const nextSiblingVariableStatement = | ||||||
|  |       createConstWithMethodCallInitializer(nextSibling, 'nextSibling'); | ||||||
|  | 
 | ||||||
|  |   // for (let i = 0; i < rootNodes.length; i++) {
 | ||||||
|  |   //   renderer.insertBefore(parentElement, rootNodes[i], nextSibling);
 | ||||||
|  |   // }
 | ||||||
|  |   const loopInitializer = ts.createVariableDeclarationList( | ||||||
|  |       [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], | ||||||
|  |       ts.NodeFlags.Let); | ||||||
|  |   const loopCondition = ts.createBinary( | ||||||
|  |       incrementor, ts.SyntaxKind.LessThanToken, | ||||||
|  |       ts.createPropertyAccess(rootNodes, ts.createIdentifier('length'))); | ||||||
|  |   const insertBeforeCall = ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'insertBefore'), [], | ||||||
|  |       [parent, ts.createElementAccess(rootNodes, incrementor), nextSibling]); | ||||||
|  |   const loop = ts.createFor( | ||||||
|  |       loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), | ||||||
|  |       ts.createBlock([ts.createExpressionStatement(insertBeforeCall)])); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.attachViewAfter, [], | ||||||
|  |       [renderer, node, rootNodes].map(name => getAnyTypedParameter(name)), undefined, | ||||||
|  |       ts.createBlock([parentVariableStatement, nextSiblingVariableStatement, loop], true)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for `setElementAttribute` */ | ||||||
|  | function getSetElementAttributeHelper(): ts.FunctionDeclaration { | ||||||
|  |   const renderer = ts.createIdentifier('renderer'); | ||||||
|  |   const element = ts.createIdentifier('element'); | ||||||
|  |   const namespaceAndName = ts.createIdentifier('namespaceAndName'); | ||||||
|  |   const value = ts.createIdentifier('value'); | ||||||
|  |   const name = ts.createIdentifier('name'); | ||||||
|  |   const namespace = ts.createIdentifier('namespace'); | ||||||
|  | 
 | ||||||
|  |   // [namespace, name] = splitNamespace(namespaceAndName);
 | ||||||
|  |   const namespaceAndNameVariable = ts.createVariableDeclaration( | ||||||
|  |       ts.createArrayBindingPattern( | ||||||
|  |           [namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))), | ||||||
|  |       undefined, | ||||||
|  |       ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName])); | ||||||
|  | 
 | ||||||
|  |   // renderer.setAttribute(element, name, value, namespace);
 | ||||||
|  |   const setCall = ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'setAttribute'), [], [element, name, value, namespace]); | ||||||
|  | 
 | ||||||
|  |   // renderer.removeAttribute(element, name, namespace);
 | ||||||
|  |   const removeCall = ts.createCall( | ||||||
|  |       ts.createPropertyAccess(renderer, 'removeAttribute'), [], [element, name, namespace]); | ||||||
|  | 
 | ||||||
|  |   // if (value != null) { setCall() } else { removeCall }
 | ||||||
|  |   const ifStatement = ts.createIf( | ||||||
|  |       ts.createBinary(value, ts.SyntaxKind.ExclamationEqualsToken, ts.createNull()), | ||||||
|  |       ts.createBlock([ts.createExpressionStatement(setCall)], true), | ||||||
|  |       ts.createBlock([ts.createExpressionStatement(removeCall)], true)); | ||||||
|  | 
 | ||||||
|  |   const functionBody = ts.createBlock( | ||||||
|  |       [ | ||||||
|  |         ts.createVariableStatement( | ||||||
|  |             undefined, | ||||||
|  |             ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const)), | ||||||
|  |         ifStatement | ||||||
|  |       ], | ||||||
|  |       true); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.setElementAttribute, [], | ||||||
|  |       [ | ||||||
|  |         getAnyTypedParameter(renderer), getAnyTypedParameter(element), | ||||||
|  |         getAnyTypedParameter(namespaceAndName), getAnyTypedParameter(value, false) | ||||||
|  |       ], | ||||||
|  |       undefined, functionBody); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Creates a helper for splitting a name that might contain a namespace. */ | ||||||
|  | function getSplitNamespaceHelper(): ts.FunctionDeclaration { | ||||||
|  |   const name = ts.createIdentifier('name'); | ||||||
|  |   const match = ts.createIdentifier('match'); | ||||||
|  |   const regex = ts.createRegularExpressionLiteral('/^:([^:]+):(.+)$/'); | ||||||
|  |   const matchCall = ts.createCall(ts.createPropertyAccess(name, 'match'), [], [regex]); | ||||||
|  | 
 | ||||||
|  |   // const match = name.split(regex);
 | ||||||
|  |   const matchVariable = ts.createVariableDeclarationList( | ||||||
|  |       [ts.createVariableDeclaration(match, undefined, matchCall)], ts.NodeFlags.Const); | ||||||
|  | 
 | ||||||
|  |   // return [match[1], match[2]];
 | ||||||
|  |   const matchReturn = ts.createReturn( | ||||||
|  |       ts.createArrayLiteral([ts.createElementAccess(match, 1), ts.createElementAccess(match, 2)])); | ||||||
|  | 
 | ||||||
|  |   // if (name[0] === ':') { const match = ...; return ...; }
 | ||||||
|  |   const ifStatement = ts.createIf( | ||||||
|  |       ts.createBinary( | ||||||
|  |           ts.createElementAccess(name, 0), ts.SyntaxKind.EqualsEqualsEqualsToken, | ||||||
|  |           ts.createStringLiteral(':')), | ||||||
|  |       ts.createBlock([ts.createVariableStatement([], matchVariable), matchReturn], true)); | ||||||
|  | 
 | ||||||
|  |   // return ['', name];
 | ||||||
|  |   const elseReturn = ts.createReturn(ts.createArrayLiteral([ts.createStringLiteral(''), name])); | ||||||
|  | 
 | ||||||
|  |   return ts.createFunctionDeclaration( | ||||||
|  |       [], [], undefined, HelperFunction.splitNamespace, [], [getAnyTypedParameter(name)], undefined, | ||||||
|  |       ts.createBlock([ifStatement, elseReturn], true)); | ||||||
|  | } | ||||||
| @ -0,0 +1,134 @@ | |||||||
|  | /** | ||||||
|  |  * @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 {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; | ||||||
|  | import {dirname, relative} from 'path'; | ||||||
|  | import * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; | ||||||
|  | import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig'; | ||||||
|  | 
 | ||||||
|  | import {HelperFunction, getHelper} from './helpers'; | ||||||
|  | import {migrateExpression, replaceImport} from './migration'; | ||||||
|  | import {findCoreImport, findRendererReferences} from './util'; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migration that switches from `Renderer` to `Renderer2`. More information on how it works: | ||||||
|  |  * https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw
 | ||||||
|  |  */ | ||||||
|  | export default function(): Rule { | ||||||
|  |   return (tree: Tree, context: SchematicContext) => { | ||||||
|  |     const {buildPaths, testPaths} = getProjectTsConfigPaths(tree); | ||||||
|  |     const basePath = process.cwd(); | ||||||
|  |     const allPaths = [...buildPaths, ...testPaths]; | ||||||
|  |     const logger = context.logger; | ||||||
|  | 
 | ||||||
|  |     logger.info('------ Renderer to Renderer2 Migration ------'); | ||||||
|  |     logger.info('As of Angular 9, the Renderer class is no longer available.'); | ||||||
|  |     logger.info('Renderer2 should be used instead.'); | ||||||
|  | 
 | ||||||
|  |     if (!allPaths.length) { | ||||||
|  |       throw new SchematicsException( | ||||||
|  |           'Could not find any tsconfig file. Cannot migrate Renderer usages to Renderer2.'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const tsconfigPath of allPaths) { | ||||||
|  |       runRendererToRenderer2Migration(tree, tsconfigPath, basePath); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function runRendererToRenderer2Migration(tree: Tree, tsconfigPath: string, basePath: string) { | ||||||
|  |   const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath)); | ||||||
|  |   const host = ts.createCompilerHost(parsed.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. Otherwise
 | ||||||
|  |   // if we run the migration for multiple tsconfig files which have intersecting
 | ||||||
|  |   // source files, it can end up updating query definitions multiple times.
 | ||||||
|  |   host.readFile = fileName => { | ||||||
|  |     const buffer = tree.read(relative(basePath, fileName)); | ||||||
|  |     return buffer ? buffer.toString() : undefined; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const program = ts.createProgram(parsed.fileNames, parsed.options, host); | ||||||
|  |   const typeChecker = program.getTypeChecker(); | ||||||
|  |   const printer = ts.createPrinter(); | ||||||
|  |   const sourceFiles = program.getSourceFiles().filter( | ||||||
|  |       f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f)); | ||||||
|  | 
 | ||||||
|  |   sourceFiles.forEach(sourceFile => { | ||||||
|  |     const rendererImport = findCoreImport(sourceFile, 'Renderer'); | ||||||
|  | 
 | ||||||
|  |     // If there are no imports for the `Renderer`, we can exit early.
 | ||||||
|  |     if (!rendererImport) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const {typedNodes, methodCalls, forwardRefs} = | ||||||
|  |         findRendererReferences(sourceFile, typeChecker, rendererImport); | ||||||
|  |     const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); | ||||||
|  |     const helpersToAdd = new Set<HelperFunction>(); | ||||||
|  | 
 | ||||||
|  |     // Change the `Renderer` import to `Renderer2`.
 | ||||||
|  |     update.remove(rendererImport.getStart(), rendererImport.getWidth()); | ||||||
|  |     update.insertRight( | ||||||
|  |         rendererImport.getStart(), | ||||||
|  |         printer.printNode( | ||||||
|  |             ts.EmitHint.Unspecified, replaceImport(rendererImport, 'Renderer', 'Renderer2'), | ||||||
|  |             sourceFile)); | ||||||
|  | 
 | ||||||
|  |     // Change the method parameter and property types to `Renderer2`.
 | ||||||
|  |     typedNodes.forEach(node => { | ||||||
|  |       const type = node.type; | ||||||
|  | 
 | ||||||
|  |       if (type) { | ||||||
|  |         update.remove(type.getStart(), type.getWidth()); | ||||||
|  |         update.insertRight(type.getStart(), 'Renderer2'); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Change all identifiers inside `forwardRef` referring to the `Renderer`.
 | ||||||
|  |     forwardRefs.forEach(identifier => { | ||||||
|  |       update.remove(identifier.getStart(), identifier.getWidth()); | ||||||
|  |       update.insertRight(identifier.getStart(), 'Renderer2'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Migrate all of the method calls.
 | ||||||
|  |     methodCalls.forEach(call => { | ||||||
|  |       const {node, requiredHelpers} = migrateExpression(call, typeChecker); | ||||||
|  | 
 | ||||||
|  |       if (node) { | ||||||
|  |         // If we migrated the node to a new expression, replace only the call expression.
 | ||||||
|  |         update.remove(call.getStart(), call.getWidth()); | ||||||
|  |         update.insertRight( | ||||||
|  |             call.getStart(), printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)); | ||||||
|  |       } else if (call.parent && ts.isExpressionStatement(call.parent)) { | ||||||
|  |         // Otherwise if the call is inside an expression statement, drop the entire statement.
 | ||||||
|  |         // This takes care of any trailing semicolons. We only need to drop nodes for cases like
 | ||||||
|  |         // `setBindingDebugInfo` which have been noop for a while so they can be removed safely.
 | ||||||
|  |         update.remove(call.parent.getStart(), call.parent.getWidth()); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (requiredHelpers) { | ||||||
|  |         requiredHelpers.forEach(helperName => helpersToAdd.add(helperName)); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them.
 | ||||||
|  |     // The safest way to do so is to declare helper functions similar to the ones emitted by TS
 | ||||||
|  |     // which encapsulate the extra "glue" logic. We should only emit these functions once per file.
 | ||||||
|  |     helpersToAdd.forEach(helperName => { | ||||||
|  |       update.insertLeft( | ||||||
|  |           sourceFile.endOfFileToken.getStart(), getHelper(helperName, sourceFile, printer)); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     tree.commitUpdate(update); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -0,0 +1,268 @@ | |||||||
|  | /** | ||||||
|  |  * @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 {HelperFunction} from './helpers'; | ||||||
|  | import {findImportSpecifier} from './util'; | ||||||
|  | 
 | ||||||
|  | /** A call expression that is based on a property access. */ | ||||||
|  | type PropertyAccessCallExpression = ts.CallExpression & {expression: ts.PropertyAccessExpression}; | ||||||
|  | 
 | ||||||
|  | /** Replaces an import inside an import statement with a different one. */ | ||||||
|  | export function replaceImport(node: ts.NamedImports, oldImport: string, newImport: string) { | ||||||
|  |   const isAlreadyImported = findImportSpecifier(node.elements, newImport); | ||||||
|  | 
 | ||||||
|  |   if (isAlreadyImported) { | ||||||
|  |     return node; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const existingImport = findImportSpecifier(node.elements, oldImport); | ||||||
|  | 
 | ||||||
|  |   if (!existingImport) { | ||||||
|  |     throw new Error(`Could not find an import to replace using ${oldImport}.`); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ts.updateNamedImports(node, [ | ||||||
|  |     ...node.elements.filter(current => current !== existingImport), | ||||||
|  |     // Create a new import while trying to preserve the alias of the old one.
 | ||||||
|  |     ts.createImportSpecifier( | ||||||
|  |         existingImport.propertyName ? ts.createIdentifier(newImport) : undefined, | ||||||
|  |         existingImport.propertyName ? existingImport.name : ts.createIdentifier(newImport)) | ||||||
|  |   ]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migrates a function call expression from `Renderer` to `Renderer2`. | ||||||
|  |  * Returns null if the expression should be dropped. | ||||||
|  |  */ | ||||||
|  | export function migrateExpression(node: ts.CallExpression, typeChecker: ts.TypeChecker): | ||||||
|  |     {node: ts.Node | null, requiredHelpers?: HelperFunction[]} { | ||||||
|  |   if (isPropertyAccessCallExpression(node)) { | ||||||
|  |     switch (node.expression.name.getText()) { | ||||||
|  |       case 'setElementProperty': | ||||||
|  |         return {node: renameMethodCall(node, 'setProperty')}; | ||||||
|  |       case 'setText': | ||||||
|  |         return {node: renameMethodCall(node, 'setValue')}; | ||||||
|  |       case 'listenGlobal': | ||||||
|  |         return {node: renameMethodCall(node, 'listen')}; | ||||||
|  |       case 'selectRootElement': | ||||||
|  |         return {node: migrateSelectRootElement(node)}; | ||||||
|  |       case 'setElementClass': | ||||||
|  |         return {node: migrateSetElementClass(node)}; | ||||||
|  |       case 'setElementStyle': | ||||||
|  |         return {node: migrateSetElementStyle(node, typeChecker)}; | ||||||
|  |       case 'invokeElementMethod': | ||||||
|  |         return {node: migrateInvokeElementMethod(node)}; | ||||||
|  |       case 'setBindingDebugInfo': | ||||||
|  |         return {node: null}; | ||||||
|  |       case 'createViewRoot': | ||||||
|  |         return {node: migrateCreateViewRoot(node)}; | ||||||
|  |       case 'setElementAttribute': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.setElementAttribute, node.arguments), | ||||||
|  |           requiredHelpers: [ | ||||||
|  |             HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.setElementAttribute | ||||||
|  |           ] | ||||||
|  |         }; | ||||||
|  |       case 'createElement': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.createElement, node.arguments.slice(0, 2)), | ||||||
|  |           requiredHelpers: | ||||||
|  |               [HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.createElement] | ||||||
|  |         }; | ||||||
|  |       case 'createText': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.createText, node.arguments.slice(0, 2)), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.createText] | ||||||
|  |         }; | ||||||
|  |       case 'createTemplateAnchor': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall( | ||||||
|  |               node, HelperFunction.createTemplateAnchor, node.arguments.slice(0, 1)), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.createTemplateAnchor] | ||||||
|  |         }; | ||||||
|  |       case 'projectNodes': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.projectNodes, node.arguments), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.projectNodes] | ||||||
|  |         }; | ||||||
|  |       case 'animate': | ||||||
|  |         return { | ||||||
|  |           node: migrateAnimateCall(), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.animate] | ||||||
|  |         }; | ||||||
|  |       case 'destroyView': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.destroyView, [node.arguments[1]]), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.destroyView] | ||||||
|  |         }; | ||||||
|  |       case 'detachView': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.detachView, [node.arguments[0]]), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.detachView] | ||||||
|  |         }; | ||||||
|  |       case 'attachViewAfter': | ||||||
|  |         return { | ||||||
|  |           node: switchToHelperCall(node, HelperFunction.attachViewAfter, node.arguments), | ||||||
|  |           requiredHelpers: [HelperFunction.any, HelperFunction.attachViewAfter] | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return {node}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Checks whether a node is a PropertyAccessExpression. */ | ||||||
|  | function isPropertyAccessCallExpression(node: ts.Node): node is PropertyAccessCallExpression { | ||||||
|  |   return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Renames a method call while keeping all of the parameters in place. */ | ||||||
|  | function renameMethodCall(node: PropertyAccessCallExpression, newName: string): ts.CallExpression { | ||||||
|  |   const newExpression = ts.updatePropertyAccess( | ||||||
|  |       node.expression, node.expression.expression, ts.createIdentifier(newName)); | ||||||
|  | 
 | ||||||
|  |   return ts.updateCall(node, newExpression, node.typeArguments, node.arguments); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migrates a `selectRootElement` call by removing the last argument which is no longer supported. | ||||||
|  |  */ | ||||||
|  | function migrateSelectRootElement(node: ts.CallExpression): ts.Node { | ||||||
|  |   // The only thing we need to do is to drop the last argument
 | ||||||
|  |   // (`debugInfo`), if the consumer was passing it in.
 | ||||||
|  |   if (node.arguments.length > 1) { | ||||||
|  |     return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return node; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migrates a call to `setElementClass` either to a call to `addClass` or `removeClass`, or | ||||||
|  |  * to an expression like `isAdd ? addClass(el, className) : removeClass(el, className)`. | ||||||
|  |  */ | ||||||
|  | function migrateSetElementClass(node: PropertyAccessCallExpression): ts.Node { | ||||||
|  |   // Clone so we don't mutate by accident. Note that we assume that
 | ||||||
|  |   // the user's code is providing all three required arguments.
 | ||||||
|  |   const outputMethodArgs = node.arguments.slice(); | ||||||
|  |   const isAddArgument = outputMethodArgs.pop() !; | ||||||
|  |   const createRendererCall = (isAdd: boolean) => { | ||||||
|  |     const innerExpression = node.expression.expression; | ||||||
|  |     const topExpression = | ||||||
|  |         ts.createPropertyAccess(innerExpression, isAdd ? 'addClass' : 'removeClass'); | ||||||
|  |     return ts.createCall(topExpression, [], node.arguments.slice(0, 2)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // If the call has the `isAdd` argument as a literal boolean, we can map it directly to
 | ||||||
|  |   // `addClass` or `removeClass`. Note that we can't use the type checker here, because it
 | ||||||
|  |   // won't tell us whether the value resolves to true or false.
 | ||||||
|  |   if (isAddArgument.kind === ts.SyntaxKind.TrueKeyword || | ||||||
|  |       isAddArgument.kind === ts.SyntaxKind.FalseKeyword) { | ||||||
|  |     return createRendererCall(isAddArgument.kind === ts.SyntaxKind.TrueKeyword); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Otherwise create a ternary on the variable.
 | ||||||
|  |   return ts.createConditional(isAddArgument, createRendererCall(true), createRendererCall(false)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migrates a call to `setElementStyle` call either to a call to | ||||||
|  |  * `setStyle` or `removeStyle`. or to an expression like | ||||||
|  |  * `value == null ? removeStyle(el, key) : setStyle(el, key, value)`. | ||||||
|  |  */ | ||||||
|  | function migrateSetElementStyle( | ||||||
|  |     node: PropertyAccessCallExpression, typeChecker: ts.TypeChecker): ts.Node { | ||||||
|  |   const args = node.arguments; | ||||||
|  |   const addMethodName = 'setStyle'; | ||||||
|  |   const removeMethodName = 'removeStyle'; | ||||||
|  |   const lastArgType = args[2] ? | ||||||
|  |       typeChecker.typeToString( | ||||||
|  |           typeChecker.getTypeAtLocation(args[2]), node, ts.TypeFormatFlags.AddUndefined) : | ||||||
|  |       null; | ||||||
|  | 
 | ||||||
|  |   // Note that for a literal null, TS considers it a `NullKeyword`,
 | ||||||
|  |   // whereas a literal `undefined` is just an Identifier.
 | ||||||
|  |   if (args.length === 2 || lastArgType === 'null' || lastArgType === 'undefined') { | ||||||
|  |     // If we've got a call with two arguments, or one with three arguments where the last one is
 | ||||||
|  |     // `undefined` or `null`, we can safely switch to a `removeStyle` call.
 | ||||||
|  |     const innerExpression = node.expression.expression; | ||||||
|  |     const topExpression = ts.createPropertyAccess(innerExpression, removeMethodName); | ||||||
|  |     return ts.createCall(topExpression, [], args.slice(0, 2)); | ||||||
|  |   } else if (args.length === 3) { | ||||||
|  |     // We need the checks for string literals, because the type of something
 | ||||||
|  |     // like `"blue"` is the literal `blue`, not `string`.
 | ||||||
|  |     if (lastArgType === 'string' || lastArgType === 'number' || ts.isStringLiteral(args[2]) || | ||||||
|  |         ts.isNoSubstitutionTemplateLiteral(args[2]) || ts.isNumericLiteral(args[2])) { | ||||||
|  |       // If we've got three arguments and the last one is a string literal or a number, we
 | ||||||
|  |       // can safely rename to `setStyle`.
 | ||||||
|  |       return renameMethodCall(node, addMethodName); | ||||||
|  |     } else { | ||||||
|  |       // Otherwise migrate to a ternary that looks like:
 | ||||||
|  |       // `value == null ? removeStyle(el, key) : setStyle(el, key, value)`
 | ||||||
|  |       const condition = ts.createBinary(args[2], ts.SyntaxKind.EqualsEqualsToken, ts.createNull()); | ||||||
|  |       const whenNullCall = renameMethodCall( | ||||||
|  |           ts.createCall(node.expression, [], args.slice(0, 2)) as PropertyAccessCallExpression, | ||||||
|  |           removeMethodName); | ||||||
|  |       return ts.createConditional(condition, whenNullCall, renameMethodCall(node, addMethodName)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return node; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migrates a call to `invokeElementMethod(target, method, [arg1, arg2])` either to | ||||||
|  |  * `target.method(arg1, arg2)` or `(target as any)[method].apply(target, [arg1, arg2])`. | ||||||
|  |  */ | ||||||
|  | function migrateInvokeElementMethod(node: ts.CallExpression): ts.Node { | ||||||
|  |   const [target, name, args] = node.arguments; | ||||||
|  |   const isNameStatic = ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name); | ||||||
|  |   const isArgsStatic = !args || ts.isArrayLiteralExpression(args); | ||||||
|  | 
 | ||||||
|  |   if (isNameStatic && isArgsStatic) { | ||||||
|  |     // If the name is a static string and the arguments are an array literal,
 | ||||||
|  |     // we can safely convert the node into a call expression.
 | ||||||
|  |     const expression = ts.createPropertyAccess( | ||||||
|  |         target, (name as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text); | ||||||
|  |     const callArguments = args ? (args as ts.ArrayLiteralExpression).elements : []; | ||||||
|  |     return ts.createCall(expression, [], callArguments); | ||||||
|  |   } else { | ||||||
|  |     // Otherwise create an expression in the form of `(target as any)[name].apply(target, args)`.
 | ||||||
|  |     const asExpression = ts.createParen( | ||||||
|  |         ts.createAsExpression(target, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); | ||||||
|  |     const elementAccess = ts.createElementAccess(asExpression, name); | ||||||
|  |     const applyExpression = ts.createPropertyAccess(elementAccess, 'apply'); | ||||||
|  |     return ts.createCall(applyExpression, [], args ? [target, args] : [target]); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Migrates a call to `createViewRoot` to whatever node was passed in as the first argument. */ | ||||||
|  | function migrateCreateViewRoot(node: ts.CallExpression): ts.Node { | ||||||
|  |   return node.arguments[0]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Migrates a call to `migrate` a direct call to the helper. */ | ||||||
|  | function migrateAnimateCall() { | ||||||
|  |   return ts.createCall(ts.createIdentifier(HelperFunction.animate), [], []); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Switches out a call to the `Renderer` to a call to one of our helper functions. | ||||||
|  |  * Most of the helpers accept an instance of `Renderer2` as the first argument and all | ||||||
|  |  * subsequent arguments differ. | ||||||
|  |  * @param node Node of the original method call. | ||||||
|  |  * @param helper Name of the helper with which to replace the original call. | ||||||
|  |  * @param args Arguments that should be passed into the helper after the renderer argument. | ||||||
|  |  */ | ||||||
|  | function switchToHelperCall( | ||||||
|  |     node: PropertyAccessCallExpression, helper: HelperFunction, | ||||||
|  |     args: ts.Expression[] | ts.NodeArray<ts.Expression>): ts.Node { | ||||||
|  |   return ts.createCall(ts.createIdentifier(helper), [], [node.expression.expression, ...args]); | ||||||
|  | } | ||||||
| @ -0,0 +1,125 @@ | |||||||
|  | /** | ||||||
|  |  * @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'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Finds typed nodes (e.g. function parameters or class properties) that are referencing the old | ||||||
|  |  * `Renderer`, as well as calls to the `Renderer` methods. | ||||||
|  |  */ | ||||||
|  | export function findRendererReferences( | ||||||
|  |     sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, rendererImport: ts.NamedImports) { | ||||||
|  |   const typedNodes = new Set<ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression>(); | ||||||
|  |   const methodCalls = new Set<ts.CallExpression>(); | ||||||
|  |   const forwardRefs = new Set<ts.Identifier>(); | ||||||
|  |   const importSpecifier = findImportSpecifier(rendererImport.elements, 'Renderer'); | ||||||
|  |   const forwardRefImport = findCoreImport(sourceFile, 'forwardRef'); | ||||||
|  |   const forwardRefSpecifier = | ||||||
|  |       forwardRefImport ? findImportSpecifier(forwardRefImport.elements, 'forwardRef') : null; | ||||||
|  | 
 | ||||||
|  |   ts.forEachChild(sourceFile, function visitNode(node: ts.Node) { | ||||||
|  |     if ((ts.isParameter(node) || ts.isPropertyDeclaration(node)) && | ||||||
|  |         isReferenceToImport(typeChecker, node.name, importSpecifier)) { | ||||||
|  |       typedNodes.add(node); | ||||||
|  |     } else if ( | ||||||
|  |         ts.isAsExpression(node) && isReferenceToImport(typeChecker, node.type, importSpecifier)) { | ||||||
|  |       typedNodes.add(node); | ||||||
|  |     } else if (ts.isCallExpression(node)) { | ||||||
|  |       if (ts.isPropertyAccessExpression(node.expression) && | ||||||
|  |           isReferenceToImport(typeChecker, node.expression.expression, importSpecifier)) { | ||||||
|  |         methodCalls.add(node); | ||||||
|  |       } else if ( | ||||||
|  |           // If we're dealing with a forwardRef that's returning a Renderer.
 | ||||||
|  |           forwardRefSpecifier && ts.isIdentifier(node.expression) && | ||||||
|  |           isReferenceToImport(typeChecker, node.expression, forwardRefSpecifier) && | ||||||
|  |           node.arguments.length) { | ||||||
|  |         const rendererIdentifier = | ||||||
|  |             findRendererIdentifierInForwardRef(typeChecker, node, importSpecifier); | ||||||
|  |         if (rendererIdentifier) { | ||||||
|  |           forwardRefs.add(rendererIdentifier); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ts.forEachChild(node, visitNode); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return {typedNodes, methodCalls, forwardRefs}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Finds the import from @angular/core that has a symbol with a particular name. */ | ||||||
|  | export function findCoreImport(sourceFile: ts.SourceFile, symbolName: string): ts.NamedImports| | ||||||
|  |     null { | ||||||
|  |   // Only look through the top-level imports.
 | ||||||
|  |   for (const node of sourceFile.statements) { | ||||||
|  |     if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier) || | ||||||
|  |         node.moduleSpecifier.text !== '@angular/core') { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const namedBindings = node.importClause && node.importClause.namedBindings; | ||||||
|  | 
 | ||||||
|  |     if (!namedBindings || !ts.isNamedImports(namedBindings)) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (findImportSpecifier(namedBindings.elements, symbolName)) { | ||||||
|  |       return namedBindings; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Finds an import specifier with a particular name, accounting for aliases. */ | ||||||
|  | export function findImportSpecifier( | ||||||
|  |     elements: ts.NodeArray<ts.ImportSpecifier>, importName: string) { | ||||||
|  |   return elements.find(element => { | ||||||
|  |     const {name, propertyName} = element; | ||||||
|  |     return propertyName ? propertyName.text === importName : name.text === importName; | ||||||
|  |   }) || | ||||||
|  |       null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Checks whether a node is referring to an import spcifier. */ | ||||||
|  | function isReferenceToImport( | ||||||
|  |     typeChecker: ts.TypeChecker, node: ts.Node, | ||||||
|  |     importSpecifier: ts.ImportSpecifier | null): boolean { | ||||||
|  |   if (importSpecifier) { | ||||||
|  |     const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol(); | ||||||
|  |     const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol(); | ||||||
|  |     return !!(nodeSymbol && importSymbol) && | ||||||
|  |         nodeSymbol.valueDeclaration === importSymbol.valueDeclaration; | ||||||
|  |   } | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Finds the identifier referring to the `Renderer` inside a `forwardRef` call expression. */ | ||||||
|  | function findRendererIdentifierInForwardRef( | ||||||
|  |     typeChecker: ts.TypeChecker, node: ts.CallExpression, | ||||||
|  |     rendererImport: ts.ImportSpecifier | null): ts.Identifier|null { | ||||||
|  |   const firstArg = node.arguments[0]; | ||||||
|  | 
 | ||||||
|  |   if (ts.isArrowFunction(firstArg)) { | ||||||
|  |     // Check if the function is `forwardRef(() => Renderer)`.
 | ||||||
|  |     if (ts.isIdentifier(firstArg.body) && | ||||||
|  |         isReferenceToImport(typeChecker, firstArg.body, rendererImport)) { | ||||||
|  |       return firstArg.body; | ||||||
|  |     } else if (ts.isBlock(firstArg.body) && ts.isReturnStatement(firstArg.body.statements[0])) { | ||||||
|  |       // Otherwise check if the expression is `forwardRef(() => { return Renderer })`.
 | ||||||
|  |       const returnStatement = firstArg.body.statements[0] as ts.ReturnStatement; | ||||||
|  | 
 | ||||||
|  |       if (returnStatement.expression && ts.isIdentifier(returnStatement.expression) && | ||||||
|  |           isReferenceToImport(typeChecker, returnStatement.expression, rendererImport)) { | ||||||
|  |         return returnStatement.expression; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return null; | ||||||
|  | } | ||||||
| @ -12,6 +12,8 @@ ts_library( | |||||||
|         "//packages/core/schematics/migrations/injectable-pipe", |         "//packages/core/schematics/migrations/injectable-pipe", | ||||||
|         "//packages/core/schematics/migrations/injectable-pipe/google3", |         "//packages/core/schematics/migrations/injectable-pipe/google3", | ||||||
|         "//packages/core/schematics/migrations/move-document", |         "//packages/core/schematics/migrations/move-document", | ||||||
|  |         "//packages/core/schematics/migrations/renderer-to-renderer2", | ||||||
|  |         "//packages/core/schematics/migrations/renderer-to-renderer2/google3", | ||||||
|         "//packages/core/schematics/migrations/static-queries", |         "//packages/core/schematics/migrations/static-queries", | ||||||
|         "//packages/core/schematics/migrations/static-queries/google3", |         "//packages/core/schematics/migrations/static-queries/google3", | ||||||
|         "//packages/core/schematics/migrations/template-var-assignment", |         "//packages/core/schematics/migrations/template-var-assignment", | ||||||
|  | |||||||
| @ -0,0 +1,415 @@ | |||||||
|  | /** | ||||||
|  |  * @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 {readFileSync, writeFileSync} from 'fs'; | ||||||
|  | import {dirname, join} from 'path'; | ||||||
|  | import * as shx from 'shelljs'; | ||||||
|  | import {Configuration, Linter} from 'tslint'; | ||||||
|  | 
 | ||||||
|  | describe('Google3 Renderer to Renderer2 TSLint rule', () => { | ||||||
|  |   const rulesDirectory = dirname( | ||||||
|  |       require.resolve('../../migrations/renderer-to-renderer2/google3/rendererToRenderer2Rule')); | ||||||
|  | 
 | ||||||
|  |   let tmpDir: string; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     tmpDir = join(process.env['TEST_TMPDIR'] !, 'google3-test'); | ||||||
|  |     shx.mkdir('-p', tmpDir); | ||||||
|  | 
 | ||||||
|  |     // We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
 | ||||||
|  |     writeFile('angular.d.ts', ` | ||||||
|  |       export declare abstract class Renderer {} | ||||||
|  |       export declare function forwardRef(fn: () => any): any {} | ||||||
|  |     `);
 | ||||||
|  | 
 | ||||||
|  |     writeFile('tsconfig.json', JSON.stringify({ | ||||||
|  |       compilerOptions: { | ||||||
|  |         module: 'es2015', | ||||||
|  |         baseUrl: './', | ||||||
|  |         paths: { | ||||||
|  |           '@angular/core': ['angular.d.ts'], | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     })); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   afterEach(() => shx.rm('-r', tmpDir)); | ||||||
|  | 
 | ||||||
|  |   function runTSLint(fix: boolean) { | ||||||
|  |     const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); | ||||||
|  |     const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); | ||||||
|  |     const config = Configuration.parseConfigFile( | ||||||
|  |         {rules: {'renderer-to-renderer2': true}, linterOptions: {typeCheck: true}}); | ||||||
|  | 
 | ||||||
|  |     program.getRootFileNames().forEach(fileName => { | ||||||
|  |       linter.lint(fileName, program.getSourceFile(fileName) !.getFullText(), config); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return linter; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function writeFile(fileName: string, content: string) { | ||||||
|  |     writeFileSync(join(tmpDir, fileName), content); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function getFile(fileName: string) { return readFileSync(join(tmpDir, fileName), 'utf8'); } | ||||||
|  | 
 | ||||||
|  |   it('should flag Renderer imports and typed nodes', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           public renderer: Renderer; | ||||||
|  | 
 | ||||||
|  |           constructor(renderer: Renderer) { | ||||||
|  |             this.renderer = renderer; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     const linter = runTSLint(false); | ||||||
|  |     const failures = linter.getResult().failures.map(failure => failure.getFailure()); | ||||||
|  | 
 | ||||||
|  |     expect(failures.length).toBe(3); | ||||||
|  |     expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); | ||||||
|  |     expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); | ||||||
|  |     expect(failures[2]).toMatch(/References to deprecated Renderer are not allowed/); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should change Renderer imports and typed nodes to Renderer2', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           public renderer: Renderer; | ||||||
|  | 
 | ||||||
|  |           constructor(renderer: Renderer) { | ||||||
|  |             this.renderer = renderer; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content).toContain(`import { Component, Renderer2 } from '@angular/core';`); | ||||||
|  |     expect(content).toContain('public renderer: Renderer2;'); | ||||||
|  |     expect(content).toContain('(renderer: Renderer2)'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should change Renderer inside single-line forwardRefs to Renderer2', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |       import { Renderer, Component, forwardRef, Inject } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |       @Component({template: ''}) | ||||||
|  |       export class MyComp { | ||||||
|  |         constructor(@Inject(forwardRef(() => Renderer)) private _renderer: Renderer) {} | ||||||
|  |       } | ||||||
|  |     `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `constructor(@Inject(forwardRef(() => Renderer2)) private _renderer: Renderer2) {}`); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should change Renderer inside multi-line forwardRefs to Renderer2', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |       import { Renderer, Component, forwardRef, Inject } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |       @Component({template: ''}) | ||||||
|  |       export class MyComp { | ||||||
|  |         constructor(@Inject(forwardRef(() => { return Renderer; })) private _renderer: Renderer) {} | ||||||
|  |       } | ||||||
|  |     `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `constructor(@Inject(forwardRef(() => { return Renderer2; })) private _renderer: Renderer2) {}`); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should flag something that was cast to Renderer', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           setColor(maybeRenderer: any, element: ElementRef) { | ||||||
|  |             const renderer = maybeRenderer as Renderer; | ||||||
|  |             renderer.setElementStyle(element.nativeElement, 'color', 'red'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     const linter = runTSLint(false); | ||||||
|  |     const failures = linter.getResult().failures.map(failure => failure.getFailure()); | ||||||
|  | 
 | ||||||
|  |     expect(failures.length).toBe(3); | ||||||
|  |     expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); | ||||||
|  |     expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); | ||||||
|  |     expect(failures[2]).toMatch(/Calls to Renderer methods are not allowed/); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should change the type of something that was cast to Renderer', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           setColor(maybeRenderer: any, element: ElementRef) { | ||||||
|  |             const renderer = maybeRenderer as Renderer; | ||||||
|  |             renderer.setElementStyle(element.nativeElement, 'color', 'red'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content).toContain(`const renderer = maybeRenderer as Renderer2;`); | ||||||
|  |     expect(content).toContain(`renderer.setStyle(element.nativeElement, 'color', 'red');`); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should be able to insert helper functions', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           constructor(renderer: Renderer, element: ElementRef) { | ||||||
|  |             const el = renderer.createElement(element.nativeElement, 'div'); | ||||||
|  |             renderer.setElementAttribute(el, 'title', 'hello'); | ||||||
|  |             renderer.projectNodes(element.nativeElement, [el]); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content).toContain(`function __ngRendererCreateElementHelper(`); | ||||||
|  |     expect(content).toContain(`function __ngRendererSetElementAttributeHelper(`); | ||||||
|  |     expect(content).toContain(`function __ngRendererProjectNodesHelper(`); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should only insert each helper only once per file', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           constructor(renderer: Renderer, element: ElementRef) { | ||||||
|  |             const el = renderer.createElement(element.nativeElement, 'div'); | ||||||
|  |             renderer.setElementAttribute(el, 'title', 'hello'); | ||||||
|  | 
 | ||||||
|  |             const el1 = renderer.createElement(element.nativeElement, 'div'); | ||||||
|  |             renderer.setElementAttribute(el2, 'title', 'hello'); | ||||||
|  | 
 | ||||||
|  |             const el2 = renderer.createElement(element.nativeElement, 'div'); | ||||||
|  |             renderer.setElementAttribute(el2, 'title', 'hello'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content.match(/function __ngRendererCreateElementHelper\(/g) !.length).toBe(1); | ||||||
|  |     expect(content.match(/function __ngRendererSetElementAttributeHelper\(/g) !.length).toBe(1); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should insert helpers after the user\'s code', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           constructor(renderer: Renderer, element: ElementRef) { | ||||||
|  |             const el = renderer.createElement(element.nativeElement, 'div'); | ||||||
|  |             renderer.setElementAttribute(el, 'title', 'hello'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         //---
 | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  |     const [contentBeforeSeparator, contentAfterSeparator] = content.split('//---'); | ||||||
|  | 
 | ||||||
|  |     expect(contentBeforeSeparator).not.toContain('function __ngRendererCreateElementHelper('); | ||||||
|  |     expect(contentAfterSeparator).toContain('function __ngRendererCreateElementHelper('); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // Note that this is intended primarily as a sanity test. All of the replacement logic is the
 | ||||||
|  |   // same between the lint rule and the CLI migration so there's not much value in repeating and
 | ||||||
|  |   // maintaining the same tests twice. The migration's tests are more exhaustive.
 | ||||||
|  |   it('should flag calls to Renderer methods', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           constructor(private _renderer: Renderer, private _element: ElementRef) { | ||||||
|  |             const span = _renderer.createElement(_element.nativeElement, 'span'); | ||||||
|  |             const greeting = _renderer.createText(_element.nativeElement, 'hello'); | ||||||
|  |             const color = 'red'; | ||||||
|  | 
 | ||||||
|  |             _renderer.setElementProperty(_element.nativeElement, 'disabled', true); | ||||||
|  |             _renderer.listenGlobal('window', 'resize', () => console.log('resized')); | ||||||
|  |             _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); | ||||||
|  |             _renderer.createViewRoot(_element.nativeElement); | ||||||
|  |             _renderer.animate(_element.nativeElement); | ||||||
|  |             _renderer.detachView([]); | ||||||
|  |             _renderer.destroyView(_element.nativeElement, []); | ||||||
|  |             _renderer.invokeElementMethod(_element.nativeElement, 'focus', []); | ||||||
|  |             _renderer.setElementStyle(_element.nativeElement, 'color', color); | ||||||
|  |             _renderer.setText(_element.nativeElement.querySelector('span'), 'Hello'); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           getRootElement() { | ||||||
|  |             return this._renderer.selectRootElement(this._element.nativeElement, {}); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           toggleClass(className: string, shouldAdd: boolean) { | ||||||
|  |             this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           setInfo() { | ||||||
|  |             this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           createAndAppendAnchor() { | ||||||
|  |             return this._renderer.createTemplateAnchor(this._element.nativeElement); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           attachViewAfter(rootNodes) { | ||||||
|  |             this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           projectNodes(nodesToProject: Node[]) { | ||||||
|  |             this._renderer.projectNodes(this._element.nativeElement, nodesToProject); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     const linter = runTSLint(false); | ||||||
|  |     const failures = linter.getResult().failures.map(failure => failure.getFailure()); | ||||||
|  | 
 | ||||||
|  |     // One failure for the import, one for the constructor param, one at the end that is used as
 | ||||||
|  |     // an anchor for inserting helper functions and the rest are for method calls.
 | ||||||
|  |     expect(failures.length).toBe(21); | ||||||
|  |     expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); | ||||||
|  |     expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); | ||||||
|  |     expect(failures[failures.length - 1]).toMatch(/File should contain Renderer helper functions/); | ||||||
|  |     expect(failures.slice(2, -1).every(message => { | ||||||
|  |       return /Calls to Renderer methods are not allowed/.test(message); | ||||||
|  |     })).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // Note that this is intended primarily as a sanity test. All of the replacement logic is the
 | ||||||
|  |   // same between the lint rule and the CLI migration so there's not much value in repeating and
 | ||||||
|  |   // maintaining the same tests twice. The migration's tests are more exhaustive.
 | ||||||
|  |   it('should fix calls to Renderer methods', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |         import { Renderer, Component, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  |         @Component({template: ''}) | ||||||
|  |         export class MyComp { | ||||||
|  |           constructor(private _renderer: Renderer, private _element: ElementRef) { | ||||||
|  |             const span = _renderer.createElement(_element.nativeElement, 'span'); | ||||||
|  |             const greeting = _renderer.createText(_element.nativeElement, 'hello'); | ||||||
|  |             const color = 'red'; | ||||||
|  | 
 | ||||||
|  |             _renderer.setElementProperty(_element.nativeElement, 'disabled', true); | ||||||
|  |             _renderer.listenGlobal('window', 'resize', () => console.log('resized')); | ||||||
|  |             _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); | ||||||
|  |             _renderer.animate(_element.nativeElement); | ||||||
|  |             _renderer.detachView([]); | ||||||
|  |             _renderer.destroyView(_element.nativeElement, []); | ||||||
|  |             _renderer.invokeElementMethod(_element.nativeElement, 'focus', []); | ||||||
|  |             _renderer.setElementStyle(_element.nativeElement, 'color', color); | ||||||
|  |             _renderer.setText(_element.nativeElement.querySelector('span'), 'Hello'); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           createRoot() { | ||||||
|  |             return this._renderer.createViewRoot(this._element.nativeElement); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           getRootElement() { | ||||||
|  |             return this._renderer.selectRootElement(this._element.nativeElement, {}); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           toggleClass(className: string, shouldAdd: boolean) { | ||||||
|  |             this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           setInfo() { | ||||||
|  |             this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           createAndAppendAnchor() { | ||||||
|  |             return this._renderer.createTemplateAnchor(this._element.nativeElement); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           attachViewAfter(rootNodes: Node[]) { | ||||||
|  |             this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           projectNodes(nodesToProject: Node[]) { | ||||||
|  |             this._renderer.projectNodes(this._element.nativeElement, nodesToProject); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  |     const content = getFile('index.ts'); | ||||||
|  | 
 | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `const span = __ngRendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `const greeting = __ngRendererCreateTextHelper(_renderer, _element.nativeElement, 'hello');`); | ||||||
|  |     expect(content).toContain(`_renderer.setProperty(_element.nativeElement, 'disabled', true);`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `_renderer.listen('window', 'resize', () => console.log('resized'));`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `__ngRendererSetElementAttributeHelper(_renderer, _element.nativeElement, 'title', 'hello');`); | ||||||
|  |     expect(content).toContain('__ngRendererAnimateHelper();'); | ||||||
|  |     expect(content).toContain('__ngRendererDetachViewHelper(_renderer, []);'); | ||||||
|  |     expect(content).toContain('__ngRendererDestroyViewHelper(_renderer, []);'); | ||||||
|  |     expect(content).toContain(`_element.nativeElement.focus()`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `color == null ? _renderer.removeStyle(_element.nativeElement, 'color') : ` + | ||||||
|  |         `_renderer.setStyle(_element.nativeElement, 'color', color);`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `_renderer.setValue(_element.nativeElement.querySelector('span'), 'Hello')`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `return this._renderer.selectRootElement(this._element.nativeElement);`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `shouldAdd ? this._renderer.addClass(this._element.nativeElement, className) : ` + | ||||||
|  |         `this._renderer.removeClass(this._element.nativeElement, className);`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `return __ngRendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `__ngRendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);`); | ||||||
|  |     expect(content).toContain( | ||||||
|  |         `__ngRendererProjectNodesHelper(this._renderer, this._element.nativeElement, nodesToProject);`); | ||||||
|  | 
 | ||||||
|  |     // Expect the `createRoot` only to return `this._element.nativeElement`.
 | ||||||
|  |     expect(content).toMatch(/createRoot\(\) \{\s+return this\._element\.nativeElement;\s+\}/); | ||||||
|  | 
 | ||||||
|  |     // Expect the `setInfo` method to only contain whitespace.
 | ||||||
|  |     expect(content).toMatch(/setInfo\(\) \{\s+\}/); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  | }); | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user