react_compiler_diagnostics/
code_frame.rs1use crate::{CompilerDiagnosticDetail, CompilerErrorOrDiagnostic, CompilerError};
9
10const CODEFRAME_LINES_ABOVE: u32 = 2;
11const CODEFRAME_LINES_BELOW: u32 = 3;
12const CODEFRAME_MAX_LINES: u32 = 10;
13const CODEFRAME_ABBREVIATED_SOURCE_LINES: usize = 5;
14
15fn split_lines(source: &str) -> Vec<&str> {
17 let mut lines = Vec::new();
18 let mut start = 0;
19 let bytes = source.as_bytes();
20 let len = bytes.len();
21 let mut i = 0;
22 while i < len {
23 let ch = bytes[i];
24 if ch == b'\r' {
25 lines.push(&source[start..i]);
26 if i + 1 < len && bytes[i + 1] == b'\n' {
27 i += 2;
28 } else {
29 i += 1;
30 }
31 start = i;
32 } else if ch == b'\n' {
33 lines.push(&source[start..i]);
34 i += 1;
35 start = i;
36 } else {
37 if ch == 0xE2 && i + 2 < len && bytes[i + 1] == 0x80
40 && (bytes[i + 2] == 0xA8 || bytes[i + 2] == 0xA9)
41 {
42 lines.push(&source[start..i]);
43 i += 3;
44 start = i;
45 } else {
46 i += 1;
47 }
48 }
49 }
50 lines.push(&source[start..]);
51 lines
52}
53
54#[derive(Clone, Debug)]
56enum MarkerEntry {
57 WholeLine,
58 Range(usize, usize), }
60
61fn get_marker_lines(
64 start_line: u32,
65 start_column: u32, end_line: u32,
67 end_column: u32, source_line_count: usize,
69 lines_above: u32,
70 lines_below: u32,
71) -> (usize, usize, Vec<(usize, MarkerEntry)>) {
72 let start_line = start_line as usize;
73 let end_line = end_line as usize;
74 let start_column = start_column as usize;
75 let end_column = end_column as usize;
76
77 let start = if start_line > (lines_above as usize + 1) {
79 start_line - (lines_above as usize + 1)
80 } else {
81 0
82 };
83 let end = std::cmp::min(source_line_count, end_line + lines_below as usize);
84
85 let line_diff = end_line - start_line;
86 let mut marker_lines: Vec<(usize, MarkerEntry)> = Vec::new();
87
88 if line_diff > 0 {
89 for i in 0..=line_diff {
91 let line_number = i + start_line;
92 if start_column == 0 {
93 marker_lines.push((line_number, MarkerEntry::WholeLine));
94 } else if i == 0 {
95 marker_lines.push((line_number, MarkerEntry::Range(start_column, 0))); } else if i == line_diff {
103 marker_lines.push((line_number, MarkerEntry::Range(0, end_column)));
104 } else {
105 marker_lines.push((line_number, MarkerEntry::Range(0, 0))); }
107 }
108 } else {
109 if start_column == end_column {
111 if start_column != 0 {
112 marker_lines.push((start_line, MarkerEntry::Range(start_column, 0)));
113 } else {
114 marker_lines.push((start_line, MarkerEntry::WholeLine));
115 }
116 } else {
117 marker_lines.push((
118 start_line,
119 MarkerEntry::Range(start_column, end_column - start_column),
120 ));
121 }
122 }
123
124 (start, end, marker_lines)
125}
126
127pub fn code_frame_columns(
132 source: &str,
133 start_line: u32,
134 start_col: u32,
135 end_line: u32,
136 end_col: u32,
137 message: &str,
138) -> String {
139 let start_column_1 = start_col + 1;
141 let end_column_1 = end_col + 1;
142
143 let lines = split_lines(source);
144 let source_line_count = lines.len();
145
146 let (start, end, marker_lines_raw) = get_marker_lines(
147 start_line,
148 start_column_1,
149 end_line,
150 end_column_1,
151 source_line_count,
152 CODEFRAME_LINES_ABOVE,
153 CODEFRAME_LINES_BELOW,
154 );
155
156 let has_columns = start_column_1 > 0;
157 let number_max_width = format!("{}", end).len();
158
159 let mut marker_map: std::collections::HashMap<usize, MarkerEntry> = std::collections::HashMap::new();
161 let line_diff = end_line as usize - start_line as usize;
162 for (line_number, entry) in marker_lines_raw {
163 let resolved = match &entry {
165 MarkerEntry::Range(col, len) => {
166 if line_diff > 0 {
167 let i = line_number - start_line as usize;
168 if i == 0 && *len == 0 {
169 let source_length = if line_number >= 1 && line_number <= lines.len() {
171 lines[line_number - 1].len()
172 } else {
173 0
174 };
175 MarkerEntry::Range(*col, source_length.saturating_sub(*col) + 1)
176 } else if i > 0 && i < line_diff && *col == 0 && *len == 0 {
177 let source_length = if (start_line as usize) < lines.len() {
182 lines[start_line as usize].len()
183 } else {
184 0
185 };
186 MarkerEntry::Range(0, source_length)
187 } else {
188 entry
189 }
190 } else {
191 entry
192 }
193 }
194 _ => entry,
195 };
196 marker_map.insert(line_number, resolved);
197 }
198
199 let mut frame_parts: Vec<String> = Vec::new();
201 let display_lines = &lines[start..end];
202
203 for (index, line) in display_lines.iter().enumerate() {
204 let number = start + 1 + index;
205 let number_str = format!("{}", number);
207 let padded_number = if number_str.len() >= number_max_width {
208 number_str
209 } else {
210 let padding = " ".repeat(number_max_width - number_str.len());
211 format!("{}{}", padding, number_str)
212 };
213 let gutter = format!(" {} |", padded_number);
214
215 let has_marker = marker_map.get(&number);
216 let has_next_marker = marker_map.contains_key(&(number + 1));
217 let last_marker_line = has_marker.is_some() && !has_next_marker;
218
219 if let Some(marker_entry) = has_marker {
220 let line_content = if line.is_empty() {
222 String::new()
223 } else {
224 format!(" {}", line)
225 };
226
227 let marker_line_str = match marker_entry {
228 MarkerEntry::Range(col, len) => {
229 let max_col = if *col > 0 { col - 1 } else { 0 };
231 let byte_end = std::cmp::min(max_col, line.len());
232 let safe_end = if byte_end < line.len() && !line.is_char_boundary(byte_end) {
234 line.floor_char_boundary(byte_end)
235 } else {
236 byte_end
237 };
238 let prefix = &line[..safe_end];
239 let marker_spacing: String = prefix
240 .chars()
241 .map(|c| if c == '\t' { '\t' } else { ' ' })
242 .collect();
243 let number_of_markers = if *len == 0 { 1 } else { *len };
244 let carets = "^".repeat(number_of_markers);
245 let gutter_spaces = gutter.replace(|c: char| c.is_ascii_digit(), " ");
246 let mut marker_str = format!(
247 "\n {} {}{}",
248 gutter_spaces, marker_spacing, carets
249 );
250 if last_marker_line && !message.is_empty() {
251 marker_str.push(' ');
252 marker_str.push_str(message);
253 }
254 marker_str
255 }
256 MarkerEntry::WholeLine => String::new(),
257 };
258
259 frame_parts.push(format!(">{}{}{}", gutter, line_content, marker_line_str));
260 } else {
261 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 }
270
271 let mut frame = frame_parts.join("\n");
272
273 if !message.is_empty() && !has_columns {
275 frame = format!(
276 "{}{}\n{}",
277 " ".repeat(number_max_width + 1),
278 message,
279 frame
280 );
281 }
282
283 frame
284}
285
286pub fn print_code_frame(
289 source: &str,
290 start_line: u32,
291 start_col: u32,
292 end_line: u32,
293 end_col: u32,
294 message: &str,
295) -> String {
296 let printed = code_frame_columns(source, start_line, start_col, end_line, end_col, message);
297
298 if end_line - start_line < CODEFRAME_MAX_LINES {
299 return printed;
300 }
301
302 let lines: Vec<&str> = printed.split('\n').collect();
304 let head_count = CODEFRAME_LINES_ABOVE as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES;
305 let tail_count = CODEFRAME_LINES_BELOW as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES;
306
307 if lines.len() <= head_count + tail_count {
308 return printed;
309 }
310
311 let pipe_index = lines[0].find('|').unwrap_or(0);
313 let tail_start = lines.len() - tail_count;
314
315 let mut parts: Vec<String> = Vec::new();
316 for line in &lines[..head_count] {
317 parts.push(line.to_string());
318 }
319 parts.push(format!("{}\u{2026}", " ".repeat(pipe_index)));
320 for line in &lines[tail_start..] {
321 parts.push(line.to_string());
322 }
323 parts.join("\n")
324}
325
326use crate::format_category_heading;
327
328pub fn format_compiler_error(
335 err: &CompilerError,
336 source: &str,
337 filename: Option<&str>,
338) -> String {
339 let detail_messages: Vec<String> = err
340 .details
341 .iter()
342 .map(|d| format_error_detail(d, source, filename))
343 .collect();
344
345 let count = err.details.len();
346 let plural = if count == 1 { "" } else { "s" };
347 let header = format!("Found {} error{}:\n\n", count, plural);
348
349 let trimmed: Vec<String> = detail_messages.iter().map(|m| m.trim().to_string()).collect();
350 format!("{}{}", header, trimmed.join("\n\n"))
351}
352
353fn format_error_detail(
355 detail: &CompilerErrorOrDiagnostic,
356 source: &str,
357 filename: Option<&str>,
358) -> String {
359 match detail {
360 CompilerErrorOrDiagnostic::Diagnostic(d) => {
361 let heading = format_category_heading(d.category);
362 let mut buffer = vec![format!("{}: {}", heading, d.reason)];
363
364 if let Some(ref description) = d.description {
365 buffer.push(format!("\n\n{}.", description));
366 }
367 for item in &d.details {
368 match item {
369 CompilerDiagnosticDetail::Error { loc, message, .. } => {
370 if let Some(loc) = loc {
371 let frame = print_code_frame(
372 source,
373 loc.start.line,
374 loc.start.column,
375 loc.end.line,
376 loc.end.column,
377 message.as_deref().unwrap_or(""),
378 );
379 buffer.push("\n\n".to_string());
380 if let Some(fname) = filename {
381 buffer.push(format!(
382 "{}:{}:{}\n",
383 fname, loc.start.line, loc.start.column
384 ));
385 }
386 buffer.push(frame);
387 }
388 }
389 CompilerDiagnosticDetail::Hint { message } => {
390 buffer.push("\n\n".to_string());
391 buffer.push(message.clone());
392 }
393 }
394 }
395
396 buffer.join("")
397 }
398 CompilerErrorOrDiagnostic::ErrorDetail(d) => {
399 let heading = format_category_heading(d.category);
400 let mut buffer = vec![format!("{}: {}", heading, d.reason)];
401
402 if let Some(ref description) = d.description {
403 buffer.push(format!("\n\n{}.", description));
404 if let Some(ref loc) = d.loc {
405 let frame = print_code_frame(
406 source,
407 loc.start.line,
408 loc.start.column,
409 loc.end.line,
410 loc.end.column,
411 &d.reason,
412 );
413 buffer.push("\n\n".to_string());
414 if let Some(fname) = filename {
415 buffer.push(format!(
416 "{}:{}:{}\n",
417 fname, loc.start.line, loc.start.column
418 ));
419 }
420 buffer.push(frame);
421 buffer.push("\n\n".to_string());
422 }
423 } else if let Some(ref loc) = d.loc {
424 let frame = print_code_frame(
425 source,
426 loc.start.line,
427 loc.start.column,
428 loc.end.line,
429 loc.end.column,
430 &d.reason,
431 );
432 buffer.push("\n\n".to_string());
433 if let Some(fname) = filename {
434 buffer.push(format!(
435 "{}:{}:{}\n",
436 fname, loc.start.line, loc.start.column
437 ));
438 }
439 buffer.push(frame);
440 buffer.push("\n\n".to_string());
441 }
442
443 buffer.join("")
444 }
445 }
446}