• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    const fixId = "convertToAsyncFunction";
4    const errorCodes = [Diagnostics.This_may_be_converted_to_an_async_function.code];
5    let codeActionSucceeded = true;
6    registerCodeFix({
7        errorCodes,
8        getCodeActions(context: CodeFixContext) {
9            codeActionSucceeded = true;
10            const changes = textChanges.ChangeTracker.with(context, (t) => convertToAsyncFunction(t, context.sourceFile, context.span.start, context.program.getTypeChecker()));
11            return codeActionSucceeded ? [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_async_function, fixId, Diagnostics.Convert_all_to_async_functions)] : [];
12        },
13        fixIds: [fixId],
14        getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) => convertToAsyncFunction(changes, err.file, err.start, context.program.getTypeChecker())),
15    });
16
17    const enum SynthBindingNameKind {
18        Identifier,
19        BindingPattern,
20    }
21
22    type SynthBindingName = SynthBindingPattern | SynthIdentifier;
23
24    interface SynthBindingPattern {
25        readonly kind: SynthBindingNameKind.BindingPattern;
26        readonly elements: readonly SynthBindingName[];
27        readonly bindingPattern: BindingPattern;
28        readonly types: Type[];
29    }
30
31    interface SynthIdentifier {
32        readonly kind: SynthBindingNameKind.Identifier;
33        readonly identifier: Identifier;
34        readonly types: Type[];
35        /** A declaration for this identifier has already been generated */
36        hasBeenDeclared: boolean;
37    }
38
39    interface Transformer {
40        readonly checker: TypeChecker;
41        readonly synthNamesMap: ESMap<string, SynthIdentifier>; // keys are the symbol id of the identifier
42        readonly setOfExpressionsToReturn: ReadonlySet<number>; // keys are the node ids of the expressions
43        readonly isInJSFile: boolean;
44    }
45
46    function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker): void {
47        // get the function declaration - returns a promise
48        const tokenAtPosition = getTokenAtPosition(sourceFile, position);
49        let functionToConvert: FunctionLikeDeclaration | undefined;
50
51        // if the parent of a FunctionLikeDeclaration is a variable declaration, the convertToAsync diagnostic will be reported on the variable name
52        if (isIdentifier(tokenAtPosition) && isVariableDeclaration(tokenAtPosition.parent) &&
53            tokenAtPosition.parent.initializer && isFunctionLikeDeclaration(tokenAtPosition.parent.initializer)) {
54            functionToConvert = tokenAtPosition.parent.initializer;
55        }
56        else {
57            functionToConvert = tryCast(getContainingFunction(getTokenAtPosition(sourceFile, position)), isFunctionLikeDeclaration);
58        }
59
60        if (!functionToConvert) {
61            return;
62        }
63
64        const synthNamesMap = new Map<string, SynthIdentifier>();
65        const isInJavascript = isInJSFile(functionToConvert);
66        const setOfExpressionsToReturn = getAllPromiseExpressionsToReturn(functionToConvert, checker);
67        const functionToConvertRenamed = renameCollidingVarNames(functionToConvert, checker, synthNamesMap);
68        const returnStatements = functionToConvertRenamed.body && isBlock(functionToConvertRenamed.body) ? getReturnStatementsWithPromiseHandlers(functionToConvertRenamed.body, checker) : emptyArray;
69        const transformer: Transformer = { checker, synthNamesMap, setOfExpressionsToReturn, isInJSFile: isInJavascript };
70        if (!returnStatements.length) {
71            return;
72        }
73
74        const pos = functionToConvert.modifiers ? functionToConvert.modifiers.end :
75            functionToConvert.decorators ? skipTrivia(sourceFile.text, functionToConvert.decorators.end) :
76                functionToConvert.getStart(sourceFile);
77        const options = functionToConvert.modifiers ? { prefix: " " } : { suffix: " " };
78        changes.insertModifierAt(sourceFile, pos, SyntaxKind.AsyncKeyword, options);
79
80        for (const returnStatement of returnStatements) {
81            forEachChild(returnStatement, function visit(node) {
82                if (isCallExpression(node)) {
83                    const newNodes = transformExpression(node, transformer);
84                    changes.replaceNodeWithNodes(sourceFile, returnStatement, newNodes);
85                }
86                else if (!isFunctionLike(node)) {
87                    forEachChild(node, visit);
88                }
89            });
90        }
91    }
92
93    function getReturnStatementsWithPromiseHandlers(body: Block, checker: TypeChecker): readonly ReturnStatement[] {
94        const res: ReturnStatement[] = [];
95        forEachReturnStatement(body, ret => {
96            if (isReturnStatementWithFixablePromiseHandler(ret, checker)) res.push(ret);
97        });
98        return res;
99    }
100
101    /*
102        Finds all of the expressions of promise type that should not be saved in a variable during the refactor
103    */
104    function getAllPromiseExpressionsToReturn(func: FunctionLikeDeclaration, checker: TypeChecker): Set<number> {
105        if (!func.body) {
106            return new Set();
107        }
108
109        const setOfExpressionsToReturn = new Set<number>();
110        forEachChild(func.body, function visit(node: Node) {
111            if (isPromiseReturningCallExpression(node, checker, "then")) {
112                setOfExpressionsToReturn.add(getNodeId(node));
113                forEach(node.arguments, visit);
114            }
115            else if (isPromiseReturningCallExpression(node, checker, "catch")) {
116                setOfExpressionsToReturn.add(getNodeId(node));
117                // if .catch() is the last call in the chain, move leftward in the chain until we hit something else that should be returned
118                forEachChild(node, visit);
119            }
120            else if (isPromiseTypedExpression(node, checker)) {
121                setOfExpressionsToReturn.add(getNodeId(node));
122                // don't recurse here, since we won't refactor any children or arguments of the expression
123            }
124            else {
125                forEachChild(node, visit);
126            }
127        });
128
129        return setOfExpressionsToReturn;
130    }
131
132    function isPromiseReturningCallExpression(node: Node, checker: TypeChecker, name: string): node is CallExpression {
133        if (!isCallExpression(node)) return false;
134        const isExpressionOfName = hasPropertyAccessExpressionWithName(node, name);
135        const nodeType = isExpressionOfName && checker.getTypeAtLocation(node);
136        return !!(nodeType && checker.getPromisedTypeOfPromise(nodeType));
137    }
138
139    function isPromiseTypedExpression(node: Node, checker: TypeChecker): node is Expression {
140        if (!isExpression(node)) return false;
141        return !!checker.getPromisedTypeOfPromise(checker.getTypeAtLocation(node));
142    }
143
144    /*
145        Renaming of identifiers may be necessary as the refactor changes scopes -
146        This function collects all existing identifier names and names of identifiers that will be created in the refactor.
147        It then checks for any collisions and renames them through getSynthesizedDeepClone
148    */
149    function renameCollidingVarNames(nodeToRename: FunctionLikeDeclaration, checker: TypeChecker, synthNamesMap: ESMap<string, SynthIdentifier>): FunctionLikeDeclaration {
150        const identsToRenameMap = new Map<string, Identifier>(); // key is the symbol id
151        const collidingSymbolMap = createMultiMap<Symbol>();
152        forEachChild(nodeToRename, function visit(node: Node) {
153            if (!isIdentifier(node)) {
154                forEachChild(node, visit);
155                return;
156            }
157            const symbol = checker.getSymbolAtLocation(node);
158            if (symbol) {
159                const type = checker.getTypeAtLocation(node);
160                // Note - the choice of the last call signature is arbitrary
161                const lastCallSignature = getLastCallSignature(type, checker);
162                const symbolIdString = getSymbolId(symbol).toString();
163
164                // If the identifier refers to a function, we want to add the new synthesized variable for the declaration. Example:
165                //   fetch('...').then(response => { ... })
166                // will eventually become
167                //   const response = await fetch('...')
168                // so we push an entry for 'response'.
169                if (lastCallSignature && !isParameter(node.parent) && !isFunctionLikeDeclaration(node.parent) && !synthNamesMap.has(symbolIdString)) {
170                    const firstParameter = firstOrUndefined(lastCallSignature.parameters);
171                    const ident = firstParameter && isParameter(firstParameter.valueDeclaration) && tryCast(firstParameter.valueDeclaration.name, isIdentifier) || factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic);
172                    const synthName = getNewNameIfConflict(ident, collidingSymbolMap);
173                    synthNamesMap.set(symbolIdString, synthName);
174                    collidingSymbolMap.add(ident.text, symbol);
175                }
176                // We only care about identifiers that are parameters, variable declarations, or binding elements
177                else if (node.parent && (isParameter(node.parent) || isVariableDeclaration(node.parent) || isBindingElement(node.parent))) {
178                    const originalName = node.text;
179                    const collidingSymbols = collidingSymbolMap.get(originalName);
180
181                    // if the identifier name conflicts with a different identifier that we've already seen
182                    if (collidingSymbols && collidingSymbols.some(prevSymbol => prevSymbol !== symbol)) {
183                        const newName = getNewNameIfConflict(node, collidingSymbolMap);
184                        identsToRenameMap.set(symbolIdString, newName.identifier);
185                        synthNamesMap.set(symbolIdString, newName);
186                        collidingSymbolMap.add(originalName, symbol);
187                    }
188                    else {
189                        const identifier = getSynthesizedDeepClone(node);
190                        synthNamesMap.set(symbolIdString, createSynthIdentifier(identifier));
191                        collidingSymbolMap.add(originalName, symbol);
192                    }
193                }
194            }
195        });
196
197        return getSynthesizedDeepCloneWithReplacements(nodeToRename, /*includeTrivia*/ true, original => {
198            if (isBindingElement(original) && isIdentifier(original.name) && isObjectBindingPattern(original.parent)) {
199                const symbol = checker.getSymbolAtLocation(original.name);
200                const renameInfo = symbol && identsToRenameMap.get(String(getSymbolId(symbol)));
201                if (renameInfo && renameInfo.text !== (original.name || original.propertyName).getText()) {
202                    return factory.createBindingElement(
203                        original.dotDotDotToken,
204                        original.propertyName || original.name,
205                        renameInfo,
206                        original.initializer);
207                }
208            }
209            else if (isIdentifier(original)) {
210                const symbol = checker.getSymbolAtLocation(original);
211                const renameInfo = symbol && identsToRenameMap.get(String(getSymbolId(symbol)));
212                if (renameInfo) {
213                    return factory.createIdentifier(renameInfo.text);
214                }
215            }
216        });
217    }
218
219    function getNewNameIfConflict(name: Identifier, originalNames: ReadonlyESMap<string, Symbol[]>): SynthIdentifier {
220        const numVarsSameName = (originalNames.get(name.text) || emptyArray).length;
221        const identifier = numVarsSameName === 0 ? name : factory.createIdentifier(name.text + "_" + numVarsSameName);
222        return createSynthIdentifier(identifier);
223    }
224
225    function silentFail() {
226        codeActionSucceeded = false;
227        return emptyArray;
228    }
229
230    // dispatch function to recursively build the refactoring
231    // should be kept up to date with isFixablePromiseHandler in suggestionDiagnostics.ts
232    function transformExpression(node: Expression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
233        if (isPromiseReturningCallExpression(node, transformer.checker, "then")) {
234            if (node.arguments.length === 0) return silentFail();
235            return transformThen(node, transformer, prevArgName);
236        }
237        if (isPromiseReturningCallExpression(node, transformer.checker, "catch")) {
238            return transformCatch(node, transformer, prevArgName);
239        }
240        if (isPropertyAccessExpression(node)) {
241            return transformExpression(node.expression, transformer, prevArgName);
242        }
243
244        const nodeType = transformer.checker.getTypeAtLocation(node);
245        if (nodeType && transformer.checker.getPromisedTypeOfPromise(nodeType)) {
246            Debug.assertNode(node.original!.parent, isPropertyAccessExpression);
247            return transformPromiseExpressionOfPropertyAccess(node, transformer, prevArgName);
248        }
249
250        return silentFail();
251    }
252
253    function transformCatch(node: CallExpression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
254        const func = singleOrUndefined(node.arguments);
255        const argName = func ? getArgBindingName(func, transformer) : undefined;
256        let possibleNameForVarDecl: SynthIdentifier | undefined;
257
258        // If there is another call in the chain after the .catch() we are transforming, we will need to save the result of both paths (try block and catch block)
259        // To do this, we will need to synthesize a variable that we were not aware of while we were adding identifiers to the synthNamesMap
260        // We will use the prevArgName and then update the synthNamesMap with a new variable name for the next transformation step
261        if (prevArgName && !shouldReturn(node, transformer)) {
262            if (isSynthIdentifier(prevArgName)) {
263                possibleNameForVarDecl = prevArgName;
264                transformer.synthNamesMap.forEach((val, key) => {
265                    if (val.identifier.text === prevArgName.identifier.text) {
266                        const newSynthName = createUniqueSynthName(prevArgName);
267                        transformer.synthNamesMap.set(key, newSynthName);
268                    }
269                });
270            }
271            else {
272                possibleNameForVarDecl = createSynthIdentifier(factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic), prevArgName.types);
273            }
274
275            // We are about to write a 'let' variable declaration, but `transformExpression` for both
276            // the try block and catch block will assign to this name. Setting this flag indicates
277            // that future assignments should be written as `name = value` instead of `const name = value`.
278            possibleNameForVarDecl.hasBeenDeclared = true;
279        }
280
281        const tryBlock = factory.createBlock(transformExpression(node.expression, transformer, possibleNameForVarDecl));
282        const transformationBody = func ? getTransformationBody(func, possibleNameForVarDecl, argName, node, transformer) : emptyArray;
283        const catchArg = argName ? isSynthIdentifier(argName) ? argName.identifier.text : argName.bindingPattern : "e";
284        const catchVariableDeclaration = factory.createVariableDeclaration(catchArg);
285        const catchClause = factory.createCatchClause(catchVariableDeclaration, factory.createBlock(transformationBody));
286
287        // 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)
288        let varDeclList: VariableStatement | undefined;
289        let varDeclIdentifier: Identifier | undefined;
290        if (possibleNameForVarDecl && !shouldReturn(node, transformer)) {
291            varDeclIdentifier = getSynthesizedDeepClone(possibleNameForVarDecl.identifier);
292            const typeArray: Type[] = possibleNameForVarDecl.types;
293            const unionType = transformer.checker.getUnionType(typeArray, UnionReduction.Subtype);
294            const unionTypeNode = transformer.isInJSFile ? undefined : transformer.checker.typeToTypeNode(unionType, /*enclosingDeclaration*/ undefined, /*flags*/ undefined);
295            const varDecl = [factory.createVariableDeclaration(varDeclIdentifier, /*exclamationToken*/ undefined, unionTypeNode)];
296            varDeclList = factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList(varDecl, NodeFlags.Let));
297        }
298
299        const tryStatement = factory.createTryStatement(tryBlock, catchClause, /*finallyBlock*/ undefined);
300        const destructuredResult = prevArgName && varDeclIdentifier && isSynthBindingPattern(prevArgName)
301            && factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(getSynthesizedDeepClone(prevArgName.bindingPattern), /*exclamationToken*/ undefined, /*type*/ undefined, varDeclIdentifier)], NodeFlags.Const));
302        return compact([varDeclList, tryStatement, destructuredResult]);
303    }
304
305    function createUniqueSynthName(prevArgName: SynthIdentifier): SynthIdentifier {
306        const renamedPrevArg = factory.createUniqueName(prevArgName.identifier.text, GeneratedIdentifierFlags.Optimistic);
307        return createSynthIdentifier(renamedPrevArg);
308    }
309
310    function transformThen(node: CallExpression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
311        const [onFulfilled, onRejected] = node.arguments;
312        const onFulfilledArgumentName = getArgBindingName(onFulfilled, transformer);
313        const transformationBody = getTransformationBody(onFulfilled, prevArgName, onFulfilledArgumentName, node, transformer);
314        if (onRejected) {
315            const onRejectedArgumentName = getArgBindingName(onRejected, transformer);
316            const tryBlock = factory.createBlock(transformExpression(node.expression, transformer, onFulfilledArgumentName).concat(transformationBody));
317            const transformationBody2 = getTransformationBody(onRejected, prevArgName, onRejectedArgumentName, node, transformer);
318            const catchArg = onRejectedArgumentName ? isSynthIdentifier(onRejectedArgumentName) ? onRejectedArgumentName.identifier.text : onRejectedArgumentName.bindingPattern : "e";
319            const catchVariableDeclaration = factory.createVariableDeclaration(catchArg);
320            const catchClause = factory.createCatchClause(catchVariableDeclaration, factory.createBlock(transformationBody2));
321            return [factory.createTryStatement(tryBlock, catchClause, /* finallyBlock */ undefined)];
322        }
323        return transformExpression(node.expression, transformer, onFulfilledArgumentName).concat(transformationBody);
324    }
325
326    /**
327     * Transforms the 'x' part of `x.then(...)`, or the 'y()' part of `y().catch(...)`, where 'x' and 'y()' are Promises.
328     */
329    function transformPromiseExpressionOfPropertyAccess(node: Expression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
330        if (shouldReturn(node, transformer)) {
331            return [factory.createReturnStatement(getSynthesizedDeepClone(node))];
332        }
333
334        return createVariableOrAssignmentOrExpressionStatement(prevArgName, factory.createAwaitExpression(node), /*typeAnnotation*/ undefined);
335    }
336
337    function createVariableOrAssignmentOrExpressionStatement(variableName: SynthBindingName | undefined, rightHandSide: Expression, typeAnnotation: TypeNode | undefined): readonly Statement[] {
338        if (!variableName || isEmptyBindingName(variableName)) {
339            // if there's no argName to assign to, there still might be side effects
340            return [factory.createExpressionStatement(rightHandSide)];
341        }
342
343        if (isSynthIdentifier(variableName) && variableName.hasBeenDeclared) {
344            // if the variable has already been declared, we don't need "let" or "const"
345            return [factory.createExpressionStatement(factory.createAssignment(getSynthesizedDeepClone(variableName.identifier), rightHandSide))];
346        }
347
348        return [
349            factory.createVariableStatement(
350                /*modifiers*/ undefined,
351                factory.createVariableDeclarationList([
352                    factory.createVariableDeclaration(
353                        getSynthesizedDeepClone(getNode(variableName)),
354                        /*exclamationToken*/ undefined,
355                        typeAnnotation,
356                        rightHandSide)],
357                    NodeFlags.Const))];
358    }
359
360    function maybeAnnotateAndReturn(expressionToReturn: Expression | undefined, typeAnnotation: TypeNode | undefined): readonly Statement[] {
361        if (typeAnnotation && expressionToReturn) {
362            const name = factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic);
363            return [
364                ...createVariableOrAssignmentOrExpressionStatement(createSynthIdentifier(name), expressionToReturn, typeAnnotation),
365                factory.createReturnStatement(name)
366            ];
367        }
368        return [factory.createReturnStatement(expressionToReturn)];
369    }
370
371    // should be kept up to date with isFixablePromiseArgument in suggestionDiagnostics.ts
372    function getTransformationBody(func: Expression, prevArgName: SynthBindingName | undefined, argName: SynthBindingName | undefined, parent: CallExpression, transformer: Transformer): readonly Statement[] {
373        switch (func.kind) {
374            case SyntaxKind.NullKeyword:
375                // do not produce a transformed statement for a null argument
376                break;
377            case SyntaxKind.PropertyAccessExpression:
378            case SyntaxKind.Identifier: // identifier includes undefined
379                if (!argName) {
380                    // undefined was argument passed to promise handler
381                    break;
382                }
383
384                const synthCall = factory.createCallExpression(getSynthesizedDeepClone(func as Identifier | PropertyAccessExpression), /*typeArguments*/ undefined, isSynthIdentifier(argName) ? [argName.identifier] : []);
385                if (shouldReturn(parent, transformer)) {
386                    return maybeAnnotateAndReturn(synthCall, parent.typeArguments?.[0]);
387                }
388
389                const type = transformer.checker.getTypeAtLocation(func);
390                const callSignatures = transformer.checker.getSignaturesOfType(type, SignatureKind.Call);
391                if (!callSignatures.length) {
392                    // if identifier in handler has no call signatures, it's invalid
393                    return silentFail();
394                }
395                const returnType = callSignatures[0].getReturnType();
396                const varDeclOrAssignment = createVariableOrAssignmentOrExpressionStatement(prevArgName, factory.createAwaitExpression(synthCall), parent.typeArguments?.[0]);
397                if (prevArgName) {
398                    prevArgName.types.push(returnType);
399                }
400                return varDeclOrAssignment;
401
402            case SyntaxKind.FunctionExpression:
403            case SyntaxKind.ArrowFunction: {
404                const funcBody = (func as FunctionExpression | ArrowFunction).body;
405                const returnType = getLastCallSignature(transformer.checker.getTypeAtLocation(func), transformer.checker)?.getReturnType();
406
407                // Arrow functions with block bodies { } will enter this control flow
408                if (isBlock(funcBody)) {
409                    let refactoredStmts: Statement[] = [];
410                    let seenReturnStatement = false;
411                    for (const statement of funcBody.statements) {
412                        if (isReturnStatement(statement)) {
413                            seenReturnStatement = true;
414                            if (isReturnStatementWithFixablePromiseHandler(statement, transformer.checker)) {
415                                refactoredStmts = refactoredStmts.concat(getInnerTransformationBody(transformer, [statement], prevArgName));
416                            }
417                            else {
418                                const possiblyAwaitedRightHandSide = returnType && statement.expression ? getPossiblyAwaitedRightHandSide(transformer.checker, returnType, statement.expression) : statement.expression;
419                                refactoredStmts.push(...maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, parent.typeArguments?.[0]));
420                            }
421                        }
422                        else {
423                            refactoredStmts.push(statement);
424                        }
425                    }
426
427                    return shouldReturn(parent, transformer)
428                        ? refactoredStmts.map(s => getSynthesizedDeepClone(s))
429                        : removeReturns(
430                            refactoredStmts,
431                            prevArgName,
432                            transformer,
433                            seenReturnStatement);
434                }
435                else {
436                    const innerRetStmts = isFixablePromiseHandler(funcBody, transformer.checker) ? [factory.createReturnStatement(funcBody)] : emptyArray;
437                    const innerCbBody = getInnerTransformationBody(transformer, innerRetStmts, prevArgName);
438
439                    if (innerCbBody.length > 0) {
440                        return innerCbBody;
441                    }
442
443                    if (returnType) {
444                        const possiblyAwaitedRightHandSide = getPossiblyAwaitedRightHandSide(transformer.checker, returnType, funcBody);
445                        if (!shouldReturn(parent, transformer)) {
446                            const transformedStatement = createVariableOrAssignmentOrExpressionStatement(prevArgName, possiblyAwaitedRightHandSide, /*typeAnnotation*/ undefined);
447                            if (prevArgName) {
448                                prevArgName.types.push(returnType);
449                            }
450                            return transformedStatement;
451                        }
452                        else {
453                            return maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, parent.typeArguments?.[0]);
454                        }
455                    }
456                    else {
457                        return silentFail();
458                    }
459                }
460            }
461            default:
462                // 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.
463                return silentFail();
464        }
465        return emptyArray;
466    }
467
468    function getPossiblyAwaitedRightHandSide(checker: TypeChecker, type: Type, expr: Expression): AwaitExpression | Expression {
469        const rightHandSide = getSynthesizedDeepClone(expr);
470        return !!checker.getPromisedTypeOfPromise(type) ? factory.createAwaitExpression(rightHandSide) : rightHandSide;
471    }
472
473    function getLastCallSignature(type: Type, checker: TypeChecker): Signature | undefined {
474        const callSignatures = checker.getSignaturesOfType(type, SignatureKind.Call);
475        return lastOrUndefined(callSignatures);
476    }
477
478    function removeReturns(stmts: readonly Statement[], prevArgName: SynthBindingName | undefined, transformer: Transformer, seenReturnStatement: boolean): readonly Statement[] {
479        const ret: Statement[] = [];
480        for (const stmt of stmts) {
481            if (isReturnStatement(stmt)) {
482                if (stmt.expression) {
483                    const possiblyAwaitedExpression = isPromiseTypedExpression(stmt.expression, transformer.checker) ? factory.createAwaitExpression(stmt.expression) : stmt.expression;
484                    if (prevArgName === undefined) {
485                        ret.push(factory.createExpressionStatement(possiblyAwaitedExpression));
486                    }
487                    else {
488                        ret.push(factory.createVariableStatement(/*modifiers*/ undefined,
489                            (factory.createVariableDeclarationList([factory.createVariableDeclaration(getNode(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, possiblyAwaitedExpression)], NodeFlags.Const))));
490                    }
491                }
492            }
493            else {
494                ret.push(getSynthesizedDeepClone(stmt));
495            }
496        }
497
498        // if block has no return statement, need to define prevArgName as undefined to prevent undeclared variables
499        if (!seenReturnStatement && prevArgName !== undefined) {
500            ret.push(factory.createVariableStatement(/*modifiers*/ undefined,
501                (factory.createVariableDeclarationList([factory.createVariableDeclaration(getNode(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, factory.createIdentifier("undefined"))], NodeFlags.Const))));
502        }
503
504        return ret;
505    }
506
507
508    function getInnerTransformationBody(transformer: Transformer, innerRetStmts: readonly Node[], prevArgName?: SynthBindingName) {
509        let innerCbBody: Statement[] = [];
510        for (const stmt of innerRetStmts) {
511            forEachChild(stmt, function visit(node) {
512                if (isCallExpression(node)) {
513                    const temp = transformExpression(node, transformer, prevArgName);
514                    innerCbBody = innerCbBody.concat(temp);
515                    if (innerCbBody.length > 0) {
516                        return;
517                    }
518                }
519                else if (!isFunctionLike(node)) {
520                    forEachChild(node, visit);
521                }
522            });
523        }
524        return innerCbBody;
525    }
526
527    function getArgBindingName(funcNode: Expression, transformer: Transformer): SynthBindingName | undefined {
528        const types: Type[] = [];
529        let name: SynthBindingName | undefined;
530
531        if (isFunctionLikeDeclaration(funcNode)) {
532            if (funcNode.parameters.length > 0) {
533                const param = funcNode.parameters[0].name;
534                name = getMappedBindingNameOrDefault(param);
535            }
536        }
537        else if (isIdentifier(funcNode)) {
538            name = getMapEntryOrDefault(funcNode);
539        }
540        else if (isPropertyAccessExpression(funcNode) && isIdentifier(funcNode.name)) {
541            name = getMapEntryOrDefault(funcNode.name);
542        }
543
544        // return undefined argName when arg is null or undefined
545        // eslint-disable-next-line no-in-operator
546        if (!name || "identifier" in name && name.identifier.text === "undefined") {
547            return undefined;
548        }
549
550        return name;
551
552        function getMappedBindingNameOrDefault(bindingName: BindingName): SynthBindingName {
553            if (isIdentifier(bindingName)) return getMapEntryOrDefault(bindingName);
554            const elements = flatMap(bindingName.elements, element => {
555                if (isOmittedExpression(element)) return [];
556                return [getMappedBindingNameOrDefault(element.name)];
557            });
558
559            return createSynthBindingPattern(bindingName, elements);
560        }
561
562        function getMapEntryOrDefault(identifier: Identifier): SynthIdentifier {
563            const originalNode = getOriginalNode(identifier);
564            const symbol = getSymbol(originalNode);
565
566            if (!symbol) {
567                return createSynthIdentifier(identifier, types);
568            }
569
570            const mapEntry = transformer.synthNamesMap.get(getSymbolId(symbol).toString());
571            return mapEntry || createSynthIdentifier(identifier, types);
572        }
573
574        function getSymbol(node: Node): Symbol | undefined {
575            return node.symbol ? node.symbol : transformer.checker.getSymbolAtLocation(node);
576        }
577
578        function getOriginalNode(node: Node): Node {
579            return node.original ? node.original : node;
580        }
581    }
582
583    function isEmptyBindingName(bindingName: SynthBindingName | undefined): boolean {
584        if (!bindingName) {
585            return true;
586        }
587        if (isSynthIdentifier(bindingName)) {
588            return !bindingName.identifier.text;
589        }
590        return every(bindingName.elements, isEmptyBindingName);
591    }
592
593    function getNode(bindingName: SynthBindingName) {
594        return isSynthIdentifier(bindingName) ? bindingName.identifier : bindingName.bindingPattern;
595    }
596
597    function createSynthIdentifier(identifier: Identifier, types: Type[] = []): SynthIdentifier {
598        return { kind: SynthBindingNameKind.Identifier, identifier, types, hasBeenDeclared: false };
599    }
600
601    function createSynthBindingPattern(bindingPattern: BindingPattern, elements: readonly SynthBindingName[] = emptyArray, types: Type[] = []): SynthBindingPattern {
602        return { kind: SynthBindingNameKind.BindingPattern, bindingPattern, elements, types };
603    }
604
605    function isSynthIdentifier(bindingName: SynthBindingName): bindingName is SynthIdentifier {
606        return bindingName.kind === SynthBindingNameKind.Identifier;
607    }
608
609    function isSynthBindingPattern(bindingName: SynthBindingName): bindingName is SynthBindingPattern {
610        return bindingName.kind === SynthBindingNameKind.BindingPattern;
611    }
612
613    function shouldReturn(expression: Expression, transformer: Transformer): boolean {
614        return !!expression.original && transformer.setOfExpressionsToReturn.has(getNodeId(expression.original));
615    }
616}
617