diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 4548e06360..30942fb2fd 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -6,7 +6,10 @@ namespace ts.projectSystem { describe("Project errors", () => { function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: string[]) { assert.isTrue(projectFiles !== undefined, "missing project files"); - const errors = projectFiles.projectErrors; + checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); + } + + function checkProjectErrorsWorker(errors: Diagnostic[], expectedErrors: string[]) { assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); if (expectedErrors.length) { for (let i = 0; i < errors.length; i++) { @@ -122,9 +125,13 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ configuredProjects: 1 }); const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, [ + checkProjectErrors(configuredProject, []); + const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, [ "'{' expected." ]); + assert.isNotNull(projectErrors[0].file); + assert.equal(projectErrors[0].file.fileName, corruptedConfig.path); } // fix config and trigger watcher host.reloadFS([file1, file2, correctConfig]); @@ -134,6 +141,8 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, []); + const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); } }); @@ -163,6 +172,8 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, []); + const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); } // break config and trigger watcher host.reloadFS([file1, file2, corruptedConfig]); @@ -171,10 +182,14 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ configuredProjects: 1 }); const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, [ + checkProjectErrors(configuredProject, []); + const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, [ "'{' expected." ]); + assert.isNotNull(projectErrors[0].file); + assert.equal(projectErrors[0].file.fileName, corruptedConfig.path); } }); }); -} \ No newline at end of file +} diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index c252b9b4f5..7836988e9d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -4005,11 +4005,11 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); const projectName = projectService.configuredProjects[0].getProjectName(); - const diags = session.executeCommand({ + const diags = session.executeCommand({ type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, + command: server.CommandNames.SemanticDiagnosticsSync, seq: 2, - arguments: { projectFileName: projectName } + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } }).response; assert.isTrue(diags.length === 2); @@ -4017,18 +4017,18 @@ namespace ts.projectSystem { host.reloadFS([file, configFile]); host.triggerFileWatcherCallback(configFile.path); - const diagsAfterEdit = session.executeCommand({ + const diagsAfterEdit = session.executeCommand({ type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, + command: server.CommandNames.SemanticDiagnosticsSync, seq: 2, - arguments: { projectFileName: projectName } + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } }).response; assert.isTrue(diagsAfterEdit.length === 2); verifyDiagnostic(diags[0], diagsAfterEdit[0]); verifyDiagnostic(diags[1], diagsAfterEdit[1]); - function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePositionAndFileName, afterEditDiag: server.protocol.DiagnosticWithLinePositionAndFileName) { + function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { assert.equal(beforeEditDiag.message, afterEditDiag.message); assert.equal(beforeEditDiag.code, afterEditDiag.code); assert.equal(beforeEditDiag.category, afterEditDiag.category); @@ -4036,7 +4036,6 @@ namespace ts.projectSystem { assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); - assert.equal(beforeEditDiag.fileName, afterEditDiag.fileName); } }); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 14558a7b70..e8289bf248 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1048,7 +1048,7 @@ namespace ts.server { return { success: conversionResult.success, project, - errors: project.getProjectErrors() + errors: project.getGlobalProjectErrors() }; } diff --git a/src/server/project.ts b/src/server/project.ts index 3095b02c41..0663d25318 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -221,7 +221,14 @@ namespace ts.server { } } - getProjectErrors() { + /** + * Get the errors that dont have any file name associated + */ + getGlobalProjectErrors() { + return filter(this.projectErrors, diagnostic => !diagnostic.file); + } + + getAllProjectErrors() { return this.projectErrors; } @@ -399,6 +406,25 @@ namespace ts.server { return result; } + hasConfigFile(configFilePath: NormalizedPath) { + if (this.program && this.languageServiceEnabled) { + const configFile = this.program.getCompilerOptions().configFile; + if (configFile) { + if (configFilePath === asNormalizedPath(configFile.fileName)) { + return true; + } + if (configFile.extendedSourceFiles) { + for (const f of configFile.extendedSourceFiles) { + if (configFilePath === asNormalizedPath(f)) { + return true; + } + } + } + } + } + return false; + } + getAllEmittableFiles() { if (!this.languageServiceEnabled) { return []; @@ -655,7 +681,7 @@ namespace ts.server { if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { // if current structure version is the same - return info without any changes if (this.projectStructureVersion === this.lastReportedVersion && !updatedFileNames) { - return { info, projectErrors: this.projectErrors }; + return { info, projectErrors: this.getGlobalProjectErrors() }; } // compute and return the difference const lastReportedFileNames = this.lastReportedFileNames; @@ -677,14 +703,14 @@ namespace ts.server { }); this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectStructureVersion; - return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors }; + return { info, changes: { added, removed, updated }, projectErrors: this.getGlobalProjectErrors() }; } else { // unknown version - return everything const projectFileNames = this.getFileNames(); this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); this.lastReportedVersion = this.projectStructureVersion; - return { info, files: projectFileNames, projectErrors: this.projectErrors }; + return { info, files: projectFileNames, projectErrors: this.getGlobalProjectErrors() }; } } @@ -1114,4 +1140,4 @@ namespace ts.server { this.typeAcquisition = newTypeAcquisition; } } -} \ No newline at end of file +} diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3bce6d7c78..cab1601fb4 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -358,10 +358,6 @@ namespace ts.server.protocol { code: number; } - export interface DiagnosticWithLinePositionAndFileName extends DiagnosticWithLinePosition { - fileName: string; - } - /** * Response message for "projectInfo" request */ @@ -969,7 +965,7 @@ namespace ts.server.protocol { /** * List of errors in project */ - projectErrors: DiagnosticWithLinePositionAndFileName[]; + projectErrors: DiagnosticWithLinePosition[]; } /** diff --git a/src/server/session.ts b/src/server/session.ts index 1de3c3a208..af57e0dfd4 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -88,16 +88,16 @@ namespace ts.server { return { line: lineAndCharacter.line + 1, offset: lineAndCharacter.character + 1 }; } - function formatConfigFileDiag(diag: ts.Diagnostic): protocol.DiagnosticWithFileName { - return { - start: diag.file && convertToILineInfo(getLineAndCharacterOfPosition(diag.file, diag.start)), - end: diag.file && convertToILineInfo(getLineAndCharacterOfPosition(diag.file, diag.start + diag.length)), - text: ts.flattenDiagnosticMessageText(diag.messageText, "\n"), - code: diag.code, - category: DiagnosticCategory[diag.category].toLowerCase(), - fileName: diag.file && diag.file.fileName, - source: diag.source - }; + function formatConfigFileDiag(diag: ts.Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName; + function formatConfigFileDiag(diag: ts.Diagnostic, includeFileName: false): protocol.Diagnostic; + function formatConfigFileDiag(diag: ts.Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName { + const start = diag.file && convertToILineInfo(getLineAndCharacterOfPosition(diag.file, diag.start)); + const end = diag.file && convertToILineInfo(getLineAndCharacterOfPosition(diag.file, diag.start + diag.length)); + const text = ts.flattenDiagnosticMessageText(diag.messageText, "\n"); + const { code, source } = diag; + const category = DiagnosticCategory[diag.category].toLowerCase(); + return includeFileName ? { start, end, text, code, category, source, fileName: diag.file && diag.file.fileName } : + { start, end, text, code, category, source }; } export interface PendingErrorCheck { @@ -225,17 +225,6 @@ namespace ts.server { return `Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`; } - /** - * Should Remap project Files with ts diagnostics if - * - there are project errors - * - options contain configFile - so we can remove it from options before serializing - * @param p project files with ts diagnostics - */ - function shouldRemapProjectFilesWithTSDiagnostics(p: ProjectFilesWithTSDiagnostics) { - return (p.projectErrors && !!p.projectErrors.length) || - (p.info && !!p.info.options.configFile); - } - /** * Get the compiler options without configFile key * @param options @@ -491,7 +480,7 @@ namespace ts.server { } public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ts.Diagnostic[]) { - const bakedDiags = ts.map(diagnostics, formatConfigFileDiag); + const bakedDiags = ts.map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true)); const ev: protocol.ConfigFileDiagnosticEvent = { seq: 0, type: "event", @@ -624,24 +613,57 @@ namespace ts.server { return projectFileName && this.projectService.findProject(projectFileName); } - private getCompilerOptionsDiagnostics(args: protocol.CompilerOptionsDiagnosticsRequestArgs) { + private getConfigFileAndProject(args: protocol.FileRequestArgs) { const project = this.getProject(args.projectFileName); - return this.convertToCompilerOptionsDiagnosticsWithLinePosition(project.getLanguageService().getCompilerOptionsDiagnostics()); + const file = toNormalizedPath(args.file); + + return { + configFile: project && project.hasConfigFile(file) && file, + project + }; } - private convertToCompilerOptionsDiagnosticsWithLinePosition(diagnostics: Diagnostic[]) { - return diagnostics.map(d => { + private getConfigFileDiagnostics(configFile: NormalizedPath, project: Project, includeLinePosition: boolean) { + const projectErrors = project.getAllProjectErrors(); + const optionsErrors = project.getLanguageService().getCompilerOptionsDiagnostics(); + const diagnosticsForConfigFile = filter( + concatenate(projectErrors, optionsErrors), + diagnostic => diagnostic.file && diagnostic.file.fileName === configFile + ); + return includeLinePosition ? + this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnosticsForConfigFile) : + map( + diagnosticsForConfigFile, + diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ false) + ); + } + + private convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnostics: Diagnostic[]) { + return diagnostics.map(d => { message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), start: d.start, length: d.length, category: DiagnosticCategory[d.category].toLowerCase(), code: d.code, startLocation: d.file && convertToILineInfo(getLineAndCharacterOfPosition(d.file, d.start)), - endLocation: d.file && convertToILineInfo(getLineAndCharacterOfPosition(d.file, d.start + d.length)), - fileName: d.file && d.file.fileName + endLocation: d.file && convertToILineInfo(getLineAndCharacterOfPosition(d.file, d.start + d.length)) }); } + private getCompilerOptionsDiagnostics(args: protocol.CompilerOptionsDiagnosticsRequestArgs) { + const project = this.getProject(args.projectFileName); + // Get diagnostics that dont have associated file with them + // The diagnostics which have file would be in config file and + // would be reported as part of configFileDiagnostics + return this.convertToDiagnosticsWithLinePosition( + filter( + project.getLanguageService().getCompilerOptionsDiagnostics(), + diagnostic => !diagnostic.file + ), + /*scriptInfo*/ undefined + ); + } + private convertToDiagnosticsWithLinePosition(diagnostics: Diagnostic[], scriptInfo: ScriptInfo) { return diagnostics.map(d => { message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), @@ -765,10 +787,20 @@ namespace ts.server { } private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs): protocol.Diagnostic[] | protocol.DiagnosticWithLinePosition[] { + const { configFile } = this.getConfigFileAndProject(args); + if (configFile) { + // all the config file errors are reported as part of semantic check so nothing to report here + return []; + } + return this.getDiagnosticsWorker(args, /*isSemantic*/ false, (project, file) => project.getLanguageService().getSyntacticDiagnostics(file), args.includeLinePosition); } private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs): protocol.Diagnostic[] | protocol.DiagnosticWithLinePosition[] { + const { configFile, project } = this.getConfigFileAndProject(args); + if (configFile) { + return this.getConfigFileDiagnostics(configFile, project, args.includeLinePosition); + } return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file), args.includeLinePosition); } @@ -1662,15 +1694,21 @@ namespace ts.server { }, [CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => { const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects); - if (!result.some(shouldRemapProjectFilesWithTSDiagnostics)) { + // Remapping of the result is needed if + // - there are project errors + // - options contain configFile - need to remove it from options before serializing + const shouldRemapProjectFilesWithTSDiagnostics = (p: ProjectFilesWithTSDiagnostics) => (p.projectErrors && !!p.projectErrors.length) || (p.info && !!p.info.options.configFile); + if (!some(result, shouldRemapProjectFilesWithTSDiagnostics)) { return this.requiredResponse(result); } const converted = map(result, p => { if (shouldRemapProjectFilesWithTSDiagnostics(p)) { + // Map the project errors const projectErrors = p.projectErrors && p.projectErrors.length ? - this.convertToCompilerOptionsDiagnosticsWithLinePosition(p.projectErrors) : + this.convertToDiagnosticsWithLinePosition(p.projectErrors, /*scriptInfo*/ undefined) : p.projectErrors; + // Remove the configFile in the options before serializing const info = p.info && !!p.info.options.configFile ? { projectName: p.info.projectName,