Skip to content

Commit

Permalink
feat(core): add automatic migration from Renderer to Renderer2 (#30936)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
crisbeto authored and alxhub committed Jul 3, 2019
1 parent 9515f17 commit c095597
Show file tree
Hide file tree
Showing 13 changed files with 2,786 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Expand Up @@ -12,6 +12,7 @@ npm_package(
deps = [
"//packages/core/schematics/migrations/injectable-pipe",
"//packages/core/schematics/migrations/move-document",
"//packages/core/schematics/migrations/renderer-to-renderer2",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
],
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Expand Up @@ -14,6 +14,11 @@
"version": "8-beta",
"description": "Warns developers if values are assigned to template variables",
"factory": "./migrations/template-var-assignment/index"
},
"migration-v9-renderer-to-renderer2": {
"version": "9-beta",
"description": "Migrates usages of Renderer to Renderer2",
"factory": "./migrations/renderer-to-renderer2/index"
}
}
}
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "renderer-to-renderer2",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/renderer-to-renderer2/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
deps = [
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)
@@ -0,0 +1,33 @@
## Renderer -> Renderer2 migration

Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports
and renaming types. Tries to either map method calls directly from one renderer to the other, or
if that's not possible, inserts custom helper functions at the bottom of the file.

#### Before
```ts
import { Renderer, ElementRef } from '@angular/core';

@Component({})
export class MyComponent {
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}

changeColor() {
this._renderer.setElementStyle(this._element.nativeElement, 'color', 'purple');
}
}
```

#### After
```ts
import { Renderer2, ElementRef } from '@angular/core';

@Component({})
export class MyComponent {
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}

changeColor() {
this._renderer.setStyle(this._element.nativeElement, 'color', 'purple');
}
}
```
@@ -0,0 +1,13 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "google3",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = ["//packages/core/schematics/test:__pkg__"],
deps = [
"//packages/core/schematics/migrations/renderer-to-renderer2",
"@npm//tslint",
"@npm//typescript",
],
)
@@ -0,0 +1,139 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Replacement, RuleFailure, Rules} from 'tslint';
import * as ts from 'typescript';

import {HelperFunction, getHelper} from '../helpers';
import {migrateExpression, replaceImport} from '../migration';
import {findCoreImport, findRendererReferences} from '../util';

/**
* TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works:
* https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw
*/
export class Rule extends Rules.TypedRule {
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
const typeChecker = program.getTypeChecker();
const printer = ts.createPrinter();
const failures: RuleFailure[] = [];
const rendererImport = findCoreImport(sourceFile, 'Renderer');

// If there are no imports for the `Renderer`, we can exit early.
if (!rendererImport) {
return failures;
}

const {typedNodes, methodCalls, forwardRefs} =
findRendererReferences(sourceFile, typeChecker, rendererImport);
const helpersToAdd = new Set<HelperFunction>();

failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer));
typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile)));
forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));

methodCalls.forEach(call => {
const {failure, requiredHelpers} =
this._getMethodCallFailure(call, sourceFile, typeChecker, printer);

failures.push(failure);

if (requiredHelpers) {
requiredHelpers.forEach(helperName => helpersToAdd.add(helperName));
}
});

// Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them.
// The safest way to do so is to declare helper functions similar to the ones emitted by TS
// which encapsulate the extra "glue" logic. We should only emit these functions once per
// file and only if they're needed.
if (helpersToAdd.size) {
failures.push(this._getHelpersFailure(helpersToAdd, sourceFile, printer));
}

return failures;
}

/** Gets a failure for an import of the Renderer. */
private _getNamedImportsFailure(
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
const replacementText = printer.printNode(
ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile);

return new RuleFailure(
sourceFile, node.getStart(), node.getEnd(),
'Imports of deprecated Renderer are not allowed. Please use Renderer2 instead.',
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
}

/** Gets a failure for a typed node (e.g. function parameter or property). */
private _getTypedNodeFailure(
node: ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression,
sourceFile: ts.SourceFile): RuleFailure {
const type = node.type !;

return new RuleFailure(
sourceFile, type.getStart(), type.getEnd(),
'References to deprecated Renderer are not allowed. Please use Renderer2 instead.',
this.ruleName, new Replacement(type.getStart(), type.getWidth(), 'Renderer2'));
}

/** Gets a failure for an identifier node. */
private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure {
return new RuleFailure(
sourceFile, node.getStart(), node.getEnd(),
'References to deprecated Renderer are not allowed. Please use Renderer2 instead.',
this.ruleName, new Replacement(node.getStart(), node.getWidth(), 'Renderer2'));
}

/** Gets a failure for a Renderer method call. */
private _getMethodCallFailure(
call: ts.CallExpression, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker,
printer: ts.Printer): {failure: RuleFailure, requiredHelpers?: HelperFunction[]} {
const {node, requiredHelpers} = migrateExpression(call, typeChecker);
let fix: Replacement|undefined;

if (node) {
// If we migrated the node to a new expression, replace only the call expression.
fix = new Replacement(
call.getStart(), call.getWidth(),
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile));
} else if (call.parent && ts.isExpressionStatement(call.parent)) {
// Otherwise if the call is inside an expression statement, drop the entire statement.
// This takes care of any trailing semicolons. We only need to drop nodes for cases like
// `setBindingDebugInfo` which have been noop for a while so they can be removed safely.
fix = new Replacement(call.parent.getStart(), call.parent.getWidth(), '');
}

return {
failure: new RuleFailure(
sourceFile, call.getStart(), call.getEnd(), 'Calls to Renderer methods are not allowed',
this.ruleName, fix),
requiredHelpers
};
}

/** Gets a failure that inserts the required helper functions at the bottom of the file. */
private _getHelpersFailure(
helpersToAdd: Set<HelperFunction>, sourceFile: ts.SourceFile,
printer: ts.Printer): RuleFailure {
const helpers: Replacement[] = [];
const endOfFile = sourceFile.endOfFileToken;

helpersToAdd.forEach(helperName => {
helpers.push(new Replacement(
endOfFile.getStart(), endOfFile.getWidth(), getHelper(helperName, sourceFile, printer)));
});

// Add a failure at the end of the file which we can use as an anchor to insert the helpers.
return new RuleFailure(
sourceFile, endOfFile.getStart(), endOfFile.getStart() + 1,
'File should contain Renderer helper functions. Run tslint with --fix to generate them.',
this.ruleName, helpers);
}
}

0 comments on commit c095597

Please sign in to comment.