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 = [ | ||||
|         "//packages/core/schematics/migrations/injectable-pipe", | ||||
|         "//packages/core/schematics/migrations/move-document", | ||||
|         "//packages/core/schematics/migrations/renderer-to-renderer2", | ||||
|         "//packages/core/schematics/migrations/static-queries", | ||||
|         "//packages/core/schematics/migrations/template-var-assignment", | ||||
|     ], | ||||
|  | ||||
| @ -14,6 +14,11 @@ | ||||
|       "version": "8-beta", | ||||
|       "description": "Warns developers if values are assigned to template variables", | ||||
|       "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/google3", | ||||
|         "//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/google3", | ||||
|         "//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