Skip to main content

presk_syntax/
diag.rs

1//! Diagnostics: a renderer-independent description of an error or warning,
2//! rendered with ariadne. Messages are phrased in the terms of the source.
3//!
4//! Error codes: `E0xxx` syntax, `E1xxx` elaboration, `E2xxx` typing, `E3xxx`
5//! regularity, `E4xxx` effects, `W0xxx` warnings.
6
7use std::fmt;
8
9use ariadne::{Color, Config, IndexType, Label as ALabel, Report, ReportKind};
10
11use crate::source::{FileId, SourceMap, Span};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub enum Severity {
15    Warning,
16    Error,
17}
18
19#[derive(Clone, Debug)]
20pub struct Label {
21    pub span: Span,
22    pub text: String,
23}
24
25#[derive(Clone, Debug)]
26pub struct Diagnostic {
27    pub code: &'static str,
28    pub severity: Severity,
29    pub message: String,
30    /// The first label is the primary one.
31    pub labels: Vec<Label>,
32    pub notes: Vec<String>,
33    pub help: Option<String>,
34}
35
36impl Diagnostic {
37    pub fn error(code: &'static str, message: impl Into<String>) -> Self {
38        Diagnostic { code, severity: Severity::Error, message: message.into(), labels: vec![], notes: vec![], help: None }
39    }
40
41    pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
42        Diagnostic { code, severity: Severity::Warning, message: message.into(), labels: vec![], notes: vec![], help: None }
43    }
44
45    pub fn label(mut self, span: Span, text: impl Into<String>) -> Self {
46        self.labels.push(Label { span, text: text.into() });
47        self
48    }
49
50    pub fn note(mut self, text: impl Into<String>) -> Self {
51        self.notes.push(text.into());
52        self
53    }
54
55    pub fn help(mut self, text: impl Into<String>) -> Self {
56        self.help = Some(text.into());
57        self
58    }
59
60    pub fn primary_span(&self) -> Option<Span> {
61        self.labels.first().map(|l| l.span)
62    }
63
64    pub fn is_error(&self) -> bool {
65        self.severity == Severity::Error
66    }
67
68    /// Render with ariadne. `color` selects ANSI colors; tests use `false`,
69    /// which also selects the ASCII character set for stable snapshots.
70    pub fn render(&self, sm: &SourceMap, color: bool) -> String {
71        let kind = match self.severity {
72            Severity::Error => ReportKind::Error,
73            Severity::Warning => ReportKind::Warning,
74        };
75        let primary = self.labels.first().map(|l| l.span);
76        let anchor = primary.map(|s| (s.file, s.range())).unwrap_or((FileId(0), 0..0));
77        let config = Config::default()
78            .with_color(color)
79            .with_index_type(IndexType::Byte)
80            .with_char_set(if color { ariadne::CharSet::Unicode } else { ariadne::CharSet::Ascii });
81        let mut report = Report::build(kind, anchor).with_code(self.code).with_message(&self.message).with_config(config);
82        for (i, l) in self.labels.iter().enumerate() {
83            let mut label = ALabel::new((l.span.file, l.span.range())).with_message(&l.text).with_order(i as i32);
84            if color {
85                label = label.with_color(if i == 0 { Color::Red } else { Color::Blue });
86            }
87            report = report.with_label(label);
88        }
89        for n in &self.notes {
90            report = report.with_note(n);
91        }
92        if let Some(h) = &self.help {
93            report = report.with_help(h);
94        }
95        let mut out = Vec::new();
96        report.finish().write(Cache(sm), &mut out).expect("rendering a diagnostic");
97        String::from_utf8(out).expect("ariadne emits UTF-8")
98    }
99}
100
101impl fmt::Display for Diagnostic {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "[{}] {}", self.code, self.message)
104    }
105}
106
107struct Cache<'a>(&'a SourceMap);
108
109impl ariadne::Cache<FileId> for Cache<'_> {
110    type Storage = String;
111
112    fn fetch(&mut self, id: &FileId) -> Result<&ariadne::Source<String>, impl fmt::Debug> {
113        Ok::<_, std::convert::Infallible>(&self.0.file(*id).ariadne)
114    }
115
116    fn display<'b>(&self, id: &'b FileId) -> Option<impl fmt::Display + 'b> {
117        Some(self.0.file(*id).name.clone())
118    }
119}
120
121/// Render a list of diagnostics, one after the other.
122pub fn render_all(diags: &[Diagnostic], sm: &SourceMap, color: bool) -> String {
123    diags.iter().map(|d| d.render(sm, color)).collect::<Vec<_>>().join("")
124}
125
126pub fn has_errors(diags: &[Diagnostic]) -> bool {
127    diags.iter().any(Diagnostic::is_error)
128}