Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
13 changed files
with
2,786 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
], | ||
) |
33 changes: 33 additions & 0 deletions
33
packages/core/schematics/migrations/renderer-to-renderer2/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} | ||
} | ||
``` |
13 changes: 13 additions & 0 deletions
13
packages/core/schematics/migrations/renderer-to-renderer2/google3/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
], | ||
) |
139 changes: 139 additions & 0 deletions
139
packages/core/schematics/migrations/renderer-to-renderer2/google3/rendererToRenderer2Rule.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Oops, something went wrong.