diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cb3fc5614fb86..1e7d342fefcef 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20682,6 +20682,8 @@ namespace ts { getReturnTypeOfSignature(getSignatureFromDeclaration(node)); } + checkForAsyncCodeFix(node); + if (node.body.kind === SyntaxKind.Block) { checkSourceElement(node.body); } @@ -22147,6 +22149,19 @@ namespace ts { } } + function checkForAsyncCodeFix(node: FunctionDeclaration | FunctionExpression | MethodDeclaration | ArrowFunction) { + if (!isAsyncFunction(node)) { + const returnType = getReturnTypeOfSignature(getSignatureFromDeclaration(node)); + if (getPromisedTypeOfPromise(returnType)) { + const returnStatements = getReturnStatementsWithPromiseHandlers(node); + if (returnStatements.length > 0) { + const nodeToReport = isVariableDeclaration(node.parent) ? node.parent.name : node; + errorOrSuggestion(/*isError*/ false, nodeToReport, Diagnostics.This_may_be_converted_to_an_async_function); + } + } + } + } + function checkClassForDuplicateDeclarations(node: ClassLikeDeclaration) { const enum Declaration { Getter = 1, @@ -23650,18 +23665,24 @@ namespace ts { checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnOrPromisedType); } - if (produceDiagnostics && !getEffectiveReturnTypeNode(node)) { - // Report an implicit any error if there is no body, no explicit return type, and node is not a private method - // in an ambient context - if (noImplicitAny && nodeIsMissing(body) && !isPrivateWithinAmbient(node)) { - reportImplicitAnyError(node, anyType); + if (produceDiagnostics) { + if (!getEffectiveReturnTypeNode(node)) { + // Report an implicit any error if there is no body, no explicit return type, and node is not a private method + // in an ambient context + if (noImplicitAny && nodeIsMissing(body) && !isPrivateWithinAmbient(node)) { + reportImplicitAnyError(node, anyType); + } + + if (functionFlags & FunctionFlags.Generator && nodeIsPresent(body)) { + // A generator with a body and no type annotation can still cause errors. It can error if the + // yielded values have no common supertype, or it can give an implicit any error if it has no + // yielded values. The only way to trigger these errors is to try checking its return type. + getReturnTypeOfSignature(getSignatureFromDeclaration(node)); + } } - if (functionFlags & FunctionFlags.Generator && nodeIsPresent(body)) { - // A generator with a body and no type annotation can still cause errors. It can error if the - // yielded values have no common supertype, or it can give an implicit any error if it has no - // yielded values. The only way to trigger these errors is to try checking its return type. - getReturnTypeOfSignature(getSignatureFromDeclaration(node)); + if (node.kind !== SyntaxKind.MethodSignature) { + checkForAsyncCodeFix(node); } } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index d04f921ec959d..610e7a1bcd3fe 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4870,6 +4870,41 @@ namespace ts { return unescapeLeadingUnderscores(symbol.escapedName); } + /** @internal */ + export function getReturnStatementsWithPromiseHandlers(node: Node): Node[] { + const returnStatements: Node[] = []; + if (isFunctionLike(node)) { + forEachChild(node, visit); + } + else { + visit(node); + } + + function visit(child: Node) { + if (isFunctionLike(child)) { + return; + } + + if (isReturnStatement(child)) { + forEachChild(child, addHandlers); + } + + function addHandlers(returnChild: Node) { + if (isPromiseHandler(returnChild)) { + returnStatements.push(child as ReturnStatement); + } + } + + forEachChild(child, visit); + } + return returnStatements; + } + + function isPromiseHandler(node: Node): boolean { + return (isCallExpression(node) && isPropertyAccessExpression(node.expression) && + (node.expression.name.escapedText === "then" || node.expression.name.escapedText === "catch")); + } + /** * A JSDocTypedef tag has an _optional_ name field - if a name is not directly present, we should * attempt to draw the name from the node the declaration is on (as that declaration is what its' symbol diff --git a/src/services/codefixes/convertToAsyncFunction.ts b/src/services/codefixes/convertToAsyncFunction.ts index 195586c6b6eba..6162e311266a7 100644 --- a/src/services/codefixes/convertToAsyncFunction.ts +++ b/src/services/codefixes/convertToAsyncFunction.ts @@ -42,7 +42,18 @@ namespace ts.codefix { function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker, context: CodeFixContextBase): void { // get the function declaration - returns a promise - const functionToConvert: FunctionLikeDeclaration = getContainingFunction(getTokenAtPosition(sourceFile, position)) as FunctionLikeDeclaration; + const tokenAtPosition = getTokenAtPosition(sourceFile, position); + let functionToConvert: FunctionLikeDeclaration; + + // if the parent of a FunctionLikeDeclaration is a variable declaration, the convertToAsync diagnostic will be reported on the variable name + if (isIdentifier(tokenAtPosition) && isVariableDeclaration(tokenAtPosition.parent) && + tokenAtPosition.parent.initializer && isFunctionLikeDeclaration(tokenAtPosition.parent.initializer)) { + functionToConvert = tokenAtPosition.parent.initializer; + } + else { + functionToConvert = getContainingFunction(getTokenAtPosition(sourceFile, position)) as FunctionLikeDeclaration; + } + if (!functionToConvert) { return; } diff --git a/src/services/suggestionDiagnostics.ts b/src/services/suggestionDiagnostics.ts index 51a1701c351dd..9c2859bcc8d9a 100644 --- a/src/services/suggestionDiagnostics.ts +++ b/src/services/suggestionDiagnostics.ts @@ -1,9 +1,10 @@ /* @internal */ namespace ts { + // Syntactic suggestion diagnostics should be calculated in the check function here. + // Semantic suggestion diagnostics should be calculated while typechecking, and be reported via program.getSuggestionDiagnostics. export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] { program.getSemanticDiagnostics(sourceFile, cancellationToken); const diags: DiagnosticWithLocation[] = []; - const checker = program.getDiagnosticsProducingTypeChecker(); if (sourceFile.commonJsModuleIndicator && (programContainsEs6Modules(program) || compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) && @@ -69,9 +70,6 @@ namespace ts { } } - if (isFunctionLikeDeclaration(node)) { - addConvertToAsyncFunctionDiagnostics(node, checker, diags); - } node.forEachChild(check); } } @@ -113,64 +111,7 @@ namespace ts { } } - function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: DiagnosticWithLocation[]): void { - - const functionType = node.type ? checker.getTypeFromTypeNode(node.type) : undefined; - if (isAsyncFunction(node) || !node.body || !functionType) { - return; - } - - const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call); - const returnType = callSignatures.length ? checker.getReturnTypeOfSignature(callSignatures[0]) : undefined; - - if (!returnType || !checker.getPromisedTypeOfPromise(returnType)) { - return; - } - - // collect all the return statements - // check that a property access expression exists in there and that it is a handler - const returnStatements = getReturnStatementsWithPromiseHandlers(node); - if (returnStatements.length > 0) { - diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_may_be_converted_to_an_async_function)); - } - } - function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: Node): Node { return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator; } - - /** @internal */ - export function getReturnStatementsWithPromiseHandlers(node: Node): Node[] { - const returnStatements: Node[] = []; - if (isFunctionLike(node)) { - forEachChild(node, visit); - } - else { - visit(node); - } - - function visit(child: Node) { - if (isFunctionLike(child)) { - return; - } - - if (isReturnStatement(child)) { - forEachChild(child, addHandlers); - } - - function addHandlers(returnChild: Node) { - if (isPromiseHandler(returnChild)) { - returnStatements.push(child as ReturnStatement); - } - } - - forEachChild(child, visit); - } - return returnStatements; - } - - function isPromiseHandler(node: Node): boolean { - return (isCallExpression(node) && isPropertyAccessExpression(node.expression) && - (node.expression.name.text === "then" || node.expression.name.text === "catch")); - } } diff --git a/src/testRunner/unittests/convertToAsyncFunction.ts b/src/testRunner/unittests/convertToAsyncFunction.ts index f39b8080ed234..99788e1310e98 100644 --- a/src/testRunner/unittests/convertToAsyncFunction.ts +++ b/src/testRunner/unittests/convertToAsyncFunction.ts @@ -319,7 +319,7 @@ interface String { charAt: any; } interface Array {}` }; - function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { + function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, diagnosticDescription: DiagnosticMessage, codeFixDescription: DiagnosticMessage, includeLib?: boolean) { const t = getTest(text); const selectionRange = t.ranges.get("selection")!; if (!selectionRange) { @@ -361,12 +361,14 @@ interface Array {}` }; const diagnostics = languageService.getSuggestionDiagnostics(f.path); - const diagnostic = find(diagnostics, diagnostic => diagnostic.messageText === description.message); - assert.isNotNull(diagnostic); + const diagnostic = find(diagnostics, diagnostic => diagnostic.messageText === diagnosticDescription.message); + assert.exists(diagnostic); + assert.equal(diagnostic!.start, context.span.start); + assert.equal(diagnostic!.length, context.span.length); const actions = codefix.getFixes(context); - const action = find(actions, action => action.description === description.message)!; - assert.isNotNull(action); + const action = find(actions, action => action.description === codeFixDescription.message)!; + assert.exists(action); const data: string[] = []; data.push(`// ==ORIGINAL==`); @@ -423,6 +425,10 @@ interface Array {}` _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(result => { console.log(result) }); +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_basicNoReturnTypeAnnotation", ` +function [#|f|]() { + return fetch('https://typescriptlang.org').then(result => { console.log(result) }); }`); _testConvertToAsyncFunction("convertToAsyncFunction_basicWithComments", ` function [#|f|](): Promise{ @@ -436,6 +442,10 @@ function [#|f|](): Promise{ _testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunction", ` [#|():Promise => {|] return fetch('https://typescriptlang.org').then(result => console.log(result)); +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunctionNoAnnotation", ` +[#|() => {|] + return fetch('https://typescriptlang.org').then(result => console.log(result)); }`); _testConvertToAsyncFunction("convertToAsyncFunction_Catch", ` function [#|f|]():Promise { @@ -1178,11 +1188,17 @@ function [#|f|]() { } `); + _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpression", ` +const [#|foo|] = function () { + return fetch('https://typescriptlang.org').then(result => { console.log(result) }); +} +`); + }); function _testConvertToAsyncFunction(caption: string, text: string) { - testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", Diagnostics.Convert_to_async_function, /*includeLib*/ true); + testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", Diagnostics.This_may_be_converted_to_an_async_function, Diagnostics.Convert_to_async_function, /*includeLib*/ true); } function _testConvertToAsyncFunctionFailed(caption: string, text: string) { diff --git a/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_ArrowFunctionNoAnnotation.js b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_ArrowFunctionNoAnnotation.js new file mode 100644 index 0000000000000..500d546971bfb --- /dev/null +++ b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_ArrowFunctionNoAnnotation.js @@ -0,0 +1,11 @@ +// ==ORIGINAL== + +/*[#|*/() => {/*|]*/ + return fetch('https://typescriptlang.org').then(result => console.log(result)); +} +// ==ASYNC FUNCTION::Convert to async function== + +async () => { + const result = await fetch('https://typescriptlang.org'); + return console.log(result); +} \ No newline at end of file diff --git a/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_ArrowFunctionNoAnnotation.ts b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_ArrowFunctionNoAnnotation.ts new file mode 100644 index 0000000000000..500d546971bfb --- /dev/null +++ b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_ArrowFunctionNoAnnotation.ts @@ -0,0 +1,11 @@ +// ==ORIGINAL== + +/*[#|*/() => {/*|]*/ + return fetch('https://typescriptlang.org').then(result => console.log(result)); +} +// ==ASYNC FUNCTION::Convert to async function== + +async () => { + const result = await fetch('https://typescriptlang.org'); + return console.log(result); +} \ No newline at end of file diff --git a/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_basicNoReturnTypeAnnotation.js b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_basicNoReturnTypeAnnotation.js new file mode 100644 index 0000000000000..8aec78c667066 --- /dev/null +++ b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_basicNoReturnTypeAnnotation.js @@ -0,0 +1,11 @@ +// ==ORIGINAL== + +function /*[#|*/f/*|]*/() { + return fetch('https://typescriptlang.org').then(result => { console.log(result) }); +} +// ==ASYNC FUNCTION::Convert to async function== + +async function f() { + const result = await fetch('https://typescriptlang.org'); + console.log(result); +} \ No newline at end of file diff --git a/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_basicNoReturnTypeAnnotation.ts b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_basicNoReturnTypeAnnotation.ts new file mode 100644 index 0000000000000..8aec78c667066 --- /dev/null +++ b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_basicNoReturnTypeAnnotation.ts @@ -0,0 +1,11 @@ +// ==ORIGINAL== + +function /*[#|*/f/*|]*/() { + return fetch('https://typescriptlang.org').then(result => { console.log(result) }); +} +// ==ASYNC FUNCTION::Convert to async function== + +async function f() { + const result = await fetch('https://typescriptlang.org'); + console.log(result); +} \ No newline at end of file diff --git a/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_simpleFunctionExpression.js b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_simpleFunctionExpression.js new file mode 100644 index 0000000000000..a92497ca937d9 --- /dev/null +++ b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_simpleFunctionExpression.js @@ -0,0 +1,12 @@ +// ==ORIGINAL== + +const /*[#|*/foo/*|]*/ = function () { + return fetch('https://typescriptlang.org').then(result => { console.log(result) }); +} + +// ==ASYNC FUNCTION::Convert to async function== + +const foo = async function () { + const result = await fetch('https://typescriptlang.org'); + console.log(result); +} diff --git a/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_simpleFunctionExpression.ts b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_simpleFunctionExpression.ts new file mode 100644 index 0000000000000..a92497ca937d9 --- /dev/null +++ b/tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_simpleFunctionExpression.ts @@ -0,0 +1,12 @@ +// ==ORIGINAL== + +const /*[#|*/foo/*|]*/ = function () { + return fetch('https://typescriptlang.org').then(result => { console.log(result) }); +} + +// ==ASYNC FUNCTION::Convert to async function== + +const foo = async function () { + const result = await fetch('https://typescriptlang.org'); + console.log(result); +}