Skip to content

Commit b062bcf

Browse files
committed
fix(39858): generate valid async/await code for imported functions
1 parent 6101fbc commit b062bcf

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

@@ -139,28 +139,21 @@ namespace ts.codefix {
139139
return !!checker.getPromisedTypeOfPromise(checker.getTypeAtLocation(node));
140140
}
141141

142-
function declaredInFile(symbol: Symbol, sourceFile: SourceFile): boolean {
143-
return symbol.valueDeclaration && symbol.valueDeclaration.getSourceFile() === sourceFile;
144-
}
145-
146142
/*
147143
Renaming of identifiers may be neccesary as the refactor changes scopes -
148144
This function collects all existing identifier names and names of identifiers that will be created in the refactor.
149145
It then checks for any collisions and renames them through getSynthesizedDeepClone
150146
*/
151-
function renameCollidingVarNames(nodeToRename: FunctionLikeDeclaration, checker: TypeChecker, synthNamesMap: ESMap<string, SynthIdentifier>, sourceFile: SourceFile): FunctionLikeDeclaration {
147+
function renameCollidingVarNames(nodeToRename: FunctionLikeDeclaration, checker: TypeChecker, synthNamesMap: ESMap<string, SynthIdentifier>): FunctionLikeDeclaration {
152148
const identsToRenameMap = new Map<string, Identifier>(); // key is the symbol id
153149
const collidingSymbolMap = createMultiMap<Symbol>();
154150
forEachChild(nodeToRename, function visit(node: Node) {
155151
if (!isIdentifier(node)) {
156152
forEachChild(node, visit);
157153
return;
158154
}
159-
160155
const symbol = checker.getSymbolAtLocation(node);
161-
const isDefinedInFile = symbol && declaredInFile(symbol, sourceFile);
162-
163-
if (symbol && isDefinedInFile) {
156+
if (symbol) {
164157
const type = checker.getTypeAtLocation(node);
165158
// Note - the choice of the last call signature is arbitrary
166159
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", () => {
@@ -1453,6 +1471,13 @@ const fn = (): Promise<(message: string) => void> =>
14531471
function [#|f|]() {
14541472
return fn().then(res => res("test"));
14551473
}
1474+
`);
1475+
1476+
_testConvertToAsyncFunctionWithModule("convertToAsyncFunction_importedFunction", `
1477+
import { fn } from "./module";
1478+
function [#|f|]() {
1479+
return Promise.resolve(0).then(fn);
1480+
}
14561481
`);
14571482

14581483
});
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)