diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 31e64e2c931..522cac95f30 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -375,8 +375,47 @@ namespace fakes { } } - export type ExpectedDiagnostic = [ts.DiagnosticMessage, ...(string | number)[]]; - function expectedDiagnosticToText([message, ...args]: ExpectedDiagnostic) { + export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]]; + export interface ExpectedDiagnosticMessageChain { + message: ExpectedDiagnosticMessage; + next?: ExpectedDiagnosticMessageChain[]; + } + + export interface ExpectedDiagnosticLocation { + file: string; + start: number; + length: number; + } + export interface ExpectedDiagnosticRelatedInformation extends ExpectedDiagnosticMessageChain { + location?: ExpectedDiagnosticLocation; + } + + export enum DiagnosticKind { + Error = "Error", + Status = "Status" + } + export interface ExpectedErrorDiagnostic extends ExpectedDiagnosticRelatedInformation { + relatedInformation?: ExpectedDiagnosticRelatedInformation[]; + } + + export type ExpectedDiagnostic = ExpectedDiagnosticMessage | ExpectedErrorDiagnostic; + + interface SolutionBuilderDiagnostic { + kind: DiagnosticKind; + diagnostic: ts.Diagnostic; + } + + function indentedText(indent: number, text: string) { + if (!indent) return text; + let indentText = ""; + for (let i = 0; i < indent; i++) { + indentText += " "; + } + return ` +${indentText}${text}`; + } + + function expectedDiagnosticMessageToText([message, ...args]: ExpectedDiagnosticMessage) { let text = ts.getLocaleSpecificMessage(message); if (args.length) { text = ts.formatStringFromArgs(text, args); @@ -384,6 +423,70 @@ namespace fakes { return text; } + function expectedDiagnosticMessageChainToText({ message, next }: ExpectedDiagnosticMessageChain, indent = 0) { + let text = indentedText(indent, expectedDiagnosticMessageToText(message)); + if (next) { + indent++; + next.forEach(kid => text += expectedDiagnosticMessageChainToText(kid, indent)); + } + return text; + } + + function expectedDiagnosticRelatedInformationToText({ location, ...diagnosticMessage }: ExpectedDiagnosticRelatedInformation) { + const text = expectedDiagnosticMessageChainToText(diagnosticMessage); + if (location) { + const { file, start, length } = location; + return `${file}(${start}:${length}):: ${text}`; + } + return text; + } + + function expectedErrorDiagnosticToText({ relatedInformation, ...diagnosticRelatedInformation }: ExpectedErrorDiagnostic) { + let text = `${DiagnosticKind.Error}!: ${expectedDiagnosticRelatedInformationToText(diagnosticRelatedInformation)}`; + if (relatedInformation) { + for (const kid of relatedInformation) { + text += ` + related:: ${expectedDiagnosticRelatedInformationToText(kid)}`; + } + } + return text; + } + + function expectedDiagnosticToText(errorOrStatus: ExpectedDiagnostic) { + return ts.isArray(errorOrStatus) ? + `${DiagnosticKind.Status}!: ${expectedDiagnosticMessageToText(errorOrStatus)}` : + expectedErrorDiagnosticToText(errorOrStatus); + } + + function diagnosticMessageChainToText({ messageText, next}: ts.DiagnosticMessageChain, indent = 0) { + let text = indentedText(indent, messageText); + if (next) { + indent++; + next.forEach(kid => text += diagnosticMessageChainToText(kid, indent)); + } + return text; + } + + function diagnosticRelatedInformationToText({ file, start, length, messageText }: ts.DiagnosticRelatedInformation) { + const text = typeof messageText === "string" ? + messageText : + diagnosticMessageChainToText(messageText); + return file ? + `${file.fileName}(${start}:${length}):: ${text}` : + text; + } + + function diagnosticToText({ kind, diagnostic: { relatedInformation, ...diagnosticRelatedInformation } }: SolutionBuilderDiagnostic) { + let text = `${kind}!: ${diagnosticRelatedInformationToText(diagnosticRelatedInformation)}`; + if (relatedInformation) { + for (const kid of relatedInformation) { + text += ` + related:: ${diagnosticRelatedInformationToText(kid)}`; + } + } + return text; + } + function compareProgramBuildInfoDiagnostic(a: ts.ProgramBuildInfoDiagnostic, b: ts.ProgramBuildInfoDiagnostic) { return ts.compareStringsCaseSensitive(ts.isString(a) ? a : a[0], ts.isString(b) ? b : b[0]); } @@ -446,14 +549,14 @@ namespace fakes { return new Date(this.sys.vfs.time()); } - diagnostics: ts.Diagnostic[] = []; + diagnostics: SolutionBuilderDiagnostic[] = []; reportDiagnostic(diagnostic: ts.Diagnostic) { - this.diagnostics.push(diagnostic); + this.diagnostics.push({ kind: DiagnosticKind.Error, diagnostic }); } reportSolutionBuilderStatus(diagnostic: ts.Diagnostic) { - this.diagnostics.push(diagnostic); + this.diagnostics.push({ kind: DiagnosticKind.Status, diagnostic }); } clearDiagnostics() { @@ -461,7 +564,7 @@ namespace fakes { } assertDiagnosticMessages(...expectedDiagnostics: ExpectedDiagnostic[]) { - const actual = this.diagnostics.slice().map(d => d.messageText as string); + const actual = this.diagnostics.slice().map(diagnosticToText); const expected = expectedDiagnostics.map(expectedDiagnosticToText); assert.deepEqual(actual, expected, `Diagnostic arrays did not match: Actual: ${JSON.stringify(actual, /*replacer*/ undefined, " ")} @@ -471,8 +574,8 @@ Expected: ${JSON.stringify(expected, /*replacer*/ undefined, " ")}`); printDiagnostics(header = "== Diagnostics ==") { const out = ts.createDiagnosticReporter(ts.sys); ts.sys.write(header + "\r\n"); - for (const d of this.diagnostics) { - out(d); + for (const { diagnostic } of this.diagnostics) { + out(diagnostic); } } } diff --git a/src/testRunner/unittests/tsbuild/demo.ts b/src/testRunner/unittests/tsbuild/demo.ts index 71e82fc5334..559e49718b9 100644 --- a/src/testRunner/unittests/tsbuild/demo.ts +++ b/src/testRunner/unittests/tsbuild/demo.ts @@ -93,7 +93,7 @@ namespace ts { expectedExitStatus: ExitStatus.ProjectReferenceCycle_OutputsSkupped, expectedDiagnostics: [ getExpectedDiagnosticForProjectsInBuild("src/animals/tsconfig.json", "src/zoo/tsconfig.json", "src/core/tsconfig.json", "src/tsconfig.json"), - [ + errorDiagnostic([ Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, [ "/src/tsconfig.json", @@ -101,7 +101,7 @@ namespace ts { "/src/zoo/tsconfig.json", "/src/animals/tsconfig.json" ].join("\r\n") - ] + ]) ], expectedOutputs: emptyArray, notExpectedOutputs: [...coreOutputs(), ...animalOutputs(), ...zooOutputs()] @@ -121,12 +121,12 @@ namespace ts { getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/animals/tsconfig.json", "src/zoo/tsconfig.json", "src/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/lib/core/utilities.js"], [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, "/src/animals/animal.ts", "/src/core"], - [Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, "/src/animals/dog.ts", "/src/core"], - [Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, "/src/animals/index.ts", "/src/core"], - [Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, "/src/animals/animal.ts", "/src/core/tsconfig.json"], - [Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, "/src/animals/dog.ts", "/src/core/tsconfig.json"], - [Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, "/src/animals/index.ts", "/src/core/tsconfig.json"], + errorDiagnostic([Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, "/src/animals/animal.ts", "/src/core"]), + errorDiagnostic([Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, "/src/animals/dog.ts", "/src/core"]), + errorDiagnostic([Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, "/src/animals/index.ts", "/src/core"]), + errorDiagnostic([Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, "/src/animals/animal.ts", "/src/core/tsconfig.json"]), + errorDiagnostic([Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, "/src/animals/dog.ts", "/src/core/tsconfig.json"]), + errorDiagnostic([Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, "/src/animals/index.ts", "/src/core/tsconfig.json"]), [Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, "src/animals/tsconfig.json", "src/core"], [Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, "/src/animals/tsconfig.json", "/src/core"], [Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_was_not_built, "src/zoo/tsconfig.json", "src/animals"], diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts index 38b8b4b5379..0b52c4f8485 100644 --- a/src/testRunner/unittests/tsbuild/emptyFiles.ts +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -15,7 +15,14 @@ namespace ts { host.clearDiagnostics(); builder.build(); - host.assertDiagnosticMessages([Diagnostics.The_files_list_in_config_file_0_is_empty, "/src/no-references/tsconfig.json"]); + host.assertDiagnosticMessages({ + message: [Diagnostics.The_files_list_in_config_file_0_is_empty, "/src/no-references/tsconfig.json"], + location: { + file: "/src/no-references/tsconfig.json", + start: lastIndexOf(fs, "/src/no-references/tsconfig.json", "[]"), + length: 2 + } + }); // Check for outputs to not be written. verifyOutputsAbsent(fs, allExpectedOutputs); diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts index 7aeaba36199..7b33e3018d1 100644 --- a/src/testRunner/unittests/tsbuild/helpers.ts +++ b/src/testRunner/unittests/tsbuild/helpers.ts @@ -1,4 +1,8 @@ namespace ts { + export function errorDiagnostic(message: fakes.ExpectedDiagnosticMessage): fakes.ExpectedErrorDiagnostic { + return { message }; + } + export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { return [Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; } @@ -42,6 +46,22 @@ namespace ts { fs.writeFileSync(path, `${old}${additionalContent}`); } + export function indexOf(fs: vfs.FileSystem, path: string, searchStr: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + const content = fs.readFileSync(path, "utf-8"); + return content.indexOf(searchStr); + } + + export function lastIndexOf(fs: vfs.FileSystem, path: string, searchStr: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + const content = fs.readFileSync(path, "utf-8"); + return content.lastIndexOf(searchStr); + } + export function getTime() { let currentTime = 100; return { tick, time, touch }; diff --git a/src/testRunner/unittests/tsbuild/missingExtendedFile.ts b/src/testRunner/unittests/tsbuild/missingExtendedFile.ts index 881f3fbc030..907ed9202fa 100644 --- a/src/testRunner/unittests/tsbuild/missingExtendedFile.ts +++ b/src/testRunner/unittests/tsbuild/missingExtendedFile.ts @@ -7,10 +7,10 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tsconfig.json"], {}); builder.build(); host.assertDiagnosticMessages( - [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], - [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.first.json", "[\"**/*\"]", "[]"], - [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], - [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.second.json", "[\"**/*\"]", "[]"] + errorDiagnostic([Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"]), + errorDiagnostic([Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.first.json", "[\"**/*\"]", "[]"]), + errorDiagnostic([Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"]), + errorDiagnostic([Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.second.json", "[\"**/*\"]", "[]"]) ); }); }); diff --git a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts index f6a2dca6c2e..9e067367c41 100644 --- a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -43,7 +43,14 @@ namespace ts { [Diagnostics.Building_project_0, "/src/src/other/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/src/main/tsconfig.json", "src/dist/a.js"], [Diagnostics.Building_project_0, "/src/src/main/tsconfig.json"], - [Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, "/src/dist/tsconfig.tsbuildinfo", "/src/src/other"] + { + message: [Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, "/src/dist/tsconfig.tsbuildinfo", "/src/src/other"], + location: { + file: "/src/src/main/tsconfig.json", + start: indexOf(fs, "/src/src/main/tsconfig.json", `{ "path": "../other" }`), + length: `{ "path": "../other" }`.length + } + } ); verifyOutputsPresent(fs, allExpectedOutputs); verifyOutputsAbsent(fs, missingOutputs); @@ -75,7 +82,14 @@ namespace ts { [Diagnostics.Building_project_0, "/src/src/other/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/src/main/tsconfig.json", "src/dist/a.js"], [Diagnostics.Building_project_0, "/src/src/main/tsconfig.json"], - [Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, "/src/dist/tsconfig.tsbuildinfo", "/src/src/other"] + { + message: [Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, "/src/dist/tsconfig.tsbuildinfo", "/src/src/other"], + location: { + file: "/src/src/main/tsconfig.json", + start: indexOf(fs, "/src/src/main/tsconfig.json", `{"path":"../other"}`), + length: `{"path":"../other"}`.length + } + } ); verifyOutputsPresent(fs, allExpectedOutputs); verifyOutputsAbsent(fs, missingOutputs); diff --git a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts index 7793ef5375c..719e4beaed7 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -28,11 +28,14 @@ namespace ts { } it("with resolveJsonModule and include only", () => { - verifyProjectWithResolveJsonModule("/src/tsconfig_withInclude.json", [ - Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, - "/src/src/hello.json", - "/src/tsconfig_withInclude.json" - ]); + verifyProjectWithResolveJsonModule( + "/src/tsconfig_withInclude.json", + errorDiagnostic([ + Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, + "/src/src/hello.json", + "/src/tsconfig_withInclude.json" + ]) + ); }); it("with resolveJsonModule and include of *.json along with other include", () => { diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 420c7a468aa..260bbff1436 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -450,7 +450,14 @@ namespace ts { [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.Property_0_does_not_exist_on_type_1, "muitply", `typeof import("/src/core/index")`], + { + message: [Diagnostics.Property_0_does_not_exist_on_type_1, "muitply", `typeof import("/src/core/index")`], + location: { + file: "/src/logic/index.ts", + start: indexOf(fs, "/src/logic/index.ts", "muitply"), + length: "muitply".length + } + }, [Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, "src/tests/tsconfig.json", "src/logic"], [Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, "/src/tests/tsconfig.json", "/src/logic"] ); diff --git a/src/testRunner/unittests/tsbuild/transitiveReferences.ts b/src/testRunner/unittests/tsbuild/transitiveReferences.ts index 614796b2a85..6245a692c9d 100644 --- a/src/testRunner/unittests/tsbuild/transitiveReferences.ts +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -69,7 +69,14 @@ export const b = new A();`); verifyBuild(fs => modifyFsBTsToNonRelativeImport(fs, "node"), allExpectedOutputs, expectedFileTraces, - [Diagnostics.Cannot_find_module_0, "a"], + { + message: [Diagnostics.Cannot_find_module_0, "a"], + location: { + file: "/src/b.ts", + start: `import {A} from 'a';`.indexOf(`'a'`), + length: `'a'`.length + } + }, ); }); });