diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 7c098066321..d2a54fdc1bb 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -7,7 +7,7 @@ namespace ts.projectSystem { const file = makeFile("/a.js"); const et = new EventTracker([file]); et.service.openClientFile(file.path); - assert.equal(et.getEvents().length, 0); + assert.equal(et.getEventsWithName(ts.server.ProjectInfoTelemetryEvent).length, 0); }); it("only sends an event once", () => { @@ -25,12 +25,12 @@ namespace ts.projectSystem { et.service.openClientFile(file2.path); checkNumberOfProjects(et.service, { inferredProjects: 1 }); - assert.equal(et.getEvents().length, 0); + assert.equal(et.getEventsWithName(ts.server.ProjectInfoTelemetryEvent).length, 0); et.service.openClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 1, inferredProjects: 1 }); - assert.equal(et.getEvents().length, 0); + assert.equal(et.getEventsWithName(ts.server.ProjectInfoTelemetryEvent).length, 0); }); it("counts files by extension", () => { @@ -219,7 +219,7 @@ namespace ts.projectSystem { const et = new EventTracker([tsconfig, file]); et.host.getFileSize = () => server.maxProgramSizeForNonTsFiles + 1; et.service.openClientFile(file.path); - et.getEvent(server.ProjectLanguageServiceStateEvent, /*mayBeMore*/ true); + et.getEvent(server.ProjectLanguageServiceStateEvent); et.assertProjectInfoTelemetryEvent({ projectId: Harness.mockHash("/jsconfig.json"), fileStats: fileStats({ js: 1 }), @@ -255,6 +255,17 @@ namespace ts.projectSystem { return events; } + getEventsWithName(eventName: T["eventName"]): ReadonlyArray { + let events: T[]; + removeWhere(this.events, event => { + if (event.eventName === eventName) { + (events || (events = [])).push(event as T); + return true; + } + }); + return events || emptyArray; + } + assertProjectInfoTelemetryEvent(partial: Partial, configFile?: string): void { assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { projectId: Harness.mockHash(configFile || "/tsconfig.json"), @@ -278,10 +289,17 @@ namespace ts.projectSystem { }); } - getEvent(eventName: T["eventName"], mayBeMore = false): T["data"] { - if (mayBeMore) { assert(this.events.length !== 0); } - else { assert.equal(this.events.length, 1); } - const event = this.events.shift(); + getEvent(eventName: T["eventName"]): T["data"] { + let event: server.ProjectServiceEvent; + removeWhere(this.events, e => { + if (e.eventName === eventName) { + if (event) { + assert(false, "more than one event found"); + } + event = e; + return true; + } + }); assert.equal(event.eventName, eventName); return event.data; } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 06e2beeaaa6..e8b18ea3b54 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -463,7 +463,7 @@ namespace ts.projectSystem { const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); checkNumberOfInferredProjects(projectService, 0); checkNumberOfConfiguredProjects(projectService, 1); @@ -503,7 +503,7 @@ namespace ts.projectSystem { const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); checkNumberOfInferredProjects(projectService, 0); checkNumberOfConfiguredProjects(projectService, 1); @@ -3169,6 +3169,69 @@ namespace ts.projectSystem { host.runQueuedTimeoutCallbacks(); serverEventManager.checkEventCountOfType("configFileDiag", 3); }); + + it("are generated when the config file doesnot include file opened but has errors", () => { + const serverEventManager = new TestServerEventManager(); + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2 = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + }, + "files": ["app.ts"] + }` + }; + + const host = createServerHost([file, file2, libFile, configFile]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: serverEventManager.handler + }); + openFilesForSession([file2], session); + serverEventManager.checkEventCountOfType("configFileDiag", 1); + for (const event of serverEventManager.events) { + if (event.eventName === "configFileDiag") { + assert.equal(event.data.configFileName, configFile.path); + assert.equal(event.data.triggerFile, file2.path); + return; + } + } + }); + + it("are not generated when the config file doesnot include file opened and doesnt contain any errors", () => { + const serverEventManager = new TestServerEventManager(); + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2 = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["app.ts"] + }` + }; + + const host = createServerHost([file, file2, libFile, configFile]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: serverEventManager.handler + }); + openFilesForSession([file2], session); + serverEventManager.checkEventCountOfType("configFileDiag", 0); + }); }); describe("skipLibCheck", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 25a285929be..32358ba3c4c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1561,14 +1561,17 @@ namespace ts.server { project.watchWildcards(projectOptions.wildcardDirectories); } this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave); + this.sendConfigFileDiagEvent(project, configFileName); + } + private sendConfigFileDiagEvent(project: ConfiguredProject, triggerFile: NormalizedPath) { if (!this.eventHandler) { return; } this.eventHandler({ eventName: ConfigFileDiagEvent, - data: { configFileName, diagnostics: project.getGlobalProjectErrors() || [], triggerFile: configFileName } + data: { configFileName: project.getConfigFilePath(), diagnostics: project.getAllProjectErrors(), triggerFile } }); } @@ -1888,6 +1891,7 @@ namespace ts.server { openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult { let configFileName: NormalizedPath; + let sendConfigFileDiagEvent = false; let configFileErrors: ReadonlyArray; const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, fileContent, scriptKind, hasMixedContent); @@ -1898,14 +1902,8 @@ namespace ts.server { project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { project = this.createConfiguredProject(configFileName); - - // even if opening config file was successful, it could still - // contain errors that were tolerated. - const errors = project.getGlobalProjectErrors(); - if (errors && errors.length > 0) { - // set configFileErrors only when the errors array is non-empty - configFileErrors = errors; - } + // Send the event only if the project got created as part of this open request + sendConfigFileDiagEvent = true; } } } @@ -1919,10 +1917,21 @@ namespace ts.server { // At this point if file is part of any any configured or external project, then it would be present in the containing projects // So if it still doesnt have any containing projects, it needs to be part of inferred project if (info.isOrphan()) { + // Since the file isnt part of configured project, + // report config file and its error only if config file found had errors (and hence may be didnt include the file) + if (sendConfigFileDiagEvent && !project.getAllProjectErrors().length) { + configFileName = undefined; + sendConfigFileDiagEvent = false; + } this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); } this.addToListOfOpenFiles(info); + if (sendConfigFileDiagEvent) { + configFileErrors = project.getAllProjectErrors(); + this.sendConfigFileDiagEvent(project as ConfiguredProject, fileName); + } + // Remove the configured projects that have zero references from open files. // This was postponed from closeOpenFile to after opening next file, // so that we can reuse the project if we need to right away @@ -1938,6 +1947,7 @@ namespace ts.server { // the file from that old project is reopened because of opening file from here. this.deleteOrphanScriptInfoNotInAnyProject(); this.printProjects(); + return { configFileName, configFileErrors }; } diff --git a/src/server/project.ts b/src/server/project.ts index 655f3ed0cfc..203355ad9db 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1271,14 +1271,14 @@ namespace ts.server { * Get the errors that dont have any file name associated */ getGlobalProjectErrors(): ReadonlyArray { - return filter(this.projectErrors, diagnostic => !diagnostic.file); + return filter(this.projectErrors, diagnostic => !diagnostic.file) || emptyArray; } /** * Get all the project errors */ getAllProjectErrors(): ReadonlyArray { - return this.projectErrors; + return this.projectErrors || emptyArray; } setProjectErrors(projectErrors: Diagnostic[]) { @@ -1335,6 +1335,8 @@ namespace ts.server { } this.stopWatchingWildCards(); + this.projectErrors = undefined; + this.configFileSpecs = undefined; } addOpenRef() { diff --git a/src/server/session.ts b/src/server/session.ts index 57dac7ec799..59762f8191b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -969,13 +969,7 @@ namespace ts.server { * @param fileContent is a version of the file content that is known to be more up to date than the one on disk */ private openClientFile(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: NormalizedPath) { - const { configFileName, configFileErrors } = this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath); - if (this.eventHandler) { - this.eventHandler({ - eventName: "configFileDiag", - data: { triggerFile: fileName, configFileName, diagnostics: configFileErrors || emptyArray } - }); - } + this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath); } private getPosition(args: protocol.FileLocationRequestArgs, scriptInfo: ScriptInfo): number { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 1feb6b7d073..b9b72dec922 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7517,6 +7517,7 @@ declare namespace ts.server { private createConfiguredProject(configFileName); private updateNonInferredProjectFiles(project, files, propertyReader); private updateNonInferredProject(project, newUncheckedFiles, propertyReader, newOptions, newTypeAcquisition, compileOnSave); + private sendConfigFileDiagEvent(project, triggerFile); private getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath); private getOrCreateSingleInferredProjectIfEnabled(); private createInferredProject(rootDirectoryForResolution, isSingleInferredProject?, projectRootPath?);