react_compiler_diagnostics/
code_frame.rs1use crate::{CompilerDiagnosticDetail, CompilerError, CompilerErrorOrDiagnostic};
8
9const CODEFRAME_LINES_ABOVE: u32 = 2;
10const CODEFRAME_LINES_BELOW: u32 = 3;
11const CODEFRAME_MAX_LINES: u32 = 10;
12const CODEFRAME_ABBREVIATED_SOURCE_LINES: usize = 5;
13
14fn 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 if ch == 0xE2
39 && i + 2 < len
40 && bytes[i + 1] == 0x80
41 && (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 lines
53}
54
55#[derive(Clone, Debug)]
57enum MarkerEntry {
58 WholeLine,
59 Range(usize, usize), }
61
62fn get_marker_lines(
65 start_line: u32,
66 start_column: u32, end_line: u32,
68 end_column: u32, 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;
77
78 let start = if start_line > (lines_above as usize + 1) {
80 start_line - (lines_above as usize + 1)
81 } else {
82 0
83 };
84 let end = std::cmp::min(source_line_count, end_line + lines_below as usize);
85
86 let line_diff = end_line - start_line;
87 let mut marker_lines: Vec<(usize, MarkerEntry)> = Vec::new();
88
89 if line_diff > 0 {
90 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 marker_lines.push((line_number, MarkerEntry::Range(start_column, 0))); } 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))); }
108 }
109 } else {
110 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 }
124
125 (start, end, marker_lines)
126}
127
128pub 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 let start_column_1 = start_col + 1;
142 let end_column_1 = end_col + 1;
143
144 let lines = split_lines(source);
145 let source_line_count = lines.len();
146
147 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 );
156
157 let has_columns = start_column_1 > 0;
158 let number_max_width = format!("{}", end).len();
159
160 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 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 let source_length = if line_number >= 1 && line_number <= lines.len() {
173 lines[line_number - 1].len()
174 } else {
175 0
176 };
177 MarkerEntry::Range(*col, source_length.saturating_sub(*col) + 1)
178 } else if i > 0 && i < line_diff && *col == 0 && *len == 0 {
179 let source_length = if (start_line as usize) < lines.len() {
184 lines[start_line as usize].len()
185 } else {
186 0
187 };
188 MarkerEntry::Range(0, source_length)
189 } else {
190 entry
191 }
192 } else {
193 entry
194 }
195 }
196 _ => entry,
197 };
198 marker_map.insert(line_number, resolved);
199 }
200
201 let mut frame_parts: Vec<String> = Vec::new();
203 let display_lines = &lines[start..end];
204
205 for (index, line) in display_lines.iter().enumerate() {
206 let number = start + 1 + index;
207 let number_str = format!("{}", number);
209 let padded_number = if number_str.len() >= number_max_width {
210 number_str
211 } else {
212 let padding = " ".repeat(number_max_width - number_str.len());
213 format!("{}{}", padding, number_str)
214 };
215 let gutter = format!(" {} |", padded_number);
216
217 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;
220
221 if let Some(marker_entry) = has_marker {
222 let line_content = if line.is_empty() {
224 String::new()
225 } else {
226 format!(" {}", line)
227 };
228
229 let marker_line_str = match marker_entry {
230 MarkerEntry::Range(col, len) => {
231 let max_col = if *col > 0 { col - 1 } else { 0 };
233 let byte_end = std::cmp::min(max_col, line.len());
234 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_end
239 };
240 let prefix = &line[..safe_end];
241 let marker_spacing: String = prefix
242 .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_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!("{}{}\n{}", " ".repeat(number_max_width + 1), message, frame);
276 }
277
278 frame
279}
280
281pub 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);
292
293 if end_line - start_line < CODEFRAME_MAX_LINES {
294 return printed;
295 }
296
297 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;
301
302 if lines.len() <= head_count + tail_count {
303 return printed;
304 }
305
306 let pipe_index = lines[0].find('|').unwrap_or(0);
308 let tail_start = lines.len() - tail_count;
309
310 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}
320
321use crate::format_category_heading;
322
323pub fn format_compiler_error(err: &CompilerError, source: &str, filename: Option<&str>) -> String {
330 let detail_messages: Vec<String> = err
331 .details
332 .iter()
333 .map(|d| format_error_detail(d, source, filename))
334 .collect();
335
336 let count = err.details.len();
337 let plural = if count == 1 { "" } else { "s" };
338 let header = format!("Found {} error{}:\n\n", count, plural);
339
340 let trimmed: Vec<String> = detail_messages
341 .iter()
342 .map(|m| m.trim().to_string())
343 .collect();
344 format!("{}{}", header, trimmed.join("\n\n"))
345}
346
347fn 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)];
357
358 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.column
378 ));
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 }
389
390 buffer.join("")
391 }
392 CompilerErrorOrDiagnostic::ErrorDetail(d) => {
393 let heading = format_category_heading(d.category);
394 let mut buffer = vec![format!("{}: {}", heading, d.reason)];
395
396 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.column
412 ));
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.column
431 ));
432 }
433 buffer.push(frame);
434 buffer.push("\n\n".to_string());
435 }
436
437 buffer.join("")
438 }
439 }
440}