Skip to content

Commit 799656a

Browse files
Merge pull request #29214 from uniqueiniquity/nestedAsyncCodeFix
Only provide suggestion for outermost async fix
2 parents 11585d2 + cb57f17 commit 799656a

21 files changed

+134
-54
lines changed

src/services/suggestionDiagnostics.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* @internal */
22
namespace ts {
3+
const visitedNestedConvertibleFunctions = createMap<true>();
4+
35
export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] {
46
program.getSemanticDiagnostics(sourceFile, cancellationToken);
57
const diags: DiagnosticWithLocation[] = [];
@@ -13,6 +15,7 @@ namespace ts {
1315

1416
const isJsFile = isSourceFileJS(sourceFile);
1517

18+
visitedNestedConvertibleFunctions.clear();
1619
check(sourceFile);
1720

1821
if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) {
@@ -114,17 +117,22 @@ namespace ts {
114117
}
115118

116119
function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push<DiagnosticWithLocation>): void {
117-
if (!isAsyncFunction(node) &&
118-
node.body &&
119-
isBlock(node.body) &&
120-
hasReturnStatementWithPromiseHandler(node.body) &&
121-
returnsPromise(node, checker)) {
120+
// need to check function before checking map so that deeper levels of nested callbacks are checked
121+
if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) {
122122
diags.push(createDiagnosticForNode(
123123
!node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node,
124124
Diagnostics.This_may_be_converted_to_an_async_function));
125125
}
126126
}
127127

128+
function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) {
129+
return !isAsyncFunction(node) &&
130+
node.body &&
131+
isBlock(node.body) &&
132+
hasReturnStatementWithPromiseHandler(node.body) &&
133+
returnsPromise(node, checker);
134+
}
135+
128136
function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean {
129137
const functionType = checker.getTypeAtLocation(node);
130138
const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call);
@@ -169,14 +177,20 @@ namespace ts {
169177
// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts
170178
function isFixablePromiseArgument(arg: Expression): boolean {
171179
switch (arg.kind) {
172-
case SyntaxKind.NullKeyword:
173-
case SyntaxKind.Identifier: // identifier includes undefined
174180
case SyntaxKind.FunctionDeclaration:
175181
case SyntaxKind.FunctionExpression:
176182
case SyntaxKind.ArrowFunction:
183+
visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true);
184+
/* falls through */
185+
case SyntaxKind.NullKeyword:
186+
case SyntaxKind.Identifier: // identifier includes undefined
177187
return true;
178188
default:
179189
return false;
180190
}
181191
}
192+
193+
function getKeyFromNode(exp: FunctionLikeDeclaration) {
194+
return `${exp.pos.toString()}:${exp.end.toString()}`;
195+
}
182196
}

src/testRunner/unittests/services/convertToAsyncFunction.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ interface String { charAt: any; }
255255
interface Array<T> {}`
256256
};
257257

258-
function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false) {
258+
function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false, onlyProvideAction = false) {
259259
const t = extractTest(text);
260260
const selectionRange = t.ranges.get("selection")!;
261261
if (!selectionRange) {
@@ -307,7 +307,7 @@ interface Array<T> {}`
307307

