Skip to content

Commit c095597

Browse files
crisbetoalxhub
authored andcommittedJul 3, 2019
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
1 parent 9515f17 commit c095597

File tree

13 files changed

+2786
-0
lines changed

13 files changed

+2786
-0
lines changed
 

‎packages/core/schematics/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ npm_package(
1212
deps = [
1313
"//packages/core/schematics/migrations/injectable-pipe",
1414
"//packages/core/schematics/migrations/move-document",
15+
"//packages/core/schematics/migrations/renderer-to-renderer2",
1516
"//packages/core/schematics/migrations/static-queries",
1617
"//packages/core/schematics/migrations/template-var-assignment",
1718
],

‎packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"version": "8-beta",
1515
"description": "Warns developers if values are assigned to template variables",
1616
"factory": "./migrations/template-var-assignment/index"
17+
},
18+
"migration-v9-renderer-to-renderer2": {
19+
"version": "9-beta",
20+
"description": "Migrates usages of Renderer to Renderer2",
21+
"factory": "./migrations/renderer-to-renderer2/index"
1722
}
1823
}
1924
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "renderer-to-renderer2",
5+
srcs = glob(["**/*.ts"]),
6+
tsconfig = "//packages/core/schematics:tsconfig.json",
7+
visibility = [
8+
"//packages/core/schematics:__pkg__",
9+
"//packages/core/schematics/migrations/renderer-to-renderer2/google3:__pkg__",
10+
"//packages/core/schematics/test:__pkg__",
11+
],
12+
deps = [
13+
"//packages/core/schematics/utils",
14+
"@npm//@angular-devkit/schematics",
15+
"@npm//@types/node",
16+
"@npm//typescript",
17+
],
18+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## Renderer -> Renderer2 migration
2+
3+
Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports
4+
and renaming types. Tries to either map method calls directly from one renderer to the other, or
5+
if that's not possible, inserts custom helper functions at the bottom of the file.
6+
7+
#### Before
8+
```ts
9+
import { Renderer, ElementRef } from '@angular/core';
10+
11+
@Component({})
12+
export class MyComponent {
13+
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
14+
15+
changeColor() {
16+
this._renderer.setElementStyle(this._element.nativeElement, 'color', 'purple');
17+
}
18+
}
19+
```
20+
21+
#### After
22+
```ts
23+
import { Renderer2, ElementRef } from '@angular/core';
24+
25+
@Component({})
26+
export class MyComponent {
27+
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
28+
29+
changeColor() {
30+
this._renderer.setStyle(this._element.nativeElement, 'color', 'purple');
31+
}
32+
}
33+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "google3",
5+
srcs = glob(["**/*.ts"]),
6+
tsconfig = "//packages/core/schematics:tsconfig.json",
7+
visibility = ["//packages/core/schematics/test:__pkg__"],
8+
deps = [
9+
"//packages/core/schematics/migrations/renderer-to-renderer2",
10+
"@npm//tslint",
11+
"@npm//typescript",
12+
],
13+
)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Replacement, RuleFailure, Rules} from 'tslint';
10+
import * as ts from 'typescript';
11+
12+
import {HelperFunction, getHelper} from '../helpers';
13+
import {migrateExpression, replaceImport} from '../migration';
14+
import {findCoreImport, findRendererReferences} from '../util';
15+
16+
/**
17+
* TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works:
18+
* https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw
19+
*/
20+
export class Rule extends Rules.TypedRule {
21+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
22+
const typeChecker = program.getTypeChecker();
23+
const printer = ts.createPrinter();
24+
const failures: RuleFailure[] = [];
25+
const rendererImport = findCoreImport(sourceFile, 'Renderer');
26+
27+
// If there are no imports for the `Renderer`, we can exit early.
28+
if (!rendererImport) {
29+
return failures;
30+
}
31+
32+
const {typedNodes, methodCalls, forwardRefs} =
33+
findRendererReferences(sourceFile, typeChecker, rendererImport);
34+
const helpersToAdd = new Set<HelperFunction>();
35+
36+
failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer));
37+
typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile)));
38+
forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
39+
40+
methodCalls.forEach(call => {
41+
const {failure, requiredHelpers} =
42+
this._getMethodCallFailure(call, sourceFile, typeChecker, printer);
43+
44+
failures.push(failure);
45+
46+
if (requiredHelpers) {
47+
requiredHelpers.forEach(helperName => helpersToAdd.add(helperName));
48+
}
49+
});
50+
51+
// Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them.
52+
// The safest way to do so is to declare helper functions similar to the ones emitted by TS
53+
// which encapsulate the extra "glue" logic. We should only emit these functions once per
54+
// file and only if they're needed.
55+
if (helpersToAdd.size) {
56+
failures.push(this._getHelpersFailure(helpersToAdd, sourceFile, printer));
57+
}
58+
59+
return failures;
60+
}
61+
62+
/** Gets a failure for an import of the Renderer. */
63+
private _getNamedImportsFailure(
64+
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
65+
const replacementText = printer.printNode(
66+
ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile);
67+
68+
return new RuleFailure(
69+
sourceFile, node.getStart(), node.getEnd(),
70+
'Imports of deprecated Renderer are not allowed. Please use Renderer2 instead.',
71+
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
72+
}
73+
74+
/** Gets a failure for a typed node (e.g. function parameter or property). */
75+
private _getTypedNodeFailure(
76+
node: ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression,
77+
sourceFile: ts.SourceFile): RuleFailure {
78+
const type = node.type !;
79+
80+
return new RuleFailure(
81+
sourceFile, type.getStart(), type.getEnd(),
82+
'References to deprecated Renderer are not allowed. Please use Renderer2 instead.',
83+
this.ruleName, new Replacement(type.getStart(), type.getWidth(), 'Renderer2'));
84+
}
85+
86+
/** Gets a failure for an identifier node. */
87+
private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure {
88+
return new RuleFailure(
89+
sourceFile, node.getStart(), node.getEnd(),
90+
'References to deprecated Renderer are not allowed. Please use Renderer2 instead.',
91+
this.ruleName, new Replacement(node.getStart(), node.getWidth(), 'Renderer2'));
92+
}
93+
94+
/** Gets a failure for a Renderer method call. */
95+
private _getMethodCallFailure(
96+
call: ts.CallExpression, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker,
97+
printer: ts.Printer): {failure: RuleFailure, requiredHelpers?: HelperFunction[]} {
98+
const {node, requiredHelpers} = migrateExpression(call, typeChecker);
99+
let fix: Replacement|undefined;
100+
101+
if (node) {
102+
// If we migrated the node to a new expression, replace only the call expression.
103+
fix = new Replacement(
104+
call.getStart(), call.getWidth(),
105+
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile));
106+
} else if (call.parent && ts.isExpressionStatement(call.parent)) {
107+
// Otherwise if the call is inside an expression statement, drop the entire statement.
108+
// This takes care of any trailing semicolons. We only need to drop nodes for cases like
109+
// `setBindingDebugInfo` which have been noop for a while so they can be removed safely.
110+
fix = new Replacement(call.parent.getStart(), call.parent.getWidth(), '');
111+
}
112+
113+
return {
114+
failure: new RuleFailure(
115+
sourceFile, call.getStart(), call.getEnd(), 'Calls to Renderer methods are not allowed',
116+
this.ruleName, fix),
117+
requiredHelpers
118+
};
119+
}
120+
121+
/** Gets a failure that inserts the required helper functions at the bottom of the file. */
122+
private _getHelpersFailure(
123+
helpersToAdd: Set<HelperFunction>, sourceFile: ts.SourceFile,
124+
printer: ts.Printer): RuleFailure {
125+
const helpers: Replacement[] = [];
126+
const endOfFile = sourceFile.endOfFileToken;
127+
128+
helpersToAdd.forEach(helperName => {
129+
helpers.push(new Replacement(
130+
endOfFile.getStart(), endOfFile.getWidth(), getHelper(helperName, sourceFile, printer)));
131+
});
132+
133+
// Add a failure at the end of the file which we can use as an anchor to insert the helpers.
134+
return new RuleFailure(
135+
sourceFile, endOfFile.getStart(), endOfFile.getStart() + 1,
136+
'File should contain Renderer helper functions. Run tslint with --fix to generate them.',
137+
this.ruleName, helpers);
138+
}
139+
}

0 commit comments

Comments
 (0)
Please sign in to comment.