compiler/crates/react_compiler_diagnostics/src/code_frame.rs RUST 441 lines View on github.com → Search inside
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}

Code quality findings 23

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>.
warning correctness unchecked-indexing
let ch = bytes[i];
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>.
warning correctness unchecked-indexing
lines.push(&source[start..i]);
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>.
warning correctness unchecked-indexing
if i + 1 < len && bytes[i + 1] == b'\n' {
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>.
warning correctness unchecked-indexing
lines.push(&source[start..i]);
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>.
warning correctness unchecked-indexing
&& bytes[i + 1] == 0x80
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>.
warning correctness unchecked-indexing
&& (bytes[i + 2] == 0xA8 || bytes[i + 2] == 0xA9)
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>.
warning correctness unchecked-indexing
lines.push(&source[start..i]);
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>.
warning correctness unchecked-indexing
lines.push(&source[start..]);
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>.
warning correctness unchecked-indexing
/// Represents a marker line entry: either mark the whole line (true) or a [column, length] range.
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>.
warning correctness unchecked-indexing
// source[lineNumber - 1] gives us the source line (0-indexed array, 1-indexed line numbers)
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>.
warning correctness unchecked-indexing
// Actually, Babel accesses source[lineNumber - 1].length. We need to thread source lines.
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>.
warning correctness unchecked-indexing
let display_lines = &lines[start..end];
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>.
warning correctness unchecked-indexing
let pipe_index = lines[0].find('|').unwrap_or(0);
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>.
warning correctness unchecked-indexing
for line in &lines[..head_count] {
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>.
warning correctness unchecked-indexing
for line in &lines[tail_start..] {
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
lines.push(&source[start..i]);
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
marker_lines.push((line_number, MarkerEntry::WholeLine));
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info performance to-string-in-loop
parts.push(line.to_string());
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
parts.push(line.to_string());
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
parts.push(format!("{}\u{2026}", " ".repeat(pipe_index)));
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info performance to-string-in-loop
parts.push(line.to_string());
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
parts.push(line.to_string());
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
buffer.push(format!("\n\n{}.", description));

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.