Skip to content

Commit 6ec7638

Browse files
committed
Fix the crash when reporting errors of file that was referenced by inferred project root, is opened right after closing the root file
Fixes the crash reported in microsoft#23255 (comment)
1 parent 222f35d commit 6ec7638

File tree

3 files changed

+100
-13
lines changed

3 files changed

+100
-13
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,10 @@ namespace ts.projectSystem {
480480
checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent);
481481
}
482482

483+
function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray<string> = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}): protocol.Diagnostic {
484+
return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, source: undefined };
485+
}
486+
483487
function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void {
484488
checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent);
485489
}
@@ -496,7 +500,7 @@ namespace ts.projectSystem {
496500

497501
function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) {
498502
const events = session.events;
499-
assert.deepEqual(events[index], expectedEvent);
503+
assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`);
500504

501505
const outputs = session.host.getOutput();
502506
assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine));
@@ -3333,6 +3337,89 @@ namespace ts.projectSystem {
33333337
checkCompleteEvent(session, 1, expectedSequenceId);
33343338
session.clearMessages();
33353339
});
3340+
3341+
it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => {
3342+
const projectRoot = "/user/username/projects/myproject";
3343+
const app: FileOrFolder = {
3344+
path: `${projectRoot}/src/client/app.js`,
3345+
content: ""
3346+
};
3347+
const serverUtilities: FileOrFolder = {
3348+
path: `${projectRoot}/src/server/utilities.js`,
3349+
content: `function getHostName() { return "hello"; } export { getHostName };`
3350+
};
3351+
const backendTest: FileOrFolder = {
3352+
path: `${projectRoot}/test/backend/index.js`,
3353+
content: `import { getHostName } from '../../src/server/utilities';export default getHostName;`
3354+
};
3355+
const files = [libFile, app, serverUtilities, backendTest];
3356+
const host = createServerHost(files);
3357+
const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true });
3358+
session.executeCommandSeq<protocol.OpenRequest>({
3359+
command: protocol.CommandTypes.Open,
3360+
arguments: {
3361+
file: app.path,
3362+
projectRootPath: projectRoot
3363+
}
3364+
});
3365+
const service = session.getProjectService();
3366+
checkNumberOfProjects(service, { inferredProjects: 1 });
3367+
const project = service.inferredProjects[0];
3368+
checkProjectActualFiles(project, [libFile.path, app.path]);
3369+
session.executeCommandSeq<protocol.OpenRequest>({
3370+
command: protocol.CommandTypes.Open,
3371+
arguments: {
3372+
file: backendTest.path,
3373+
projectRootPath: projectRoot
3374+
}
3375+
});
3376+
checkNumberOfProjects(service, { inferredProjects: 1 });
3377+
checkProjectActualFiles(project, files.map(f => f.path));
3378+
checkErrors([backendTest.path, app.path]);
3379+
session.executeCommandSeq<protocol.CloseRequest>({
3380+
command: protocol.CommandTypes.Close,
3381+
arguments: {
3382+
file: backendTest.path
3383+
}
3384+
});
3385+
session.executeCommandSeq<protocol.OpenRequest>({
3386+
command: protocol.CommandTypes.Open,
3387+
arguments: {
3388+
file: serverUtilities.path,
3389+
projectRootPath: projectRoot
3390+
}
3391+
});
3392+
checkErrors([serverUtilities.path, app.path]);
3393+
3394+
function checkErrors(openFiles: [string, string]) {
3395+
const expectedSequenceId = session.getNextSeq();
3396+
session.executeCommandSeq<protocol.GeterrRequest>({
3397+
command: protocol.CommandTypes.Geterr,
3398+
arguments: {
3399+
delay: 0,
3400+
files: openFiles
3401+
}
3402+
});
3403+
3404+
for (const openFile of openFiles) {
3405+
session.clearMessages();
3406+
host.checkTimeoutQueueLength(3);
3407+
host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1);
3408+
3409+
checkErrorMessage(session, "syntaxDiag", { file: openFile, diagnostics: [] });
3410+
session.clearMessages();
3411+
3412+
host.runQueuedImmediateCallbacks();
3413+
checkErrorMessage(session, "semanticDiag", { file: openFile, diagnostics: [] });
3414+
session.clearMessages();
3415+
3416+
host.runQueuedImmediateCallbacks(1);
3417+
checkErrorMessage(session, "suggestionDiag", { file: openFile, diagnostics: [] });
3418+
}
3419+
checkCompleteEvent(session, 2, expectedSequenceId);
3420+
session.clearMessages();
3421+
}
3422+
});
33363423
});
33373424

33383425
describe("tsserverProjectSystem autoDiscovery", () => {
@@ -4293,10 +4380,6 @@ namespace ts.projectSystem {
42934380

42944381
session.clearMessages();
42954382
});
4296-
4297-
function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray<string> = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}): protocol.Diagnostic {
4298-
return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, source: undefined };
4299-
}
43004383
});
43014384

43024385
describe("tsserverProjectSystem Configure file diagnostics events", () => {

src/server/editorServices.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ namespace ts.server {
310310
return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`;
311311
}
312312

