Skip to content

Commit 29bb857

Browse files
authored
Fix crash when there are multiple packages to install per code fix action (microsoft#56400)
1 parent 4515089 commit 29bb857

File tree

8 files changed

+1015
-15
lines changed

8 files changed

+1015
-15
lines changed

src/jsTyping/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface InstallPackageRequest extends TypingInstallerRequestWithProject
5151
readonly fileName: Path;
5252
readonly packageName: string;
5353
readonly projectRootPath: Path;
54+
readonly id: number;
5455
}
5556

5657
/** @internal */
@@ -61,6 +62,7 @@ export interface TypesRegistryResponse extends TypingInstallerResponse {
6162

6263
export interface PackageInstalledResponse extends ProjectResponse {
6364
readonly kind: ActionPackageInstalled;
65+
readonly id: number;
6466
readonly success: boolean;
6567
readonly message: string;
6668
}

src/server/typingInstallerAdapter.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export interface TypingsInstallerWorkerProcess {
4646
send<T extends TypingInstallerRequestUnion>(rq: T): void;
4747
}
4848

49+
interface PackageInstallPromise {
50+
resolve(value: ApplyCodeActionCommandResult): void;
51+
reject(reason: unknown): void;
52+
}
53+
4954
/** @internal */
5055
export abstract class TypingsInstallerAdapter implements ITypingsInstaller {
5156
protected installer!: TypingsInstallerWorkerProcess;
@@ -63,10 +68,8 @@ export abstract class TypingsInstallerAdapter implements ITypingsInstaller {
6368
// It would be preferable to base our limit on the amount of space left in the
6469
// buffer, but we have yet to find a way to retrieve that value.
6570
private static readonly requestDelayMillis = 100;
66-
private packageInstalledPromise: {
67-
resolve(value: ApplyCodeActionCommandResult): void;
68-
reject(reason: unknown): void;
69-
} | undefined;
71+
private packageInstalledPromise: Map<number, PackageInstallPromise> | undefined;
72+
private packageInstallId = 0;
7073

7174
constructor(
7275
protected readonly telemetryEnabled: boolean,
@@ -92,11 +95,13 @@ export abstract class TypingsInstallerAdapter implements ITypingsInstaller {
9295
}
9396

9497
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult> {
95-
this.installer.send<InstallPackageRequest>({ kind: "installPackage", ...options });
96-
Debug.assert(this.packageInstalledPromise === undefined);
97-
return new Promise<ApplyCodeActionCommandResult>((resolve, reject) => {
98-
this.packageInstalledPromise = { resolve, reject };
98+
this.packageInstallId++;
99+
const request: InstallPackageRequest = { kind: "installPackage", ...options, id: this.packageInstallId };
100+
const promise = new Promise<ApplyCodeActionCommandResult>((resolve, reject) => {
101+
(this.packageInstalledPromise ??= new Map()).set(this.packageInstallId, { resolve, reject });
99102
});
103+
this.installer.send(request);
104+
return promise;
100105
}
101106

102107
attach(projectService: ProjectService) {
@@ -136,15 +141,15 @@ export abstract class TypingsInstallerAdapter implements ITypingsInstaller {
136141
this.typesRegistryCache = new Map(Object.entries(response.typesRegistry));
137142
break;
138143
case ActionPackageInstalled: {
139-
const { success, message } = response;
140-
if (success) {
141-
this.packageInstalledPromise!.resolve({ successMessage: message });
144+
const promise = this.packageInstalledPromise?.get(response.id);
145+
Debug.assertIsDefined(promise, "Should find the promise for package install");
146+
this.packageInstalledPromise?.delete(response.id);
147+
if (response.success) {
148+
promise.resolve({ successMessage: response.message });
142149
}
143150
else {
144-
this.packageInstalledPromise!.reject(message);
151+
promise.reject(response.message);
145152
}
146-
this.packageInstalledPromise = undefined;
147-
148153
this.projectService.updateTypingsForProject(response);
149154

150155
// The behavior is the same as for setTypings, so send the same event.

src/testRunner/tests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ import "./unittests/tsserver/autoImportProvider";
140140
import "./unittests/tsserver/auxiliaryProject";
141141
import "./unittests/tsserver/cachingFileSystemInformation";
142142
import "./unittests/tsserver/cancellationToken";
143+
import "./unittests/tsserver/codeFix";
143144
import "./unittests/tsserver/compileOnSave";
144145
import "./unittests/tsserver/completions";
145146
import "./unittests/tsserver/completionsIncomplete";
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as ts from "../../_namespaces/ts";
2+
import {
3+
dedent,
4+
} from "../../_namespaces/Utils";
5+
import {
6+
baselineTsserverLogs,
7+
openFilesForSession,
8+
TestSession,
9+
} from "../helpers/tsserver";
10+
import {
11+
createServerHost,
12+
libFile,
13+
} from "../helpers/virtualFileSystemWithWatch";
14+
15+
describe("unittests:: tsserver:: codeFix::", () => {
16+
function setup() {
17+
const host = createServerHost({
18+
"/home/src/projects/project/src/file.ts": dedent`
19+
import * as os from "os";
20+
import * as https from "https";
21+
import * as vscode from "vscode";
22+
`,
23+
"/home/src/projects/project/tsconfig.json": "{ }",
24+
"/home/src/projects/project/node_modules/vscode/index.js": "export const x = 10;",
25+
[libFile.path]: libFile.content,
26+
});
27+
const session = new TestSession({ host, typesRegistry: ["vscode"] });
28+
openFilesForSession(["/home/src/projects/project/src/file.ts"], session);
29+
const actions = session.executeCommandSeq<ts.server.protocol.GetCombinedCodeFixRequest>({
30+
command: ts.server.protocol.CommandTypes.GetCombinedCodeFix,
31+
arguments: {
32+
scope: { type: "file", args: { file: "/home/src/projects/project/src/file.ts" } },
33+
fixId: "installTypesPackage",
34+
},
35+
}).response as ts.server.protocol.CombinedCodeActions;
36+
return { host, session, actions };
37+
}
38+
it("install package", () => {
39+
const { host, session, actions } = setup();
40+
actions.commands?.forEach(command =>
41+
session.executeCommandSeq<ts.server.protocol.ApplyCodeActionCommandRequest>({
42+
command: ts.server.protocol.CommandTypes.ApplyCodeActionCommand,
43+
arguments: {
44+
command,
45+
},
46+
})
47+
);
48+
host.runPendingInstalls();
49+
baselineTsserverLogs("codeFix", "install package", session);
50+
});
51+
52+
it("install package when serialized", () => {
53+
const { host, session, actions } = setup();
54+
actions.commands?.forEach(command => {
55+
session.executeCommandSeq<ts.server.protocol.ApplyCodeActionCommandRequest>({
56+
command: ts.server.protocol.CommandTypes.ApplyCodeActionCommand,
57+
arguments: {
58+
command,
59+
},
60+
});
61+
host.runPendingInstalls();
62+
});
63+
baselineTsserverLogs("codeFix", "install package when serialized", session);
64+
});
65+
});

src/typingsInstallerCore/typingsInstaller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export abstract class TypingsInstaller {
233233

234234
/** @internal */
235235
installPackage(req: InstallPackageRequest) {
236-
const { fileName, packageName, projectName, projectRootPath } = req;
236+
const { fileName, packageName, projectName, projectRootPath, id } = req;
237237
const cwd = forEachAncestorDirectory(getDirectoryPath(fileName), directory => {
238238
if (this.installTypingHost.fileExists(combinePaths(directory, "package.json"))) {
239239
return directory;
@@ -247,6 +247,7 @@ export abstract class TypingsInstaller {
247247
const response: PackageInstalledResponse = {
248248
kind: ActionPackageInstalled,
249249
projectName,
250+
id,
250251
success,
251252
message,
252253
};
@@ -257,6 +258,7 @@ export abstract class TypingsInstaller {
257258
const response: PackageInstalledResponse = {
258259
kind: ActionPackageInstalled,
259260
projectName,
261+
id,
260262
success: false,
261263
message: "Could not determine a project root path.",
262264
};

tests/baselines/reference/api/typescript.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ declare namespace ts {
4949
readonly fileName: Path;
5050
readonly packageName: string;
5151
readonly projectRootPath: Path;
52+
readonly id: number;
5253
}
5354
interface PackageInstalledResponse extends ProjectResponse {
5455
readonly kind: ActionPackageInstalled;
56+
readonly id: number;
5557
readonly success: boolean;
5658
readonly message: string;
5759
}

0 commit comments

Comments
 (0)