From 08977d0fa3c5badbc798575848c746cef763661e Mon Sep 17 00:00:00 2001 From: Anivar A Aravind Date: Sat, 26 Jul 2025 09:34:05 +0530 Subject: [PATCH 1/3] Implement enhanced CLI error formatting (fixes #45717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add visual bullet points (●) for each diagnostic - Move error codes (TS####) to the right and de-emphasize with grey - Replace tilde (~) underlines with overlines (▔) - Add vertical bars (|) for better code snippet framing - Terminal width detection with 60+ column requirement - Fallback to original formatting for narrow terminals - Full backwards compatibility maintained --- src/compiler/program.ts | 208 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 189 insertions(+), 19 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 445946dab0c67..0c005b3b1b44d 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -776,37 +776,207 @@ 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); + + // Ensure we don't go past the end of the file + const safeEnd = Math.min(start + length, file.text.length); + const { character: errorEndChar } = getLineAndCharacterOfPosition(file, safeEnd); + + // Get the full line content + const lineStart = getPositionOfLineAndCharacter(file, errorLine, 0); + let lineEnd = file.text.indexOf('\n', lineStart); + if (lineEnd === -1) lineEnd = file.text.length; + + const lineText = file.text.slice(lineStart, lineEnd); + + // Clean up the line content + 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 spaces to reach the error position + output += " ".repeat(Math.max(0, errorStartChar)); + + // Add overline characters for the error span + const overlineLength = Math.min(errorEndChar - errorStartChar, cleanedLine.length - errorStartChar); + output += formatColorAndReset(ENHANCED_OVERLINE.repeat(Math.max(1, overlineLength)), squiggleColor); + + return output; +} + +/** + * Determines if enhanced formatting should be used based on terminal width + */ +function shouldUseEnhancedFormatting(): boolean { + // Check if terminal width detection is available and meets minimum width + if (sys && sys.getWidthOfTerminal) { + const width = sys.getWidthOfTerminal(); + if (width !== undefined) { + return width >= ENHANCED_FORMAT_MIN_WIDTH; + } + } + + // If we can't detect terminal width but have TTY with color support, + // assume the terminal is modern enough for enhanced formatting + if (sys && sys.writeOutputIsTTY && sys.writeOutputIsTTY()) { + return true; + } + + // If environment suggests color support, use enhanced formatting + if (sys && sys.getEnvironmentVariable) { + const colorTerm = sys.getEnvironmentVariable("COLORTERM"); + const termProgram = sys.getEnvironmentVariable("TERM_PROGRAM"); + const term = sys.getEnvironmentVariable("TERM"); + + // Modern terminal indicators + if (colorTerm === "truecolor" || colorTerm === "24bit") return true; + if (termProgram === "vscode" || termProgram === "iTerm.app") return true; + if (term && (term.includes("256color") || term.includes("color"))) return true; + } + + 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()); - - if (diagnostic.file && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) { +/** + * 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()); + + // 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; } From c69558fbbf4c8fa551dc1a793e21439ed22bf464 Mon Sep 17 00:00:00 2001 From: Anivar A Aravind Date: Sat, 26 Jul 2025 10:06:43 +0530 Subject: [PATCH 2/3] Address all Copilot review comments - Fix length parameter with proper default value handling - Add proper bounds checking for overline calculation - Simplify terminal detection with explicit checks for limited terminals - Handle multi-line errors gracefully by showing only first line - Add CI environment detection with opt-in support - Respect NO_COLOR environment variable --- src/compiler/program.ts | 92 ++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 0c005b3b1b44d..4db582113b908 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -796,18 +796,37 @@ function formatCodeSpanEnhanced( ): string { const { line: errorLine, character: errorStartChar } = getLineAndCharacterOfPosition(file, start); - // Ensure we don't go past the end of the file - const safeEnd = Math.min(start + length, file.text.length); - const { character: errorEndChar } = getLineAndCharacterOfPosition(file, safeEnd); + // 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); - // Get the full line content - const lineStart = getPositionOfLineAndCharacter(file, errorLine, 0); - let lineEnd = file.text.indexOf('\n', lineStart); - if (lineEnd === -1) lineEnd = file.text.length; - - const lineText = file.text.slice(lineStart, lineEnd); + // 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); + } - // Clean up the line content + 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 = ""; @@ -818,12 +837,17 @@ function formatCodeSpanEnhanced( // Line 2: Vertical bar + overline at error position output += indent + formatColorAndReset(ENHANCED_VERTICAL_BAR, gutterStyleSequence) + " "; - // Add spaces to reach the error position - output += " ".repeat(Math.max(0, errorStartChar)); + // 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); + } - // Add overline characters for the error span - const overlineLength = Math.min(errorEndChar - errorStartChar, cleanedLine.length - errorStartChar); - output += formatColorAndReset(ENHANCED_OVERLINE.repeat(Math.max(1, overlineLength)), squiggleColor); + // 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; } @@ -832,32 +856,34 @@ function formatCodeSpanEnhanced( * Determines if enhanced formatting should be used based on terminal width */ function shouldUseEnhancedFormatting(): boolean { - // Check if terminal width detection is available and meets minimum width - if (sys && sys.getWidthOfTerminal) { + 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) { - return width >= ENHANCED_FORMAT_MIN_WIDTH; + if (width !== undefined && width < ENHANCED_FORMAT_MIN_WIDTH) { + return false; } } - // If we can't detect terminal width but have TTY with color support, - // assume the terminal is modern enough for enhanced formatting - if (sys && sys.writeOutputIsTTY && sys.writeOutputIsTTY()) { - return true; + // For CI environments, require explicit opt-in + if (sys.getEnvironmentVariable?.("CI") === "true") { + return sys.getEnvironmentVariable("TS_ENHANCED_ERRORS") === "true"; } - // If environment suggests color support, use enhanced formatting - if (sys && sys.getEnvironmentVariable) { - const colorTerm = sys.getEnvironmentVariable("COLORTERM"); - const termProgram = sys.getEnvironmentVariable("TERM_PROGRAM"); - const term = sys.getEnvironmentVariable("TERM"); - - // Modern terminal indicators - if (colorTerm === "truecolor" || colorTerm === "24bit") return true; - if (termProgram === "vscode" || termProgram === "iTerm.app") return true; - if (term && (term.includes("256color") || term.includes("color"))) return 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; } From 5b01534c5a188f1dbf4155729383ff29f47f0997 Mon Sep 17 00:00:00 2001 From: Anivar A Aravind Date: Sat, 26 Jul 2025 10:29:45 +0530 Subject: [PATCH 3/3] Fix code formatting issues - Fix trailing commas in function parameters - Fix quote consistency (single quotes to double quotes) - Fix empty line spacing according to dprint style guide --- src/compiler/program.ts | 66 ++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 4db582113b908..819c075473c82 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -792,25 +792,25 @@ function formatCodeSpanEnhanced( length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, - host: FormatDiagnosticsHost + 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 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); } @@ -821,34 +821,34 @@ function formatCodeSpanEnhancedSingleLine( length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, - host: FormatDiagnosticsHost + host: FormatDiagnosticsHost, ): string { // Get line content const lineStart = getPositionOfLineAndCharacter(file, line, 0); - const lineEnd = file.text.indexOf('\n', lineStart); + 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; } @@ -857,12 +857,12 @@ function formatCodeSpanEnhancedSingleLine( */ 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(); @@ -870,26 +870,26 @@ function shouldUseEnhancedFormatting(): boolean { 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 (useEnhancedFormatting) { @@ -914,18 +914,18 @@ export function formatDiagnosticsWithColorAndContext(diagnostics: readonly Diagn */ 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( @@ -934,20 +934,20 @@ function formatDiagnosticEnhanced(diagnostic: Diagnostic, host: FormatDiagnostic diagnostic.length || 1, "", getCategoryFormat(diagnostic.category), - host + host, ); output += host.getNewLine(); } - + // Error message output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); - + // Related information if (diagnostic.relatedInformation) { for (const related of diagnostic.relatedInformation) { output += host.getNewLine(); output += host.getNewLine(); - + if (related.file && related.start !== undefined) { output += halfIndent + formatLocation(related.file, related.start, host); output += host.getNewLine(); @@ -957,15 +957,15 @@ function formatDiagnosticEnhanced(diagnostic: Diagnostic, host: FormatDiagnostic related.length || 1, halfIndent, ForegroundColorEscapeSequences.Cyan, - host + host, ); output += host.getNewLine(); } - + output += halfIndent + flattenDiagnosticMessageText(related.messageText, host.getNewLine()); } } - + return output; } @@ -974,7 +974,7 @@ function formatDiagnosticEnhanced(diagnostic: Diagnostic, host: FormatDiagnostic */ function formatDiagnosticOriginal(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { let output = ""; - + if (diagnostic.file) { const { file, start } = diagnostic; output += formatLocation(file, start!, host); // TODO: GH#18217 @@ -989,7 +989,7 @@ function formatDiagnosticOriginal(diagnostic: Diagnostic, host: FormatDiagnostic 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) { @@ -1002,7 +1002,7 @@ function formatDiagnosticOriginal(diagnostic: Diagnostic, host: FormatDiagnostic output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine()); } } - + return output; }