Skip to content

Clean up async refactoring #26720

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

Closed
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);
}