1import { 2 ArrowFunction, AwaitExpression, BindingName, BindingPattern, Block, CallExpression, canBeConvertedToAsync, 3 CodeFixContext, concatenate, createMultiMap, Debug, Diagnostics, elementAt, emptyArray, ESMap, every, Expression, 4 factory, firstOrUndefined, flatMap, forEach, forEachChild, forEachReturnStatement, FunctionExpression, 5 FunctionLikeDeclaration, GeneratedIdentifierFlags, getContainingFunction, getNodeId, getObjectFlags, 6 getOriginalNode, getSymbolId, getSynthesizedDeepClone, getSynthesizedDeepCloneWithReplacements, getTokenAtPosition, 7 hasPropertyAccessExpressionWithName, Identifier, idText, isBindingElement, isBlock, isCallExpression, isExpression, 8 isFixablePromiseHandler, isFunctionLike, isFunctionLikeDeclaration, isGeneratedIdentifier, isIdentifier, isInJSFile, 9 isObjectBindingPattern, isOmittedExpression, isParameter, isPropertyAccessExpression, isReturnStatement, 10 isReturnStatementWithFixablePromiseHandler, isVariableDeclaration, lastOrUndefined, Map, moveRangePastModifiers, 11 Node, NodeFlags, ObjectFlags, PropertyAccessExpression, ReadonlyESMap, ReadonlySet, returnsPromise, ReturnStatement, 12 returnTrue, Set, Signature, SignatureKind, skipTrivia, SourceFile, Statement, Symbol, SyntaxKind, textChanges, 13 tryCast, TryStatement, Type, TypeChecker, TypeNode, TypeReference, UnionReduction, 14} from "../_namespaces/ts"; 15import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix"; 16 17const fixId = "convertToAsyncFunction"; 18const errorCodes = [Diagnostics.This_may_be_converted_to_an_async_function.code]; 19let codeActionSucceeded = true; 20registerCodeFix({ 21 errorCodes, 22 getCodeActions(context: CodeFixContext) { 23 codeActionSucceeded = true; 24 const changes = textChanges.ChangeTracker.with(context, (t) => convertToAsyncFunction(t, context.sourceFile, context.span.start, context.program.getTypeChecker())); 25 return codeActionSucceeded ? [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_async_function, fixId, Diagnostics.Convert_all_to_async_functions)] : []; 26 }, 27 fixIds: [fixId], 28 getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) => convertToAsyncFunction(changes, err.file, err.start, context.program.getTypeChecker())), 29}); 30 31const enum SynthBindingNameKind { 32 Identifier, 33 BindingPattern, 34} 35 36type SynthBindingName = SynthBindingPattern | SynthIdentifier; 37 38interface SynthBindingPattern { 39 readonly kind: SynthBindingNameKind.BindingPattern; 40 readonly elements: readonly SynthBindingName[]; 41 readonly bindingPattern: BindingPattern; 42 readonly types: Type[]; 43} 44 45interface SynthIdentifier { 46 readonly kind: SynthBindingNameKind.Identifier; 47 readonly identifier: Identifier; 48 readonly types: Type[]; 49 /** A declaration for this identifier has already been generated */ 50 hasBeenDeclared: boolean; 51 hasBeenReferenced: boolean; 52} 53 54interface Transformer { 55 readonly checker: TypeChecker; 56 readonly synthNamesMap: ESMap<string, SynthIdentifier>; // keys are the symbol id of the identifier 57 readonly setOfExpressionsToReturn: ReadonlySet<number>; // keys are the node ids of the expressions 58 readonly isInJSFile: boolean; 59} 60 61interface PromiseReturningCallExpression<Name extends string> extends CallExpression { 62 readonly expression: PropertyAccessExpression & { 63 readonly escapedText: Name; 64 }; 65} 66 67function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker): void { 68 // get the function declaration - returns a promise 69 const tokenAtPosition = getTokenAtPosition(sourceFile, position); 70 let functionToConvert: FunctionLikeDeclaration | undefined; 71 72 // if the parent of a FunctionLikeDeclaration is a variable declaration, the convertToAsync diagnostic will be reported on the variable name 73 if (isIdentifier(tokenAtPosition) && isVariableDeclaration(tokenAtPosition.parent) && 74 tokenAtPosition.parent.initializer && isFunctionLikeDeclaration(tokenAtPosition.parent.initializer)) { 75 functionToConvert = tokenAtPosition.parent.initializer; 76 } 77 else { 78 functionToConvert = tryCast(getContainingFunction(getTokenAtPosition(sourceFile, position)), canBeConvertedToAsync); 79 } 80 81 if (!functionToConvert) { 82 return; 83 } 84 85 const synthNamesMap = new Map<string, SynthIdentifier>(); 86 const isInJavascript = isInJSFile(functionToConvert); 87 const setOfExpressionsToReturn = getAllPromiseExpressionsToReturn(functionToConvert, checker); 88 const functionToConvertRenamed = renameCollidingVarNames(functionToConvert, checker, synthNamesMap); 89 if (!returnsPromise(functionToConvertRenamed, checker)) { 90 return; 91 } 92 93 const returnStatements = functionToConvertRenamed.body && isBlock(functionToConvertRenamed.body) ? getReturnStatementsWithPromiseHandlers(functionToConvertRenamed.body, checker) : emptyArray; 94 const transformer: Transformer = { checker, synthNamesMap, setOfExpressionsToReturn, isInJSFile: isInJavascript }; 95 if (!returnStatements.length) { 96 return; 97 } 98 99 const pos = skipTrivia(sourceFile.text, moveRangePastModifiers(functionToConvert).pos); 100 changes.insertModifierAt(sourceFile, pos, SyntaxKind.AsyncKeyword, { suffix: " " }); 101 102 for (const returnStatement of returnStatements) { 103 forEachChild(returnStatement, function visit(node) { 104 if (isCallExpression(node)) { 105 const newNodes = transformExpression(node, node, transformer, /*hasContinuation*/ false); 106 if (hasFailed()) { 107 return true; // return something truthy to shortcut out of more work 108 } 109 changes.replaceNodeWithNodes(sourceFile, returnStatement, newNodes); 110 } 111 else if (!isFunctionLike(node)) { 112 forEachChild(node, visit); 113 if (hasFailed()) { 114 return true; // return something truthy to shortcut out of more work 115 } 116 } 117 }); 118 if (hasFailed()) { 119 return; // shortcut out of more work 120 } 121 } 122} 123 124function getReturnStatementsWithPromiseHandlers(body: Block, checker: TypeChecker): readonly ReturnStatement[] { 125 const res: ReturnStatement[] = []; 126 forEachReturnStatement(body, ret => { 127 if (isReturnStatementWithFixablePromiseHandler(ret, checker)) res.push(ret); 128 }); 129 return res; 130} 131 132/* 133 Finds all of the expressions of promise type that should not be saved in a variable during the refactor 134*/ 135function getAllPromiseExpressionsToReturn(func: FunctionLikeDeclaration, checker: TypeChecker): Set<number> { 136 if (!func.body) { 137 return new Set(); 138 } 139 140 const setOfExpressionsToReturn = new Set<number>(); 141 forEachChild(func.body, function visit(node: Node) { 142 if (isPromiseReturningCallExpression(node, checker, "then")) { 143 setOfExpressionsToReturn.add(getNodeId(node)); 144 forEach(node.arguments, visit); 145 } 146 else if (isPromiseReturningCallExpression(node, checker, "catch") || 147 isPromiseReturningCallExpression(node, checker, "finally")) { 148 setOfExpressionsToReturn.add(getNodeId(node)); 149 // if .catch() or .finally() is the last call in the chain, move leftward in the chain until we hit something else that should be returned 150 forEachChild(node, visit); 151 } 152 else if (isPromiseTypedExpression(node, checker)) { 153 setOfExpressionsToReturn.add(getNodeId(node)); 154 // don't recurse here, since we won't refactor any children or arguments of the expression 155 } 156 else { 157 forEachChild(node, visit); 158 } 159 }); 160 161 return setOfExpressionsToReturn; 162} 163 164function isPromiseReturningCallExpression<Name extends string>(node: Node, checker: TypeChecker, name: Name): node is PromiseReturningCallExpression<Name> { 165 if (!isCallExpression(node)) return false; 166 const isExpressionOfName = hasPropertyAccessExpressionWithName(node, name); 167 const nodeType = isExpressionOfName && checker.getTypeAtLocation(node); 168 return !!(nodeType && checker.getPromisedTypeOfPromise(nodeType)); 169} 170 171// NOTE: this is a mostly copy of `isReferenceToType` from checker.ts. While this violates DRY, it keeps 172// `isReferenceToType` in checker local to the checker to avoid the cost of a property lookup on `ts`. 173function isReferenceToType(type: Type, target: Type) { 174 return (getObjectFlags(type) & ObjectFlags.Reference) !== 0 175 && (type as TypeReference).target === target; 176} 177 178function getExplicitPromisedTypeOfPromiseReturningCallExpression(node: PromiseReturningCallExpression<"then" | "catch" | "finally">, callback: Expression, checker: TypeChecker) { 179 if (node.expression.name.escapedText === "finally") { 180 // for a `finally`, there's no type argument 181 return undefined; 182 } 183 184 // If the call to `then` or `catch` comes from the global `Promise` or `PromiseLike` type, we can safely use the 185 // type argument supplied for the callback. For other promise types we would need a more complex heuristic to determine 186 // which type argument is safe to use as an annotation. 187 const promiseType = checker.getTypeAtLocation(node.expression.expression); 188 if (isReferenceToType(promiseType, checker.getPromiseType()) || 189 isReferenceToType(promiseType, checker.getPromiseLikeType())) { 190 if (node.expression.name.escapedText === "then") { 191 if (callback === elementAt(node.arguments, 0)) { 192 // for the `onfulfilled` callback, use the first type argument 193 return elementAt(node.typeArguments, 0); 194 } 195 else if (callback === elementAt(node.arguments, 1)) { 196 // for the `onrejected` callback, use the second type argument 197 return elementAt(node.typeArguments, 1); 198 } 199 } 200 else { 201 return elementAt(node.typeArguments, 0); 202 } 203 } 204} 205 206function isPromiseTypedExpression(node: Node, checker: TypeChecker): node is Expression { 207 if (!isExpression(node)) return false; 208 return !!checker.getPromisedTypeOfPromise(checker.getTypeAtLocation(node)); 209} 210 211/* 212 Renaming of identifiers may be necessary as the refactor changes scopes - 213 This function collects all existing identifier names and names of identifiers that will be created in the refactor. 214 It then checks for any collisions and renames them through getSynthesizedDeepClone 215*/ 216function renameCollidingVarNames(nodeToRename: FunctionLikeDeclaration, checker: TypeChecker, synthNamesMap: ESMap<string, SynthIdentifier>): FunctionLikeDeclaration { 217 const identsToRenameMap = new Map<string, Identifier>(); // key is the symbol id 218 const collidingSymbolMap = createMultiMap<Symbol>(); 219 forEachChild(nodeToRename, function visit(node: Node) { 220 if (!isIdentifier(node)) { 221 forEachChild(node, visit); 222 return; 223 } 224 const symbol = checker.getSymbolAtLocation(node); 225 if (symbol) { 226 const type = checker.getTypeAtLocation(node); 227 // Note - the choice of the last call signature is arbitrary 228 const lastCallSignature = getLastCallSignature(type, checker); 229 const symbolIdString = getSymbolId(symbol).toString(); 230 231 // If the identifier refers to a function, we want to add the new synthesized variable for the declaration. Example: 232 // fetch('...').then(response => { ... }) 233 // will eventually become 234 // const response = await fetch('...') 235 // so we push an entry for 'response'. 236 if (lastCallSignature && !isParameter(node.parent) && !isFunctionLikeDeclaration(node.parent) && !synthNamesMap.has(symbolIdString)) { 237 const firstParameter = firstOrUndefined(lastCallSignature.parameters); 238 const ident = firstParameter?.valueDeclaration 239 && isParameter(firstParameter.valueDeclaration) 240 && tryCast(firstParameter.valueDeclaration.name, isIdentifier) 241 || factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic); 242 const synthName = getNewNameIfConflict(ident, collidingSymbolMap); 243 synthNamesMap.set(symbolIdString, synthName); 244 collidingSymbolMap.add(ident.text, symbol); 245 } 246 // We only care about identifiers that are parameters, variable declarations, or binding elements 247 else if (node.parent && (isParameter(node.parent) || isVariableDeclaration(node.parent) || isBindingElement(node.parent))) { 248 const originalName = node.text; 249 const collidingSymbols = collidingSymbolMap.get(originalName); 250 251 // if the identifier name conflicts with a different identifier that we've already seen 252 if (collidingSymbols && collidingSymbols.some(prevSymbol => prevSymbol !== symbol)) { 253 const newName = getNewNameIfConflict(node, collidingSymbolMap); 254 identsToRenameMap.set(symbolIdString, newName.identifier); 255 synthNamesMap.set(symbolIdString, newName); 256 collidingSymbolMap.add(originalName, symbol); 257 } 258 else { 259 const identifier = getSynthesizedDeepClone(node); 260 synthNamesMap.set(symbolIdString, createSynthIdentifier(identifier)); 261 collidingSymbolMap.add(originalName, symbol); 262 } 263 } 264 } 265 }); 266 267 return getSynthesizedDeepCloneWithReplacements(nodeToRename, /*includeTrivia*/ true, original => { 268 if (isBindingElement(original) && isIdentifier(original.name) && isObjectBindingPattern(original.parent)) { 269 const symbol = checker.getSymbolAtLocation(original.name); 270 const renameInfo = symbol && identsToRenameMap.get(String(getSymbolId(symbol))); 271 if (renameInfo && renameInfo.text !== (original.name || original.propertyName).getText()) { 272 return factory.createBindingElement( 273 original.dotDotDotToken, 274 original.propertyName || original.name, 275 renameInfo, 276 original.initializer); 277 } 278 } 279 else if (isIdentifier(original)) { 280 const symbol = checker.getSymbolAtLocation(original); 281 const renameInfo = symbol && identsToRenameMap.get(String(getSymbolId(symbol))); 282 if (renameInfo) { 283 return factory.createIdentifier(renameInfo.text); 284 } 285 } 286 }); 287} 288 289function getNewNameIfConflict(name: Identifier, originalNames: ReadonlyESMap<string, Symbol[]>): SynthIdentifier { 290 const numVarsSameName = (originalNames.get(name.text) || emptyArray).length; 291 const identifier = numVarsSameName === 0 ? name : factory.createIdentifier(name.text + "_" + numVarsSameName); 292 return createSynthIdentifier(identifier); 293} 294 295function hasFailed() { 296 return !codeActionSucceeded; 297} 298 299function silentFail() { 300 codeActionSucceeded = false; 301 return emptyArray; 302} 303 304// dispatch function to recursively build the refactoring 305// should be kept up to date with isFixablePromiseHandler in suggestionDiagnostics.ts 306/** 307 * @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows the continuation to which this expression belongs. 308 * @param continuationArgName The argument name for the continuation that follows this call. 309 */ 310function transformExpression(returnContextNode: Expression, node: Expression, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] { 311 if (isPromiseReturningCallExpression(node, transformer.checker, "then")) { 312 return transformThen(node, elementAt(node.arguments, 0), elementAt(node.arguments, 1), transformer, hasContinuation, continuationArgName); 313 } 314 if (isPromiseReturningCallExpression(node, transformer.checker, "catch")) { 315 return transformCatch(node, elementAt(node.arguments, 0), transformer, hasContinuation, continuationArgName); 316 } 317 if (isPromiseReturningCallExpression(node, transformer.checker, "finally")) { 318 return transformFinally(node, elementAt(node.arguments, 0), transformer, hasContinuation, continuationArgName); 319 } 320 if (isPropertyAccessExpression(node)) { 321 return transformExpression(returnContextNode, node.expression, transformer, hasContinuation, continuationArgName); 322 } 323 324 const nodeType = transformer.checker.getTypeAtLocation(node); 325 if (nodeType && transformer.checker.getPromisedTypeOfPromise(nodeType)) { 326 Debug.assertNode(getOriginalNode(node).parent, isPropertyAccessExpression); 327 return transformPromiseExpressionOfPropertyAccess(returnContextNode, node, transformer, hasContinuation, continuationArgName); 328 } 329 330 return silentFail(); 331} 332 333function isNullOrUndefined({ checker }: Transformer, node: Expression) { 334 if (node.kind === SyntaxKind.NullKeyword) return true; 335 if (isIdentifier(node) && !isGeneratedIdentifier(node) && idText(node) === "undefined") { 336 const symbol = checker.getSymbolAtLocation(node); 337 return !symbol || checker.isUndefinedSymbol(symbol); 338 } 339 return false; 340} 341 342function createUniqueSynthName(prevArgName: SynthIdentifier): SynthIdentifier { 343 const renamedPrevArg = factory.createUniqueName(prevArgName.identifier.text, GeneratedIdentifierFlags.Optimistic); 344 return createSynthIdentifier(renamedPrevArg); 345} 346 347function getPossibleNameForVarDecl(node: PromiseReturningCallExpression<"then" | "catch" | "finally">, transformer: Transformer, continuationArgName?: SynthBindingName) { 348 let possibleNameForVarDecl: SynthIdentifier | undefined; 349 350 // If there is another call in the chain after the .catch() or .finally() we are transforming, we will need to save the result of both paths 351 // (try block and catch/finally block). To do this, we will need to synthesize a variable that we were not aware of while we were adding 352 // identifiers to the synthNamesMap. We will use the continuationArgName and then update the synthNamesMap with a new variable name for 353 // the next transformation step 354 355 if (continuationArgName && !shouldReturn(node, transformer)) { 356 if (isSynthIdentifier(continuationArgName)) { 357 possibleNameForVarDecl = continuationArgName; 358 transformer.synthNamesMap.forEach((val, key) => { 359 if (val.identifier.text === continuationArgName.identifier.text) { 360 const newSynthName = createUniqueSynthName(continuationArgName); 361 transformer.synthNamesMap.set(key, newSynthName); 362 } 363 }); 364 } 365 else { 366 possibleNameForVarDecl = createSynthIdentifier(factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic), continuationArgName.types); 367 } 368 369 // We are about to write a 'let' variable declaration, but `transformExpression` for both 370 // the try block and catch/finally block will assign to this name. Setting this flag indicates 371 // that future assignments should be written as `name = value` instead of `const name = value`. 372 declareSynthIdentifier(possibleNameForVarDecl); 373 } 374 375 return possibleNameForVarDecl; 376} 377 378function finishCatchOrFinallyTransform(node: PromiseReturningCallExpression<"then" | "catch" | "finally">, transformer: Transformer, tryStatement: TryStatement, possibleNameForVarDecl: SynthIdentifier | undefined, continuationArgName?: SynthBindingName) { 379 const statements: Statement[] = []; 380 381 // In order to avoid an implicit any, we will synthesize a type for the declaration using the unions of the types of both paths (try block and catch block) 382 let varDeclIdentifier: Identifier | undefined; 383 384 if (possibleNameForVarDecl && !shouldReturn(node, transformer)) { 385 varDeclIdentifier = getSynthesizedDeepClone(declareSynthIdentifier(possibleNameForVarDecl)); 386 const typeArray: Type[] = possibleNameForVarDecl.types; 387 const unionType = transformer.checker.getUnionType(typeArray, UnionReduction.Subtype); 388 const unionTypeNode = transformer.isInJSFile ? undefined : transformer.checker.typeToTypeNode(unionType, /*enclosingDeclaration*/ undefined, /*flags*/ undefined); 389 const varDecl = [factory.createVariableDeclaration(varDeclIdentifier, /*exclamationToken*/ undefined, unionTypeNode)]; 390 const varDeclList = factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList(varDecl, NodeFlags.Let)); 391 statements.push(varDeclList); 392 } 393 394 statements.push(tryStatement); 395 396 if (continuationArgName && varDeclIdentifier && isSynthBindingPattern(continuationArgName)) { 397 statements.push(factory.createVariableStatement( 398 /*modifiers*/ undefined, 399 factory.createVariableDeclarationList([ 400 factory.createVariableDeclaration( 401 getSynthesizedDeepClone(declareSynthBindingPattern(continuationArgName)), 402 /*exclamationToken*/ undefined, 403 /*type*/ undefined, 404 varDeclIdentifier 405 )], 406 NodeFlags.Const))); 407 } 408 409 return statements; 410} 411 412/** 413 * @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows this continuation. 414 * @param continuationArgName The argument name for the continuation that follows this call. 415 */ 416function transformFinally(node: PromiseReturningCallExpression<"finally">, onFinally: Expression | undefined, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] { 417 if (!onFinally || isNullOrUndefined(transformer, onFinally)) { 418 // Ignore this call as it has no effect on the result 419 return transformExpression(/* returnContextNode */ node, node.expression.expression, transformer, hasContinuation, continuationArgName); 420 } 421 422 const possibleNameForVarDecl = getPossibleNameForVarDecl(node, transformer, continuationArgName); 423 424 // Transform the left-hand-side of `.finally` into an array of inlined statements. We pass `true` for hasContinuation as `node` is the outer continuation. 425 const inlinedLeftHandSide = transformExpression(/*returnContextNode*/ node, node.expression.expression, transformer, /*hasContinuation*/ true, possibleNameForVarDecl); 426 if (hasFailed()) return silentFail(); // shortcut out of more work 427 428 // Transform the callback argument into an array of inlined statements. We pass whether we have an outer continuation here 429 // as that indicates whether `return` is valid. 430 const inlinedCallback = transformCallbackArgument(onFinally, hasContinuation, /*continuationArgName*/ undefined, /*argName*/ undefined, node, transformer); 431 if (hasFailed()) return silentFail(); // shortcut out of more work 432 433 const tryBlock = factory.createBlock(inlinedLeftHandSide); 434 const finallyBlock = factory.createBlock(inlinedCallback); 435 const tryStatement = factory.createTryStatement(tryBlock, /*catchClause*/ undefined, finallyBlock); 436 return finishCatchOrFinallyTransform(node, transformer, tryStatement, possibleNameForVarDecl, continuationArgName); 437} 438 439/** 440 * @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows this continuation. 441 * @param continuationArgName The argument name for the continuation that follows this call. 442 */ 443function transformCatch(node: PromiseReturningCallExpression<"then" | "catch">, onRejected: Expression | undefined, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] { 444 if (!onRejected || isNullOrUndefined(transformer, onRejected)) { 445 // Ignore this call as it has no effect on the result 446 return transformExpression(/* returnContextNode */ node, node.expression.expression, transformer, hasContinuation, continuationArgName); 447 } 448 449 const inputArgName = getArgBindingName(onRejected, transformer); 450 const possibleNameForVarDecl = getPossibleNameForVarDecl(node, transformer, continuationArgName); 451 452 // Transform the left-hand-side of `.then`/`.catch` into an array of inlined statements. We pass `true` for hasContinuation as `node` is the outer continuation. 453 const inlinedLeftHandSide = transformExpression(/*returnContextNode*/ node, node.expression.expression, transformer, /*hasContinuation*/ true, possibleNameForVarDecl); 454 if (hasFailed()) return silentFail(); // shortcut out of more work 455 456 // Transform the callback argument into an array of inlined statements. We pass whether we have an outer continuation here 457 // as that indicates whether `return` is valid. 458 const inlinedCallback = transformCallbackArgument(onRejected, hasContinuation, possibleNameForVarDecl, inputArgName, node, transformer); 459 if (hasFailed()) return silentFail(); // shortcut out of more work 460 461 const tryBlock = factory.createBlock(inlinedLeftHandSide); 462 const catchClause = factory.createCatchClause(inputArgName && getSynthesizedDeepClone(declareSynthBindingName(inputArgName)), factory.createBlock(inlinedCallback)); 463 const tryStatement = factory.createTryStatement(tryBlock, catchClause, /*finallyBlock*/ undefined); 464 return finishCatchOrFinallyTransform(node, transformer, tryStatement, possibleNameForVarDecl, continuationArgName); 465} 466 467/** 468 * @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows this continuation. 469 * @param continuationArgName The argument name for the continuation that follows this call. 470 */ 471function transformThen(node: PromiseReturningCallExpression<"then">, onFulfilled: Expression | undefined, onRejected: Expression | undefined, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] { 472 if (!onFulfilled || isNullOrUndefined(transformer, onFulfilled)) { 473 // If we don't have an `onfulfilled` callback, try treating this as a `.catch`. 474 return transformCatch(node, onRejected, transformer, hasContinuation, continuationArgName); 475 } 476 477 // We don't currently support transforming a `.then` with both onfulfilled and onrejected handlers, per GH#38152. 478 if (onRejected && !isNullOrUndefined(transformer, onRejected)) { 479 return silentFail(); 480 } 481 482 const inputArgName = getArgBindingName(onFulfilled, transformer); 483 484 // Transform the left-hand-side of `.then` into an array of inlined statements. We pass `true` for hasContinuation as `node` is the outer continuation. 485 const inlinedLeftHandSide = transformExpression(node.expression.expression, node.expression.expression, transformer, /*hasContinuation*/ true, inputArgName); 486 if (hasFailed()) return silentFail(); // shortcut out of more work 487 488 // Transform the callback argument into an array of inlined statements. We pass whether we have an outer continuation here 489 // as that indicates whether `return` is valid. 490 const inlinedCallback = transformCallbackArgument(onFulfilled, hasContinuation, continuationArgName, inputArgName, node, transformer); 491 if (hasFailed()) return silentFail(); // shortcut out of more work 492 493 return concatenate(inlinedLeftHandSide, inlinedCallback); 494} 495 496/** 497 * Transforms the 'x' part of `x.then(...)`, or the 'y()' part of `y().catch(...)`, where 'x' and 'y()' are Promises. 498 */ 499function transformPromiseExpressionOfPropertyAccess(returnContextNode: Expression, node: Expression, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] { 500 if (shouldReturn(returnContextNode, transformer)) { 501 let returnValue = getSynthesizedDeepClone(node); 502 if (hasContinuation) { 503 returnValue = factory.createAwaitExpression(returnValue); 504 } 505 return [factory.createReturnStatement(returnValue)]; 506 } 507 508 return createVariableOrAssignmentOrExpressionStatement(continuationArgName, factory.createAwaitExpression(node), /*typeAnnotation*/ undefined); 509} 510 511function createVariableOrAssignmentOrExpressionStatement(variableName: SynthBindingName | undefined, rightHandSide: Expression, typeAnnotation: TypeNode | undefined): readonly Statement[] { 512 if (!variableName || isEmptyBindingName(variableName)) { 513 // if there's no argName to assign to, there still might be side effects 514 return [factory.createExpressionStatement(rightHandSide)]; 515 } 516 517 if (isSynthIdentifier(variableName) && variableName.hasBeenDeclared) { 518 // if the variable has already been declared, we don't need "let" or "const" 519 return [factory.createExpressionStatement(factory.createAssignment(getSynthesizedDeepClone(referenceSynthIdentifier(variableName)), rightHandSide))]; 520 } 521 522 return [ 523 factory.createVariableStatement( 524 /*modifiers*/ undefined, 525 factory.createVariableDeclarationList([ 526 factory.createVariableDeclaration( 527 getSynthesizedDeepClone(declareSynthBindingName(variableName)), 528 /*exclamationToken*/ undefined, 529 typeAnnotation, 530 rightHandSide)], 531 NodeFlags.Const))]; 532} 533 534function maybeAnnotateAndReturn(expressionToReturn: Expression | undefined, typeAnnotation: TypeNode | undefined): Statement[] { 535 if (typeAnnotation && expressionToReturn) { 536 const name = factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic); 537 return [ 538 ...createVariableOrAssignmentOrExpressionStatement(createSynthIdentifier(name), expressionToReturn, typeAnnotation), 539 factory.createReturnStatement(name) 540 ]; 541 } 542 return [factory.createReturnStatement(expressionToReturn)]; 543} 544 545// should be kept up to date with isFixablePromiseArgument in suggestionDiagnostics.ts 546/** 547 * @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows the continuation to which this callback belongs. 548 * @param continuationArgName The argument name for the continuation that follows this call. 549 * @param inputArgName The argument name provided to this call 550 */ 551function transformCallbackArgument(func: Expression, hasContinuation: boolean, continuationArgName: SynthBindingName | undefined, inputArgName: SynthBindingName | undefined, parent: PromiseReturningCallExpression<"then" | "catch" | "finally">, transformer: Transformer): readonly Statement[] { 552 switch (func.kind) { 553 case SyntaxKind.NullKeyword: 554 // do not produce a transformed statement for a null argument 555 break; 556 case SyntaxKind.PropertyAccessExpression: 557 case SyntaxKind.Identifier: // identifier includes undefined 558 if (!inputArgName) { 559 // undefined was argument passed to promise handler 560 break; 561 } 562 563 const synthCall = factory.createCallExpression(getSynthesizedDeepClone(func as Identifier | PropertyAccessExpression), /*typeArguments*/ undefined, isSynthIdentifier(inputArgName) ? [referenceSynthIdentifier(inputArgName)] : []); 564 565 if (shouldReturn(parent, transformer)) { 566 return maybeAnnotateAndReturn(synthCall, getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker)); 567 } 568 569 const type = transformer.checker.getTypeAtLocation(func); 570 const callSignatures = transformer.checker.getSignaturesOfType(type, SignatureKind.Call); 571 if (!callSignatures.length) { 572 // if identifier in handler has no call signatures, it's invalid 573 return silentFail(); 574 } 575 const returnType = callSignatures[0].getReturnType(); 576 const varDeclOrAssignment = createVariableOrAssignmentOrExpressionStatement(continuationArgName, factory.createAwaitExpression(synthCall), getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker)); 577 if (continuationArgName) { 578 continuationArgName.types.push(transformer.checker.getAwaitedType(returnType) || returnType); 579 } 580 return varDeclOrAssignment; 581 582 case SyntaxKind.FunctionExpression: 583 case SyntaxKind.ArrowFunction: { 584 const funcBody = (func as FunctionExpression | ArrowFunction).body; 585 const returnType = getLastCallSignature(transformer.checker.getTypeAtLocation(func), transformer.checker)?.getReturnType(); 586 587 // Arrow functions with block bodies { } will enter this control flow 588 if (isBlock(funcBody)) { 589 let refactoredStmts: Statement[] = []; 590 let seenReturnStatement = false; 591 for (const statement of funcBody.statements) { 592 if (isReturnStatement(statement)) { 593 seenReturnStatement = true; 594 if (isReturnStatementWithFixablePromiseHandler(statement, transformer.checker)) { 595 refactoredStmts = refactoredStmts.concat(transformReturnStatementWithFixablePromiseHandler(transformer, statement, hasContinuation, continuationArgName)); 596 } 597 else { 598 const possiblyAwaitedRightHandSide = returnType && statement.expression ? getPossiblyAwaitedRightHandSide(transformer.checker, returnType, statement.expression) : statement.expression; 599 refactoredStmts.push(...maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker))); 600 } 601 } 602 else if (hasContinuation && forEachReturnStatement(statement, returnTrue)) { 603 // If there is a nested `return` in a callback that has a trailing continuation, we don't transform it as the resulting complexity is too great. For example: 604 // 605 // source | result 606 // -------------------------------------| --------------------------------------- 607 // function f(): Promise<number> { | async function f9(): Promise<number> { 608 // return foo().then(() => { | await foo(); 609 // if (Math.random()) { | if (Math.random()) { 610 // return 1; | return 1; // incorrect early return 611 // } | } 612 // return 2; | return 2; // incorrect early return 613 // }).then(a => { | const a = undefined; 614 // return a + 1; | return a + 1; 615 // }); | } 616 // } | 617 // 618 // However, branching returns in the outermost continuation are acceptable as no other continuation follows it: 619 // 620 // source | result 621 //--------------------------------------|--------------------------------------- 622 // function f() { | async function f() { 623 // return foo().then(res => { | const res = await foo(); 624 // if (res.ok) { | if (res.ok) { 625 // return 1; | return 1; 626 // } | } 627 // else { | else { 628 // if (res.buffer.length > 5) { | if (res.buffer.length > 5) { 629 // return 2; | return 2; 630 // } | } 631 // else { | else { 632 // return 3; | return 3; 633 // } | } 634 // } | } 635 // }); | } 636 // } | 637 // 638 // We may improve this in the future, but for now the heuristics are too complex 639 640 return silentFail(); 641 } 642 else { 643 refactoredStmts.push(statement); 644 } 645 } 646 647 return shouldReturn(parent, transformer) 648 ? refactoredStmts.map(s => getSynthesizedDeepClone(s)) 649 : removeReturns( 650 refactoredStmts, 651 continuationArgName, 652 transformer, 653 seenReturnStatement); 654 } 655 else { 656 const inlinedStatements = isFixablePromiseHandler(funcBody, transformer.checker) ? 657 transformReturnStatementWithFixablePromiseHandler(transformer, factory.createReturnStatement(funcBody), hasContinuation, continuationArgName) : 658 emptyArray; 659 660 if (inlinedStatements.length > 0) { 661 return inlinedStatements; 662 } 663 664 if (returnType) { 665 const possiblyAwaitedRightHandSide = getPossiblyAwaitedRightHandSide(transformer.checker, returnType, funcBody); 666 667 if (!shouldReturn(parent, transformer)) { 668 const transformedStatement = createVariableOrAssignmentOrExpressionStatement(continuationArgName, possiblyAwaitedRightHandSide, /*typeAnnotation*/ undefined); 669 if (continuationArgName) { 670 continuationArgName.types.push(transformer.checker.getAwaitedType(returnType) || returnType); 671 } 672 return transformedStatement; 673 } 674 else { 675 return maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker)); 676 } 677 } 678 else { 679 return silentFail(); 680 } 681 } 682 } 683 default: 684 // If no cases apply, we've found a transformation body we don't know how to handle, so the refactoring should no-op to avoid deleting code. 685 return silentFail(); 686 } 687 return emptyArray; 688} 689 690function getPossiblyAwaitedRightHandSide(checker: TypeChecker, type: Type, expr: Expression): AwaitExpression | Expression { 691 const rightHandSide = getSynthesizedDeepClone(expr); 692 return !!checker.getPromisedTypeOfPromise(type) ? factory.createAwaitExpression(rightHandSide) : rightHandSide; 693} 694 695function getLastCallSignature(type: Type, checker: TypeChecker): Signature | undefined { 696 const callSignatures = checker.getSignaturesOfType(type, SignatureKind.Call); 697 return lastOrUndefined(callSignatures); 698} 699 700function removeReturns(stmts: readonly Statement[], prevArgName: SynthBindingName | undefined, transformer: Transformer, seenReturnStatement: boolean): readonly Statement[] { 701 const ret: Statement[] = []; 702 for (const stmt of stmts) { 703 if (isReturnStatement(stmt)) { 704 if (stmt.expression) { 705 const possiblyAwaitedExpression = isPromiseTypedExpression(stmt.expression, transformer.checker) ? factory.createAwaitExpression(stmt.expression) : stmt.expression; 706 if (prevArgName === undefined) { 707 ret.push(factory.createExpressionStatement(possiblyAwaitedExpression)); 708 } 709 else if (isSynthIdentifier(prevArgName) && prevArgName.hasBeenDeclared) { 710 ret.push(factory.createExpressionStatement(factory.createAssignment(referenceSynthIdentifier(prevArgName), possiblyAwaitedExpression))); 711 } 712 else { 713 ret.push(factory.createVariableStatement(/*modifiers*/ undefined, 714 (factory.createVariableDeclarationList([factory.createVariableDeclaration(declareSynthBindingName(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, possiblyAwaitedExpression)], NodeFlags.Const)))); 715 } 716 } 717 } 718 else { 719 ret.push(getSynthesizedDeepClone(stmt)); 720 } 721 } 722 723 // if block has no return statement, need to define prevArgName as undefined to prevent undeclared variables 724 if (!seenReturnStatement && prevArgName !== undefined) { 725 ret.push(factory.createVariableStatement(/*modifiers*/ undefined, 726 (factory.createVariableDeclarationList([factory.createVariableDeclaration(declareSynthBindingName(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, factory.createIdentifier("undefined"))], NodeFlags.Const)))); 727 } 728 729 return ret; 730} 731 732/** 733 * @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows the continuation to which this statement belongs. 734 * @param continuationArgName The argument name for the continuation that follows this call. 735 */ 736function transformReturnStatementWithFixablePromiseHandler(transformer: Transformer, innerRetStmt: ReturnStatement, hasContinuation: boolean, continuationArgName?: SynthBindingName) { 737 let innerCbBody: Statement[] = []; 738 forEachChild(innerRetStmt, function visit(node) { 739 if (isCallExpression(node)) { 740 const temp = transformExpression(node, node, transformer, hasContinuation, continuationArgName); 741 innerCbBody = innerCbBody.concat(temp); 742 if (innerCbBody.length > 0) { 743 return; 744 } 745 } 746 else if (!isFunctionLike(node)) { 747 forEachChild(node, visit); 748 } 749 }); 750 return innerCbBody; 751} 752 753function getArgBindingName(funcNode: Expression, transformer: Transformer): SynthBindingName | undefined { 754 const types: Type[] = []; 755 let name: SynthBindingName | undefined; 756 757 if (isFunctionLikeDeclaration(funcNode)) { 758 if (funcNode.parameters.length > 0) { 759 const param = funcNode.parameters[0].name; 760 name = getMappedBindingNameOrDefault(param); 761 } 762 } 763 else if (isIdentifier(funcNode)) { 764 name = getMapEntryOrDefault(funcNode); 765 } 766 else if (isPropertyAccessExpression(funcNode) && isIdentifier(funcNode.name)) { 767 name = getMapEntryOrDefault(funcNode.name); 768 } 769 770 // return undefined argName when arg is null or undefined 771 // eslint-disable-next-line local/no-in-operator 772 if (!name || "identifier" in name && name.identifier.text === "undefined") { 773 return undefined; 774 } 775 776 return name; 777 778 function getMappedBindingNameOrDefault(bindingName: BindingName): SynthBindingName { 779 if (isIdentifier(bindingName)) return getMapEntryOrDefault(bindingName); 780 const elements = flatMap(bindingName.elements, element => { 781 if (isOmittedExpression(element)) return []; 782 return [getMappedBindingNameOrDefault(element.name)]; 783 }); 784 785 return createSynthBindingPattern(bindingName, elements); 786 } 787 788 function getMapEntryOrDefault(identifier: Identifier): SynthIdentifier { 789 const originalNode = getOriginalNode(identifier); 790 const symbol = getSymbol(originalNode); 791 792 if (!symbol) { 793 return createSynthIdentifier(identifier, types); 794 } 795 796 const mapEntry = transformer.synthNamesMap.get(getSymbolId(symbol).toString()); 797 return mapEntry || createSynthIdentifier(identifier, types); 798 } 799 800 function getSymbol(node: Node): Symbol | undefined { 801 return node.symbol ? node.symbol : transformer.checker.getSymbolAtLocation(node); 802 } 803 804 function getOriginalNode(node: Node): Node { 805 return node.original ? node.original : node; 806 } 807} 808 809function isEmptyBindingName(bindingName: SynthBindingName | undefined): boolean { 810 if (!bindingName) { 811 return true; 812 } 813 if (isSynthIdentifier(bindingName)) { 814 return !bindingName.identifier.text; 815 } 816 return every(bindingName.elements, isEmptyBindingName); 817} 818 819function createSynthIdentifier(identifier: Identifier, types: Type[] = []): SynthIdentifier { 820 return { kind: SynthBindingNameKind.Identifier, identifier, types, hasBeenDeclared: false, hasBeenReferenced: false }; 821} 822 823function createSynthBindingPattern(bindingPattern: BindingPattern, elements: readonly SynthBindingName[] = emptyArray, types: Type[] = []): SynthBindingPattern { 824 return { kind: SynthBindingNameKind.BindingPattern, bindingPattern, elements, types }; 825} 826 827function referenceSynthIdentifier(synthId: SynthIdentifier) { 828 synthId.hasBeenReferenced = true; 829 return synthId.identifier; 830} 831 832function declareSynthBindingName(synthName: SynthBindingName) { 833 return isSynthIdentifier(synthName) ? declareSynthIdentifier(synthName) : declareSynthBindingPattern(synthName); 834} 835 836function declareSynthBindingPattern(synthPattern: SynthBindingPattern) { 837 for (const element of synthPattern.elements) { 838 declareSynthBindingName(element); 839 } 840 return synthPattern.bindingPattern; 841} 842 843function declareSynthIdentifier(synthId: SynthIdentifier) { 844 synthId.hasBeenDeclared = true; 845 return synthId.identifier; 846} 847 848function isSynthIdentifier(bindingName: SynthBindingName): bindingName is SynthIdentifier { 849 return bindingName.kind === SynthBindingNameKind.Identifier; 850} 851 852function isSynthBindingPattern(bindingName: SynthBindingName): bindingName is SynthBindingPattern { 853 return bindingName.kind === SynthBindingNameKind.BindingPattern; 854} 855 856function shouldReturn(expression: Expression, transformer: Transformer): boolean { 857 return !!expression.original && transformer.setOfExpressionsToReturn.has(getNodeId(expression.original)); 858} 859