2019-06-09 09:38:18 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2019-06-09 09:38:18 -04:00
|
|
|
*
|
|
|
|
* 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';
|
|
|
|
|
2020-10-12 12:07:34 -04:00
|
|
|
import {getImportSpecifier, replaceImport} from '../../utils/typescript/imports';
|
|
|
|
import {closestNode} from '../../utils/typescript/nodes';
|
2020-04-13 19:40:21 -04:00
|
|
|
import {getHelper, HelperFunction} from '../renderer-to-renderer2/helpers';
|
2020-10-12 12:07:34 -04:00
|
|
|
import {migrateExpression} from '../renderer-to-renderer2/migration';
|
|
|
|
import {findRendererReferences} from '../renderer-to-renderer2/util';
|
2019-06-09 09:38:18 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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[] = [];
|
2020-09-12 08:33:06 -04:00
|
|
|
const rendererImportSpecifier = getImportSpecifier(sourceFile, '@angular/core', 'Renderer');
|
2020-10-12 12:07:34 -04:00
|
|
|
const rendererImport = rendererImportSpecifier ?
|
|
|
|
closestNode<ts.NamedImports>(rendererImportSpecifier, ts.SyntaxKind.NamedImports) :
|
|
|
|
null;
|
2019-06-09 09:38:18 -04:00
|
|
|
|
|
|
|
// If there are no imports for the `Renderer`, we can exit early.
|
2020-09-12 08:33:06 -04:00
|
|
|
if (!rendererImportSpecifier || !rendererImport) {
|
2019-06-09 09:38:18 -04:00
|
|
|
return failures;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {typedNodes, methodCalls, forwardRefs} =
|
2020-09-12 08:33:06 -04:00
|
|
|
findRendererReferences(sourceFile, typeChecker, rendererImportSpecifier);
|
2019-06-09 09:38:18 -04:00
|
|
|
const helpersToAdd = new Set<HelperFunction>();
|
|
|
|
|
2020-10-12 12:07:34 -04:00
|
|
|
failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer));
|
2019-06-09 09:38:18 -04:00
|
|
|
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(
|
2020-10-12 12:07:34 -04:00
|
|
|
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
|
2019-06-09 09:38:18 -04:00
|
|
|
const replacementText = printer.printNode(
|
2020-10-12 12:07:34 -04:00
|
|
|
ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile);
|
2019-06-09 09:38:18 -04:00
|
|
|
|
|
|
|
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 {
|
2020-04-13 19:40:21 -04:00
|
|
|
const type = node.type!;
|
2019-06-09 09:38:18 -04:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|