308308
const actions = codefix.getFixes(context);
309309
const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message);
310-
if (expectFailure) {
310+
if (expectFailure && !onlyProvideAction) {
311311
assert.isNotTrue(action && action.changes.length > 0);
312312
return;
313313
}
@@ -1151,25 +1151,25 @@ function [#|f|]() {
11511151
_testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpression", `
11521152
const [#|foo|] = function () {
11531153
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
1154-
}
1154+
}
11551155
`);
11561156

11571157
_testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionWithName", `
11581158
const foo = function [#|f|]() {
11591159
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
1160-
}
1160+
}
11611161
`);
11621162

11631163
_testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionAssignedToBindingPattern", `
11641164
const { length } = [#|function|] () {
11651165
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
1166-
}
1166+
}
11671167
`);
11681168

11691169
_testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParams", `
11701170
function [#|f|]() {
1171-
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
1172-
}
1171+
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
1172+
}
11731173
`);
11741174

11751175
_testConvertToAsyncFunction("convertToAsyncFunction_bindingPattern", `
@@ -1178,7 +1178,7 @@ function [#|f|]() {
11781178
}
11791179
function res({ status, trailer }){
11801180
console.log(status);
1181-
}
1181+
}
11821182
`);
11831183

11841184
_testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", `
@@ -1188,7 +1188,7 @@ function [#|f|]() {
11881188
}
11891189
function res({ status, trailer }){
11901190
console.log(status);
1191-
}
1191+
}
11921192
`);
11931193

11941194
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunction", `
@@ -1209,7 +1209,7 @@ function [#|f|]() {
12091209
}
12101210
function res(result) {
12111211
return Promise.resolve().then(x => console.log(result));
1212-
}
1212+
}
12131213
`);
12141214

12151215
_testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromise", `
@@ -1241,7 +1241,7 @@ function [#|f|]() {
12411241
return Promise.resolve(1)
12421242
.then(x => Promise.reject(x))
12431243
.catch(err => console.log(err));
1244-
}
1244+
}
12451245
`);
12461246

12471247
_testConvertToAsyncFunction("convertToAsyncFunction_nestedPromises", `
@@ -1266,6 +1266,22 @@ _testConvertToAsyncFunction("convertToAsyncFunction_exportModifier", `
12661266
export function [#|foo|]() {
12671267
return fetch('https://typescriptlang.org').then(s => console.log(s));
12681268
}
1269+
`);
1270+
1271+
_testConvertToAsyncFunction("convertToAsyncFunction_OutermostOnlySuccess", `
1272+
function [#|foo|]() {
1273+
return fetch('a').then(() => {
1274+
return fetch('b').then(() => 'c');
1275+
})
1276+
}
1277+
`);
1278+
1279+
_testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", `
1280+
function foo() {
1281+
return fetch('a').then([#|() => {|]
1282+
return fetch('b').then(() => 'c');
1283+
})
1284+
}
12691285
`);
12701286
});
12711287

@@ -1276,4 +1292,8 @@ export function [#|foo|]() {
12761292
function _testConvertToAsyncFunctionFailed(caption: string, text: string) {
12771293
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true);
12781294
}
1295+
1296+
function _testConvertToAsyncFunctionFailedSuggestion(caption: string, text: string) {
1297+
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true, /*onlyProvideAction*/ true);
1298+
}
12791299
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// ==ORIGINAL==
2+
3+
function foo() {
4+
return fetch('a').then(/*[#|*/() => {/*|]*/
5+
return fetch('b').then(() => 'c');
6+
})
7+
}
8+
9+
// ==ASYNC FUNCTION::Convert to async function==
10+
11+
function foo() {
12+
return fetch('a').then(async () => {
13+
await fetch('b');
14+
return 'c';
15+
})
16+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// ==ORIGINAL==
2+
3+
function /*[#|*/foo/*|]*/() {
4+
return fetch('a').then(() => {
5+
return fetch('b').then(() => 'c');
6+
})
7+
}
8+
9+
// ==ASYNC FUNCTION::Convert to async function==
10+
11+
async function foo() {
12+
await fetch('a');
13+
await fetch('b');
14+
return 'c';
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// ==ORIGINAL==
2+
3+
function /*[#|*/foo/*|]*/() {
4+
return fetch('a').then(() => {
5+
return fetch('b').then(() => 'c');
6+
})
7+
}
8+
9+
// ==ASYNC FUNCTION::Convert to async function==
10+
11+
async function foo() {
12+
await fetch('a');
13+
await fetch('b');
14+
return 'c';
15+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_bindingPattern.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
55
}
66
function res({ status, trailer }){
77
console.log(status);
8-
}
8+
}
99

1010
// ==ASYNC FUNCTION::Convert to async function==
1111

@@ -15,4 +15,4 @@ async function f() {
1515
}
1616
function res({ status, trailer }){
1717
console.log(status);
18-
}
18+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_bindingPattern.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
55
}
66
function res({ status, trailer }){
77
console.log(status);
8-
}
8+
}
99

1010
// ==ASYNC FUNCTION::Convert to async function==
1111

@@ -15,4 +15,4 @@ async function f() {
1515
}
1616
function res({ status, trailer }){
1717
console.log(status);
18-
}
18+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_bindingPatternNameCollision.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function /*[#|*/f/*|]*/() {
66
}
77
function res({ status, trailer }){
88
console.log(status);
9-
}
9+
}
1010

1111
// ==ASYNC FUNCTION::Convert to async function==
1212

@@ -17,4 +17,4 @@ async function f() {
1717
}
1818
function res({ status, trailer }){
1919
console.log(status);
20-
}
20+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_bindingPatternNameCollision.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function /*[#|*/f/*|]*/() {
66
}
77
function res({ status, trailer }){
88
console.log(status);
9-
}
9+
}
1010

1111
// ==ASYNC FUNCTION::Convert to async function==
1212

@@ -17,4 +17,4 @@ async function f() {
1717
}
1818
function res({ status, trailer }){
1919
console.log(status);
20-
}
20+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_callbackReturnsRejectedPromiseInTryBlock.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function /*[#|*/f/*|]*/() {
44
return Promise.resolve(1)
55
.then(x => Promise.reject(x))
66
.catch(err => console.log(err));
7-
}
7+
}
88

99
// ==ASYNC FUNCTION::Convert to async function==
1010

@@ -16,4 +16,4 @@ async function f() {
1616
catch (err) {
1717
return console.log(err);
1818
}
19-
}
19+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_callbackReturnsRejectedPromiseInTryBlock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function /*[#|*/f/*|]*/() {
44
return Promise.resolve(1)
55
.then(x => Promise.reject(x))
66
.catch(err => console.log(err));
7-
}
7+
}
88

99
// ==ASYNC FUNCTION::Convert to async function==
1010

@@ -16,4 +16,4 @@ async function f() {
1616
catch (err) {
1717
return console.log(err);
1818
}
19-
}
19+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_catchBlockUniqueParams.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// ==ORIGINAL==
22

33
function /*[#|*/f/*|]*/() {
4-
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
5-
}
4+
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
5+
}
66

77
// ==ASYNC FUNCTION::Convert to async function==
88

@@ -15,5 +15,5 @@ async function f() {
1515
catch (x_1) {
1616
x_2 = "a";
1717
}
18-
return !!x_2;
19-
}
18+
return !!x_2;
19+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_catchBlockUniqueParams.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// ==ORIGINAL==
22

33
function /*[#|*/f/*|]*/() {
4-
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
5-
}
4+
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
5+
}
66

77
// ==ASYNC FUNCTION::Convert to async function==
88

@@ -15,5 +15,5 @@ async function f() {
1515
catch (x_1) {
1616
x_2 = "a";
1717
}
18-
return !!x_2;
19-
}
18+
return !!x_2;
19+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_runEffectfulContinuation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
55
}
66
function res(result) {
77
return Promise.resolve().then(x => console.log(result));
8-
}
8+
}
99

1010
// ==ASYNC FUNCTION::Convert to async function==
1111

@@ -16,4 +16,4 @@ async function f() {
1616
}
1717
function res(result) {
1818
return Promise.resolve().then(x => console.log(result));
19-
}
19+
}

tests/baselines/reference/convertToAsyncFunction/convertToAsyncFunction_runEffectfulContinuation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
55
}
66
function res(result) {
77
return Promise.resolve().then(x => console.log(result));
8-
}
8+
}
99

1010
// ==ASYNC FUNCTION::Convert to async function==
1111

@@ -16,4 +16,4 @@ async function f() {
1616
}
1717
function res(result) {
1818
return Promise.resolve().then(x => console.log(result));
19-
}
19+
}

0 commit comments

Comments
 (0)