Skip to content

Commit 13deedf

Browse files
Merge pull request microsoft#26930 from uniqueiniquity/onlyReportExpectedPromiseArgs
Only perform async code fix if it can successfully refactor all parts
2 parents 95c1570 + 57a6dbd commit 13deedf

File tree

8 files changed

+196
-121
lines changed

8 files changed

+196
-121
lines changed

src/services/codefixes/convertToAsyncFunction.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
namespace ts.codefix {
33
const fixId = "convertToAsyncFunction";
44
const errorCodes = [Diagnostics.This_may_be_converted_to_an_async_function.code];
5+
let codeActionSucceeded = true;
56
registerCodeFix({
67
errorCodes,
78
getCodeActions(context: CodeFixContext) {
9+
codeActionSucceeded = true;
810
const changes = textChanges.ChangeTracker.with(context, (t) => convertToAsyncFunction(t, context.sourceFile, context.span.start, context.program.getTypeChecker(), context));
9-
return [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_async_function, fixId, Diagnostics.Convert_all_to_async_functions)];
11+
return codeActionSucceeded ? [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_async_function, fixId, Diagnostics.Convert_all_to_async_functions)] : [];
1012
},
1113
fixIds: [fixId],
1214
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) => convertToAsyncFunction(changes, err.file, err.start, context.program.getTypeChecker(), context)),
@@ -252,6 +254,7 @@ namespace ts.codefix {
252254
}
253255

254256
// dispatch function to recursively build the refactoring
257+
// should be kept up to date with isFixablePromiseHandler in suggestionDiagnostics.ts
255258
function transformExpression(node: Expression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): Statement[] {
256259
if (!node) {
257260
return [];
@@ -273,6 +276,7 @@ namespace ts.codefix {
273276
return transformPromiseCall(node, transformer, prevArgName);
274277
}
275278

279+
codeActionSucceeded = false;
276280
return [];
277281
}
278282

@@ -381,13 +385,18 @@ namespace ts.codefix {
381385
(createVariableDeclarationList([createVariableDeclaration(getSynthesizedDeepClone(prevArgName.identifier), /*type*/ undefined, rightHandSide)], getFlagOfIdentifier(prevArgName.identifier, transformer.constIdentifiers))))]);
382386
}
383387

388+
// should be kept up to date with isFixablePromiseArgument in suggestionDiagnostics.ts
384389
function getTransformationBody(func: Node, prevArgName: SynthIdentifier | undefined, argName: SynthIdentifier, parent: CallExpression, transformer: Transformer): NodeArray<Statement> {
385390

386391
const hasPrevArgName = prevArgName && prevArgName.identifier.text.length > 0;
387392
const hasArgName = argName && argName.identifier.text.length > 0;
388393
const shouldReturn = transformer.setOfExpressionsToReturn.get(getNodeId(parent).toString());
389394
switch (func.kind) {
395+
case SyntaxKind.NullKeyword:
396+
// do not produce a transformed statement for a null argument
397+
break;
390398
case SyntaxKind.Identifier:
399+
// identifier includes undefined
391400
if (!hasArgName) break;
392401

393402
const synthCall = createCall(getSynthesizedDeepClone(func) as Identifier, /*typeArguments*/ undefined, [argName.identifier]);
@@ -443,6 +452,9 @@ namespace ts.codefix {
443452
return createNodeArray([createReturn(getSynthesizedDeepClone(funcBody) as Expression)]);
444453
}
445454
}
455+
default:
456+
// We've found a transformation body we don't know how to handle, so the refactoring should no-op to avoid deleting code.
457+
codeActionSucceeded = false;
446458
break;
447459
}
448460
return createNodeArray([]);
@@ -492,14 +504,6 @@ namespace ts.codefix {
492504
return innerCbBody;
493505
}
494506

495-
function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean {
496-
if (!isPropertyAccessExpression(node.expression)) {
497-
return false;
498-
}
499-
500-
return node.expression.name.text === funcName;
501-
}
502-
503507
function getArgName(funcNode: Node, transformer: Transformer): SynthIdentifier {
504508

505509
const numberOfAssignmentsOriginal = 0;

src/services/suggestionDiagnostics.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ namespace ts {
160160
}
161161

162162
function addHandlers(returnChild: Node) {
163-
if (isPromiseHandler(returnChild)) {
163+
if (isFixablePromiseHandler(returnChild)) {
164164
returnStatements.push(child as ReturnStatement);
165165
}
166166
}
@@ -170,8 +170,39 @@ namespace ts {
170170
return returnStatements;
171171
}
172172

173-
function isPromiseHandler(node: Node): boolean {
174-
return (isCallExpression(node) && isPropertyAccessExpression(node.expression) &&
175-
(node.expression.name.text === "then" || node.expression.name.text === "catch"));
173+
// Should be kept up to date with transformExpression in convertToAsyncFunction.ts
174+
function isFixablePromiseHandler(node: Node): boolean {
175+
// ensure outermost call exists and is a promise handler
176+
if (!isPromiseHandler(node) || !node.arguments.every(isFixablePromiseArgument)) {
177+
return false;
178+
}
179+
180+
// ensure all chained calls are valid
181+
let currentNode = node.expression;
182+
while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) {
183+
if (isCallExpression(currentNode) && !currentNode.arguments.every(isFixablePromiseArgument)) {
184+
return false;
185+
}
186+
currentNode = currentNode.expression;
187+
}
188+
return true;
189+
}
190+
191+
function isPromiseHandler(node: Node): node is CallExpression {
192+
return isCallExpression(node) && (hasPropertyAccessExpressionWithName(node, "then") || hasPropertyAccessExpressionWithName(node, "catch"));
193+
}
194+
195+
// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts
196+
function isFixablePromiseArgument(arg: Expression): boolean {
197+
switch (arg.kind) {
198+
case SyntaxKind.NullKeyword:
199+
case SyntaxKind.Identifier: // identifier includes undefined
200+
case SyntaxKind.FunctionDeclaration:
201+
case SyntaxKind.FunctionExpression:
202+
case SyntaxKind.ArrowFunction:
203+
return true;
204+
default:
205+
return false;
206+
}
176207
}
177208
}

src/services/utilities.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ namespace ts {
226226
return undefined;
227227
}
228228

229+
export function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean {
230+
if (!isPropertyAccessExpression(node.expression)) {
231+
return false;
232+
}
233+
234+
return node.expression.name.text === funcName;
235+
}
236+
229237
export function isJumpStatementTarget(node: Node): node is Identifier & { parent: BreakOrContinueStatement } {
230238
return node.kind === SyntaxKind.Identifier && isBreakOrContinueStatement(node.parent) && node.parent.label === node;
231239
}

0 commit comments

Comments
 (0)