269 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			269 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * @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]); | ||
|  | } |