Skip to content

Commit ad70313

Browse files
authored
fix(39858): generate valid async/await code for imported functions (microsoft#40154)
1 parent 620217c commit ad70313

File tree

4 files changed

+69
-23
lines changed

4 files changed

+69
-23
lines changed

src/services/codefixes/convertToAsyncFunction.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ namespace ts.codefix {
77
errorCodes,
88
getCodeActions(context: CodeFixContext) {
99
codeActionSucceeded = true;
10-
const changes = textChanges.ChangeTracker.with(context, (t) => convertToAsyncFunction(t, context.sourceFile, context.span.start, context.program.getTypeChecker(), context));
10+
const changes = textChanges.ChangeTracker.with(context, (t) => convertToAsyncFunction(t, context.sourceFile, context.span.start, context.program.getTypeChecker()));
1111
return codeActionSucceeded ? [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_async_function, fixId, Diagnostics.Convert_all_to_async_functions)] : [];
1212
},
1313
fixIds: [fixId],
14-
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) => convertToAsyncFunction(changes, err.file, err.start, context.program.getTypeChecker(), context)),
14+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) => convertToAsyncFunction(changes, err.file, err.start, context.program.getTypeChecker())),
1515
});
1616

1717
const enum SynthBindingNameKind {
@@ -43,7 +43,7 @@ namespace ts.codefix {
4343
readonly isInJSFile: boolean;
4444
}
4545

46-
function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker, context: CodeFixContextBase): void {
46+
function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker): void {
4747
// get the function declaration - returns a promise
4848
const tokenAtPosition = getTokenAtPosition(sourceFile, position);
4949
let functionToConvert: FunctionLikeDeclaration | undefined;
@@ -64,7 +64,7 @@ namespace ts.codefix {
6464
const synthNamesMap = new Map<string, SynthIdentifier>();
6565
const isInJavascript = isInJSFile(functionToConvert);
6666
const setOfExpressionsToReturn = getAllPromiseExpressionsToReturn(functionToConvert, checker);
67-
const functionToConvertRenamed = renameCollidingVarNames(functionToConvert, checker, synthNamesMap, context.sourceFile);
67+
const functionToConvertRenamed = renameCollidingVarNames(functionToConvert, checker, synthNamesMap);
6868
const returnStatements = functionToConvertRenamed.body && isBlock(functionToConvertRenamed.body) ? getReturnStatementsWithPromiseHandlers(functionToConvertRenamed.body) : emptyArray;
6969
const transformer: Transformer = { checker, synthNamesMap, setOfExpressionsToReturn, isInJSFile: isInJavascript };
7070
if (!returnStatements.length) {
@@ -141,28 +141,21 @@ namespace ts.codefix {
141141
return !!checker.getPromisedTypeOfPromise(checker.getTypeAtLocation(node));
142142
}
143143

144-
function declaredInFile(symbol: Symbol, sourceFile: SourceFile): boolean {
145-
return symbol.valueDeclaration && symbol.valueDeclaration.getSourceFile() === sourceFile;
146-
}
147-
148144
/*
149145
Renaming of identifiers may be neccesary as the refactor changes scopes -
150146
This function collects all existing identifier names and names of identifiers that will be created in the refactor.
151147
It then checks for any collisions and renames them through getSynthesizedDeepClone
152148
*/
153-
function renameCollidingVarNames(nodeToRename: FunctionLikeDeclaration, checker: TypeChecker, synthNamesMap: ESMap<string, SynthIdentifier>, sourceFile: SourceFile): FunctionLikeDeclaration {
149+
function renameCollidingVarNames(nodeToRename: FunctionLikeDeclaration, checker: TypeChecker, synthNamesMap: ESMap<string, SynthIdentifier>): FunctionLikeDeclaration {
154150
const identsToRenameMap = new Map<string, Identifier>(); // key is the symbol id
155151
const collidingSymbolMap = createMultiMap<Symbol>();
156152
forEachChild(nodeToRename, function visit(node: Node) {
157153
if (!isIdentifier(node)) {
158154
forEachChild(node, visit);
159155
return;
160156
}
161-
162157
const symbol = checker.getSymbolAtLocation(node);
163-
const isDefinedInFile = symbol && declaredInFile(symbol, sourceFile);
164-
165-
if (symbol && isDefinedInFile) {
158+
if (symbol) {
166159
const type = checker.getTypeAtLocation(node);
167160
// Note - the choice of the last call signature is arbitrary
168161
const lastCallSignature = getLastCallSignature(type, checker);

src/testRunner/unittests/services/convertToAsyncFunction.ts

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

258+
const moduleFile: TestFSWithWatch.File = {
259+
path: "/module.ts",
260+
content:
261+
`export function fn(res: any): any {
262+
return res;
263+
}`
264+
};
265+
258266
type WithSkipAndOnly<T extends any[]> = ((...args: T) => void) & {
259267
skip: (...args: T) => void;
260268
only: (...args: T) => void;
@@ -269,7 +277,7 @@ interface Array<T> {}`
269277
}
270278
}
271279

272-
function testConvertToAsyncFunction(it: Mocha.PendingTestFunction, caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false, onlyProvideAction = false) {
280+
function testConvertToAsyncFunction(it: Mocha.PendingTestFunction, caption: string, text: string, baselineFolder: string, includeLib?: boolean, includeModule?: boolean, expectFailure = false, onlyProvideAction = false) {
273281
const t = extractTest(text);
274282
const selectionRange = t.ranges.get("selection")!;
275283
if (!selectionRange) {
@@ -283,7 +291,7 @@ interface Array<T> {}`
283291

284292
function runBaseline(extension: Extension) {
285293
const path = "/a" + extension;
286-
const languageService = makeLanguageService({ path, content: t.source }, includeLib);
294+
const languageService = makeLanguageService({ path, content: t.source }, includeLib, includeModule);
287295
const program = languageService.getProgram()!;
288296

289297
if (hasSyntacticDiagnostics(program)) {
@@ -338,17 +346,23 @@ interface Array<T> {}`
338346
const newText = textChanges.applyChanges(sourceFile.text, changes[0].textChanges);
339347
data.push(newText);
340348

341-
const diagProgram = makeLanguageService({ path, content: newText }, includeLib).getProgram()!;
349+
const diagProgram = makeLanguageService({ path, content: newText }, includeLib, includeModule).getProgram()!;
342350
assert.isFalse(hasSyntacticDiagnostics(diagProgram));
343351
Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter));
344352
}
345353

346-
function makeLanguageService(f: { path: string, content: string }, includeLib?: boolean) {
347-
348-
const host = projectSystem.createServerHost(includeLib ? [f, libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required
354+
function makeLanguageService(file: TestFSWithWatch.File, includeLib?: boolean, includeModule?: boolean) {
355+
const files = [file];
356+
if (includeLib) {
357+
files.push(libFile); // libFile is expensive to parse repeatedly - only test when required
358+
}
359+
if (includeModule) {
360+
files.push(moduleFile);
361+
}
362+
const host = projectSystem.createServerHost(files);
349363
const projectService = projectSystem.createProjectService(host);
350-
projectService.openClientFile(f.path);
351-
return projectService.inferredProjects[0].getLanguageService();
364+
projectService.openClientFile(file.path);
365+
return first(projectService.inferredProjects).getLanguageService();
352366
}
353367

354368
function hasSyntacticDiagnostics(program: Program) {
@@ -362,11 +376,15 @@ interface Array<T> {}`
362376
});
363377

364378
const _testConvertToAsyncFunctionFailed = createTestWrapper((it, caption: string, text: string) => {
365-
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true);
379+
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*includeModule*/ false, /*expectFailure*/ true);
366380
});
367381

368382
const _testConvertToAsyncFunctionFailedSuggestion = createTestWrapper((it, caption: string, text: string) => {
369-
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true, /*onlyProvideAction*/ true);
383+
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*includeModule*/ false, /*expectFailure*/ true, /*onlyProvideAction*/ true);
384+
});
385+
386+
const _testConvertToAsyncFunctionWithModule = createTestWrapper((it, caption: string, text: string) => {
387+
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*includeModule*/ true);
370388
});
371389

372390
describe("unittests:: services:: convertToAsyncFunction", () => {
@@ -1559,6 +1577,13 @@ const fn = (): Promise<(message: string) => void> =>
15591577
function [#|f|]() {
15601578
return fn().then(res => res("test"));
15611579
}
1580+
`);
1581+
1582+
_testConvertToAsyncFunctionWithModule("convertToAsyncFunction_importedFunction", `
1583+
import { fn } from "./module";
1584+
function [#|f|]() {
1585+
return Promise.resolve(0).then(fn);
1586+
}
15621587
`);
15631588

15641589
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// ==ORIGINAL==
2+
3+
import { fn } from "./module";
4+
function /*[#|*/f/*|]*/() {
5+
return Promise.resolve(0).then(fn);
6+
}
7+
8+
// ==ASYNC FUNCTION::Convert to async function==
9+
10+
import { fn } from "./module";
11+
async function f() {
12+
const res = await Promise.resolve(0);
13+
return fn(res);
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// ==ORIGINAL==
2+
3+
import { fn } from "./module";
4+
function /*[#|*/f/*|]*/() {
5+
return Promise.resolve(0).then(fn);
6+
}
7+
8+
// ==ASYNC FUNCTION::Convert to async function==
9+
10+
import { fn } from "./module";
11+
async function f() {
12+
const res = await Promise.resolve(0);
13+
return fn(res);
14+
}

0 commit comments

Comments
 (0)