Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3843,6 +3843,10 @@
"category": "Message",
"code": 90028
},
"Convert to async": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Add async modifier to containing function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DanielRosenwasser any better suggestions for the message?

"category": "Message",
"code": 90029
},
"Convert function to an ES2015 class": {
"category": "Message",
"code": 95001
Expand Down
70 changes: 70 additions & 0 deletions src/services/codefixes/fixAwaitInSyncFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* @internal */
namespace ts.codefix {
const fixId = "fixAwaitInSyncFunction";
const errorCodes = [
Diagnostics.await_expression_is_only_allowed_within_an_async_function.code,
Diagnostics.A_for_await_of_statement_is_only_allowed_within_an_async_function_or_async_generator.code,
];
registerCodeFix({
errorCodes,
getCodeActions(context) {
const { sourceFile, span } = context;
const token = getTokenAtPosition(sourceFile, span.start, /*includeJsDocComment*/ false);
const containingFunction = getContainingFunction(token);
const insertBefore = getNodeToInsertBefore(sourceFile, containingFunction);
const returnType = getReturnTypeNode(containingFunction);
if (!insertBefore) return undefined;
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, insertBefore, returnType));
return [{ description: getLocaleSpecificMessage(Diagnostics.Convert_to_async), changes, fixId }];
},
fixIds: [fixId],
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const token = getTokenAtPosition(diag.file, diag.start!, /*includeJsDocComment*/ false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a function getNodes(...): { insertBefore: Node, returnType: Type | undefined } | undefined to avoid repeating this. That would also let you combine the two switch statements from getNodeToInsertBefore and getReturnTypeNode.

const containingFunction = getContainingFunction(token);
const insertBefore = getNodeToInsertBefore(diag.file, containingFunction);
const returnType = getReturnTypeNode(containingFunction);
if (insertBefore) {
doChange(changes, context.sourceFile, insertBefore, returnType);
}
}),
});

function getReturnTypeNode(containingFunction: FunctionLike): TypeNode | undefined {
switch (containingFunction.kind) {
case SyntaxKind.MethodDeclaration:
case SyntaxKind.FunctionDeclaration:
return containingFunction.type;
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
if (isVariableDeclaration(containingFunction.parent) &&
containingFunction.parent.type &&
isFunctionTypeNode(containingFunction.parent.type)) {
return containingFunction.parent.type.type;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check for a .type immediately on either of these, i.e. function(): number { ... } or (): number => { ... }.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After adding this change I refactored getReturnType to not use a switch statement at all since with the added condition the two if statements handle all cases.

}
}
}

function getNodeToInsertBefore(sourceFile: SourceFile, containingFunction: FunctionLike): Node | undefined {
switch (containingFunction.kind) {
case SyntaxKind.MethodDeclaration:
return containingFunction.name;
case SyntaxKind.FunctionExpression:
case SyntaxKind.FunctionDeclaration:
return findChildOfKind(containingFunction, SyntaxKind.FunctionKeyword, sourceFile);
case SyntaxKind.ArrowFunction:
return findChildOfKind(containingFunction, SyntaxKind.OpenParenToken, sourceFile) || first(containingFunction.parameters);
default:
return undefined;
}
}

function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, insertBefore: Node, returnType: TypeNode | undefined): void {
if (returnType) {
const entityName = getEntityNameFromTypeNode(returnType);
if (!entityName || entityName.getText() !== "Promise") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!entityName || entityName.kind !== SyntaxKind.Identifier || entityName.text !== "Promise")

changes.replaceNode(sourceFile, returnType, createTypeReferenceNode("Promise", createNodeArray([returnType])));
}
}
changes.insertModifierBefore(sourceFile, SyntaxKind.AsyncKeyword, insertBefore);
}
}
1 change: 1 addition & 0 deletions src/services/codefixes/fixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
/// <reference path="fixForgottenThisPropertyAccess.ts" />
/// <reference path='fixUnusedIdentifier.ts' />
/// <reference path='fixJSDocTypes.ts' />
/// <reference path='fixAwaitInSyncFunction.ts' />
/// <reference path='importFixes.ts' />
/// <reference path='disableJsDiagnostics.ts' />
/// <reference path='helpers.ts' />
Expand Down
5 changes: 5 additions & 0 deletions src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,11 @@ namespace ts.textChanges {
return this.replaceWithSingle(sourceFile, startPosition, startPosition, newNode, this.getOptionsForInsertNodeBefore(before, blankLineBetween));
}

public insertModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void {
const pos = before.getStart(sourceFile);
this.replaceWithSingle(sourceFile, pos, pos, createToken(modifier), { suffix: " " });
}

public changeIdentifierToPropertyAccess(sourceFile: SourceFile, prefix: string, node: Identifier): void {
const startPosition = getAdjustedStartPosition(sourceFile, node, {}, Position.Start);
this.replaceWithSingle(sourceFile, startPosition, startPosition, createPropertyAccess(createIdentifier(prefix), ""), {});
Expand Down
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////function f() {
//// await Promise.resolve();
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`async function f() {
await Promise.resolve();
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction10.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////const f: () => number | string = () => {
//// await Promise.resolve('foo');
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`const f: () => Promise<number | string> = async () => {
await Promise.resolve('foo');
}`,
});
14 changes: 14 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction11.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

////const f: string = () => {
//// await Promise.resolve('foo');
////}

// should not change type if it's incorrectly set
verify.codeFix({
description: "Convert to async",
newFileContent:
`const f: string = async () => {
await Promise.resolve('foo');
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction12.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////const f: () => Array<number | string> = function() {
//// await Promise.resolve([]);
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`const f: () => Promise<Array<number | string>> = async function() {
await Promise.resolve([]);
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction13.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////const f: () => Promise<number | string> = () => {
//// await Promise.resolve('foo');
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`const f: () => Promise<number | string> = async () => {
await Promise.resolve('foo');
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////const f = function() {
//// await Promise.resolve();
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`const f = async function() {
await Promise.resolve();
}`,
});
12 changes: 12 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

////const f = {
//// get a() {
//// return await Promise.resolve();
//// },
//// get a() {
//// await Promise.resolve();
//// },
////}

verify.not.codeFixAvailable();
9 changes: 9 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />

////class Foo {
//// constructor {
//// await Promise.resolve();
//// }
////}

verify.not.codeFixAvailable();
17 changes: 17 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />

////class Foo {
//// bar() {
//// await Promise.resolve();
//// }
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`class Foo {
async bar() {
await Promise.resolve();
}
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction6.5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////const f = promise => {
//// await promise;
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`const f = async promise => {
await promise;
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction6.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////const f = (promise) => {
//// await promise;
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`const f = async (promise) => {
await promise;
}`,
});
17 changes: 17 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />

////function f() {
//// for await (const x of g()) {
//// console.log(x);
//// }
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`async function f() {
for await (const x of g()) {
console.log(x);
}
}`,
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction8.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />

////function f(): number | string {
//// await Promise.resolve(8);
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`async function f(): Promise<number | string> {
await Promise.resolve(8);
}`,
});
17 changes: 17 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction9.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />

////class Foo {
//// bar(): string {
//// await Promise.resolve('baz');
//// }
////}

verify.codeFix({
description: "Convert to async",
newFileContent:
`class Foo {
async bar(): Promise<string> {
await Promise.resolve('baz');
}
}`,
});
21 changes: 21 additions & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction_all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />

////function f() {
//// await Promise.resolve();
////}
////
////const g = () => {
//// await f();
////}

verify.codeFixAll({
fixId: "fixAwaitInSyncFunction",
newFileContent:
`async function f() {
await Promise.resolve();
}

const g = async () => {
await f();
}`,
});