313+
function updateProjectIfDirty(project: Project) {
314+
return project.dirty && project.updateGraph();
315+
}
316+
313317
export class ProjectService {
314318

315319
/*@internal*/
@@ -673,7 +677,7 @@ namespace ts.server {
673677
let hasChanges = this.pendingEnsureProjectForOpenFiles;
674678
this.pendingProjectUpdates.clear();
675679
const updateGraph = (project: Project) => {
676-
hasChanges = this.updateProjectIfDirty(project) || hasChanges;
680+
hasChanges = updateProjectIfDirty(project) || hasChanges;
677681
};
678682

679683
this.externalProjects.forEach(updateGraph);
@@ -684,10 +688,6 @@ namespace ts.server {
684688
}
685689
}
686690

687-
private updateProjectIfDirty(project: Project) {
688-
return project.dirty && project.updateGraph();
689-
}
690-
691691
getFormatCodeOptions(file: NormalizedPath) {
692692
const info = this.getScriptInfoForNormalizedPath(file);
693693
return info && info.getFormatCodeSettings() || this.hostConfiguration.formatCodeOptions;
@@ -1980,7 +1980,7 @@ namespace ts.server {
19801980
}
19811981
});
19821982
this.pendingEnsureProjectForOpenFiles = false;
1983-
this.inferredProjects.forEach(p => this.updateProjectIfDirty(p));
1983+
this.inferredProjects.forEach(updateProjectIfDirty);
19841984

19851985
this.logger.info("Structure after ensureProjectForOpenFiles:");
19861986
this.printProjects();
@@ -2027,7 +2027,7 @@ namespace ts.server {
20272027
}
20282028
else {
20292029
// Ensure project is ready to check if it contains opened script info
2030-
project.updateGraph();
2030+
updateProjectIfDirty(project);
20312031
}
20322032
}
20332033
}
@@ -2036,6 +2036,11 @@ namespace ts.server {
20362036
// - external project search, which updates the project before checking if info is present in it
20372037
// - configured project - either created or updated to ensure we know correct status of info
20382038

2039+
// At this point we need to ensure that containing projects of the info are uptodate
2040+
// This will ensure that later question of info.isOrphan() will return correct answer
2041+
// and we correctly create inferred project for the info
2042+
info.containingProjects.forEach(updateProjectIfDirty);
2043+
20392044
// At this point if file is part of any any configured or external project, then it would be present in the containing projects
20402045
// So if it still doesnt have any containing projects, it needs to be part of inferred project
20412046
if (info.isOrphan()) {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8050,7 +8050,6 @@ declare namespace ts.server {
80508050
* ensure that each open script info has project
80518051
*/
80528052
private ensureProjectStructuresUptoDate;
8053-
private updateProjectIfDirty;
80548053
getFormatCodeOptions(file: NormalizedPath): FormatCodeSettings;
80558054
getPreferences(file: NormalizedPath): UserPreferences;
80568055
private onSourceFileChanged;

0 commit comments

Comments
 (0)