1use crate::diag::{DiagHandler, DiagKind, OutputOptions, ResolvedDiagnostic};
5
6pub fn build_source_and_caret_line(
12 source_line_text: &str,
13 col: u32,
14 ranges: &[(usize, usize)],
15 opts: &OutputOptions,
16) -> (String, String) {
17 let mut byte_to_column: Vec<usize> = Vec::new();
20 let mut source_line: Vec<char> = Vec::new();
21 for ch in source_line_text.chars() {
22 let column = source_line.len();
25 for _ in 0..ch.len_utf8() {
26 byte_to_column.push(column);
27 }
28 source_line.push(ch);
29 }
30 let num_columns = source_line.len();
31
32 let widen_column = |narrow_column: usize| -> usize {
35 byte_to_column
36 .get(narrow_column)
37 .copied()
38 .unwrap_or(num_columns)
39 };
40
41 let column_no = widen_column((col as usize).saturating_sub(1));
43
44 let widened_ranges: Vec<(usize, usize)> = ranges
45 .iter()
46 .map(|&(s, e)| (widen_column(s), widen_column(e)))
47 .collect();
48
49 let mut caret_line: Vec<u8> = vec![b' '; num_columns + 1];
51 for &(first, second) in &widened_ranges {
52 if first < caret_line.len() {
53 let end = std::cmp::min(second, caret_line.len());
54 for c in &mut caret_line[first..end] {
55 *c = b'~';
56 }
57 }
58 }
59 caret_line[std::cmp::min(column_no, num_columns)] = b'^';
60
61 if let Some(last) = caret_line.iter().rposition(|&c| c != b' ') {
63 caret_line.truncate(last + 1);
64 } else {
65 caret_line.clear();
66 }
67
68 let tab_stop = OutputOptions::TAB_STOP;
70 let mut pos = 0;
71 while pos < source_line.len() {
72 if source_line[pos] == '\t' {
73 let expand_count = tab_stop - (pos % tab_stop);
74 source_line.splice(pos..pos + 1, std::iter::repeat_n(' ', expand_count));
76 if pos < caret_line.len() {
79 let fill = caret_line[pos];
80 caret_line.splice(pos..pos + 1, std::iter::repeat_n(fill, expand_count));
81 }
82 pos += expand_count;
83 } else {
84 pos += 1;
85 }
86 }
87
88 let preferred_max_error_width = opts.preferred_max_error_width.unwrap_or(usize::MAX);
92 let mut focus_start: usize = column_no;
93 let mut focus_length: usize = 1;
94 for &(first, second) in &widened_ranges {
95 if first <= column_no && column_no < second {
96 focus_start = first;
97 focus_length = second - first;
98 break;
99 }
100 }
101 let desired_line_length = std::cmp::max(
102 preferred_max_error_width,
103 focus_length + OutputOptions::MINIMUM_SOURCE_CONTEXT,
104 );
105 if source_line.len() > desired_line_length {
106 let focus_center = focus_start + focus_length / 2;
107 let half = desired_line_length / 2;
109 if focus_center > half {
110 let left_trim_amount = focus_center - half;
111 let ct = std::cmp::min(left_trim_amount, caret_line.len());
113 caret_line.drain(0..ct);
114 let st = std::cmp::min(left_trim_amount, source_line.len());
115 source_line.drain(0..st);
116 for c in source_line.iter_mut().take(3) {
118 *c = '.';
119 }
120 }
121 if source_line.len() > desired_line_length {
122 let ce = std::cmp::min(caret_line.len(), desired_line_length);
123 caret_line.truncate(ce);
124 source_line.truncate(desired_line_length);
125 let len = source_line.len();
127 for c in source_line.iter_mut().skip(len.saturating_sub(3)) {
128 *c = '.';
129 }
130 }
131 }
132
133 let narrow_source_line: String = source_line.into_iter().collect();
135 let caret_string: String = caret_line.into_iter().map(|c| c as char).collect();
136 (narrow_source_line, caret_string)
137}
138
139pub fn render_diagnostic(diag: &ResolvedDiagnostic, opts: &OutputOptions) -> String {
147 let kind_str = match diag.kind {
148 DiagKind::Error => "error",
149 DiagKind::Warning => "warning",
150 DiagKind::Note => "note",
151 };
152 let mut out = String::new();
164 if !diag.file_name.is_empty() {
165 if diag.file_name == "-" {
166 out.push_str("<stdin>");
167 } else {
168 out.push_str(&diag.file_name);
169 }
170 out.push(':');
171 out.push_str(&diag.line.to_string());
172 if diag.col != 0 {
173 out.push(':');
174 out.push_str(&diag.col.to_string());
175 }
176 out.push_str(": ");
177 }
178 out.push_str(kind_str);
179 out.push_str(": ");
180 out.push_str(&diag.message);
181 out.push('\n');
182 if let Some(src) = &diag.source_line {
183 let range_arr: [(usize, usize); 1];
186 let ranges: &[(usize, usize)] = match diag.range_cols {
187 Some((s, e)) => {
188 range_arr = [(s as usize, e as usize)];
189 &range_arr
190 }
191 None => &[],
192 };
193 let (src_expanded, caret) = build_source_and_caret_line(src, diag.col, ranges, opts);
198 out.push_str(&src_expanded);
199 out.push('\n');
200 if src.is_ascii() {
201 out.push_str(&caret);
202 out.push('\n');
203 }
204 }
205 out
206}
207
208pub struct StderrHandler {
211 opts: OutputOptions,
212}
213
214impl StderrHandler {
215 pub fn new(opts: OutputOptions) -> StderrHandler {
216 StderrHandler { opts }
217 }
218}
219
220impl DiagHandler for StderrHandler {
221 fn as_any(&self) -> &dyn std::any::Any {
222 self
223 }
224
225 fn handle(&mut self, diag: &ResolvedDiagnostic) {
226 let s = render_diagnostic(diag, &self.opts);
227 eprint!("{}", s);
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::diag::{DiagKind, OutputOptions};
236
237 #[test]
247 fn header_prefix_is_conditional() {
248 use crate::diag::ResolvedDiagnostic;
249 let base = |file: &str, line: u32, col: u32| ResolvedDiagnostic {
250 kind: DiagKind::Error,
251 file_name: file.into(),
252 line,
253 col,
254 message: "m".into(),
255 source_line: None,
256 range_cols: None,
257 };
258 let opts = OutputOptions::default();
259 assert_eq!(
261 render_diagnostic(&base("<unknown>", 0, 0), &opts),
262 "<unknown>:0: error: m\n"
263 );
264 assert_eq!(render_diagnostic(&base("", 0, 0), &opts), "error: m\n");
266 assert_eq!(
268 render_diagnostic(&base("-", 3, 4), &opts),
269 "<stdin>:3:4: error: m\n"
270 );
271 assert_eq!(
273 render_diagnostic(&base("t.js", 3, 4), &opts),
274 "t.js:3:4: error: m\n"
275 );
276 }
277
278 #[test]
279 fn ranged_caret_underline() {
280 use crate::diag::ResolvedDiagnostic;
281 let d = ResolvedDiagnostic {
282 kind: DiagKind::Error,
283 file_name: "t".into(),
284 line: 1,
285 col: 5,
286 message: "m".into(),
287 source_line: Some("let x = 1;".into()),
288 range_cols: Some((4, 9)),
289 };
290 let s = render_diagnostic(&d, &OutputOptions::default());
291 assert!(
292 s.contains("t:1:5: error: m"),
293 "header not found in: {:?}",
294 s
295 );
296 assert!(
297 s.contains(" ^~~~~"),
298 "caret underline not found in: {:?}",
299 s
300 );
301 }
302
303 #[test]
304 fn render_expands_tabs_in_source_line() {
305 use crate::diag::ResolvedDiagnostic;
306 let d = ResolvedDiagnostic {
309 kind: DiagKind::Error,
310 file_name: "t".into(),
311 line: 1,
312 col: 2,
313 message: "m".into(),
314 source_line: Some("\tx".into()),
315 range_cols: None,
316 };
317 let s = render_diagnostic(&d, &OutputOptions::default());
318 assert!(
319 s.contains(" x\n"),
320 "source not tab-expanded: {:?}",
321 s
322 );
323 assert!(s.contains(" ^\n"), "caret misaligned: {:?}", s);
324 }
325
326 #[test]
327 fn caret_under_single_column() {
328 let (src, caret) =
329 build_source_and_caret_line("let x = 1;", 5, &[], &OutputOptions::default());
330 assert_eq!(src, "let x = 1;");
331 assert_eq!(caret, " ^");
332 }
333
334 #[test]
335 fn tabs_expand_to_spaces_tabstop_8() {
336 let (src, caret) = build_source_and_caret_line("\tx", 2, &[], &OutputOptions::default());
337 assert_eq!(src, " x");
338 assert_eq!(caret, " ^");
339 }
340
341 #[test]
342 fn range_underlined_with_tildes() {
343 let (_src, caret) =
344 build_source_and_caret_line("let x = 1;", 5, &[(4, 9)], &OutputOptions::default());
345 assert_eq!(caret, " ^~~~~");
348 }
349
350 #[test]
351 fn non_ascii_columns_are_codepoints() {
352 let (_src, caret) = build_source_and_caret_line("éx", 2, &[], &OutputOptions::default());
358 assert_eq!(caret, "^");
359 }
360
361 #[test]
362 fn non_ascii_columns_caret_on_second_char() {
363 let (_src, caret) = build_source_and_caret_line("éx", 3, &[], &OutputOptions::default());
367 assert_eq!(caret, " ^");
368 }
369}