Skip to content

Commit 95d5788

Browse files
author
Benjamin Lichtman
committed
Ensure diagnostic reporting matches code fix ability
1 parent f7f5b1a commit 95d5788

File tree

4 files changed

+79
-55
lines changed

4 files changed

+79
-55
lines changed

src/services/codefixes/convertToAsyncFunction.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ namespace ts.codefix {
254254
}
255255

256256
// dispatch function to recursively build the refactoring
257+
// should be kept up to date with isFixablePromiseHandler in suggestionDiagnostics.ts
257258
function transformExpression(node: Expression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): Statement[] {
258259
if (!node) {
259260
return [];
@@ -275,6 +276,7 @@ namespace ts.codefix {
275276
return transformPromiseCall(node, transformer, prevArgName);
276277
}
277278

279+
codeActionSucceeded = false;
278280
return [];
279281
}
280282

@@ -383,6 +385,7 @@ namespace ts.codefix {
383385
(createVariableDeclarationList([createVariableDeclaration(getSynthesizedDeepClone(prevArgName.identifier), /*type*/ undefined, rightHandSide)], getFlagOfIdentifier(prevArgName.identifier, transformer.constIdentifiers))))]);
384386
}
385387

388+
// should be kept up to date with isFixablePromiseArgument in suggestionDiagnostics.ts
386389
function getTransformationBody(func: Node, prevArgName: SynthIdentifier | undefined, argName: SynthIdentifier, parent: CallExpression, transformer: Transformer): NodeArray<Statement> {
387390

388391
const hasPrevArgName = prevArgName && prevArgName.identifier.text.length > 0;
@@ -500,14 +503,6 @@ namespace ts.codefix {
500503
return innerCbBody;
501504
}
502505

503-
function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean {
504-
if (!isPropertyAccessExpression(node.expression)) {
505-
return false;
506-
}
507-
508-
return node.expression.name.text === funcName;
509-
}
510-
511506
function getArgName(funcNode: Node, transformer: Transformer): SynthIdentifier {
512507

513508
const numberOfAssignmentsOriginal = 0;

src/services/suggestionDiagnostics.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ namespace ts {
160160
}
161161

162162
function addHandlers(returnChild: Node) {
163-
if (isPromiseHandler(returnChild)) {
163+
if (isFixablePromiseHandler(returnChild)) {
164164
returnStatements.push(child as ReturnStatement);
165165
}
166166
}
@@ -170,8 +170,39 @@ namespace ts {
170170
return returnStatements;
171171
}
172172

173-
function isPromiseHandler(node: Node): boolean {
174-
return (isCallExpression(node) && isPropertyAccessExpression(node.expression) &&
175-
(node.expression.name.text === "then" || node.expression.name.text === "catch"));
173+
// Should be kept up to date with transformExpression in convertToAsyncFunction.ts
174+
function isFixablePromiseHandler(node: Node): boolean {
175+
// ensure outermost call exists and is a promise handler
176+
if (!isPromiseHandler(node) || !node.arguments.every(isFixablePromiseArgument)) {
177+
return false;
178+
}
179+
180+
// ensure all chained calls are valid
181+
let currentNode = node.expression;
182+
while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) {
183+
if (isCallExpression(currentNode) && !currentNode.arguments.every(isFixablePromiseArgument)) {
184+
return false;
185+
}
186+
currentNode = currentNode.expression;
187+
}
188+
return true;
189+
}
190+
191+
function isPromiseHandler(node: Node): node is CallExpression {
192+
return isCallExpression(node) && (hasPropertyAccessExpressionWithName(node, "then") || hasPropertyAccessExpressionWithName(node, "catch"));
193+
}
194+
195+
// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts
196+
function isFixablePromiseArgument(arg: Expression): boolean {
197+
switch (arg.kind) {
198+
case SyntaxKind.NullKeyword:
199+
case SyntaxKind.Identifier:
200+
case SyntaxKind.FunctionDeclaration:
201+
case SyntaxKind.FunctionExpression:
202+
case SyntaxKind.ArrowFunction:
203+
return true;
204+
default:
205+
return false;
206+
}
176207
}
177208
}

src/services/utilities.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ namespace ts {
224224
return undefined;
225225
}
226226

227+
export function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean {
228+
if (!isPropertyAccessExpression(node.expression)) {
229+
return false;
230+
}
231+
232+
return node.expression.name.text === funcName;
233+
}
234+
227235
export function isJumpStatementTarget(node: Node): node is Identifier & { parent: BreakOrContinueStatement } {
228236
return node.kind === SyntaxKind.Identifier && isBreakOrContinueStatement(node.parent) && node.parent.label === node;
229237
}

src/testRunner/unittests/convertToAsyncFunction.ts

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
namespace ts {
2-
const enum TestExpectation {
3-
Normal,
4-
NoDiagnostic,
5-
NoAction
6-
}
7-
82
const libFile: TestFSWithWatch.File = {
93
path: "/a/lib/lib.d.ts",
104
content: `/// <reference no-default-lib="true"/>
@@ -261,14 +255,14 @@ interface String { charAt: any; }
261255
interface Array<T> {}`
262256
};
263257

264-
function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectedResult: TestExpectation = TestExpectation.Normal) {
258+
function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false) {
265259
const t = extractTest(text);
266260
const selectionRange = t.ranges.get("selection")!;
267261
if (!selectionRange) {
268262
throw new Error(`Test ${caption} does not specify selection range`);
269263
}
270264

271-
const extensions = expectedResult === TestExpectation.Normal ? [Extension.Ts, Extension.Js] : [Extension.Ts];
265+
const extensions = expectFailure ? [Extension.Ts] : [Extension.Ts, Extension.Js];
272266

273267
extensions.forEach(extension =>
274268
it(`${caption} [${extension}]`, () => runBaseline(extension)));
@@ -304,21 +298,21 @@ interface Array<T> {}`
304298
const diagnostics = languageService.getSuggestionDiagnostics(f.path);
305299
const diagnostic = find(diagnostics, diagnostic => diagnostic.messageText === Diagnostics.This_may_be_converted_to_an_async_function.message &&
306300
diagnostic.start === context.span.start && diagnostic.length === context.span.length);
307-
if (expectedResult === TestExpectation.NoDiagnostic) {
301+
if (expectFailure) {
308302
assert.isUndefined(diagnostic);
309-
return;
310303
}
311-
312-
assert.exists(diagnostic);
304+
else {
305+
assert.exists(diagnostic);
306+
}
313307

314308
const actions = codefix.getFixes(context);
315309
const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message);
316-
if (expectedResult === TestExpectation.NoAction) {
317-
assert.isUndefined(action);
310+
if (expectFailure) {
311+
assert.isNotTrue(action && action.changes.length > 0);
318312
return;
319313
}
320314

321-
assert.exists(action);
315+
assert.isTrue(action && action.changes.length > 0);
322316

323317
const data: string[] = [];
324318
data.push(`// ==ORIGINAL==`);
@@ -481,7 +475,7 @@ function [#|f|]() {
481475
}
482476
`
483477
);
484-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_NoSuggestion", `
478+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestion", `
485479
function [#|f|]():Promise<Response> {
486480
return fetch('https://typescriptlang.org');
487481
}
@@ -495,7 +489,7 @@ function [#|f|]():Promise<void>{
495489
}
496490
`
497491
);
498-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_NoSuggestionNoPromise", `
492+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionNoPromise", `
499493
function [#|f|]():void{
500494
}
501495
`
@@ -548,21 +542,21 @@ function [#|f|]():Promise<void> {
548542
}
549543
`
550544
);
551-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_Finally1", `
545+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally1", `
552546
function [#|finallyTest|](): Promise<void> {
553547
return fetch("https://typescriptlang.org").then(res => console.log(res)).catch(rej => console.log("error", rej)).finally(console.log("finally!"));
554548
}
555549
`
556550
);
557551

558-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_Finally2", `
552+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally2", `
559553
function [#|finallyTest|](): Promise<void> {
560554
return fetch("https://typescriptlang.org").then(res => console.log(res)).finally(console.log("finally!"));
561555
}
562556
`
563557
);
564558

565-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_Finally3", `
559+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally3", `
566560
function [#|finallyTest|](): Promise<void> {
567561
return fetch("https://typescriptlang.org").finally(console.log("finally!"));
568562
}
@@ -590,22 +584,22 @@ function [#|innerPromise|](): Promise<string> {
590584
`
591585
);
592586

593-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn01", `
587+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn01", `
594588
function [#|f|]() {
595589
let blob = fetch("https://typescriptlang.org").then(resp => console.log(resp));
596590
return blob;
597591
}
598592
`
599593
);
600-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn02", `
594+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn02", `
601595
function [#|f|]() {
602596
let blob = fetch("https://typescriptlang.org");
603597
blob.then(resp => console.log(resp));
604598
return blob;
605599
}
606600
`
607601
);
608-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn03", `
602+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn03", `
609603
function [#|f|]() {
610604
let blob = fetch("https://typescriptlang.org")
611605
let blob2 = blob.then(resp => console.log(resp));
@@ -618,7 +612,7 @@ function err (rej) {
618612
}
619613
`
620614
);
621-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn04", `
615+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn04", `
622616
function [#|f|]() {
623617
var blob = fetch("https://typescriptlang.org").then(res => console.log(res)), blob2 = fetch("https://microsoft.com").then(res => res.ok).catch(err);
624618
return blob;
@@ -629,7 +623,7 @@ function err (rej) {
629623
`
630624
);
631625

632-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn05", `
626+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn05", `
633627
function [#|f|]() {
634628
var blob = fetch("https://typescriptlang.org").then(res => console.log(res));
635629
blob.then(x => x);
@@ -638,15 +632,15 @@ function [#|f|]() {
638632
`
639633
);
640634

641-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn06", `
635+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn06", `
642636
function [#|f|]() {
643637
var blob = fetch("https://typescriptlang.org");
644638
return blob;
645639
}
646640
`
647641
);
648642

649-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn07", `
643+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn07", `
650644
function [#|f|]() {
651645
let blob = fetch("https://typescriptlang.org");
652646
let blob2 = fetch("https://microsoft.com");
@@ -657,7 +651,7 @@ function [#|f|]() {
657651
`
658652
);
659653

660-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn08", `
654+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn08", `
661655
function [#|f|]() {
662656
let blob = fetch("https://typescriptlang.org");
663657
if (!blob.ok){
@@ -669,7 +663,7 @@ function [#|f|]() {
669663
`
670664
);
671665

672-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn09", `
666+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn09", `
673667
function [#|f|]() {
674668
let blob3;
675669
let blob = fetch("https://typescriptlang.org");
@@ -683,7 +677,7 @@ function [#|f|]() {
683677
);
684678

685679

686-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn10", `
680+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn10", `
687681
function [#|f|]() {
688682
let blob3;
689683
let blob = fetch("https://typescriptlang.org");
@@ -697,7 +691,7 @@ function [#|f|]() {
697691
`
698692
);
699693

700-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_VarReturn11", `
694+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn11", `
701695
function [#|f|]() {
702696
let blob;
703697
return blob;
@@ -707,7 +701,7 @@ function [#|f|]() {
707701

708702

709703

710-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_Param1", `
704+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_Param1", `
711705
function [#|f|]() {
712706
return my_print(fetch("https://typescriptlang.org").then(res => console.log(res)));
713707
}
@@ -764,7 +758,7 @@ function [#|f|](): Promise<void> {
764758
);
765759

766760

767-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_SeperateLines", `
761+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_SeperateLines", `
768762
function [#|f|](): Promise<string> {
769763
var blob = fetch("https://typescriptlang.org")
770764
blob.then(resp => {
@@ -1027,7 +1021,7 @@ function [#|f|]() {
10271021
}
10281022
10291023
`);
1030-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_CatchFollowedByCall", `
1024+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchFollowedByCall", `
10311025
function [#|f|](){
10321026
return fetch("https://typescriptlang.org").then(res).catch(rej).toString();
10331027
}
@@ -1091,7 +1085,7 @@ function [#|f|]() {
10911085
`
10921086
);
10931087

1094-
_testConvertToAsyncFunctionNoDiagnostic("convertToAsyncFunction_NestedFunctionWrongLocation", `
1088+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NestedFunctionWrongLocation", `
10951089
function [#|f|]() {
10961090
function fn2(){
10971091
function fn3(){
@@ -1140,13 +1134,13 @@ const [#|foo|] = function () {
11401134
}
11411135
`);
11421136

1143-
_testConvertToAsyncFunctionNoAction("convertToAsyncFunction_thenArgumentNotFunction", `
1137+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunction", `
11441138
function [#|f|]() {
11451139
return Promise.resolve().then(f ? (x => x) : (y => y));
11461140
}
11471141
`);
11481142

1149-
_testConvertToAsyncFunctionNoAction("convertToAsyncFunction_thenArgumentNotFunctionNotLastInChain", `
1143+
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunctionNotLastInChain", `
11501144
function [#|f|]() {
11511145
return Promise.resolve().then(f ? (x => x) : (y => y)).then(q => q);
11521146
}
@@ -1159,11 +1153,7 @@ function [#|f|]() {
11591153
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true);
11601154
}
11611155

1162-
function _testConvertToAsyncFunctionNoDiagnostic(caption: string, text: string) {
1163-
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, TestExpectation.NoDiagnostic);
1164-
}
1165-
1166-
function _testConvertToAsyncFunctionNoAction(caption: string, text: string) {
1167-
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, TestExpectation.NoAction);
1156+
function _testConvertToAsyncFunctionFailed(caption: string, text: string) {
1157+
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true);
11681158
}
11691159
}

0 commit comments

Comments
 (0)