-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Implement codefix for adding missing await in return statements enclosed by try statements
#60642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)), | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |
| ExpressionStatement, | ||
| Extension, | ||
| fileExtensionIsOneOf, | ||
| findAncestor, | ||
| forEachReturnStatement, | ||
| FunctionDeclaration, | ||
| FunctionExpression, | ||
|
|
@@ -28,19 +29,22 @@ import { | |
| Identifier, | ||
| importFromModuleSpecifier, | ||
| isAsyncFunction, | ||
| isAwaitExpression, | ||
| isBinaryExpression, | ||
| isBlock, | ||
| isCallExpression, | ||
| isExportAssignment, | ||
| 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)) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: recheck the desired behavior for async generators and add related tests |
||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| // @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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| // @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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| // @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) {} | ||
| }`, | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||
| /// <reference path="fourslash.ts" /> | ||||||
|
|
||||||
| // @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); | ||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should actually produce this:
Suggested change
The current implementation is a draft and it's not yet fully implemented for more complex cases. The problem with it is that for the code like this: return Math.random() > 0.5 ? await inner1() : inner2();the compiler shouldn't produce smth like this return await (Math.random() > 0.5 ? await inner1() : inner2());but rather: return Math.random() > 0.5 ? await inner1() : await inner2(); |
||||||
| } catch (e) {} | ||||||
| }`, | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| // @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) {} | ||
| }`, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| // @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 {} | ||
| }`, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not gonna lie, those messages could use some improvement