1use std::io;
2use std::ops::Range;
3
4use crate::{IndexType, LabelDisplay};
5
6use super::draw::{self, StreamAwareFmt, StreamType};
7use super::{Cache, CharSet, LabelAttach, Report, ReportKind, Show, Span, Write};
8
9enum LabelKind {
18 Inline,
19 Multiline,
20}
21
22struct LabelInfo<'a> {
23 kind: LabelKind,
24 char_span: Range<usize>,
25 display_info: &'a LabelDisplay,
26}
27
28impl<'a> LabelInfo<'a> {
29 fn last_offset(&self) -> usize {
30 self.char_span
31 .end
32 .saturating_sub(1)
33 .max(self.char_span.start)
34 }
35}
36
37struct SourceGroup<'a, S: Span> {
38 src_id: &'a S::SourceId,
39 char_span: Range<usize>,
40 labels: Vec<LabelInfo<'a>>,
41}
42
43impl<S: Span> Report<'_, S> {
44 fn get_source_groups(&self, cache: &mut impl Cache<S::SourceId>) -> Vec<SourceGroup<S>> {
45 let mut groups = Vec::new();
46 for label in self.labels.iter() {
47 let label_source = label.span.source();
48
49 let src_display = cache.display(label_source);
50 let src = match cache.fetch(label_source) {
51 Ok(src) => src,
52 Err(e) => {
53 eprintln!("Unable to fetch source '{}': {:?}", Show(src_display), e);
54 continue;
55 }
56 };
57
58 let given_label_span = label.span.start()..label.span.end();
59
60 let (label_char_span, start_line, end_line) = match self.config.index_type {
61 IndexType::Char => {
62 let Some(start_line) = src.get_offset_line(given_label_span.start) else {
63 continue;
64 };
65 let end_line = if given_label_span.start >= given_label_span.end {
66 start_line.1
67 } else {
68 let Some(end_line) = src.get_offset_line(given_label_span.end - 1) else {
69 continue;
70 };
71 end_line.1
72 };
73 (given_label_span, start_line.1, end_line)
74 }
75 IndexType::Byte => {
76 let Some((start_line_obj, start_line, start_byte_col)) =
77 src.get_byte_line(given_label_span.start)
78 else {
79 continue;
80 };
81 let line_text = src.get_line_text(start_line_obj).unwrap();
82
83 let num_chars_before_start = line_text[..start_byte_col.min(line_text.len())]
84 .chars()
85 .count();
86 let start_char_offset = start_line_obj.offset() + num_chars_before_start;
87
88 if given_label_span.start >= given_label_span.end {
89 (start_char_offset..start_char_offset, start_line, start_line)
90 } else {
91 let end_pos = given_label_span.end - 1;
93 let Some((end_line_obj, end_line, end_byte_col)) =
94 src.get_byte_line(end_pos)
95 else {
96 continue;
97 };
98 let end_line_text = src.get_line_text(end_line_obj).unwrap();
99 let num_chars_before_end =
101 end_line_text[..end_byte_col + 1].chars().count();
102 let end_char_offset = end_line_obj.offset() + num_chars_before_end;
103
104 (start_char_offset..end_char_offset, start_line, end_line)
105 }
106 }
107 };
108
109 let label_info = LabelInfo {
110 kind: if start_line == end_line {
111 LabelKind::Inline
112 } else {
113 LabelKind::Multiline
114 },
115 char_span: label_char_span,
116 display_info: &label.display_info,
117 };
118
119 if let Some(group) = groups
120 .iter_mut()
121 .find(|g: &&mut SourceGroup<S>| g.src_id == label_source)
122 {
123 group.char_span.start = group.char_span.start.min(label_info.char_span.start);
124 group.char_span.end = group.char_span.end.max(label_info.char_span.end);
125 group.labels.push(label_info);
126 } else {
127 groups.push(SourceGroup {
128 src_id: label_source,
129 char_span: label_info.char_span.clone(),
130 labels: vec![label_info],
131 });
132 }
133 }
134 groups
135 }
136
137 pub fn write<C: Cache<S::SourceId>, W: Write>(&self, cache: C, w: W) -> io::Result<()> {
144 self.write_for_stream(cache, w, StreamType::Stderr)
145 }
146
147 pub fn write_for_stdout<C: Cache<S::SourceId>, W: Write>(
150 &self,
151 cache: C,
152 w: W,
153 ) -> io::Result<()> {
154 self.write_for_stream(cache, w, StreamType::Stdout)
155 }
156
157 fn write_for_stream<C: Cache<S::SourceId>, W: Write>(
160 &self,
161 mut cache: C,
162 mut w: W,
163 s: StreamType,
164 ) -> io::Result<()> {
165 let draw = match self.config.char_set {
166 CharSet::Unicode => draw::Characters::unicode(),
167 CharSet::Ascii => draw::Characters::ascii(),
168 };
169
170 let code = self.code.as_ref().map(|c| format!("[{}] ", c));
173 let id = format!("{}{}:", Show(code), self.kind);
174 let kind_color = match self.kind {
175 ReportKind::Error => self.config.error_color(),
176 ReportKind::Warning => self.config.warning_color(),
177 ReportKind::Advice => self.config.advice_color(),
178 ReportKind::Custom(_, color) => Some(color),
179 };
180 writeln!(w, "{} {}", id.fg(kind_color, s), Show(self.msg.as_ref()))?;
181
182 let groups = self.get_source_groups(&mut cache);
183
184 let line_no_width = groups
186 .iter()
187 .filter_map(
188 |SourceGroup {
189 char_span, src_id, ..
190 }| {
191 let src_name = cache
192 .display(src_id)
193 .map(|d| d.to_string())
194 .unwrap_or_else(|| "<unknown>".to_string());
195
196 let src = match cache.fetch(src_id) {
197 Ok(src) => src,
198 Err(e) => {
199 eprintln!("Unable to fetch source {}: {:?}", src_name, e);
200 return None;
201 }
202 };
203
204 let line_range = src.get_line_range(char_span);
205 Some(
206 (1..)
207 .map(|x| 10u32.pow(x))
208 .take_while(|x| line_range.end as u32 / x != 0)
209 .count()
210 + 1,
211 )
212 },
213 )
214 .max()
215 .unwrap_or(0);
216
217 let groups_len = groups.len();
219 for (
220 group_idx,
221 SourceGroup {
222 src_id,
223 char_span,
224 labels,
225 },
226 ) in groups.into_iter().enumerate()
227 {
228 let src_name = cache
229 .display(src_id)
230 .map(|d| d.to_string())
231 .unwrap_or_else(|| "<unknown>".to_string());
232
233 let src = match cache.fetch(src_id) {
234 Ok(src) => src,
235 Err(e) => {
236 eprintln!("Unable to fetch source {}: {:?}", src_name, e);
237 continue;
238 }
239 };
240
241 let line_range = src.get_line_range(&char_span);
242
243 let location = if src_id == self.span.source() {
245 self.span.start()
246 } else {
247 labels[0].char_span.start
248 };
249 let line_and_col = match self.config.index_type {
250 IndexType::Char => src.get_offset_line(location),
251 IndexType::Byte => src.get_byte_line(location).map(|(line_obj, idx, col)| {
252 let line_text = src.get_line_text(line_obj).unwrap();
253
254 let col = line_text[..col.min(line_text.len())].chars().count();
255
256 (line_obj, idx, col)
257 }),
258 };
259 let (line_no, col_no) = line_and_col
260 .map(|(_, idx, col)| {
261 (
262 format!("{}", idx + 1 + src.display_line_offset()),
263 format!("{}", col + 1),
264 )
265 })
266 .unwrap_or_else(|| ('?'.to_string(), '?'.to_string()));
267 let line_ref = format!("{}:{}:{}", src_name, line_no, col_no);
268 writeln!(
269 w,
270 "{}{}{}{} {} {}",
271 Show((' ', line_no_width + 2)),
272 if group_idx == 0 {
273 draw.ltop
274 } else {
275 draw.lcross
276 }
277 .fg(self.config.margin_color(), s),
278 draw.hbar.fg(self.config.margin_color(), s),
279 draw.lbox.fg(self.config.margin_color(), s),
280 line_ref,
281 draw.rbox.fg(self.config.margin_color(), s),
282 )?;
283
284 if !self.config.compact {
285 writeln!(
286 w,
287 "{}{}",
288 Show((' ', line_no_width + 2)),
289 draw.vbar.fg(self.config.margin_color(), s)
290 )?;
291 }
292
293 struct LineLabel<'a> {
294 col: usize,
295 label: &'a LabelInfo<'a>,
296 multi: bool,
297 draw_msg: bool,
298 }
299
300 let mut multi_labels = Vec::new();
302 let mut multi_labels_with_message = Vec::new();
303 for label_info in &labels {
304 if matches!(label_info.kind, LabelKind::Multiline) {
305 multi_labels.push(label_info);
306 if label_info.display_info.msg.is_some() {
307 multi_labels_with_message.push(label_info);
308 }
309 }
310 }
311
312 multi_labels.sort_by_key(|m| -(Span::len(&m.char_span) as isize));
314 multi_labels_with_message.sort_by_key(|m| -(Span::len(&m.char_span) as isize));
315
316 let write_margin = |w: &mut W,
317 idx: usize,
318 is_line: bool,
319 is_ellipsis: bool,
320 draw_labels: bool,
321 report_row: Option<(usize, bool)>,
322 line_labels: &[LineLabel],
323 margin_label: &Option<LineLabel>|
324 -> std::io::Result<()> {
325 let line_no_margin = if is_line && !is_ellipsis {
326 let line_no = format!("{}", idx + 1);
327 format!(
328 "{}{} {}",
329 Show((' ', line_no_width - line_no.chars().count())),
330 line_no,
331 draw.vbar,
332 )
333 .fg(self.config.margin_color(), s)
334 } else {
335 format!(
336 "{}{}",
337 Show((' ', line_no_width + 1)),
338 if is_ellipsis {
339 draw.vbar_gap
340 } else {
341 draw.vbar
342 }
343 )
344 .fg(self.config.skipped_margin_color(), s)
345 };
346
347 write!(
348 w,
349 " {}{}",
350 line_no_margin,
351 Show(Some(' ').filter(|_| !self.config.compact)),
352 )?;
353
354 if draw_labels {
356 for col in 0..multi_labels_with_message.len()
357 + (!multi_labels_with_message.is_empty()) as usize
358 {
359 let mut corner = None;
360 let mut hbar: Option<&LabelInfo> = None;
361 let mut vbar: Option<&LabelInfo> = None;
362 let mut margin_ptr = None;
363
364 let multi_label = multi_labels_with_message.get(col);
365 let line_span = src.line(idx).unwrap().span();
366
367 for (i, label) in multi_labels_with_message
368 [0..(col + 1).min(multi_labels_with_message.len())]
369 .iter()
370 .enumerate()
371 {
372 let margin = margin_label
373 .as_ref()
374 .filter(|m| std::ptr::eq(*label, m.label));
375
376 if label.char_span.start <= line_span.end
377 && label.char_span.end > line_span.start
378 {
379 let is_parent = i != col;
380 let is_start = line_span.contains(&label.char_span.start);
381 let is_end = line_span.contains(&label.last_offset());
382
383 if let Some(margin) = margin.filter(|_| is_line) {
384 margin_ptr = Some((margin, is_start));
385 } else if !is_start && (!is_end || is_line) {
386 vbar = vbar.or(Some(*label).filter(|_| !is_parent));
387 } else if let Some((report_row, is_arrow)) = report_row {
388 let label_row = line_labels
389 .iter()
390 .enumerate()
391 .find(|(_, l)| std::ptr::eq(*label, l.label))
392 .map_or(0, |(r, _)| r);
393 if report_row == label_row {
394 if let Some(margin) = margin {
395 vbar = Some(margin.label).filter(|_| col == i);
396 if is_start {
397 continue;
398 }
399 }
400
401 if is_arrow {
402 hbar = Some(*label);
403 if !is_parent {
404 corner = Some((label, is_start));
405 }
406 } else if !is_start {
407 vbar = vbar.or(Some(*label).filter(|_| !is_parent));
408 }
409 } else {
410 vbar = vbar.or(Some(*label).filter(|_| {
411 !is_parent && (is_start ^ (report_row < label_row))
412 }));
413 }
414 }
415 }
416 }
417
418 if let (Some((margin, _is_start)), true) = (margin_ptr, is_line) {
419 let is_col =
420 multi_label.map_or(false, |ml| std::ptr::eq(*ml, margin.label));
421 let is_limit = col + 1 == multi_labels_with_message.len();
422 if !is_col && !is_limit {
423 hbar = hbar.or(Some(margin.label));
424 }
425 }
426
427 hbar = hbar.filter(|l| {
428 margin_label
429 .as_ref()
430 .map_or(true, |margin| !std::ptr::eq(margin.label, *l))
431 || !is_line
432 });
433
434 let (a, b) = if let Some((label, is_start)) = corner {
435 (
436 if is_start { draw.ltop } else { draw.lbot }
437 .fg(label.display_info.color, s),
438 draw.hbar.fg(label.display_info.color, s),
439 )
440 } else if let Some(label) =
441 hbar.filter(|_| vbar.is_some() && !self.config.cross_gap)
442 {
443 (
444 draw.xbar.fg(label.display_info.color, s),
445 draw.hbar.fg(label.display_info.color, s),
446 )
447 } else if let Some(label) = hbar {
448 (
449 draw.hbar.fg(label.display_info.color, s),
450 draw.hbar.fg(label.display_info.color, s),
451 )
452 } else if let Some(label) = vbar {
453 (
454 if is_ellipsis {
455 draw.vbar_gap
456 } else {
457 draw.vbar
458 }
459 .fg(label.display_info.color, s),
460 ' '.fg(None, s),
461 )
462 } else if let (Some((margin, is_start)), true) = (margin_ptr, is_line) {
463 let is_col =
464 multi_label.map_or(false, |ml| std::ptr::eq(*ml, margin.label));
465 let is_limit = col == multi_labels_with_message.len();
466 (
467 if is_limit {
468 draw.rarrow
469 } else if is_col {
470 if is_start {
471 draw.ltop
472 } else {
473 draw.lcross
474 }
475 } else {
476 draw.hbar
477 }
478 .fg(margin.label.display_info.color, s),
479 if !is_limit { draw.hbar } else { ' ' }
480 .fg(margin.label.display_info.color, s),
481 )
482 } else {
483 (' '.fg(None, s), ' '.fg(None, s))
484 };
485 write!(w, "{}", a)?;
486 if !self.config.compact {
487 write!(w, "{}", b)?;
488 }
489 }
490 }
491
492 Ok(())
493 };
494
495 let mut is_ellipsis = false;
496 for idx in line_range {
497 let line = if let Some(line) = src.line(idx) {
498 line
499 } else {
500 continue;
501 };
502
503 let margin_label = multi_labels_with_message
504 .iter()
505 .enumerate()
506 .filter_map(|(_i, label)| {
507 let is_start = line.span().contains(&label.char_span.start);
508 let is_end = line.span().contains(&label.last_offset());
509 if is_start {
510 Some(LineLabel {
512 col: label.char_span.start - line.offset(),
513 label,
514 multi: true,
515 draw_msg: false, })
517 } else if is_end {
518 Some(LineLabel {
519 col: label.last_offset() - line.offset(),
520 label,
521 multi: true,
522 draw_msg: true, })
524 } else {
525 None
526 }
527 })
528 .min_by_key(|ll| (ll.col, !ll.label.char_span.start));
529
530 let mut line_labels = multi_labels_with_message
532 .iter()
533 .enumerate()
534 .filter_map(|(_i, label)| {
535 let is_start = line.span().contains(&label.char_span.start);
536 let is_end = line.span().contains(&label.last_offset());
537 if is_start
538 && margin_label
539 .as_ref()
540 .map_or(true, |m| !std::ptr::eq(*label, m.label))
541 {
542 Some(LineLabel {
544 col: label.char_span.start - line.offset(),
545 label,
546 multi: true,
547 draw_msg: false, })
549 } else if is_end {
550 Some(LineLabel {
551 col: label.last_offset() - line.offset(),
552 label,
553 multi: true,
554 draw_msg: true, })
556 } else {
557 None
558 }
559 })
560 .collect::<Vec<_>>();
561
562 for label_info in labels.iter().filter(|l| {
563 l.char_span.start >= line.span().start && l.char_span.end <= line.span().end
564 }) {
565 if matches!(label_info.kind, LabelKind::Inline) {
566 line_labels.push(LineLabel {
567 col: match &self.config.label_attach {
568 LabelAttach::Start => label_info.char_span.start,
569 LabelAttach::Middle => {
570 (label_info.char_span.start + label_info.char_span.end) / 2
571 }
572 LabelAttach::End => label_info.last_offset(),
573 }
574 .max(label_info.char_span.start)
575 - line.offset(),
576 label: label_info,
577 multi: false,
578 draw_msg: true,
579 });
580 }
581 }
582
583 if line_labels.is_empty() && margin_label.is_none() {
585 let within_label = multi_labels
586 .iter()
587 .any(|label| label.char_span.contains(&line.span().start()));
588 if !is_ellipsis && within_label {
589 is_ellipsis = true;
590 } else {
591 if !self.config.compact && !is_ellipsis {
592 write_margin(&mut w, idx, false, is_ellipsis, false, None, &[], &None)?;
593 writeln!(w)?;
594 }
595 is_ellipsis = true;
596 continue;
597 }
598 } else {
599 is_ellipsis = false;
600 }
601
602 line_labels.sort_by_key(|ll| {
604 (
605 ll.label.display_info.order,
606 ll.col,
607 !ll.label.char_span.start,
608 )
609 });
610
611 let arrow_end_space = if self.config.compact { 1 } else { 2 };
613 let arrow_len = line_labels.iter().fold(0, |l, ll| {
614 if ll.multi {
615 line.len()
616 } else {
617 l.max(ll.label.char_span.end().saturating_sub(line.offset()))
618 }
619 }) + arrow_end_space;
620
621 let get_vbar = |col, row| {
623 line_labels
624 .iter()
625 .enumerate()
627 .filter(|(_, ll)| {
628 ll.label.display_info.msg.is_some()
629 && margin_label
630 .as_ref()
631 .map_or(true, |m| !std::ptr::eq(ll.label, m.label))
632 })
633 .find(|(j, ll)| ll.col == col && row <= *j)
634 .map(|(_, ll)| ll)
635 };
636
637 let get_highlight = |col| {
638 margin_label
639 .iter()
640 .map(|ll| &ll.label)
641 .chain(multi_labels.iter())
642 .chain(line_labels.iter().map(|l| &l.label))
643 .filter(|l| l.char_span.contains(&(line.offset() + col)))
644 .min_by_key(|l| {
646 (
647 -l.display_info.priority,
648 ExactSizeIterator::len(&l.char_span),
649 )
650 })
651 };
652
653 let get_underline = |col| {
654 line_labels
655 .iter()
656 .filter(|ll| {
657 self.config.underlines
658 && !ll.multi
660 && ll.label.char_span.contains(&(line.offset() + col))
661 })
662 .min_by_key(|ll| {
664 (
665 -ll.label.display_info.priority,
666 ExactSizeIterator::len(&ll.label.char_span),
667 )
668 })
669 };
670
671 write_margin(
673 &mut w,
674 idx,
675 true,
676 is_ellipsis,
677 true,
678 None,
679 &line_labels,
680 &margin_label,
681 )?;
682
683 if !is_ellipsis {
685 for (col, c) in src
686 .get_line_text(line)
687 .unwrap()
688 .trim_end()
689 .chars()
690 .enumerate()
691 {
692 let color = if let Some(highlight) = get_highlight(col) {
693 highlight.display_info.color
694 } else {
695 self.config.unimportant_color()
696 };
697 let (c, width) = self.config.char_width(c, col);
698 if c.is_whitespace() {
699 for _ in 0..width {
700 write!(w, "{}", c.fg(color, s))?;
701 }
702 } else {
703 write!(w, "{}", c.fg(color, s))?;
704 };
705 }
706 }
707 writeln!(w)?;
708
709 for row in 0..line_labels.len() {
711 let line_label = &line_labels[row];
712 if line_label.label.display_info.msg.is_none() {
714 continue;
715 }
716 if !self.config.compact {
717 write_margin(
719 &mut w,
720 idx,
721 false,
722 is_ellipsis,
723 true,
724 Some((row, false)),
725 &line_labels,
726 &margin_label,
727 )?;
728 let mut chars = src.get_line_text(line).unwrap().trim_end().chars();
730 for col in 0..arrow_len {
731 let width =
732 chars.next().map_or(1, |c| self.config.char_width(c, col).1);
733
734 let vbar = get_vbar(col, row);
735 let underline = get_underline(col).filter(|_| row == 0);
736 let [c, tail] = if let Some(vbar_ll) = vbar {
737 let [c, tail] = if underline.is_some() {
738 #[allow(clippy::overly_complex_bool_expr)]
742 if ExactSizeIterator::len(&vbar_ll.label.char_span) <= 1 || true
743 {
744 [draw.underbar, draw.underline]
745 } else if line.offset() + col == vbar_ll.label.char_span.start {
746 [draw.ltop, draw.underbar]
747 } else if line.offset() + col == vbar_ll.label.last_offset() {
748 [draw.rtop, draw.underbar]
749 } else {
750 [draw.underbar, draw.underline]
751 }
752 } else if vbar_ll.multi && row == 0 && self.config.multiline_arrows
753 {
754 [draw.uarrow, ' ']
755 } else {
756 [draw.vbar, ' ']
757 };
758 [
759 c.fg(vbar_ll.label.display_info.color, s),
760 tail.fg(vbar_ll.label.display_info.color, s),
761 ]
762 } else if let Some(underline_ll) = underline {
763 [draw.underline.fg(underline_ll.label.display_info.color, s); 2]
764 } else {
765 [' '.fg(None, s); 2]
766 };
767
768 for i in 0..width {
769 write!(w, "{}", if i == 0 { c } else { tail })?;
770 }
771 }
772 writeln!(w)?;
773 }
774
775 write_margin(
777 &mut w,
778 idx,
779 false,
780 is_ellipsis,
781 true,
782 Some((row, true)),
783 &line_labels,
784 &margin_label,
785 )?;
786 let mut chars = src.get_line_text(line).unwrap().trim_end().chars();
788 for col in 0..arrow_len {
789 let width = chars.next().map_or(1, |c| self.config.char_width(c, col).1);
790
791 let is_hbar = (((col > line_label.col) ^ line_label.multi)
792 || (line_label.label.display_info.msg.is_some()
793 && line_label.draw_msg
794 && col > line_label.col))
795 && line_label.label.display_info.msg.is_some();
796 let [c, tail] = if col == line_label.col
797 && line_label.label.display_info.msg.is_some()
798 && margin_label
799 .as_ref()
800 .map_or(true, |m| !std::ptr::eq(line_label.label, m.label))
801 {
802 [
803 if line_label.multi {
804 if line_label.draw_msg {
805 draw.mbot
806 } else {
807 draw.rbot
808 }
809 } else {
810 draw.lbot
811 }
812 .fg(line_label.label.display_info.color, s),
813 draw.hbar.fg(line_label.label.display_info.color, s),
814 ]
815 } else if let Some(vbar_ll) = get_vbar(col, row).filter(|_| {
816 col != line_label.col || line_label.label.display_info.msg.is_some()
817 }) {
818 if !self.config.cross_gap && is_hbar {
819 [
820 draw.xbar.fg(line_label.label.display_info.color, s),
821 ' '.fg(line_label.label.display_info.color, s),
822 ]
823 } else if is_hbar {
824 [draw.hbar.fg(line_label.label.display_info.color, s); 2]
825 } else {
826 [
827 if vbar_ll.multi && row == 0 && self.config.compact {
828 draw.uarrow
829 } else {
830 draw.vbar
831 }
832 .fg(vbar_ll.label.display_info.color, s),
833 ' '.fg(line_label.label.display_info.color, s),
834 ]
835 }
836 } else if is_hbar {
837 [draw.hbar.fg(line_label.label.display_info.color, s); 2]
838 } else {
839 [' '.fg(None, s); 2]
840 };
841
842 if width > 0 {
843 write!(w, "{}", c)?;
844 }
845 for _ in 1..width {
846 write!(w, "{}", tail)?;
847 }
848 }
849 if line_label.draw_msg {
850 write!(w, " {}", Show(line_label.label.display_info.msg.as_ref()))?;
851 }
852 writeln!(w)?;
853 }
854 }
855
856 let is_final_group = group_idx + 1 == groups_len;
857
858 if is_final_group {
860 for (i, help) in self.help.iter().enumerate() {
861 if !self.config.compact {
862 write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
863 writeln!(w)?;
864 }
865 let help_prefix = format!("{} {}", "Help", i + 1);
866 let help_prefix_len = if self.help.len() > 1 {
867 help_prefix.len()
868 } else {
869 4
870 };
871 let mut lines = help.lines();
872 if let Some(line) = lines.next() {
873 write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
874 if self.help.len() > 1 {
875 writeln!(
876 w,
877 "{}: {}",
878 help_prefix.fg(self.config.note_color(), s),
879 line
880 )?;
881 } else {
882 writeln!(w, "{}: {}", "Help".fg(self.config.note_color(), s), line)?;
883 }
884 }
885 for line in lines {
886 write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
887 writeln!(w, "{:>pad$}{}", "", line, pad = help_prefix_len + 2)?;
888 }
889 }
890 }
891
892 if is_final_group {
894 for (i, note) in self.notes.iter().enumerate() {
895 if !self.config.compact {
896 write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
897 writeln!(w)?;
898 }
899 let note_prefix = format!("{} {}", "Note", i + 1);
900 let note_prefix_len = if self.notes.len() > 1 {
901 note_prefix.len()
902 } else {
903 4
904 };
905 let mut lines = note.lines();
906 if let Some(line) = lines.next() {
907 write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
908 if self.notes.len() > 1 {
909 writeln!(
910 w,
911 "{}: {}",
912 note_prefix.fg(self.config.note_color(), s),
913 line
914 )?;
915 } else {
916 writeln!(w, "{}: {}", "Note".fg(self.config.note_color(), s), line)?;
917 }
918 }
919 for line in lines {
920 write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
921 writeln!(w, "{:>pad$}{}", "", line, pad = note_prefix_len + 2)?;
922 }
923 }
924 }
925
926 if !self.config.compact {
928 if is_final_group {
929 let final_margin =
930 format!("{}{}", Show((draw.hbar, line_no_width + 2)), draw.rbot);
931 writeln!(w, "{}", final_margin.fg(self.config.margin_color(), s))?;
932 } else {
933 writeln!(
934 w,
935 "{}{}",
936 Show((' ', line_no_width + 2)),
937 draw.vbar.fg(self.config.margin_color(), s)
938 )?;
939 }
940 }
941 }
942 Ok(())
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use insta::assert_snapshot;
958
959 use crate::{Cache, CharSet, Config, IndexType, Label, Report, ReportKind, Source, Span};
960
961 impl<S: Span> Report<'_, S> {
962 fn write_to_string<C: Cache<S::SourceId>>(&self, cache: C) -> String {
963 let mut vec = Vec::new();
964 self.write(cache, &mut vec).unwrap();
965 String::from_utf8(vec).unwrap()
966 }
967 }
968
969 fn no_color_and_ascii() -> Config {
970 Config::default()
971 .with_color(false)
972 .with_char_set(CharSet::Ascii)
975 }
976
977 fn remove_trailing(s: String) -> String {
978 s.lines().flat_map(|l| [l.trim_end(), "\n"]).collect()
979 }
980
981 #[test]
982 fn one_message() {
983 let msg = remove_trailing(
984 Report::build(ReportKind::Error, 0..0)
985 .with_config(no_color_and_ascii())
986 .with_message("can't compare apples with oranges")
987 .finish()
988 .write_to_string(Source::from("")),
989 );
990 assert_snapshot!(msg, @r###"
991 Error: can't compare apples with oranges
992 "###)
993 }
994
995 #[test]
996 fn two_labels_without_messages() {
997 let source = "apple == orange;";
998 let msg = remove_trailing(
999 Report::build(ReportKind::Error, 0..0)
1000 .with_config(no_color_and_ascii())
1001 .with_message("can't compare apples with oranges")
1002 .with_label(Label::new(0..5))
1003 .with_label(Label::new(9..15))
1004 .finish()
1005 .write_to_string(Source::from(source)),
1006 );
1007 assert_snapshot!(msg, @r###"
1009 Error: can't compare apples with oranges
1010 ,-[ <unknown>:1:1 ]
1011 |
1012 1 | apple == orange;
1013 ---'
1014 "###);
1015 }
1016
1017 #[test]
1018 fn two_labels_with_messages() {
1019 let source = "apple == orange;";
1020 let msg = remove_trailing(
1021 Report::build(ReportKind::Error, 0..0)
1022 .with_config(no_color_and_ascii())
1023 .with_message("can't compare apples with oranges")
1024 .with_label(Label::new(0..5).with_message("This is an apple"))
1025 .with_label(Label::new(9..15).with_message("This is an orange"))
1026 .finish()
1027 .write_to_string(Source::from(source)),
1028 );
1029 assert_snapshot!(msg, @r###"
1031 Error: can't compare apples with oranges
1032 ,-[ <unknown>:1:1 ]
1033 |
1034 1 | apple == orange;
1035 | ^^|^^ ^^^|^^
1036 | `-------------- This is an apple
1037 | |
1038 | `---- This is an orange
1039 ---'
1040 "###);
1041 }
1042
1043 #[test]
1044 fn multi_byte_chars() {
1045 let source = "äpplë == örängë;";
1046 let msg = remove_trailing(
1047 Report::build(ReportKind::Error, 0..0)
1048 .with_config(no_color_and_ascii().with_index_type(IndexType::Char))
1049 .with_message("can't compare äpplës with örängës")
1050 .with_label(Label::new(0..5).with_message("This is an äpplë"))
1051 .with_label(Label::new(9..15).with_message("This is an örängë"))
1052 .finish()
1053 .write_to_string(Source::from(source)),
1054 );
1055 assert_snapshot!(msg, @r###"
1057 Error: can't compare äpplës with örängës
1058 ,-[ <unknown>:1:1 ]
1059 |
1060 1 | äpplë == örängë;
1061 | ^^|^^ ^^^|^^
1062 | `-------------- This is an äpplë
1063 | |
1064 | `---- This is an örängë
1065 ---'
1066 "###);
1067 }
1068
1069 #[test]
1070 fn byte_label() {
1071 let source = "äpplë == örängë;";
1072 let msg = remove_trailing(
1073 Report::build(ReportKind::Error, 0..0)
1074 .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1075 .with_message("can't compare äpplës with örängës")
1076 .with_label(Label::new(0..7).with_message("This is an äpplë"))
1077 .with_label(Label::new(11..20).with_message("This is an örängë"))
1078 .finish()
1079 .write_to_string(Source::from(source)),
1080 );
1081 assert_snapshot!(msg, @r###"
1083 Error: can't compare äpplës with örängës
1084 ,-[ <unknown>:1:1 ]
1085 |
1086 1 | äpplë == örängë;
1087 | ^^|^^ ^^^|^^
1088 | `-------------- This is an äpplë
1089 | |
1090 | `---- This is an örängë
1091 ---'
1092 "###);
1093 }
1094
1095 #[test]
1096 fn byte_column() {
1097 let source = "äpplë == örängë;";
1098 let msg = remove_trailing(
1099 Report::build(ReportKind::Error, 11..11)
1100 .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1101 .with_message("can't compare äpplës with örängës")
1102 .with_label(Label::new(0..7).with_message("This is an äpplë"))
1103 .with_label(Label::new(11..20).with_message("This is an örängë"))
1104 .finish()
1105 .write_to_string(Source::from(source)),
1106 );
1107 assert_snapshot!(msg, @r###"
1109 Error: can't compare äpplës with örängës
1110 ,-[ <unknown>:1:10 ]
1111 |
1112 1 | äpplë == örängë;
1113 | ^^|^^ ^^^|^^
1114 | `-------------- This is an äpplë
1115 | |
1116 | `---- This is an örängë
1117 ---'
1118 "###);
1119 }
1120
1121 #[test]
1122 fn label_at_end_of_long_line() {
1123 let source = format!("{}orange", "apple == ".repeat(100));
1124 let msg = remove_trailing(
1125 Report::build(ReportKind::Error, 0..0)
1126 .with_config(no_color_and_ascii())
1127 .with_message("can't compare apples with oranges")
1128 .with_label(
1129 Label::new(source.len() - 5..source.len()).with_message("This is an orange"),
1130 )
1131 .finish()
1132 .write_to_string(Source::from(source)),
1133 );
1134 assert_snapshot!(msg, @r###"
1136 Error: can't compare apples with oranges
1137 ,-[ <unknown>:1:1 ]
1138 |
1139 1 | apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == orange
1140 | ^^|^^
1141 | `---- This is an orange
1142 ---'
1143 "###);
1144 }
1145
1146 #[test]
1147 fn label_of_width_zero_at_end_of_line() {
1148 let source = "apple ==\n";
1149 let msg = remove_trailing(
1150 Report::build(ReportKind::Error, 0..0)
1151 .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1152 .with_message("unexpected end of file")
1153 .with_label(Label::new(9..9).with_message("Unexpected end of file"))
1154 .finish()
1155 .write_to_string(Source::from(source)),
1156 );
1157
1158 assert_snapshot!(msg, @r###"
1159 Error: unexpected end of file
1160 ,-[ <unknown>:1:1 ]
1161 |
1162 1 | apple ==
1163 | |
1164 | `- Unexpected end of file
1165 ---'
1166 "###);
1167 }
1168
1169 #[test]
1170 fn empty_input() {
1171 let source = "";
1172 let msg = remove_trailing(
1173 Report::build(ReportKind::Error, 0..0)
1174 .with_config(no_color_and_ascii())
1175 .with_message("unexpected end of file")
1176 .with_label(Label::new(0..0).with_message("No more fruit!"))
1177 .finish()
1178 .write_to_string(Source::from(source)),
1179 );
1180
1181 assert_snapshot!(msg, @r###"
1182 Error: unexpected end of file
1183 ,-[ <unknown>:1:1 ]
1184 |
1185 1 |
1186 | |
1187 | `- No more fruit!
1188 ---'
1189 "###);
1190 }
1191
1192 #[test]
1193 fn empty_input_help() {
1194 let source = "";
1195 let msg = remove_trailing(
1196 Report::build(ReportKind::Error, 0..0)
1197 .with_config(no_color_and_ascii())
1198 .with_message("unexpected end of file")
1199 .with_label(Label::new(0..0).with_message("No more fruit!"))
1200 .with_help("have you tried going to the farmer's market?")
1201 .finish()
1202 .write_to_string(Source::from(source)),
1203 );
1204
1205 assert_snapshot!(msg, @r###"
1206 Error: unexpected end of file
1207 ,-[ <unknown>:1:1 ]
1208 |
1209 1 |
1210 | |
1211 | `- No more fruit!
1212 |
1213 | Help: have you tried going to the farmer's market?
1214 ---'
1215 "###);
1216 }
1217
1218 #[test]
1219 fn empty_input_note() {
1220 let source = "";
1221 let msg = remove_trailing(
1222 Report::build(ReportKind::Error, 0..0)
1223 .with_config(no_color_and_ascii())
1224 .with_message("unexpected end of file")
1225 .with_label(Label::new(0..0).with_message("No more fruit!"))
1226 .with_note("eat your greens!")
1227 .finish()
1228 .write_to_string(Source::from(source)),
1229 );
1230
1231 assert_snapshot!(msg, @r###"
1232 Error: unexpected end of file
1233 ,-[ <unknown>:1:1 ]
1234 |
1235 1 |
1236 | |
1237 | `- No more fruit!
1238 |
1239 | Note: eat your greens!
1240 ---'
1241 "###);
1242 }
1243
1244 #[test]
1245 fn empty_input_help_note() {
1246 let source = "";
1247 let msg = remove_trailing(
1248 Report::build(ReportKind::Error, 0..0)
1249 .with_config(no_color_and_ascii())
1250 .with_message("unexpected end of file")
1251 .with_label(Label::new(0..0).with_message("No more fruit!"))
1252 .with_note("eat your greens!")
1253 .with_help("have you tried going to the farmer's market?")
1254 .finish()
1255 .write_to_string(Source::from(source)),
1256 );
1257
1258 assert_snapshot!(msg, @r###"
1259 Error: unexpected end of file
1260 ,-[ <unknown>:1:1 ]
1261 |
1262 1 |
1263 | |
1264 | `- No more fruit!
1265 |
1266 | Help: have you tried going to the farmer's market?
1267 |
1268 | Note: eat your greens!
1269 ---'
1270 "###);
1271 }
1272
1273 #[test]
1274 fn byte_spans_never_crash() {
1275 let source = "apple\np\n\nempty\n";
1276
1277 for i in 0..=source.len() {
1278 for j in i..=source.len() {
1279 let _ = remove_trailing(
1280 Report::build(ReportKind::Error, 0..0)
1281 .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1282 .with_message("Label")
1283 .with_label(Label::new(i..j).with_message("Label"))
1284 .finish()
1285 .write_to_string(Source::from(source)),
1286 );
1287 }
1288 }
1289 }
1290
1291 #[test]
1292 fn multiline_label() {
1293 let source = "apple\n==\norange";
1294 let msg = remove_trailing(
1295 Report::build(ReportKind::Error, 0..0)
1296 .with_config(no_color_and_ascii())
1297 .with_label(Label::new(0..source.len()).with_message("illegal comparison"))
1298 .finish()
1299 .write_to_string(Source::from(source)),
1300 );
1301 assert_snapshot!(msg, @r###"
1303 Error:
1304 ,-[ <unknown>:1:1 ]
1305 |
1306 1 | ,-> apple
1307 : :
1308 3 | |-> orange
1309 | |
1310 | `----------- illegal comparison
1311 ---'
1312 "###);
1313 }
1314
1315 #[test]
1316 fn partially_overlapping_labels() {
1317 let source = "https://example.com/";
1318 let msg = remove_trailing(
1319 Report::build(ReportKind::Error, 0..0)
1320 .with_config(no_color_and_ascii())
1321 .with_label(Label::new(0..source.len()).with_message("URL"))
1322 .with_label(Label::new(0..source.find(':').unwrap()).with_message("scheme"))
1323 .finish()
1324 .write_to_string(Source::from(source)),
1325 );
1326 assert_snapshot!(msg, @r###"
1328 Error:
1329 ,-[ <unknown>:1:1 ]
1330 |
1331 1 | https://example.com/
1332 | ^^|^^^^^^^|^^^^^^^^^
1333 | `------------------- scheme
1334 | |
1335 | `----------- URL
1336 ---'
1337 "###);
1338 }
1339
1340 #[test]
1341 fn multiple_labels_same_span() {
1342 let source = "apple == orange;";
1343 let msg = remove_trailing(
1344 Report::build(ReportKind::Error, 0..0)
1345 .with_config(no_color_and_ascii())
1346 .with_message("can't compare apples with oranges")
1347 .with_label(Label::new(0..5).with_message("This is an apple"))
1348 .with_label(
1349 Label::new(0..5).with_message("Have I mentioned that this is an apple?"),
1350 )
1351 .with_label(Label::new(0..5).with_message("No really, have I mentioned that?"))
1352 .with_label(Label::new(9..15).with_message("This is an orange"))
1353 .with_label(
1354 Label::new(9..15).with_message("Have I mentioned that this is an orange?"),
1355 )
1356 .with_label(Label::new(9..15).with_message("No really, have I mentioned that?"))
1357 .finish()
1358 .write_to_string(Source::from(source)),
1359 );
1360 assert_snapshot!(msg, @r###"
1361 Error: can't compare apples with oranges
1362 ,-[ <unknown>:1:1 ]
1363 |
1364 1 | apple == orange;
1365 | ^^|^^ ^^^|^^
1366 | `-------------- This is an apple
1367 | | |
1368 | `-------------- Have I mentioned that this is an apple?
1369 | | |
1370 | `-------------- No really, have I mentioned that?
1371 | |
1372 | `---- This is an orange
1373 | |
1374 | `---- Have I mentioned that this is an orange?
1375 | |
1376 | `---- No really, have I mentioned that?
1377 ---'
1378 "###)
1379 }
1380
1381 #[test]
1382 fn note() {
1383 let source = "apple == orange;";
1384 let msg = remove_trailing(
1385 Report::build(ReportKind::Error, 0..0)
1386 .with_config(no_color_and_ascii())
1387 .with_message("can't compare apples with oranges")
1388 .with_label(Label::new(0..5).with_message("This is an apple"))
1389 .with_label(Label::new(9..15).with_message("This is an orange"))
1390 .with_note("stop trying ... this is a fruitless endeavor")
1391 .finish()
1392 .write_to_string(Source::from(source)),
1393 );
1394 assert_snapshot!(msg, @r###"
1395 Error: can't compare apples with oranges
1396 ,-[ <unknown>:1:1 ]
1397 |
1398 1 | apple == orange;
1399 | ^^|^^ ^^^|^^
1400 | `-------------- This is an apple
1401 | |
1402 | `---- This is an orange
1403 |
1404 | Note: stop trying ... this is a fruitless endeavor
1405 ---'
1406 "###)
1407 }
1408
1409 #[test]
1410 fn help() {
1411 let source = "apple == orange;";
1412 let msg = remove_trailing(
1413 Report::build(ReportKind::Error, 0..0)
1414 .with_config(no_color_and_ascii())
1415 .with_message("can't compare apples with oranges")
1416 .with_label(Label::new(0..5).with_message("This is an apple"))
1417 .with_label(Label::new(9..15).with_message("This is an orange"))
1418 .with_help("have you tried peeling the orange?")
1419 .finish()
1420 .write_to_string(Source::from(source)),
1421 );
1422 assert_snapshot!(msg, @r###"
1423 Error: can't compare apples with oranges
1424 ,-[ <unknown>:1:1 ]
1425 |
1426 1 | apple == orange;
1427 | ^^|^^ ^^^|^^
1428 | `-------------- This is an apple
1429 | |
1430 | `---- This is an orange
1431 |
1432 | Help: have you tried peeling the orange?
1433 ---'
1434 "###)
1435 }
1436
1437 #[test]
1438 fn help_and_note() {
1439 let source = "apple == orange;";
1440 let msg = remove_trailing(
1441 Report::build(ReportKind::Error, 0..0)
1442 .with_config(no_color_and_ascii())
1443 .with_message("can't compare apples with oranges")
1444 .with_label(Label::new(0..5).with_message("This is an apple"))
1445 .with_label(Label::new(9..15).with_message("This is an orange"))
1446 .with_help("have you tried peeling the orange?")
1447 .with_note("stop trying ... this is a fruitless endeavor")
1448 .finish()
1449 .write_to_string(Source::from(source)),
1450 );
1451 assert_snapshot!(msg, @r###"
1452 Error: can't compare apples with oranges
1453 ,-[ <unknown>:1:1 ]
1454 |
1455 1 | apple == orange;
1456 | ^^|^^ ^^^|^^
1457 | `-------------- This is an apple
1458 | |
1459 | `---- This is an orange
1460 |
1461 | Help: have you tried peeling the orange?
1462 |
1463 | Note: stop trying ... this is a fruitless endeavor
1464 ---'
1465 "###)
1466 }
1467
1468 #[test]
1469 fn single_note_single_line() {
1470 let source = "apple == orange;";
1471 let msg = remove_trailing(
1472 Report::build(ReportKind::Error, 0..0)
1473 .with_config(no_color_and_ascii())
1474 .with_message("can't compare apples with oranges")
1475 .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1476 .with_note("No need to try, they can't be compared.")
1477 .finish()
1478 .write_to_string(Source::from(source)),
1479 );
1480 assert_snapshot!(msg, @r###"
1481 Error: can't compare apples with oranges
1482 ,-[ <unknown>:1:1 ]
1483 |
1484 1 | apple == orange;
1485 | ^^^^^^^|^^^^^^^
1486 | `--------- This is a strange comparison
1487 |
1488 | Note: No need to try, they can't be compared.
1489 ---'
1490 "###)
1491 }
1492
1493 #[test]
1494 fn multi_notes_single_lines() {
1495 let source = "apple == orange;";
1496 let msg = remove_trailing(
1497 Report::build(ReportKind::Error, 0..0)
1498 .with_config(no_color_and_ascii())
1499 .with_message("can't compare apples with oranges")
1500 .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1501 .with_note("No need to try, they can't be compared.")
1502 .with_note("Yeah, really, please stop.")
1503 .finish()
1504 .write_to_string(Source::from(source)),
1505 );
1506 assert_snapshot!(msg, @r###"
1507 Error: can't compare apples with oranges
1508 ,-[ <unknown>:1:1 ]
1509 |
1510 1 | apple == orange;
1511 | ^^^^^^^|^^^^^^^
1512 | `--------- This is a strange comparison
1513 |
1514 | Note 1: No need to try, they can't be compared.
1515 |
1516 | Note 2: Yeah, really, please stop.
1517 ---'
1518 "###)
1519 }
1520
1521 #[test]
1522 fn multi_notes_multi_lines() {
1523 let source = "apple == orange;";
1524 let msg = remove_trailing(
1525 Report::build(ReportKind::Error, 0..0)
1526 .with_config(no_color_and_ascii())
1527 .with_message("can't compare apples with oranges")
1528 .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1529 .with_note("No need to try, they can't be compared.")
1530 .with_note("Yeah, really, please stop.\nIt has no resemblance.")
1531 .finish()
1532 .write_to_string(Source::from(source)),
1533 );
1534 assert_snapshot!(msg, @r###"
1535 Error: can't compare apples with oranges
1536 ,-[ <unknown>:1:1 ]
1537 |
1538 1 | apple == orange;
1539 | ^^^^^^^|^^^^^^^
1540 | `--------- This is a strange comparison
1541 |
1542 | Note 1: No need to try, they can't be compared.
1543 |
1544 | Note 2: Yeah, really, please stop.
1545 | It has no resemblance.
1546 ---'
1547 "###)
1548 }
1549
1550 #[test]
1551 fn multi_helps_multi_lines() {
1552 let source = "apple == orange;";
1553 let msg = remove_trailing(
1554 Report::build(ReportKind::Error, 0..0)
1555 .with_config(no_color_and_ascii())
1556 .with_message("can't compare apples with oranges")
1557 .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1558 .with_help("No need to try, they can't be compared.")
1559 .with_help("Yeah, really, please stop.\nIt has no resemblance.")
1560 .finish()
1561 .write_to_string(Source::from(source)),
1562 );
1563 assert_snapshot!(msg, @r###"
1564 Error: can't compare apples with oranges
1565 ,-[ <unknown>:1:1 ]
1566 |
1567 1 | apple == orange;
1568 | ^^^^^^^|^^^^^^^
1569 | `--------- This is a strange comparison
1570 |
1571 | Help 1: No need to try, they can't be compared.
1572 |
1573 | Help 2: Yeah, really, please stop.
1574 | It has no resemblance.
1575 ---'
1576 "###)
1577 }
1578}