angular-cn/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts

269 lines
12 KiB
TypeScript
Raw Normal View History

/**
* @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]);
}