diff --git a/src/server/session.ts b/src/server/session.ts index 0c325cacf77..590f0bb2cbc 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -856,7 +856,7 @@ namespace ts.server { private semanticCheck(file: NormalizedPath, project: Project) { const diags = isDeclarationFileInJSOnlyNonConfiguredProject(project, file) ? emptyArray - : project.getLanguageService().getSemanticDiagnostics(file); + : project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file); this.sendDiagnosticsEvent(file, project, diags, "semanticDiag"); } @@ -1234,7 +1234,7 @@ namespace ts.server { if (configFile) { return this.getConfigFileDiagnostics(configFile, project!, !!args.includeLinePosition); // TODO: GH#18217 } - return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file), !!args.includeLinePosition); + return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file), !!args.includeLinePosition); } private getSuggestionDiagnosticsSync(args: protocol.SuggestionDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] { diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index ab24d9edc2f..f73a0f8e74b 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -807,4 +807,209 @@ namespace ts.projectSystem { lineText, }; } + + export interface GetErrDiagnostics { + file: string | File; + syntax?: protocol.Diagnostic[]; + semantic?: protocol.Diagnostic[]; + suggestion?: protocol.Diagnostic[]; + } + export interface VerifyGetErrRequestBase { + session: TestSession; + host: TestServerHost; + onErrEvent?: () => void; + existingTimeouts?: number; + } + export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { + expected: readonly GetErrDiagnostics[]; + } + export function verifyGetErrRequest(request: VerifyGetErrRequest) { + const { session, expected } = request; + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.Geterr, + arguments: { + delay: 0, + files: expected.map(f => filePath(f.file)) + } + }); + checkAllErrors({ ...request, expectedSequenceId }); + } + + export interface CheckAllErrors extends VerifyGetErrRequest { + expectedSequenceId: number; + } + function checkAllErrors({ expected, expectedSequenceId, ...rest }: CheckAllErrors) { + for (let i = 0; i < expected.length; i++) { + checkErrorsInFile({ + ...rest, + expected: expected[i], + expectedSequenceId: i === expected.length - 1 ? expectedSequenceId : undefined, + }); + } + } + + function filePath(file: string | File) { + return isString(file) ? file : file.path; + } + interface CheckErrorsInFile extends VerifyGetErrRequestBase { + expected: GetErrDiagnostics; + expectedSequenceId?: number; + } + function checkErrorsInFile({ + session, host, onErrEvent, existingTimeouts, expectedSequenceId, + expected: { file, syntax, semantic, suggestion }, + }: CheckErrorsInFile) { + onErrEvent = onErrEvent || noop; + if (existingTimeouts !== undefined) { + host.checkTimeoutQueueLength(existingTimeouts + 1); + host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); + } + else { + host.checkTimeoutQueueLengthAndRun(1); + } + if (syntax) { + onErrEvent(); + checkErrorMessage(session, "syntaxDiag", { file: filePath(file), diagnostics: syntax }); + } + if (semantic) { + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + onErrEvent(); + checkErrorMessage(session, "semanticDiag", { file: filePath(file), diagnostics: semantic }); + } + if (suggestion) { + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + onErrEvent(); + checkErrorMessage(session, "suggestionDiag", { file: filePath(file), diagnostics: suggestion }); + } + if (expectedSequenceId !== undefined) { + checkCompleteEvent(session, syntax || semantic || suggestion ? 2 : 1, expectedSequenceId); + } + session.clearMessages(); + } + + function verifyErrorsUsingGeterr({ allFiles, openFiles, expectedGetErr }: VerifyGetErrScenario) { + it("verifies the errors in open file", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession(openFiles(), session); + + verifyGetErrRequest({ session, host, expected: expectedGetErr() }); + }); + } + + function verifyErrorsUsingGeterrForProject({ allFiles, openFiles, expectedGetErrForProject }: VerifyGetErrScenario) { + it("verifies the errors in projects", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession(openFiles(), session); + + session.clearMessages(); + for (const expected of expectedGetErrForProject()) { + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.GeterrForProject, + arguments: { + delay: 0, + file: expected.project + } + }); + + checkAllErrors({ session, host, expected: expected.errors, expectedSequenceId }); + } + }); + } + + function verifyErrorsUsingSyncMethods({ allFiles, openFiles, expectedSyncDiagnostics }: VerifyGetErrScenario) { + it("verifies the errors using sync commands", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host); + openFilesForSession(openFiles(), session); + for (const { file, project, syntax, semantic, suggestion } of expectedSyncDiagnostics()) { + const actualSyntax = session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: { + file: filePath(file), + projectFileName: project + } + }).response as protocol.Diagnostic[]; + assert.deepEqual(actualSyntax, syntax, `Syntax diagnostics for file: ${filePath(file)}, project: ${project}`); + const actualSemantic = session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: { + file: filePath(file), + projectFileName: project + } + }).response as protocol.Diagnostic[]; + assert.deepEqual(actualSemantic, semantic, `Semantic diagnostics for file: ${filePath(file)}, project: ${project}`); + const actualSuggestion = session.executeCommandSeq({ + command: protocol.CommandTypes.SuggestionDiagnosticsSync, + arguments: { + file: filePath(file), + projectFileName: project + } + }).response as protocol.Diagnostic[]; + assert.deepEqual(actualSuggestion, suggestion, `Suggestion diagnostics for file: ${filePath(file)}, project: ${project}`); + } + }); + } + + function verifyConfigFileErrors({ allFiles, openFiles, expectedConfigFileDiagEvents }: VerifyGetErrScenario) { + it("verify config file errors", () => { + const host = createServerHost([...allFiles(), libFile]); + const { session, events } = createSessionWithEventTracking(host, server.ConfigFileDiagEvent); + + for (const file of openFiles()) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { file: file.path } + }); + } + + assert.deepEqual(events, expectedConfigFileDiagEvents().map(data => ({ + eventName: server.ConfigFileDiagEvent, + data + }))); + }); + } + + export interface GetErrForProjectDiagnostics { + project: string; + errors: readonly GetErrDiagnostics[]; + } + export interface SyncDiagnostics extends GetErrDiagnostics { + project?: string; + } + export interface VerifyGetErrScenario { + allFiles: () => readonly File[]; + openFiles: () => readonly File[]; + expectedGetErr: () => readonly GetErrDiagnostics[]; + expectedGetErrForProject: () => readonly GetErrForProjectDiagnostics[]; + expectedSyncDiagnostics: () => readonly SyncDiagnostics[]; + expectedConfigFileDiagEvents: () => readonly server.ConfigFileDiagEvent["data"][]; + } + export function verifyGetErrScenario(scenario: VerifyGetErrScenario) { + verifyErrorsUsingGeterr(scenario); + verifyErrorsUsingGeterrForProject(scenario); + verifyErrorsUsingSyncMethods(scenario); + verifyConfigFileErrors(scenario); + } + + export function emptyDiagnostics(file: File): GetErrDiagnostics { + return { + file, + syntax: emptyArray, + semantic: emptyArray, + suggestion: emptyArray + }; + } + + export function syncDiagnostics(diagnostics: GetErrDiagnostics, project: string): SyncDiagnostics { + return { project, ...diagnostics }; + } } diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts index e074a883649..fe187f7501a 100644 --- a/src/testRunner/unittests/tsserver/projectErrors.ts +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -493,6 +493,48 @@ declare module '@custom/plugin' { }); } }); + + describe("when semantic error returns includes global error", () => { + const file: File = { + path: `${tscWatch.projectRoot}/ui.ts`, + content: `const x = async (_action: string) => { +};` + }; + const config: File = { + path: `${tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + function expectedDiagnostics(): GetErrDiagnostics { + const span = protocolTextSpanFromSubstring(file.content, `async (_action: string) => {`); + return { + file, + syntax: [], + semantic: [ + createDiagnostic(span.start, span.end, Diagnostics.An_async_function_or_method_must_return_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option, [], "error"), + ], + suggestion: [] + }; + } + verifyGetErrScenario({ + allFiles: () => [libFile, file, config], + openFiles: () => [file], + expectedGetErr: () => [expectedDiagnostics()], + expectedGetErrForProject: () => [{ + project: file.path, + errors: [ + expectedDiagnostics(), + ] + }], + expectedSyncDiagnostics: () => [ + syncDiagnostics(expectedDiagnostics(), config.path), + ], + expectedConfigFileDiagEvents: () => [{ + triggerFile: file.path, + configFileName: config.path, + diagnostics: emptyArray + }] + }); + }); }); describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { diff --git a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts index fd6a833a22a..9444ccb7a9d 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts @@ -1,212 +1,8 @@ namespace ts.projectSystem { - export interface GetErrDiagnostics { - file: string | File; - syntax?: protocol.Diagnostic[]; - semantic?: protocol.Diagnostic[]; - suggestion?: protocol.Diagnostic[]; - } - export interface VerifyGetErrRequestBase { - session: TestSession; - host: TestServerHost; - onErrEvent?: () => void; - existingTimeouts?: number; - } - export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { - expected: readonly GetErrDiagnostics[]; - } - export function verifyGetErrRequest(request: VerifyGetErrRequest) { - const { session, expected } = request; - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: protocol.CommandTypes.Geterr, - arguments: { - delay: 0, - files: expected.map(f => filePath(f.file)) - } - }); - checkAllErrors({ ...request, expectedSequenceId }); - } - - export interface CheckAllErrors extends VerifyGetErrRequest { - expectedSequenceId: number; - } - function checkAllErrors({ expected, expectedSequenceId, ...rest }: CheckAllErrors) { - for (let i = 0; i < expected.length; i++) { - checkErrorsInFile({ - ...rest, - expected: expected[i], - expectedSequenceId: i === expected.length - 1 ? expectedSequenceId : undefined, - }); - } - } - - function filePath(file: string | File) { - return isString(file) ? file : file.path; - } - interface CheckErrorsInFile extends VerifyGetErrRequestBase { - expected: GetErrDiagnostics; - expectedSequenceId?: number; - } - function checkErrorsInFile({ - session, host, onErrEvent, existingTimeouts, expectedSequenceId, - expected: { file, syntax, semantic, suggestion }, - }: CheckErrorsInFile) { - onErrEvent = onErrEvent || noop; - if (existingTimeouts !== undefined) { - host.checkTimeoutQueueLength(existingTimeouts + 1); - host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); - } - else { - host.checkTimeoutQueueLengthAndRun(1); - } - if (syntax) { - onErrEvent(); - checkErrorMessage(session, "syntaxDiag", { file: filePath(file), diagnostics: syntax }); - } - if (semantic) { - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - onErrEvent(); - checkErrorMessage(session, "semanticDiag", { file: filePath(file), diagnostics: semantic }); - } - if (suggestion) { - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - onErrEvent(); - checkErrorMessage(session, "suggestionDiag", { file: filePath(file), diagnostics: suggestion }); - } - if (expectedSequenceId !== undefined) { - checkCompleteEvent(session, syntax || semantic || suggestion ? 2 : 1, expectedSequenceId); - } - session.clearMessages(); - } - describe("unittests:: tsserver:: with project references and error reporting", () => { const dependecyLocation = `${tscWatch.projectRoot}/dependency`; const usageLocation = `${tscWatch.projectRoot}/usage`; - function verifyErrorsUsingGeterr({ allFiles, openFiles, expectedGetErr }: VerifyScenario) { - it("verifies the errors in open file", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { canUseEvents: true, }); - openFilesForSession(openFiles(), session); - - verifyGetErrRequest({ session, host, expected: expectedGetErr() }); - }); - } - - function verifyErrorsUsingGeterrForProject({ allFiles, openFiles, expectedGetErrForProject }: VerifyScenario) { - it("verifies the errors in projects", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { canUseEvents: true, }); - openFilesForSession(openFiles(), session); - - session.clearMessages(); - for (const expected of expectedGetErrForProject()) { - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: protocol.CommandTypes.GeterrForProject, - arguments: { - delay: 0, - file: expected.project - } - }); - - checkAllErrors({ session, host, expected: expected.errors, expectedSequenceId }); - } - }); - } - - function verifyErrorsUsingSyncMethods({ allFiles, openFiles, expectedSyncDiagnostics }: VerifyScenario) { - it("verifies the errors using sync commands", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host); - openFilesForSession(openFiles(), session); - for (const { file, project, syntax, semantic, suggestion } of expectedSyncDiagnostics()) { - const actualSyntax = session.executeCommandSeq({ - command: protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: { - file: filePath(file), - projectFileName: project - } - }).response as protocol.Diagnostic[]; - assert.deepEqual(actualSyntax, syntax, `Syntax diagnostics for file: ${filePath(file)}, project: ${project}`); - const actualSemantic = session.executeCommandSeq({ - command: protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: { - file: filePath(file), - projectFileName: project - } - }).response as protocol.Diagnostic[]; - assert.deepEqual(actualSemantic, semantic, `Semantic diagnostics for file: ${filePath(file)}, project: ${project}`); - const actualSuggestion = session.executeCommandSeq({ - command: protocol.CommandTypes.SuggestionDiagnosticsSync, - arguments: { - file: filePath(file), - projectFileName: project - } - }).response as protocol.Diagnostic[]; - assert.deepEqual(actualSuggestion, suggestion, `Suggestion diagnostics for file: ${filePath(file)}, project: ${project}`); - } - }); - } - - function verifyConfigFileErrors({ allFiles, openFiles, expectedConfigFileDiagEvents }: VerifyScenario) { - it("verify config file errors", () => { - const host = createServerHost([...allFiles(), libFile]); - const { session, events } = createSessionWithEventTracking(host, server.ConfigFileDiagEvent); - - for (const file of openFiles()) { - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { file: file.path } - }); - } - - assert.deepEqual(events, expectedConfigFileDiagEvents().map(data => ({ - eventName: server.ConfigFileDiagEvent, - data - }))); - }); - } - - interface GetErrForProjectDiagnostics { - project: string; - errors: readonly GetErrDiagnostics[]; - } - interface SyncDiagnostics extends GetErrDiagnostics { - project?: string; - } - interface VerifyScenario { - allFiles: () => readonly File[]; - openFiles: () => readonly File[]; - expectedGetErr: () => readonly GetErrDiagnostics[]; - expectedGetErrForProject: () => readonly GetErrForProjectDiagnostics[]; - expectedSyncDiagnostics: () => readonly SyncDiagnostics[]; - expectedConfigFileDiagEvents: () => readonly server.ConfigFileDiagEvent["data"][]; - } - function verifyScenario(scenario: VerifyScenario) { - verifyErrorsUsingGeterr(scenario); - verifyErrorsUsingGeterrForProject(scenario); - verifyErrorsUsingSyncMethods(scenario); - verifyConfigFileErrors(scenario); - } - - function emptyDiagnostics(file: File): GetErrDiagnostics { - return { - file, - syntax: emptyArray, - semantic: emptyArray, - suggestion: emptyArray - }; - } - - function syncDiagnostics(diagnostics: GetErrDiagnostics, project: string): SyncDiagnostics { - return { project, ...diagnostics }; - } interface VerifyUsageAndDependency { allFiles: readonly [File, File, File, File]; // dependencyTs, dependencyConfig, usageTs, usageConfig @@ -252,7 +48,7 @@ namespace ts.projectSystem { } describe("when dependency project is not open", () => { - verifyScenario({ + verifyGetErrScenario({ allFiles: () => allFiles, openFiles: () => [usageTs], expectedGetErr: () => [ @@ -283,7 +79,7 @@ namespace ts.projectSystem { }); describe("when the depedency file is open", () => { - verifyScenario({ + verifyGetErrScenario({ allFiles: () => allFiles, openFiles: () => [usageTs, dependencyTs], expectedGetErr: () => [