• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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