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