Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 20 additions & 14 deletions packages/typescript/src/api/diagnosticFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions packages/typescript/test/diagnosticFormatter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
API,
flattenDiagnosticMessageText,
formatDiagnostic,
formatDiagnostics,
formatDiagnosticsWithColorAndContext,
} from "@typescript/typescript/unstable/async";
Expand Down Expand Up @@ -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";`,
Expand Down