Skip to content

Implement enhanced CLI error formatting #62130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
232 changes: 214 additions & 18 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,37 +776,233 @@ export function formatLocation(file: SourceFile, start: number, host: FormatDiag
return output;
}

// Enhanced formatting constants (Issue #45717)
const ENHANCED_FORMAT_MIN_WIDTH = 60;
const ENHANCED_BULLET = "●";
const ENHANCED_VERTICAL_BAR = "|";
const ENHANCED_OVERLINE = "▔";

/**
* Formats code span with enhanced visual formatting (Issue #45717)
* Shows code with vertical bar and overline instead of tilde underline
*/
function formatCodeSpanEnhanced(
file: SourceFile,
start: number,
length: number,
indent: string,
squiggleColor: ForegroundColorEscapeSequences,
host: FormatDiagnosticsHost,
): string {
const { line: errorLine, character: errorStartChar } = getLineAndCharacterOfPosition(file, start);

// Calculate end position safely, defaulting length to 1 if not provided
const errorLength = Math.max(1, length || 1);
const endPosition = Math.min(start + errorLength, file.text.length);
const { line: errorEndLine, character: errorEndChar } = getLineAndCharacterOfPosition(file, endPosition);

// Only show enhanced formatting for single-line errors
if (errorLine !== errorEndLine) {
// For multi-line errors, fall back to showing just the first line
const lineStart = getPositionOfLineAndCharacter(file, errorLine, 0);
const lineEnd = file.text.indexOf("\n", lineStart);
const lineText = file.text.slice(lineStart, lineEnd === -1 ? file.text.length : lineEnd);
const cleanedLine = lineText.trimEnd();
return formatCodeSpanEnhancedSingleLine(file, errorLine, errorStartChar, cleanedLine.length - errorStartChar, indent, squiggleColor, host);
}

return formatCodeSpanEnhancedSingleLine(file, errorLine, errorStartChar, errorEndChar - errorStartChar, indent, squiggleColor, host);
}

function formatCodeSpanEnhancedSingleLine(
file: SourceFile,
line: number,
startChar: number,
length: number,
indent: string,
squiggleColor: ForegroundColorEscapeSequences,
host: FormatDiagnosticsHost,
): string {
// Get line content
const lineStart = getPositionOfLineAndCharacter(file, line, 0);
const lineEnd = file.text.indexOf("\n", lineStart);
const lineText = file.text.slice(lineStart, lineEnd === -1 ? file.text.length : lineEnd);
const cleanedLine = lineText.trimEnd().replace(/\t/g, " ");

let output = "";

// Line 1: Vertical bar + code
output += indent + formatColorAndReset(ENHANCED_VERTICAL_BAR, gutterStyleSequence) + " " + cleanedLine + host.getNewLine();

// Line 2: Vertical bar + overline at error position
output += indent + formatColorAndReset(ENHANCED_VERTICAL_BAR, gutterStyleSequence) + " ";

// Add spacing - simpler approach without pre-allocated buffer
// Modern V8 optimizes repeat() well for reasonable sizes
if (startChar > 0 && startChar < cleanedLine.length) {
output += " ".repeat(startChar);
}

// Calculate overline length, ensuring we stay within line bounds
if (startChar < cleanedLine.length) {
const overlineLength = Math.max(1, Math.min(length, cleanedLine.length - startChar));
output += formatColorAndReset(ENHANCED_OVERLINE.repeat(overlineLength), squiggleColor);
}

return output;
}

/**
* Determines if enhanced formatting should be used based on terminal width
*/
function shouldUseEnhancedFormatting(): boolean {
if (!sys) return false;

// Respect NO_COLOR environment variable
if (sys.getEnvironmentVariable?.("NO_COLOR")) {
return false;
}

// Check terminal width if available
if (sys.getWidthOfTerminal) {
const width = sys.getWidthOfTerminal();
if (width !== undefined && width < ENHANCED_FORMAT_MIN_WIDTH) {
return false;
}
}

// For CI environments, require explicit opt-in
if (sys.getEnvironmentVariable?.("CI") === "true") {
return sys.getEnvironmentVariable("TS_ENHANCED_ERRORS") === "true";
}

// Check if we're in a TTY with color support
if (sys.writeOutputIsTTY?.()) {
const term = sys.getEnvironmentVariable?.("TERM");
// Exclude known limited terminals
return term !== "dumb" && term !== "unknown";
}

// Not a TTY, so no enhanced formatting
return false;
}

export function formatDiagnosticsWithColorAndContext(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string {
const useEnhancedFormatting = shouldUseEnhancedFormatting();

let output = "";
for (const diagnostic of diagnostics) {
if (diagnostic.file) {
const { file, start } = diagnostic;
output += formatLocation(file, start!, host); // TODO: GH#18217
output += " - ";
if (useEnhancedFormatting) {
// Enhanced formatting (Issue #45717)
output += formatDiagnosticEnhanced(diagnostic, host);
}
else {
// Original formatting for backwards compatibility
output += formatDiagnosticOriginal(diagnostic, host);
}
output += host.getNewLine();
}
return output;
}

output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category));
output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey);
output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine());
/**
* Enhanced diagnostic formatting as specified in Issue #45717
* Format: ● file:line:col TS####
* | code line
* ▔
* Error message
*/
function formatDiagnosticEnhanced(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string {
let output = "";

// Header line: ● file:line:col TS####
output += formatColorAndReset(ENHANCED_BULLET + " ", getCategoryFormat(diagnostic.category));

if (diagnostic.file && diagnostic.start !== undefined) {
output += formatLocation(diagnostic.file, diagnostic.start, host);
output += " ";
}

output += formatColorAndReset(`TS${diagnostic.code}`, ForegroundColorEscapeSequences.Grey);
output += host.getNewLine();

// Code span (if applicable)
if (diagnostic.file && diagnostic.start !== undefined && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) {
output += formatCodeSpanEnhanced(
diagnostic.file,
diagnostic.start,
diagnostic.length || 1,
"",
getCategoryFormat(diagnostic.category),
host,
);
output += host.getNewLine();
}

// Error message
output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine());

if (diagnostic.file && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) {
// Related information
if (diagnostic.relatedInformation) {
for (const related of diagnostic.relatedInformation) {
output += host.getNewLine();
output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217
}
if (diagnostic.relatedInformation) {
output += host.getNewLine();
for (const { file, start, length, messageText } of diagnostic.relatedInformation) {
if (file) {
output += host.getNewLine();
output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217
output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217
}

if (related.file && related.start !== undefined) {
output += halfIndent + formatLocation(related.file, related.start, host);
output += host.getNewLine();
output += formatCodeSpanEnhanced(
related.file,
related.start,
related.length || 1,
halfIndent,
ForegroundColorEscapeSequences.Cyan,
host,
);
output += host.getNewLine();
output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine());
}

output += halfIndent + flattenDiagnosticMessageText(related.messageText, host.getNewLine());
}
}

return output;
}

/**
* Original diagnostic formatting for backwards compatibility
*/
function formatDiagnosticOriginal(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string {
let output = "";

if (diagnostic.file) {
const { file, start } = diagnostic;
output += formatLocation(file, start!, host); // TODO: GH#18217
output += " - ";
}

output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category));
output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey);
output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine());

if (diagnostic.file && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) {
output += host.getNewLine();
output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217
}

if (diagnostic.relatedInformation) {
output += host.getNewLine();
for (const { file, start, length, messageText } of diagnostic.relatedInformation) {
if (file) {
output += host.getNewLine();
output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217
output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217
}
output += host.getNewLine();
output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine());
}
}

return output;
}

Expand Down
Loading