Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
let ch = bytes[i];
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */7use crate::{CompilerDiagnosticDetail, CompilerError, CompilerErrorOrDiagnostic};89const CODEFRAME_LINES_ABOVE: u32 = 2;10const CODEFRAME_LINES_BELOW: u32 = 3;11const CODEFRAME_MAX_LINES: u32 = 10;12const CODEFRAME_ABBREVIATED_SOURCE_LINES: usize = 5;1314/// Split source text on newlines, matching Babel's NEWLINE regex: /\r\n|[\n\r\u2028\u2029]/15fn split_lines(source: &str) -> Vec<&str> {16 let mut lines = Vec::new();17 let mut start = 0;18 let bytes = source.as_bytes();19 let len = bytes.len();20 let mut i = 0;21 while i < len {22 let ch = bytes[i];23 if ch == b'\r' {24 lines.push(&source[start..i]);25 if i + 1 < len && bytes[i + 1] == b'\n' {26 i += 2;27 } else {28 i += 1;29 }30 start = i;31 } else if ch == b'\n' {32 lines.push(&source[start..i]);33 i += 1;34 start = i;35 } else {36 // Check for Unicode line separators U+2028 and U+202937 // These are encoded as E2 80 A8 and E2 80 A9 in UTF-838 if ch == 0xE239 && i + 2 < len40 && bytes[i + 1] == 0x8041 && (bytes[i + 2] == 0xA8 || bytes[i + 2] == 0xA9)42 {43 lines.push(&source[start..i]);44 i += 3;45 start = i;46 } else {47 i += 1;48 }49 }50 }51 lines.push(&source[start..]);52 lines53}5455/// Represents a marker line entry: either mark the whole line (true) or a [column, length] range.56#[derive(Clone, Debug)]57enum MarkerEntry {58 WholeLine,59 Range(usize, usize), // (start_column_1based, length)60}6162/// Compute marker lines matching Babel's getMarkerLines().63/// All column values here are 1-based (Babel convention).64fn get_marker_lines(65 start_line: u32,66 start_column: u32, // 1-based67 end_line: u32,68 end_column: u32, // 1-based69 source_line_count: usize,70 lines_above: u32,71 lines_below: u32,72) -> (usize, usize, Vec<(usize, MarkerEntry)>) {73 let start_line = start_line as usize;74 let end_line = end_line as usize;75 let start_column = start_column as usize;76 let end_column = end_column as usize;7778 // Compute display range79 let start = if start_line > (lines_above as usize + 1) {80 start_line - (lines_above as usize + 1)81 } else {82 083 };84 let end = std::cmp::min(source_line_count, end_line + lines_below as usize);8586 let line_diff = end_line - start_line;87 let mut marker_lines: Vec<(usize, MarkerEntry)> = Vec::new();8889 if line_diff > 0 {90 // Multi-line error91 for i in 0..=line_diff {92 let line_number = i + start_line;93 if start_column == 0 {94 marker_lines.push((line_number, MarkerEntry::WholeLine));95 } else if i == 0 {96 // First line: from start_column to end of source line97 // source[lineNumber - 1] gives us the source line (0-indexed array, 1-indexed line numbers)98 // But we don't have access to source lines here, so we pass the length through.99 // Actually, Babel accesses source[lineNumber - 1].length. We need to thread source lines.100 // For now, this is handled in code_frame_columns where we have access to source lines.101 // We use a placeholder that will be filled in later.102 marker_lines.push((line_number, MarkerEntry::Range(start_column, 0))); // 0 = placeholder103 } else if i == line_diff {104 marker_lines.push((line_number, MarkerEntry::Range(0, end_column)));105 } else {106 marker_lines.push((line_number, MarkerEntry::Range(0, 0))); // 0 = placeholder for full line107 }108 }109 } else {110 // Single-line error111 if start_column == end_column {112 if start_column != 0 {113 marker_lines.push((start_line, MarkerEntry::Range(start_column, 0)));114 } else {115 marker_lines.push((start_line, MarkerEntry::WholeLine));116 }117 } else {118 marker_lines.push((119 start_line,120 MarkerEntry::Range(start_column, end_column - start_column),121 ));122 }123 }124125 (start, end, marker_lines)126}127128/// Produce a code frame matching @babel/code-frame's codeFrameColumns() in non-highlighted mode.129///130/// Columns are 0-based (matching the Rust/AST convention). They are converted to 1-based131/// internally to match Babel's convention (the JS caller already does column + 1).132pub fn code_frame_columns(133 source: &str,134 start_line: u32,135 start_col: u32,136 end_line: u32,137 end_col: u32,138 message: &str,139) -> String {140 // Convert 0-based columns to 1-based (Babel convention)141 let start_column_1 = start_col + 1;142 let end_column_1 = end_col + 1;143144 let lines = split_lines(source);145 let source_line_count = lines.len();146147 let (start, end, marker_lines_raw) = get_marker_lines(148 start_line,149 start_column_1,150 end_line,151 end_column_1,152 source_line_count,153 CODEFRAME_LINES_ABOVE,154 CODEFRAME_LINES_BELOW,155 );156157 let has_columns = start_column_1 > 0;158 let number_max_width = format!("{}", end).len();159160 // Build a lookup map for marker lines161 let mut marker_map: rustc_hash::FxHashMap<usize, MarkerEntry> =162 rustc_hash::FxHashMap::default();163 let line_diff = end_line as usize - start_line as usize;164 for (line_number, entry) in marker_lines_raw {165 // Resolve placeholder lengths using actual source lines166 let resolved = match &entry {167 MarkerEntry::Range(col, len) => {168 if line_diff > 0 {169 let i = line_number - start_line as usize;170 if i == 0 && *len == 0 {171 // First line of multi-line: from start_column to end of line172 let source_length = if line_number >= 1 && line_number <= lines.len() {173 lines[line_number - 1].len()174 } else {175 0176 };177 MarkerEntry::Range(*col, source_length.saturating_sub(*col) + 1)178 } else if i > 0 && i < line_diff && *col == 0 && *len == 0 {179 // Middle line of multi-line: Babel uses source[lineNumber - i].length180 // which evaluates to source[startLine] (0-indexed array, 1-indexed line number).181 // This means all middle lines use the length of source[startLine],182 // which is the line at 0-indexed position startLine in the source array.183 let source_length = if (start_line as usize) < lines.len() {184 lines[start_line as usize].len()185 } else {186 0187 };188 MarkerEntry::Range(0, source_length)189 } else {190 entry191 }192 } else {193 entry194 }195 }196 _ => entry,197 };198 marker_map.insert(line_number, resolved);199 }200201 // Build frame lines202 let mut frame_parts: Vec<String> = Vec::new();203 let display_lines = &lines[start..end];204205 for (index, line) in display_lines.iter().enumerate() {206 let number = start + 1 + index;207 // Right-align the line number: ` ${number}`.slice(-numberMaxWidth)208 let number_str = format!("{}", number);209 let padded_number = if number_str.len() >= number_max_width {210 number_str211 } else {212 let padding = " ".repeat(number_max_width - number_str.len());213 format!("{}{}", padding, number_str)214 };215 let gutter = format!(" {} |", padded_number);216217 let has_marker = marker_map.get(&number);218 let has_next_marker = marker_map.contains_key(&(number + 1));219 let last_marker_line = has_marker.is_some() && !has_next_marker;220221 if let Some(marker_entry) = has_marker {222 // This is a marked line223 let line_content = if line.is_empty() {224 String::new()225 } else {226 format!(" {}", line)227 };228229 let marker_line_str = match marker_entry {230 MarkerEntry::Range(col, len) => {231 // Build marker spacing: replace non-tab chars with spaces232 let max_col = if *col > 0 { col - 1 } else { 0 };233 let byte_end = std::cmp::min(max_col, line.len());234 // Ensure we don't slice in the middle of a multi-byte UTF-8 character235 let safe_end = if byte_end < line.len() && !line.is_char_boundary(byte_end) {236 line.floor_char_boundary(byte_end)237 } else {238 byte_end239 };240 let prefix = &line[..safe_end];241 let marker_spacing: String = prefix242 .chars()243 .map(|c| if c == '\t' { '\t' } else { ' ' })244 .collect();245 let number_of_markers = if *len == 0 { 1 } else { *len };246 let carets = "^".repeat(number_of_markers);247 let gutter_spaces = gutter.replace(|c: char| c.is_ascii_digit(), " ");248 let mut marker_str =249 format!("\n {} {}{}", gutter_spaces, marker_spacing, carets);250 if last_marker_line && !message.is_empty() {251 marker_str.push(' ');252 marker_str.push_str(message);253 }254 marker_str255 }256 MarkerEntry::WholeLine => String::new(),257 };258259 frame_parts.push(format!(">{}{}{}", gutter, line_content, marker_line_str));260 } else {261 // Non-marked line262 let line_content = if line.is_empty() {263 String::new()264 } else {265 format!(" {}", line)266 };267 frame_parts.push(format!(" {}{}", gutter, line_content));268 }269 }270271 let mut frame = frame_parts.join("\n");272273 // If message is set but no columns, prepend the message274 if !message.is_empty() && !has_columns {275 frame = format!("{}{}\n{}", " ".repeat(number_max_width + 1), message, frame);276 }277278 frame279}280281/// Format a code frame with abbreviation for long spans,282/// matching the JS printCodeFrame() function.283pub fn print_code_frame(284 source: &str,285 start_line: u32,286 start_col: u32,287 end_line: u32,288 end_col: u32,289 message: &str,290) -> String {291 let printed = code_frame_columns(source, start_line, start_col, end_line, end_col, message);292293 if end_line - start_line < CODEFRAME_MAX_LINES {294 return printed;295 }296297 // Abbreviate: truncate middle298 let lines: Vec<&str> = printed.split('\n').collect();299 let head_count = CODEFRAME_LINES_ABOVE as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES;300 let tail_count = CODEFRAME_LINES_BELOW as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES;301302 if lines.len() <= head_count + tail_count {303 return printed;304 }305306 // Find the pipe index from the first line307 let pipe_index = lines[0].find('|').unwrap_or(0);308 let tail_start = lines.len() - tail_count;309310 let mut parts: Vec<String> = Vec::new();311 for line in &lines[..head_count] {312 parts.push(line.to_string());313 }314 parts.push(format!("{}\u{2026}", " ".repeat(pipe_index)));315 for line in &lines[tail_start..] {316 parts.push(line.to_string());317 }318 parts.join("\n")319}320321use crate::format_category_heading;322323/// Format a CompilerError into a message string matching the TS compiler's324/// CompilerError.printErrorMessage() / formatCompilerError() format.325///326/// The source parameter is the full source code of the file being compiled.327/// The filename parameter is the source filename (e.g., "foo.ts") used in328/// location displays.329pub fn format_compiler_error(err: &CompilerError, source: &str, filename: Option<&str>) -> String {330 let detail_messages: Vec<String> = err331 .details332 .iter()333 .map(|d| format_error_detail(d, source, filename))334 .collect();335336 let count = err.details.len();337 let plural = if count == 1 { "" } else { "s" };338 let header = format!("Found {} error{}:\n\n", count, plural);339340 let trimmed: Vec<String> = detail_messages341 .iter()342 .map(|m| m.trim().to_string())343 .collect();344 format!("{}{}", header, trimmed.join("\n\n"))345}346347/// Format a single error detail (either Diagnostic or ErrorDetail).348fn format_error_detail(349 detail: &CompilerErrorOrDiagnostic,350 source: &str,351 filename: Option<&str>,352) -> String {353 match detail {354 CompilerErrorOrDiagnostic::Diagnostic(d) => {355 let heading = format_category_heading(d.category);356 let mut buffer = vec![format!("{}: {}", heading, d.reason)];357358 if let Some(ref description) = d.description {359 buffer.push(format!("\n\n{}.", description));360 }361 for item in &d.details {362 match item {363 CompilerDiagnosticDetail::Error { loc, message, .. } => {364 if let Some(loc) = loc {365 let frame = print_code_frame(366 source,367 loc.start.line,368 loc.start.column,369 loc.end.line,370 loc.end.column,371 message.as_deref().unwrap_or(""),372 );373 buffer.push("\n\n".to_string());374 if let Some(fname) = filename {375 buffer.push(format!(376 "{}:{}:{}\n",377 fname, loc.start.line, loc.start.column378 ));379 }380 buffer.push(frame);381 }382 }383 CompilerDiagnosticDetail::Hint { message } => {384 buffer.push("\n\n".to_string());385 buffer.push(message.clone());386 }387 }388 }389390 buffer.join("")391 }392 CompilerErrorOrDiagnostic::ErrorDetail(d) => {393 let heading = format_category_heading(d.category);394 let mut buffer = vec![format!("{}: {}", heading, d.reason)];395396 if let Some(ref description) = d.description {397 buffer.push(format!("\n\n{}.", description));398 if let Some(ref loc) = d.loc {399 let frame = print_code_frame(400 source,401 loc.start.line,402 loc.start.column,403 loc.end.line,404 loc.end.column,405 &d.reason,406 );407 buffer.push("\n\n".to_string());408 if let Some(fname) = filename {409 buffer.push(format!(410 "{}:{}:{}\n",411 fname, loc.start.line, loc.start.column412 ));413 }414 buffer.push(frame);415 buffer.push("\n\n".to_string());416 }417 } else if let Some(ref loc) = d.loc {418 let frame = print_code_frame(419 source,420 loc.start.line,421 loc.start.column,422 loc.end.line,423 loc.end.column,424 &d.reason,425 );426 buffer.push("\n\n".to_string());427 if let Some(fname) = filename {428 buffer.push(format!(429 "{}:{}:{}\n",430 fname, loc.start.line, loc.start.column431 ));432 }433 buffer.push(frame);434 buffer.push("\n\n".to_string());435 }436437 buffer.join("")438 }439 }440}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.