Skip to content

Commit 8f3561f

Browse files
Merge pull request #27671 from uniqueiniquity/portAsyncFixes
Port async fixes
2 parents cd1803f + 7c6a14e commit 8f3561f

File tree

7 files changed

+166
-45
lines changed

7 files changed

+166
-45
lines changed

src/services/codefixes/convertToAsyncFunction.ts

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,24 @@ namespace ts.codefix {
1515
});
1616

1717
interface SynthIdentifier {
18-
identifier: Identifier;
19-
types: Type[];
18+
readonly identifier: Identifier;
19+
readonly types: Type[];
2020
numberOfAssignmentsOriginal: number; // number of times the variable should be assigned in the refactor
2121
}
2222

2323
interface SymbolAndIdentifier {
24-
identifier: Identifier;
25-
symbol: Symbol;
24+
readonly identifier: Identifier;
25+
readonly symbol: Symbol;
2626
}
2727

2828
interface Transformer {
29-
checker: TypeChecker;
30-
synthNamesMap: Map<SynthIdentifier>; // keys are the symbol id of the identifier
31-
allVarNames: SymbolAndIdentifier[];
32-
setOfExpressionsToReturn: Map<true>; // keys are the node ids of the expressions
33-
constIdentifiers: Identifier[];
34-
originalTypeMap: Map<Type>; // keys are the node id of the identifier
35-
isInJSFile: boolean;
29+
readonly checker: TypeChecker;
30+
readonly synthNamesMap: Map<SynthIdentifier>; // keys are the symbol id of the identifier
31+
readonly allVarNames: ReadonlyArray<SymbolAndIdentifier>;
32+
readonly setOfExpressionsToReturn: ReadonlyMap<true>; // keys are the node ids of the expressions
33+
readonly constIdentifiers: Identifier[];
34+
readonly originalTypeMap: ReadonlyMap<Type>; // keys are the node id of the identifier
35+
readonly isInJSFile: boolean;
3636
}
3737

