1use crate::event::{EventView, Value, flatten, theme_style};
22use crate::hyperlink::Hyperlinker;
23use crate::layout::{OverflowPolicy, fit_segments};
24use crate::stacktrace::StackTrace;
25use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
26use std::ops::Range;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum DiagnosticError {
30 InvalidSpan,
31}
32impl std::fmt::Display for DiagnosticError {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(f, "source span is outside UTF-8 boundaries")
35 }
36}
37impl std::error::Error for DiagnosticError {}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum Level {
42 Error,
43 Warning,
44 Info,
45 Note,
46 Help,
47}
48
49impl Level {
50 pub fn name(self) -> &'static str {
52 match self {
53 Level::Error => "error",
54 Level::Warning => "warning",
55 Level::Info => "info",
56 Level::Note => "note",
57 Level::Help => "help",
58 }
59 }
60
61 pub fn style(self, console: &Console) -> Style {
63 let fallback = match self {
64 Level::Error => "bold red",
65 Level::Warning => "bold yellow",
66 Level::Info => "bold blue",
67 Level::Note => "bold green",
68 Level::Help => "bold cyan",
69 };
70 theme_style(console, &format!("diagnostic.{}", self.name()), fallback)
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
76pub struct Location {
77 pub path: String,
78 pub line: Option<usize>,
80 pub column: Option<usize>,
82}
83
84impl Location {
85 pub fn new(path: impl Into<String>, line: Option<usize>, column: Option<usize>) -> Self {
86 Location {
87 path: path.into(),
88 line,
89 column,
90 }
91 }
92}
93
94#[derive(Clone, Debug)]
95struct SpanLabel {
96 span: Range<usize>,
97 message: Option<String>,
98 primary: bool,
99}
100
101fn check_span(source: &str, span: &Range<usize>) -> Result<(), DiagnosticError> {
102 if span.start > span.end
103 || span.end > source.len()
104 || !source.is_char_boundary(span.start)
105 || !source.is_char_boundary(span.end)
106 {
107 return Err(DiagnosticError::InvalidSpan);
108 }
109 Ok(())
110}
111
112fn marker_cells(line: &str, a: usize, b: usize) -> (usize, usize) {
115 let (mut a, mut b) = (a.min(line.len()), b.min(line.len()));
116 for (begin, end, _) in rich::cells::split_graphemes(line).0 {
119 if begin < a && a < end {
120 a = begin;
121 }
122 if begin < b && b < end {
123 b = end;
124 }
125 }
126 let mut before = Text::new(&line[..a]);
127 before.expand_tabs(8);
128 let mut through = Text::new(&line[..b]);
129 through.expand_tabs(8);
130 let left = before.cell_len();
131 (left, through.cell_len().saturating_sub(left).max(1))
132}
133
134#[derive(Clone, Debug)]
135pub struct SourceSnippet {
136 name: String,
137 source: String,
138 labels: Vec<SpanLabel>,
140 context_lines: usize,
141}
142impl SourceSnippet {
143 pub fn new(
144 name: String,
145 source: String,
146 span: Range<usize>,
147 context_lines: usize,
148 ) -> Result<Self, DiagnosticError> {
149 check_span(&source, &span)?;
150 Ok(Self {
151 name,
152 source,
153 labels: vec![SpanLabel {
154 span,
155 message: None,
156 primary: true,
157 }],
158 context_lines,
159 })
160 }
161
162 #[must_use]
164 pub fn primary_label(mut self, message: impl Into<String>) -> Self {
165 self.labels[0].message = Some(message.into());
166 self
167 }
168
169 pub fn primary(
171 mut self,
172 span: Range<usize>,
173 message: impl Into<String>,
174 ) -> Result<Self, DiagnosticError> {
175 check_span(&self.source, &span)?;
176 self.labels.push(SpanLabel {
177 span,
178 message: Some(message.into()).filter(|m| !m.is_empty()),
179 primary: true,
180 });
181 Ok(self)
182 }
183
184 pub fn secondary(
186 mut self,
187 span: Range<usize>,
188 message: impl Into<String>,
189 ) -> Result<Self, DiagnosticError> {
190 check_span(&self.source, &span)?;
191 self.labels.push(SpanLabel {
192 span,
193 message: Some(message.into()).filter(|m| !m.is_empty()),
194 primary: false,
195 });
196 Ok(self)
197 }
198
199 pub fn name(&self) -> &str {
201 &self.name
202 }
203
204 pub fn location(&self) -> Location {
207 let start = self.labels[0].span.start;
208 let before = &self.source[..start];
209 let line = before.matches('\n').count() + 1;
210 let line_start = before.rfind('\n').map_or(0, |i| i + 1);
211 let column = before[line_start..].chars().count() + 1;
212 Location::new(self.name.clone(), Some(line), Some(column))
213 }
214
215 fn rows(
216 &self,
217 primary_style: Option<Style>,
218 secondary_style: Option<Style>,
219 linker: Option<&Hyperlinker>,
220 ) -> Vec<Vec<Segment>> {
221 struct Line<'a> {
222 number: usize,
223 offset: usize,
224 end: usize,
225 text: &'a str,
226 }
227 let mut lines = Vec::new();
228 let mut offset = 0;
229 for (i, line) in self.source.split('\n').enumerate() {
230 let end = offset + line.len();
231 lines.push(Line {
234 number: i + 1,
235 offset,
236 end,
237 text: line.strip_suffix('\r').unwrap_or(line),
238 });
239 offset = end.saturating_add(1);
240 }
241 let touches = |label: &SpanLabel, line: &Line<'_>| {
242 if label.span.is_empty() {
243 label.span.start >= line.offset && label.span.start <= line.end
244 } else {
245 label.span.start <= line.end && label.span.end > line.offset
246 }
247 };
248 let active: Vec<bool> = lines
249 .iter()
250 .map(|line| self.labels.iter().any(|label| touches(label, line)))
251 .collect();
252 let last_line: Vec<Option<usize>> = self
254 .labels
255 .iter()
256 .map(|label| lines.iter().rposition(|line| touches(label, line)))
257 .collect();
258 let first = active.iter().position(|a| *a).unwrap_or(0);
259 let last = active.iter().rposition(|a| *a).unwrap_or(first);
260 let digits = (last + 1).to_string().len();
261 let gutter = format!("{} | ", " ".repeat(digits));
262
263 let mut header = vec![Segment::new("--> ", None)];
264 match linker.and_then(|linker| {
265 let location = self.location();
266 linker.file_url(&self.name, location.line, location.column)
267 }) {
268 Some(url) => header.push(Segment::new(
269 self.name.clone(),
270 Some(Style::new().with_link(url)),
271 )),
272 None => header.push(Segment::new(self.name.clone(), None)),
273 }
274 let mut out = vec![Segment::simplify(&header)];
275
276 let window_start = first.saturating_sub(self.context_lines);
277 let window_end = last
278 .saturating_add(self.context_lines)
279 .saturating_add(1)
280 .min(lines.len());
281 for (index, line) in lines.iter().enumerate().take(window_end).skip(window_start) {
282 let mut text = Text::new(line.text);
283 text.expand_tabs(8);
284 out.push(vec![Segment::new(
285 format!("{:>digits$} | {}", line.number, text.plain()),
286 None,
287 )]);
288 if !active[index] {
289 continue;
290 }
291 let mut cells: Vec<Option<bool>> = Vec::new();
293 let mut messages: Vec<(usize, &str, bool)> = Vec::new();
294 let mut order: Vec<usize> = (0..self.labels.len()).collect();
295 order.sort_by_key(|&i| self.labels[i].primary);
296 for i in order {
297 let label = &self.labels[i];
298 if !touches(label, line) {
299 continue;
300 }
301 let a = label.span.start.saturating_sub(line.offset);
302 let b = label.span.end.saturating_sub(line.offset);
303 let (left, count) = marker_cells(line.text, a, b);
304 if cells.len() < left + count {
305 cells.resize(left + count, None);
306 }
307 for cell in &mut cells[left..left + count] {
308 *cell = Some(label.primary);
309 }
310 if let (Some(message), Some(true)) =
311 (&label.message, last_line[i].map(|l| l == index))
312 {
313 messages.push((left, message, label.primary));
314 }
315 }
316 messages.sort_by_key(|(left, _, _)| *left);
317 let style_for = |primary: bool| {
318 if primary {
319 primary_style.clone()
320 } else {
321 secondary_style.clone()
322 }
323 };
324 let mut row = vec![Segment::new(gutter.clone(), None)];
325 let mut run = String::new();
326 let mut run_kind: Option<bool> = None;
327 let flush = |row: &mut Vec<Segment>, run: &mut String, kind: Option<bool>| {
328 if !run.is_empty() {
329 let style = kind.and_then(style_for);
330 row.push(Segment::new(std::mem::take(run), style));
331 }
332 };
333 for cell in &cells {
334 if *cell != run_kind {
335 flush(&mut row, &mut run, run_kind);
336 run_kind = *cell;
337 }
338 run.push(match cell {
339 Some(true) => '^',
340 Some(false) => '-',
341 None => ' ',
342 });
343 }
344 flush(&mut row, &mut run, run_kind);
345 if let Some((_, message, primary)) = messages.pop() {
347 row.push(Segment::new(" ", None));
348 row.push(Segment::new(message.to_string(), style_for(primary)));
349 }
350 out.push(Segment::simplify(&row));
351 for (left, message, primary) in messages.into_iter().rev() {
352 out.push(Segment::simplify(&[
353 Segment::new(format!("{gutter}{}", " ".repeat(left)), None),
354 Segment::new(message.to_string(), style_for(primary)),
355 ]));
356 }
357 }
358 out
359 }
360}
361
362#[derive(Clone, Debug)]
364pub struct Suggestion {
365 message: String,
366 edit: Option<SuggestedEdit>,
367}
368
369#[derive(Clone, Debug)]
370struct SuggestedEdit {
371 line_number: usize,
372 line: String,
373 left: usize,
374 count: usize,
375}
376
377impl Suggestion {
378 pub fn new(message: impl Into<String>) -> Self {
380 Suggestion {
381 message: message.into(),
382 edit: None,
383 }
384 }
385
386 pub fn replace(
390 message: impl Into<String>,
391 source: &str,
392 span: Range<usize>,
393 replacement: &str,
394 ) -> Result<Self, DiagnosticError> {
395 check_span(source, &span)?;
396 let line_start = source[..span.start].rfind('\n').map_or(0, |i| i + 1);
397 let line_end = source[span.start..]
398 .find('\n')
399 .map_or(source.len(), |i| span.start + i);
400 let line = &source[line_start..line_end];
401 let line = line.strip_suffix('\r').unwrap_or(line);
402 let a = (span.start - line_start).min(line.len());
405 let b = (span.end.min(line_end) - line_start).clamp(a, line.len());
406 let edited = format!("{}{replacement}{}", &line[..a], &line[b..]);
407 let (left, count) = marker_cells(&edited, a, a + replacement.len());
408 let mut shown = Text::new(edited);
409 shown.expand_tabs(8);
410 Ok(Suggestion {
411 message: message.into(),
412 edit: Some(SuggestedEdit {
413 line_number: source[..span.start].matches('\n').count() + 1,
414 line: shown.plain().to_string(),
415 left,
416 count,
417 }),
418 })
419 }
420
421 pub fn message(&self) -> &str {
423 &self.message
424 }
425}
426
427#[derive(Clone, Debug)]
428#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
429pub struct Diagnostic {
430 message: String,
431 level: Option<Level>,
432 code: Option<String>,
433 code_url: Option<String>,
434 location: Option<Location>,
435 causes: Vec<String>,
436 notes: Vec<String>,
437 help: Vec<String>,
438 suggestions: Vec<Suggestion>,
439 labels: Vec<String>,
440 snippets: Vec<SourceSnippet>,
441 metadata: Vec<(String, Value)>,
442 trace: Option<StackTrace>,
443 linker: Option<Hyperlinker>,
444 view: EventView,
445 overflow: OverflowPolicy,
446}
447impl Diagnostic {
448 pub fn new(message: impl Into<String>) -> Self {
449 Self {
450 message: message.into(),
451 level: None,
452 code: None,
453 code_url: None,
454 location: None,
455 causes: Vec::new(),
456 notes: Vec::new(),
457 help: Vec::new(),
458 suggestions: Vec::new(),
459 labels: Vec::new(),
460 snippets: Vec::new(),
461 metadata: Vec::new(),
462 trace: None,
463 linker: None,
464 view: EventView::Compact,
465 overflow: OverflowPolicy::Fold,
466 }
467 }
468 pub fn error(message: impl Into<String>) -> Self {
470 Self::new(message).level(Level::Error)
471 }
472 pub fn warning(message: impl Into<String>) -> Self {
474 Self::new(message).level(Level::Warning)
475 }
476 pub fn from_error(error: &(dyn std::error::Error + 'static), max_depth: usize) -> Self {
477 let mut result = Self::new(error.to_string());
478 let mut seen = vec![error as *const dyn std::error::Error];
479 let mut next = error.source();
480 while let Some(error) = next {
481 if result.causes.len() >= max_depth {
482 result.causes.push("[truncated]".into());
483 break;
484 }
485 let ptr = error as *const dyn std::error::Error;
486 if seen.iter().any(|&p| std::ptr::eq(p, ptr)) {
490 result.causes.push("[cycle]".into());
491 break;
492 }
493 seen.push(ptr);
494 result.causes.push(error.to_string());
495 next = error.source();
496 }
497 result
498 }
499 pub fn from_info<E: DiagnosticInfo>(error: &E, max_depth: usize) -> Self {
503 let mut result = Self::from_error(error, max_depth).level(error.level());
504 result.code = error.code();
505 result.code_url = error.code_url();
506 result.location = error.location();
507 result.help.extend(error.help());
508 result.notes.extend(error.notes());
509 result
510 }
511 #[cfg(feature = "anyhow")]
515 pub fn from_anyhow(error: &anyhow::Error, max_depth: usize) -> Self {
516 let inner: &(dyn std::error::Error + 'static) = error.as_ref();
517 let mut result = Self::from_error(inner, max_depth).level(Level::Error);
518 let backtrace = error.backtrace();
519 if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
520 use crate::stacktrace::TraceParser;
521 result.trace = crate::stacktrace::RustParser.parse(&backtrace.to_string());
522 }
523 result
524 }
525 pub fn level(mut self, level: Level) -> Self {
526 self.level = Some(level);
527 self
528 }
529 pub fn code(mut self, code: impl Into<String>) -> Self {
531 self.code = Some(code.into());
532 self
533 }
534 pub fn code_url(mut self, url: impl Into<String>) -> Self {
536 self.code_url = Some(url.into());
537 self
538 }
539 pub fn location(mut self, location: Location) -> Self {
541 self.location = Some(location);
542 self
543 }
544 pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
547 self.linker = Some(linker);
548 self
549 }
550 pub fn cause(mut self, message: impl Into<String>) -> Self {
551 self.causes.push(message.into());
552 self
553 }
554 pub fn note(mut self, message: impl Into<String>) -> Self {
555 self.notes.push(message.into());
556 self
557 }
558 pub fn help(mut self, message: impl Into<String>) -> Self {
559 self.help.push(message.into());
560 self
561 }
562 pub fn suggestion(mut self, suggestion: Suggestion) -> Self {
563 self.suggestions.push(suggestion);
564 self
565 }
566 pub fn label(mut self, message: impl Into<String>) -> Self {
567 self.labels.push(message.into());
568 self
569 }
570 pub fn snippet(mut self, snippet: SourceSnippet) -> Self {
571 self.snippets.push(snippet);
572 self
573 }
574 pub fn metadata(mut self, key: impl Into<String>, value: Value) -> Self {
575 self.metadata.push((key.into(), value));
576 self
577 }
578 pub fn trace(mut self, trace: StackTrace) -> Self {
580 self.trace = Some(trace);
581 self
582 }
583 pub fn view(mut self, view: EventView) -> Self {
584 self.view = view;
585 self
586 }
587 pub fn overflow(mut self, overflow: OverflowPolicy) -> Self {
588 self.overflow = overflow;
589 self
590 }
591
592 pub fn message(&self) -> &str {
594 &self.message
595 }
596 pub fn get_level(&self) -> Option<Level> {
598 self.level
599 }
600 pub fn get_code(&self) -> Option<&str> {
602 self.code.as_deref()
603 }
604 pub fn get_location(&self) -> Option<Location> {
606 self.location
607 .clone()
608 .or_else(|| self.snippets.first().map(SourceSnippet::location))
609 }
610 pub fn causes(&self) -> &[String] {
612 &self.causes
613 }
614 pub fn get_trace(&self) -> Option<&StackTrace> {
616 self.trace.as_ref()
617 }
618 pub fn get_code_url(&self) -> Option<&str> {
620 self.code_url.as_deref()
621 }
622 pub fn notes(&self) -> &[String] {
624 &self.notes
625 }
626 pub fn help_messages(&self) -> &[String] {
628 &self.help
629 }
630 pub fn suggestions(&self) -> &[Suggestion] {
632 &self.suggestions
633 }
634 pub fn labels(&self) -> &[String] {
636 &self.labels
637 }
638
639 fn header(&self, c: &Console) -> Vec<Segment> {
640 let Some(level) = self.level else {
641 let mut segments = Vec::new();
642 if let Some(code) = &self.code {
643 segments.push(Segment::new(format!("[{code}] "), None));
644 }
645 segments.push(Segment::new(
646 &self.message,
647 Some(theme_style(c, "diagnostic.message", "bold red")),
648 ));
649 return segments;
650 };
651 let style = level.style(c);
652 let mut segments = vec![Segment::new(level.name(), Some(style.clone()))];
653 if let Some(code) = &self.code {
654 let code_style = match &self.code_url {
655 Some(url) => style.clone().with_link(url.clone()),
656 None => style.clone(),
657 };
658 segments.push(Segment::new(format!("[{code}]"), Some(code_style)));
659 }
660 segments.push(Segment::new(
661 ": ",
662 Some(theme_style(c, "diagnostic.headline", "bold")),
663 ));
664 segments.push(Segment::new(
665 &self.message,
666 Some(theme_style(c, "diagnostic.headline", "bold")),
667 ));
668 segments
669 }
670}
671
672pub trait DiagnosticInfo: std::error::Error + 'static {
676 fn level(&self) -> Level {
678 Level::Error
679 }
680 fn code(&self) -> Option<String> {
682 None
683 }
684 fn code_url(&self) -> Option<String> {
686 None
687 }
688 fn help(&self) -> Option<String> {
690 None
691 }
692 fn notes(&self) -> Vec<String> {
694 Vec::new()
695 }
696 fn location(&self) -> Option<Location> {
698 None
699 }
700 fn to_diagnostic(&self) -> Diagnostic
702 where
703 Self: Sized,
704 {
705 Diagnostic::from_info(self, 16)
706 }
707}
708
709impl Renderable for Diagnostic {
710 fn rich_render(&self, c: &Console, o: &ConsoleOptions) -> Vec<Segment> {
711 if o.max_width == 0 || o.height == Some(0) {
712 return Vec::new();
713 }
714 let mut rows = fit_segments(&self.header(c), o.max_width, self.overflow);
715 if let Some(location) = &self.location {
716 let gutter = theme_style(c, "diagnostic.gutter", "bold blue");
717 let linker = self.linker.clone().unwrap_or_else(Hyperlinker::disabled);
718 let text = linker.location(&location.path, location.line, location.column, "");
719 let mut segments = vec![Segment::new(" --> ", Some(gutter))];
720 segments.extend(text.render(c.theme(), &Style::new()));
721 rows.extend(fit_segments(&segments, o.max_width, OverflowPolicy::Crop));
722 }
723 for cause in &self.causes {
724 rows.extend(fit_segments(
725 &[Segment::new(format!("caused by: {cause}"), None)],
726 o.max_width,
727 self.overflow,
728 ));
729 }
730 if self.view == EventView::Expanded {
731 for label in &self.labels {
732 rows.extend(fit_segments(
733 &[Segment::new(label, None)],
734 o.max_width,
735 self.overflow,
736 ));
737 }
738 let (primary, secondary) = match self.level {
739 Some(level) => (
740 Some(level.style(c)),
741 Some(theme_style(c, "diagnostic.secondary", "bold blue")),
742 ),
743 None => (None, None),
744 };
745 for (index, snippet) in self.snippets.iter().enumerate() {
746 let repeated = index == 0
750 && self
751 .location
752 .as_ref()
753 .is_some_and(|location| location.path == snippet.name);
754 let lines = snippet.rows(primary.clone(), secondary.clone(), self.linker.as_ref());
756 for line in lines.into_iter().skip(usize::from(repeated)) {
757 rows.extend(fit_segments(&line, o.max_width, OverflowPolicy::Crop));
758 }
759 }
760 for (key, value) in &self.metadata {
761 rows.extend(fit_segments(
762 &[Segment::new(
763 format!("{key}={}", value.format(true, 0)),
764 None,
765 )],
766 o.max_width,
767 self.overflow,
768 ));
769 }
770 for (prefix, values) in [("note", &self.notes), ("help", &self.help)] {
771 for value in values {
772 rows.extend(fit_segments(
773 &[Segment::new(format!("{prefix}: {value}"), None)],
774 o.max_width,
775 self.overflow,
776 ));
777 }
778 }
779 let added = theme_style(c, "diagnostic.suggestion", "green");
780 for suggestion in &self.suggestions {
781 rows.extend(fit_segments(
782 &[Segment::new(format!("help: {}", suggestion.message), None)],
783 o.max_width,
784 self.overflow,
785 ));
786 if let Some(edit) = &suggestion.edit {
787 let digits = edit.line_number.to_string().len();
788 let line = format!("{:>digits$} | {}", edit.line_number, edit.line);
789 rows.extend(fit_segments(
790 &[Segment::new(line, None)],
791 o.max_width,
792 OverflowPolicy::Crop,
793 ));
794 let marks = [
795 Segment::new(
796 format!("{} | {}", " ".repeat(digits), " ".repeat(edit.left)),
797 None,
798 ),
799 Segment::new("+".repeat(edit.count), Some(added.clone())),
800 ];
801 rows.extend(fit_segments(&marks, o.max_width, OverflowPolicy::Crop));
802 }
803 }
804 if let Some(trace) = &self.trace {
805 let view = trace
806 .render_options()
807 .hyperlinker(self.linker.clone().unwrap_or_else(Hyperlinker::disabled));
808 rows.extend(c.render_lines(&view, &o.update_width(o.max_width), false));
809 }
810 }
811 if let Some(height) = o.height {
812 rows.truncate(height);
813 }
814 flatten(rows)
815 }
816}