diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 7f686076f1824..a2589cbe8f58c 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7255,6 +7255,10 @@ "category": "Suggestion", "code": 80010 }, + "This may need `await` keyword. Otherwise the enclosing try statement won't handle this.": { + "category": "Suggestion", + "code": 80011 + }, "Add missing 'super()' call": { "category": "Message", @@ -8245,6 +8249,14 @@ "category": "Message", "code": 95197 }, + "Add missing await.": { + "category": "Message", + "code": 95198 + }, + "Add all missing awaits.": { + "category": "Message", + "code": 95199 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/services/_namespaces/ts.codefix.ts b/src/services/_namespaces/ts.codefix.ts index 3cf05630388de..8515b5b66cf4a 100644 --- a/src/services/_namespaces/ts.codefix.ts +++ b/src/services/_namespaces/ts.codefix.ts @@ -5,6 +5,7 @@ export * from "../codefixes/addConvertToUnknownForNonOverlappingTypes.js"; export * from "../codefixes/addEmptyExportDeclaration.js"; export * from "../codefixes/addMissingAsync.js"; export * from "../codefixes/addMissingAwait.js"; +export * from "../codefixes/addMissingAwaitInReturn.js"; export * from "../codefixes/addMissingConst.js"; export * from "../codefixes/addMissingDeclareProperty.js"; export * from "../codefixes/addMissingInvocationForDecorator.js"; diff --git a/src/services/codefixes/addMissingAwaitInReturn.ts b/src/services/codefixes/addMissingAwaitInReturn.ts new file mode 100644 index 0000000000000..4b595e50e408a --- /dev/null +++ b/src/services/codefixes/addMissingAwaitInReturn.ts @@ -0,0 +1,41 @@ +import { + codeFixAll, + createCodeFixAction, + registerCodeFix, +} from "../_namespaces/ts.codefix.js"; +import { + CodeFixContext, + Debug, + Diagnostics, + factory, + getSynthesizedDeepClone, + getTokenAtPosition, + isReturnStatement, + SourceFile, + textChanges, +} from "../_namespaces/ts.js"; + +const fixId = "addMissingAwaitInReturn"; +const errorCodes = [Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.code]; + +registerCodeFix({ + errorCodes, + getCodeActions(context: CodeFixContext) { + const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span.start)); + return [createCodeFixAction(fixId, changes, Diagnostics.Add_missing_await, fixId, Diagnostics.Add_all_missing_awaits)]; + }, + fixIds: [fixId], + getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag.start)), +}); + +function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number) { + const token = getTokenAtPosition(sourceFile, pos); + Debug.assertNode(token.parent, isReturnStatement); + Debug.assertIsDefined(token.parent.expression); + const expression = token.parent.expression; + changeTracker.replaceNode( + sourceFile, + expression, + factory.createAwaitExpression(getSynthesizedDeepClone(expression, /*includeTrivia*/ true)), + ); +} diff --git a/src/services/suggestionDiagnostics.ts b/src/services/suggestionDiagnostics.ts index 3c4cf5c39b976..576d161df5a9d 100644 --- a/src/services/suggestionDiagnostics.ts +++ b/src/services/suggestionDiagnostics.ts @@ -15,6 +15,7 @@ import { ExpressionStatement, Extension, fileExtensionIsOneOf, + findAncestor, forEachReturnStatement, FunctionDeclaration, FunctionExpression, @@ -28,6 +29,7 @@ import { Identifier, importFromModuleSpecifier, isAsyncFunction, + isAwaitExpression, isBinaryExpression, isBlock, isCallExpression, @@ -35,12 +37,14 @@ import { isFunctionDeclaration, isFunctionExpression, isFunctionLike, + isFunctionLikeDeclaration, isIdentifier, isPropertyAccessExpression, isRequireCall, isReturnStatement, isSourceFileJS, isStringLiteral, + isTryStatement, isVariableDeclaration, isVariableStatement, MethodDeclaration, @@ -52,6 +56,7 @@ import { PropertyAccessExpression, ReturnStatement, skipAlias, + skipParentheses, some, SourceFile, SyntaxKind, @@ -132,6 +137,9 @@ export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Pr if (canBeConvertedToAsync(node)) { addConvertToAsyncFunctionDiagnostics(node, checker, diags); } + if (isFunctionLikeDeclaration(node) && isAsyncFunction(node)) { + addMissingAwaitInReturnDiagnostics(node, checker, diags); + } node.forEachChild(check); } } @@ -190,6 +198,31 @@ function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeCheck returnsPromise(node, checker); } +function addMissingAwaitInReturnDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: DiagnosticWithLocation[]) { + if (!node.body || !isBlock(node.body)) { + return; + } + + forEachReturnStatement(node.body, statement => { + if (!statement.expression) { + return; + } + const expression = skipParentheses(statement.expression); + if (isAwaitExpression(expression)) { + return; + } + const type = checker.getTypeAtLocation(expression); + if (type.isUnion() ? type.types.every(t => !checker.getPromisedTypeOfPromise(t)) : !checker.getPromisedTypeOfPromise(type)) { + return; + } + const ancestor = findAncestor(statement, n => n === node || isTryStatement(n)); + if (!ancestor || !isTryStatement(ancestor)) { + return; + } + diags.push(createDiagnosticForNode(statement, Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this)); + }); +} + /** @internal */ export function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean { const signature = checker.getSignatureFromDeclaration(node); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn1.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn1.ts new file mode 100644 index 0000000000000..5e8ab87050732 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn1.ts @@ -0,0 +1,19 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// return inner(); +//// } + +verify.getSuggestionDiagnostics([]); +verify.not.codeFixAvailable(ts.Diagnostics.Add_missing_await.message); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn10.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn10.ts new file mode 100644 index 0000000000000..18330c16c757a --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn10.ts @@ -0,0 +1,14 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// return 42; +//// } +//// +//// const outer = async () => inner(); + +verify.getSuggestionDiagnostics([]); +verify.not.codeFixAvailable(ts.Diagnostics.Add_missing_await.message); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn11.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn11.ts new file mode 100644 index 0000000000000..2ca14fc28f940 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn11.ts @@ -0,0 +1,61 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// return 42; +//// } +//// +//// async function outer(v: number) { +//// try { +//// if (v > 30) { +//// [|return|] inner(); +//// } +//// if (v > 20) { +//// return await inner(); +//// } +//// if (v > 10) { +//// [|return|] inner(); +//// } +//// [|return|] inner(); +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([{ + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[0], +}, { + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[1], +}, { + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[2], +}]); +verify.codeFixAll({ + fixId: "addMissingAwaitInReturn", + fixAllDescription: ts.Diagnostics.Add_all_missing_awaits.message, + newFileContent: +`async function inner() { + return 42; +} + +async function outer(v: number) { + try { + if (v > 30) { + return await inner(); + } + if (v > 20) { + return await inner(); + } + if (v > 10) { + return await inner(); + } + return await inner(); + } catch (e) {} +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn12.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn12.ts new file mode 100644 index 0000000000000..241c84b159845 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn12.ts @@ -0,0 +1,41 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// [|return|] Math.random() > 0.5 ? inner() : 5; +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([{ + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[0], +}]); +verify.codeFix({ + description: ts.Diagnostics.Add_missing_await.message, + index: 0, + newFileContent: +`async function inner() { + if (Math.random() > 0.5) { + throw new Error("Ooops"); + } + return 42; +} + +async function outer() { + try { + return await (Math.random() > 0.5 ? inner() : 5); + } catch (e) {} +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn2.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn2.ts new file mode 100644 index 0000000000000..da1e42a8b5851 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn2.ts @@ -0,0 +1,41 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// [|return|] inner(); +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([{ + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[0], +}]); +verify.codeFix({ + description: ts.Diagnostics.Add_missing_await.message, + index: 0, + newFileContent: +`async function inner() { + if (Math.random() > 0.5) { + throw new Error("Ooops"); + } + return 42; +} + +async function outer() { + try { + return await inner(); + } catch (e) {} +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn3.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn3.ts new file mode 100644 index 0000000000000..2e00b940a0a17 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn3.ts @@ -0,0 +1,41 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// [|return|] inner(); +//// } catch {} +//// } + +verify.getSuggestionDiagnostics([{ + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[0], +}]); +verify.codeFix({ + description: ts.Diagnostics.Add_missing_await.message, + index: 0, + newFileContent: +`async function inner() { + if (Math.random() > 0.5) { + throw new Error("Ooops"); + } + return 42; +} + +async function outer() { + try { + return await inner(); + } catch {} +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn4.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn4.ts new file mode 100644 index 0000000000000..f61e686d3f97c --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn4.ts @@ -0,0 +1,41 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// [|return|] inner(); +//// } finally {} +//// } + +verify.getSuggestionDiagnostics([{ + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[0], +}]); +verify.codeFix({ + description: ts.Diagnostics.Add_missing_await.message, + index: 0, + newFileContent: +`async function inner() { + if (Math.random() > 0.5) { + throw new Error("Ooops"); + } + return 42; +} + +async function outer() { + try { + return await inner(); + } finally {} +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn5.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn5.ts new file mode 100644 index 0000000000000..d7a54344addd0 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn5.ts @@ -0,0 +1,21 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// return await inner(); +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([]); +verify.not.codeFixAvailable(ts.Diagnostics.Add_missing_await.message); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn6.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn6.ts new file mode 100644 index 0000000000000..8130efbc86c22 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn6.ts @@ -0,0 +1,21 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// function inner() { +//// if (Math.random() > 0.5) { +//// throw new Error("Ooops"); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// return inner(); +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([]); +verify.not.codeFixAvailable(ts.Diagnostics.Add_missing_await.message); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn7.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn7.ts new file mode 100644 index 0000000000000..aa21c0562f574 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn7.ts @@ -0,0 +1,14 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// async function fn() { +//// try { +//// return 42; +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([]); +verify.not.codeFixAvailable(ts.Diagnostics.Add_missing_await.message); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn8.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn8.ts new file mode 100644 index 0000000000000..cec55f51af246 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn8.ts @@ -0,0 +1,47 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// function inner(v: number) { +//// if (Math.random() > 0.5) { +//// return Promise.reject(new Error("Ooops")); +//// } +//// if (v > 10) { +//// return Promise.resolve(100); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// [|return|] inner(); +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([{ + message: ts.Diagnostics.This_may_need_await_keyword_Otherwise_the_enclosing_try_statement_won_t_handle_this.message, + code: 80011, + range: test.ranges()[0], +}]); +verify.codeFix({ + description: ts.Diagnostics.Add_missing_await.message, + index: 0, + newFileContent: +`function inner(v: number) { + if (Math.random() > 0.5) { + return Promise.reject(new Error("Ooops")); + } + if (v > 10) { + return Promise.resolve(100); + } + return 42; +} + +async function outer() { + try { + return await inner(); + } catch (e) {} +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwaitInReturn9.ts b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn9.ts new file mode 100644 index 0000000000000..97739b8d8a287 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwaitInReturn9.ts @@ -0,0 +1,24 @@ +/// + +// @strict: true +// @target: esnext +// @lib: esnext + +//// function inner(v: number) { +//// if (Math.random() > 0.5) { +//// return Promise.reject(new Error("Ooops")); +//// } +//// if (v > 10) { +//// return Promise.resolve(100); +//// } +//// return 42; +//// } +//// +//// async function outer() { +//// try { +//// return await inner(); +//// } catch (e) {} +//// } + +verify.getSuggestionDiagnostics([]); +verify.not.codeFixAvailable(ts.Diagnostics.Add_missing_await.message);