Skip to main content

probl_syntax/
diagnostic.rs

1//! Errors and warnings with source locations, rendered with `ariadne`.
2
3use crate::span::{SourceFile, Span};
4use ariadne::{Config, IndexType, Label, Report, ReportKind, Source};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Severity {
8    Error,
9    Warning,
10}
11
12#[derive(Clone, Debug)]
13pub struct Diagnostic {
14    pub severity: Severity,
15    pub message: String,
16    pub span: Span,
17    /// Short text shown under the highlighted span.
18    pub label: Option<String>,
19    pub notes: Vec<String>,
20    pub help: Option<String>,
21}
22
23impl Diagnostic {
24    pub fn error(span: Span, message: impl Into<String>) -> Diagnostic {
25        Diagnostic {
26            severity: Severity::Error,
27            message: message.into(),
28            span,
29            label: None,
30            notes: Vec::new(),
31            help: None,
32        }
33    }
34
35    pub fn warning(span: Span, message: impl Into<String>) -> Diagnostic {
36        Diagnostic {
37            severity: Severity::Warning,
38            ..Diagnostic::error(span, message)
39        }
40    }
41
42    pub fn with_label(mut self, label: impl Into<String>) -> Diagnostic {
43        self.label = Some(label.into());
44        self
45    }
46
47    pub fn with_note(mut self, note: impl Into<String>) -> Diagnostic {
48        self.notes.push(note.into());
49        self
50    }
51
52    pub fn with_help(mut self, help: impl Into<String>) -> Diagnostic {
53        self.help = Some(help.into());
54        self
55    }
56
57    /// Add help text to a diagnostic that has already been recorded.
58    pub fn help(&mut self, help: impl Into<String>) -> &mut Diagnostic {
59        self.help = Some(help.into());
60        self
61    }
62
63    /// Add a note to a diagnostic that has already been recorded.
64    pub fn note(&mut self, note: impl Into<String>) -> &mut Diagnostic {
65        self.notes.push(note.into());
66        self
67    }
68
69    pub fn is_error(&self) -> bool {
70        self.severity == Severity::Error
71    }
72
73    /// Render the diagnostic with a source snippet.
74    pub fn render(&self, file: &SourceFile, color: bool) -> String {
75        let name = file.name.as_str();
76        let kind = match self.severity {
77            Severity::Error => ReportKind::Error,
78            Severity::Warning => ReportKind::Warning,
79        };
80        let range = clamp(self.span, file.text.len());
81        // ariadne only draws the marker under a span when its label has text.
82        let label = Label::new((name, range.clone())).with_message(self.label.as_deref().unwrap_or("here"));
83        let mut report = Report::build(kind, (name, range))
84            .with_config(Config::default().with_color(color).with_index_type(IndexType::Byte))
85            .with_message(&self.message)
86            .with_label(label);
87        for note in &self.notes {
88            report = report.with_note(note);
89        }
90        if let Some(help) = &self.help {
91            report = report.with_help(help);
92        }
93        let mut out = Vec::new();
94        report
95            .finish()
96            .write((name, Source::from(file.text.as_str())), &mut out)
97            .expect("writing to a Vec cannot fail");
98        String::from_utf8_lossy(&out).into_owned()
99    }
100}
101
102pub fn render_all(diagnostics: &[Diagnostic], file: &SourceFile, color: bool) -> String {
103    diagnostics.iter().map(|d| d.render(file, color)).collect()
104}
105
106fn clamp(span: Span, len: usize) -> std::ops::Range<usize> {
107    let lo = (span.lo as usize).min(len);
108    let hi = (span.hi as usize).clamp(lo, len);
109    lo..hi
110}