Skip to content
41 changes: 31 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20682,6 +20682,8 @@ namespace ts {
getReturnTypeOfSignature(getSignatureFromDeclaration(node));
}

checkForAsyncCodeFix(node);

if (node.body.kind === SyntaxKind.Block) {
checkSourceElement(node.body);
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}

Expand Down
35 changes: 35 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion src/services/codefixes/convertToAsyncFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
63 changes: 2 additions & 61 deletions src/services/suggestionDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -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())) &&
Expand Down Expand Up @@ -69,9 +70,6 @@ namespace ts {
}
}

if (isFunctionLikeDeclaration(node)) {
addConvertToAsyncFunctionDiagnostics(node, checker, diags);
}
node.forEachChild(check);
}
}
Expand Down Expand Up @@ -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"));
}
}
28 changes: 22 additions & 6 deletions src/testRunner/unittests/convertToAsyncFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ interface String { charAt: any; }
interface Array<T> {}`
};

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) {
Expand Down Expand Up @@ -361,12 +361,14 @@ interface Array<T> {}`
};

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==`);
Expand Down Expand Up @@ -423,6 +425,10 @@ interface Array<T> {}`
_testConvertToAsyncFunction("convertToAsyncFunction_basic", `
function [#|f|](): Promise<void>{
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<void>{
Expand All @@ -436,6 +442,10 @@ function [#|f|](): Promise<void>{
_testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunction", `
[#|():Promise<void> => {|]
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<void> {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}