From 765e201a00f75df507621efd103cc4f83bb8c1f3 Mon Sep 17 00:00:00 2001 From: Matheus Mol Date: Thu, 3 Sep 2026 13:23:22 -0300 Subject: [PATCH] API: add formatDiagnostic and flattenDiagnosticMessageText --- packages/typescript/src/api/async/api.ts | 2 +- .../typescript/src/api/diagnosticFormatter.ts | 34 +++++++----- packages/typescript/src/api/sync/api.ts | 2 +- .../test/diagnosticFormatter.test.ts | 53 +++++++++++++++++++ 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 208313dac3502..cd3454110f9f7 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -142,7 +142,7 @@ import type { UnionType, } from "./types.ts"; -export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; +export { flattenDiagnosticMessageText, formatDiagnostic, formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { diff --git a/packages/typescript/src/api/diagnosticFormatter.ts b/packages/typescript/src/api/diagnosticFormatter.ts index 32bfbb57c8a6c..9a773bb481af4 100644 --- a/packages/typescript/src/api/diagnosticFormatter.ts +++ b/packages/typescript/src/api/diagnosticFormatter.ts @@ -58,14 +58,18 @@ function diagnosticPrefix(diagnostic: Diagnostic): string { return diagnostic.source || "TS"; } -function flattenDiagnosticMessage(diagnostic: Diagnostic, newLine: string, indentLevel = 0): string { +/** + * Flattens a diagnostic's message text and its message chain into a single string, + * with each level of the chain on its own indented line. + */ +export function flattenDiagnosticMessageText(diagnostic: Diagnostic, newLine: string, indent = 0): string { let result = ""; - if (indentLevel) { - result += newLine + " ".repeat(indentLevel); + if (indent) { + result += newLine + " ".repeat(indent); } result += diagnostic.text; for (const child of diagnostic.messageChain ?? []) { - result += flattenDiagnosticMessage(child, newLine, indentLevel + 1); + result += flattenDiagnosticMessageText(child, newLine, indent + 1); } return result; } @@ -149,17 +153,19 @@ function formatCodeSpan( return context; } +export function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { + const errorMessage = `${diagnosticCategoryName(diagnostic.category)} ${diagnosticPrefix(diagnostic)}${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic, host.getNewLine())}${host.getNewLine()}`; + if (diagnostic.fileName && diagnostic.startPosition) { + const { line, character } = diagnostic.startPosition; + return `${relativeFileName(diagnostic.fileName, host)}(${line + 1},${character + 1}): ${errorMessage}`; + } + return errorMessage; +} + export function formatDiagnostics(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string { let output = ""; for (const diagnostic of diagnostics) { - const errorMessage = `${diagnosticCategoryName(diagnostic.category)} ${diagnosticPrefix(diagnostic)}${diagnostic.code}: ${flattenDiagnosticMessage(diagnostic, host.getNewLine())}${host.getNewLine()}`; - if (diagnostic.fileName && diagnostic.startPosition) { - const { line, character } = diagnostic.startPosition; - output += `${relativeFileName(diagnostic.fileName, host)}(${line + 1},${character + 1}): ${errorMessage}`; - } - else { - output += errorMessage; - } + output += formatDiagnostic(diagnostic, host); } return output; } @@ -179,7 +185,7 @@ export function formatDiagnosticsWithColorAndContext( } output += formatColorAndReset(diagnosticCategoryName(diagnostic.category), getCategoryFormat(diagnostic.category)); output += formatColorAndReset(` ${diagnosticPrefix(diagnostic)}${diagnostic.code}: `, foregroundColorEscapeGrey); - output += flattenDiagnosticMessage(diagnostic, host.getNewLine()); + output += flattenDiagnosticMessageText(diagnostic, host.getNewLine()); if (diagnostic.fileName && diagnostic.code !== fileAppearsToBeBinaryCode) { output += host.getNewLine(); @@ -192,7 +198,7 @@ export function formatDiagnosticsWithColorAndContext( if (related.fileName && related.startPosition) { output += host.getNewLine(); output += halfIndent + formatLocation(related, host); - output += " - " + flattenDiagnosticMessage(related, host.getNewLine()); + output += " - " + flattenDiagnosticMessageText(related, host.getNewLine()); output += formatCodeSpan(related, indent, foregroundColorEscapeCyan, host); } output += host.getNewLine(); diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 4ec27f4805f98..f4b3b8b83dafe 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -159,7 +159,7 @@ import type { UnionType, } from "./types.ts"; -export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; +export { flattenDiagnosticMessageText, formatDiagnostic, formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { diff --git a/packages/typescript/test/diagnosticFormatter.test.ts b/packages/typescript/test/diagnosticFormatter.test.ts index bce9f0f98371a..53711a1c177f8 100644 --- a/packages/typescript/test/diagnosticFormatter.test.ts +++ b/packages/typescript/test/diagnosticFormatter.test.ts @@ -1,5 +1,7 @@ import { API, + flattenDiagnosticMessageText, + formatDiagnostic, formatDiagnostics, formatDiagnosticsWithColorAndContext, } from "@typescript/typescript/unstable/async"; @@ -108,6 +110,57 @@ describe("diagnosticFormatter", () => { } }); + test("formatDiagnostic formats a single diagnostic and composes formatDiagnostics", async () => { + const api = spawnAPI({ + "/workspace/tsconfig.json": `{ "compilerOptions": { "strict": true } }`, + "/workspace/index.ts": `const x: number = "oops";\nconst y: string = 1;\n`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/workspace/tsconfig.json" }); + const program = snapshot.getProject("/workspace/tsconfig.json")!.program; + const diagnostics = await program.getSemanticDiagnostics("/workspace/index.ts"); + assert.equal(diagnostics.length, 2); + + const first = formatDiagnostic(diagnostics[0], program); + assert.equal(first, "index.ts(1,7): error TS2322: Type 'string' is not assignable to type 'number'.\n"); + assert.equal(first + formatDiagnostic(diagnostics[1], program), formatDiagnostics(diagnostics, program)); + + const { fileName: _, ...fileless } = diagnostics[0]; + assert.equal(formatDiagnostic(fileless, program), "error TS2322: Type 'string' is not assignable to type 'number'.\n"); + } + finally { + await api.close(); + } + }); + + test("flattenDiagnosticMessageText flattens a message chain with indentation", async () => { + const api = spawnAPI({ + "/workspace/index.ts": `const x: number = "oops";`, + }); + try { + const snapshot = await api.updateSnapshot({ openFiles: ["/workspace/index.ts"] }); + const project = await snapshot.getDefaultProjectForFile("/workspace/index.ts"); + const [diagnostic] = await project!.program.getSemanticDiagnostics("/workspace/index.ts"); + + const chained = { + ...diagnostic, + text: "Top", + messageChain: [ + { ...diagnostic, text: "Mid", messageChain: [{ ...diagnostic, text: "Leaf", messageChain: [] }] }, + ], + }; + assert.equal(flattenDiagnosticMessageText(chained, "\n"), "Top\n Mid\n Leaf"); + assert.equal(flattenDiagnosticMessageText(chained, "\r\n"), "Top\r\n Mid\r\n Leaf"); + assert.equal(flattenDiagnosticMessageText({ ...diagnostic, text: "Solo", messageChain: [] }, "\n", 1), "\n Solo"); + + const [formatted] = formatDiagnostics([chained], project!.program).split("\n").slice(0, 3); + assert.ok(formatted.includes("Top"), formatted); + } + finally { + await api.close(); + } + }); + test("uses the API directory and LF defaults for inferred projects", async () => { const api = spawnAPI({ "/workspace/index.ts": `const x: number = "oops";`,