3838
function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker, context: CodeFixContextBase): void {
@@ -61,14 +61,14 @@ namespace ts.codefix {
6161
const functionToConvertRenamed: FunctionLikeDeclaration = renameCollidingVarNames(functionToConvert, checker, synthNamesMap, context, setOfExpressionsToReturn, originalTypeMap, allVarNames);
6262
const constIdentifiers = getConstIdentifiers(synthNamesMap);
6363
const returnStatements = getReturnStatementsWithPromiseHandlers(functionToConvertRenamed);
64-
const transformer = { checker, synthNamesMap, allVarNames, setOfExpressionsToReturn, constIdentifiers, originalTypeMap, isInJSFile: isInJavascript };
64+
const transformer: Transformer = { checker, synthNamesMap, allVarNames, setOfExpressionsToReturn, constIdentifiers, originalTypeMap, isInJSFile: isInJavascript };
6565

6666
if (!returnStatements.length) {
6767
return;
6868
}
6969

7070
// add the async keyword
71-
changes.insertModifierBefore(sourceFile, SyntaxKind.AsyncKeyword, functionToConvert);
71+
changes.insertLastModifierBefore(sourceFile, SyntaxKind.AsyncKeyword, functionToConvert);
7272

7373
function startTransformation(node: CallExpression, nodeToReplace: Node) {
7474
const newNodes = transformExpression(node, transformer, node);
@@ -88,7 +88,7 @@ namespace ts.codefix {
8888
}
8989

9090
// Returns the identifiers that are never reassigned in the refactor
91-
function getConstIdentifiers(synthNamesMap: Map<SynthIdentifier>): Identifier[] {
91+
function getConstIdentifiers(synthNamesMap: ReadonlyMap<SynthIdentifier>): Identifier[] {
9292
const constIdentifiers: Identifier[] = [];
9393
synthNamesMap.forEach((val) => {
9494
if (val.numberOfAssignmentsOriginal === 0) {
@@ -249,18 +249,18 @@ namespace ts.codefix {
249249
}
250250
}
251251

252-
function getNewNameIfConflict(name: Identifier, originalNames: Map<Symbol[]>): SynthIdentifier {
253-
const numVarsSameName = (originalNames.get(name.text) || []).length;
252+
function getNewNameIfConflict(name: Identifier, originalNames: ReadonlyMap<Symbol[]>): SynthIdentifier {
253+
const numVarsSameName = (originalNames.get(name.text) || emptyArray).length;
254254
const numberOfAssignmentsOriginal = 0;
255255
const identifier = numVarsSameName === 0 ? name : createIdentifier(name.text + "_" + numVarsSameName);
256256
return { identifier, types: [], numberOfAssignmentsOriginal };
257257
}
258258

259259
// dispatch function to recursively build the refactoring
260260
// should be kept up to date with isFixablePromiseHandler in suggestionDiagnostics.ts
261-
function transformExpression(node: Expression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): Statement[] {
261+
function transformExpression(node: Expression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): ReadonlyArray<Statement> {
262262
if (!node) {
263-
return [];
263+
return emptyArray;
264264
}
265265

266266
const originalType = isIdentifier(node) && transformer.originalTypeMap.get(getNodeId(node).toString());
@@ -280,10 +280,10 @@ namespace ts.codefix {
280280
}
281281

282282
codeActionSucceeded = false;
283-
return [];
283+
return emptyArray;
284284
}
285285

286-
function transformCatch(node: CallExpression, transformer: Transformer, prevArgName?: SynthIdentifier): Statement[] {
286+
function transformCatch(node: CallExpression, transformer: Transformer, prevArgName?: SynthIdentifier): ReadonlyArray<Statement> {
287287
const func = node.arguments[0];
288288
const argName = getArgName(func, transformer);
289289
const shouldReturn = transformer.setOfExpressionsToReturn.get(getNodeId(node).toString());
@@ -336,7 +336,7 @@ namespace ts.codefix {
336336
return newSynthName;
337337
}
338338

339-
function transformThen(node: CallExpression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): Statement[] {
339+
function transformThen(node: CallExpression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): ReadonlyArray<Statement> {
340340
const [res, rej] = node.arguments;
341341

342342
if (!res) {
@@ -356,18 +356,18 @@ namespace ts.codefix {
356356
const catchArg = argNameRej ? argNameRej.identifier.text : "e";
357357
const catchClause = createCatchClause(catchArg, createBlock(transformationBody2));
358358

359-
return [createTry(tryBlock, catchClause, /* finallyBlock */ undefined) as Statement];
359+
return [createTry(tryBlock, catchClause, /* finallyBlock */ undefined)];
360360
}
361361

362362
return transformExpression(node.expression, transformer, node, argNameRes).concat(transformationBody);
363363
}
364364

365-
function getFlagOfIdentifier(node: Identifier, constIdentifiers: Identifier[]): NodeFlags {
365+
function getFlagOfIdentifier(node: Identifier, constIdentifiers: ReadonlyArray<Identifier>): NodeFlags {
366366
const inArr: boolean = constIdentifiers.some(elem => elem.text === node.text);
367367
return inArr ? NodeFlags.Const : NodeFlags.Let;
368368
}
369369

370-
function transformPromiseCall(node: Expression, transformer: Transformer, prevArgName?: SynthIdentifier): Statement[] {
370+
function transformPromiseCall(node: Expression, transformer: Transformer, prevArgName?: SynthIdentifier): ReadonlyArray<Statement> {
371371
const shouldReturn = transformer.setOfExpressionsToReturn.get(getNodeId(node).toString());
372372
// the identifier is empty when the handler (.then()) ignores the argument - In this situation we do not need to save the result of the promise returning call
373373
const originalNodeParent = node.original ? node.original.parent : node.parent;
@@ -381,23 +381,23 @@ namespace ts.codefix {
381381
return [createReturn(getSynthesizedDeepClone(node))];
382382
}
383383

384-
function createTransformedStatement(prevArgName: SynthIdentifier | undefined, rightHandSide: Expression, transformer: Transformer): MutableNodeArray<Statement> {
384+
function createTransformedStatement(prevArgName: SynthIdentifier | undefined, rightHandSide: Expression, transformer: Transformer): ReadonlyArray<Statement> {
385385
if (!prevArgName || prevArgName.identifier.text.length === 0) {
386386
// if there's no argName to assign to, there still might be side effects
387-
return createNodeArray([createStatement(rightHandSide)]);
387+
return [createStatement(rightHandSide)];
388388
}
389389

390390
if (prevArgName.types.length < prevArgName.numberOfAssignmentsOriginal) {
391391
// if the variable has already been declared, we don't need "let" or "const"
392-
return createNodeArray([createStatement(createAssignment(getSynthesizedDeepClone(prevArgName.identifier), rightHandSide))]);
392+
return [createStatement(createAssignment(getSynthesizedDeepClone(prevArgName.identifier), rightHandSide))];
393393
}
394394

395-
return createNodeArray([createVariableStatement(/*modifiers*/ undefined,
396-
(createVariableDeclarationList([createVariableDeclaration(getSynthesizedDeepClone(prevArgName.identifier), /*type*/ undefined, rightHandSide)], getFlagOfIdentifier(prevArgName.identifier, transformer.constIdentifiers))))]);
395+
return [createVariableStatement(/*modifiers*/ undefined,
396+
(createVariableDeclarationList([createVariableDeclaration(getSynthesizedDeepClone(prevArgName.identifier), /*type*/ undefined, rightHandSide)], getFlagOfIdentifier(prevArgName.identifier, transformer.constIdentifiers))))];
397397
}
398398

399399
// should be kept up to date with isFixablePromiseArgument in suggestionDiagnostics.ts
400-
function getTransformationBody(func: Expression, prevArgName: SynthIdentifier | undefined, argName: SynthIdentifier | undefined, parent: CallExpression, transformer: Transformer): NodeArray<Statement> {
400+
function getTransformationBody(func: Expression, prevArgName: SynthIdentifier | undefined, argName: SynthIdentifier | undefined, parent: CallExpression, transformer: Transformer): ReadonlyArray<Statement> {
401401

402402
const shouldReturn = transformer.setOfExpressionsToReturn.get(getNodeId(parent).toString());
403403
switch (func.kind) {
@@ -410,9 +410,9 @@ namespace ts.codefix {
410410
break;
411411
}
412412

413-
const synthCall = createCall(getSynthesizedDeepClone(func) as Identifier, /*typeArguments*/ undefined, argName ? [argName.identifier] : []);
413+
const synthCall = createCall(getSynthesizedDeepClone(func as Identifier), /*typeArguments*/ undefined, argName ? [argName.identifier] : emptyArray);
414414
if (shouldReturn) {
415-
return createNodeArray([createReturn(synthCall)]);
415+
return [createReturn(synthCall)];
416416
}
417417

418418
const type = transformer.originalTypeMap.get(getNodeId(func).toString()) || transformer.checker.getTypeAtLocation(func);
@@ -450,15 +450,19 @@ namespace ts.codefix {
450450
}
451451
}
452452

453-
return shouldReturn ? getSynthesizedDeepClones(createNodeArray(refactoredStmts)) :
454-
removeReturns(createNodeArray(refactoredStmts), prevArgName!.identifier, transformer, seenReturnStatement);
453+
return shouldReturn ? refactoredStmts.map(s => getSynthesizedDeepClone(s)) :
454+
removeReturns(
455+
refactoredStmts,
456+
prevArgName === undefined ? undefined : prevArgName.identifier,
457+
transformer,
458+
seenReturnStatement);
455459
}
456460
else {
457461
const innerRetStmts = getReturnStatementsWithPromiseHandlers(createReturn(funcBody));
458462
const innerCbBody = getInnerTransformationBody(transformer, innerRetStmts, prevArgName);
459463

460464
if (innerCbBody.length > 0) {
461-
return createNodeArray(innerCbBody);
465+
return innerCbBody;
462466
}
463467

464468
if (!shouldReturn) {
@@ -473,7 +477,7 @@ namespace ts.codefix {
473477
return transformedStatement;
474478
}
475479
else {
476-
return createNodeArray([createReturn(getSynthesizedDeepClone(funcBody))]);
480+
return [createReturn(getSynthesizedDeepClone(funcBody))];
477481
}
478482
}
479483
}
@@ -482,7 +486,7 @@ namespace ts.codefix {
482486
codeActionSucceeded = false;
483487
break;
484488
}
485-
return createNodeArray([]);
489+
return emptyArray;
486490
}
487491

488492
function getLastCallSignature(type: Type, checker: TypeChecker): Signature | undefined {
@@ -491,14 +495,19 @@ namespace ts.codefix {
491495
}
492496

493497

494-
function removeReturns(stmts: NodeArray<Statement>, prevArgName: Identifier, transformer: Transformer, seenReturnStatement: boolean): NodeArray<Statement> {
498+
function removeReturns(stmts: ReadonlyArray<Statement>, prevArgName: Identifier | undefined, transformer: Transformer, seenReturnStatement: boolean): ReadonlyArray<Statement> {
495499
const ret: Statement[] = [];
496500
for (const stmt of stmts) {
497501
if (isReturnStatement(stmt)) {
498502
if (stmt.expression) {
499503
const possiblyAwaitedExpression = isPromiseReturningExpression(stmt.expression, transformer.checker) ? createAwait(stmt.expression) : stmt.expression;
500-
ret.push(createVariableStatement(/*modifiers*/ undefined,
501-
(createVariableDeclarationList([createVariableDeclaration(prevArgName, /*type*/ undefined, possiblyAwaitedExpression)], getFlagOfIdentifier(prevArgName, transformer.constIdentifiers)))));
504+
if (prevArgName === undefined) {
505+
ret.push(createExpressionStatement(possiblyAwaitedExpression));
506+
}
507+
else {
508+
ret.push(createVariableStatement(/*modifiers*/ undefined,
509+
(createVariableDeclarationList([createVariableDeclaration(prevArgName, /*type*/ undefined, possiblyAwaitedExpression)], getFlagOfIdentifier(prevArgName, transformer.constIdentifiers)))));
510+
}
502511
}
503512
}
504513
else {
@@ -507,20 +516,20 @@ namespace ts.codefix {
507516
}
508517

509518
// if block has no return statement, need to define prevArgName as undefined to prevent undeclared variables
510-
if (!seenReturnStatement) {
519+
if (!seenReturnStatement && prevArgName !== undefined) {
511520
ret.push(createVariableStatement(/*modifiers*/ undefined,
512521
(createVariableDeclarationList([createVariableDeclaration(prevArgName, /*type*/ undefined, createIdentifier("undefined"))], getFlagOfIdentifier(prevArgName, transformer.constIdentifiers)))));
513522
}
514523

515-
return createNodeArray(ret);
524+
return ret;
516525
}
517526

518527

519-
function getInnerTransformationBody(transformer: Transformer, innerRetStmts: Node[], prevArgName?: SynthIdentifier) {
528+
function getInnerTransformationBody(transformer: Transformer, innerRetStmts: ReadonlyArray<Node>, prevArgName?: SynthIdentifier) {
520529

521530
let innerCbBody: Statement[] = [];
522531
for (const stmt of innerRetStmts) {
523-
forEachChild(stmt, function visit(node: Node) {
532+
forEachChild(stmt, function visit(node) {
524533
if (isCallExpression(node)) {
525534
const temp = transformExpression(node, transformer, node, prevArgName);
526535
innerCbBody = innerCbBody.concat(temp);

src/services/textChanges.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,16 @@ namespace ts.textChanges {
315315
this.replaceRange(sourceFile, { pos, end: pos }, createToken(modifier), { suffix: " " });
316316
}
317317

318+
public insertLastModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void {
319+
if (!before.modifiers) {
320+
this.insertModifierBefore(sourceFile, modifier, before);
321+
return;
322+
}
323+
324+
const pos = before.modifiers.end;
325+
this.replaceRange(sourceFile, { pos, end: pos }, createToken(modifier), { prefix: " " });
326+
}
327+
318328
public insertCommentBeforeLine(sourceFile: SourceFile, lineNumber: number, position: number, commentText: string): void {
319329
const lineStartPosition = getStartPositionOfLine(lineNumber, sourceFile);
320330
const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition);

src/testRunner/unittests/convertToAsyncFunction.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,24 @@ _testConvertToAsyncFunction("convertToAsyncFunction_nestedPromises", `
12411241
function [#|f|]() {
12421242
return fetch('https://typescriptlang.org').then(x => Promise.resolve(3).then(y => Promise.resolve(x.statusText.length + y)));
12431243
}
1244+
`);
1245+
_testConvertToAsyncFunction("convertToAsyncFunction_noArgs", `
1246+
function delay(millis: number): Promise<void> {
1247+
throw "no"
1248+
}
1249+
1250+
function [#|main2|]() {
1251+
console.log("Please wait. Loading.");
1252+
return delay(500)
1253+
.then(() => { console.log("."); return delay(500); })
1254+
.then(() => { console.log("."); return delay(500); })
1255+
.then(() => { console.log("."); return delay(500); })
1256+
}
1257+
`);
1258+
_testConvertToAsyncFunction("convertToAsyncFunction_exportModifier", `
1259+
export function [#|foo|]() {
1260+
return fetch('https://typescriptlang.org').then(s => console.log(s));
1261+
}
12441262
`);
12451263
});
12461264

@@ -1251,4 +1269,4 @@ function [#|f|]() {
12511269
function _testConvertToAsyncFunctionFailed(caption: string, text: string) {
12521270
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true);
12531271
}
1254-
}
1272+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// ==ORIGINAL==
2+
3+
export function /*[#|*/foo/*|]*/() {
4+
return fetch('https://typescriptlang.org').then(s => console.log(s));
5+
}
6+
7+
// ==ASYNC FUNCTION::Convert to async function==
8+
9+
export async function foo() {
10+
const s = await fetch('https://typescriptlang.org');
11+
return console.log(s);
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// ==ORIGINAL==
2+
3+
export function /*[#|*/foo/*|]*/() {
4+
return fetch('https://typescriptlang.org').then(s => console.log(s));
5+
}
6+
7+
// ==ASYNC FUNCTION::Convert to async function==
8+
9+
export async function foo() {
10+
const s = await fetch('https://typescriptlang.org');
11+
return console.log(s);
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// ==ORIGINAL==
2+
3+
function delay(millis) {
4+
throw "no"
5+
}
6+
7+
function /*[#|*/main2/*|]*/() {
8+
console.log("Please wait. Loading.");
9+
return delay(500)
10+
.then(() => { console.log("."); return delay(500); })
11+
.then(() => { console.log("."); return delay(500); })
12+
.then(() => { console.log("."); return delay(500); })
13+
}
14+
15+
// ==ASYNC FUNCTION::Convert to async function==
16+
17+
function delay(millis) {
18+
throw "no"
19+
}
20+
21+
async function main2() {
22+
console.log("Please wait. Loading.");
23+
await delay(500);
24+
console.log(".");
25+
await delay(500);
26+
console.log(".");
27+
await delay(500);
28+
console.log(".");
29+
return delay(500);
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// ==ORIGINAL==
2+
3+
function delay(millis: number): Promise<void> {
4+
throw "no"
5+
}
6+
7+
function /*[#|*/main2/*|]*/() {
8+
console.log("Please wait. Loading.");
9+
return delay(500)
10+
.then(() => { console.log("."); return delay(500); })
11+
.then(() => { console.log("."); return delay(500); })
12+
.then(() => { console.log("."); return delay(500); })
13+
}
14+
15+
// ==ASYNC FUNCTION::Convert to async function==
16+
17+
function delay(millis: number): Promise<void> {
18+
throw "no"
19+
}
20+
21+
async function main2() {
22+
console.log("Please wait. Loading.");
23+
await delay(500);
24+
console.log(".");
25+
await delay(500);
26+
console.log(".");
27+
await delay(500);
28+
console.log(".");
29+
return delay(500);
30+
}

0 commit comments

Comments
 (0)