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:
crisbeto 2019-06-09 15:38:18 +02:00 committed by Alex Rickabaugh
parent 9515f171b4
commit c0955975f4
13 changed files with 2786 additions and 0 deletions

View File

@ -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",
], ],

View File

@ -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"
} }
} }
} }

View File

@ -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",
],
)

View File

@ -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');
}
}
```

View File

@ -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",
],
)

View File

@ -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);
}
}

View File

@ -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));
}

View File

@ -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);
});
}

View File

@ -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]);
}

View File

@ -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;
}

View File

@ -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",

View File

@ -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