Skip to main content

expectation_model/
diagnostic.rs

1//! Errors and warnings about a suite, pointing at the lines they're about.
2
3use std::collections::BTreeMap;
4use std::fmt::{self, Write};
5use std::sync::Arc;
6
7/// A line in a source file.
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct Span {
10    pub file: Arc<str>,
11    /// 1-based.
12    pub line: u32,
13}
14
15impl Span {
16    pub fn new(file: &str, line: u32) -> Self {
17        Span { file: file.into(), line }
18    }
19}
20
21impl fmt::Display for Span {
22    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23        write!(f, "{}:{}", self.file, self.line)
24    }
25}
26
27/// The text of every file in a suite, so diagnostics can quote the lines they point at.
28#[derive(Clone, Debug, Default)]
29pub struct Sources(BTreeMap<Arc<str>, String>);
30
31impl Sources {
32    pub fn insert(&mut self, file: Arc<str>, text: String) {
33        self.0.insert(file, text);
34    }
35
36    pub fn line(&self, span: &Span) -> Option<&str> {
37        self.0.get(&span.file)?.lines().nth(span.line.checked_sub(1)? as usize)
38    }
39
40    pub fn files(&self) -> impl Iterator<Item = &str> {
41        self.0.keys().map(|k| &**k)
42    }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
46pub enum Severity {
47    Error,
48    Warning,
49    Note,
50}
51
52#[derive(Clone, Debug)]
53pub struct Label {
54    pub span: Span,
55    pub message: String,
56}
57
58#[derive(Clone, Debug)]
59pub struct Diagnostic {
60    pub severity: Severity,
61    /// Stable short name, e.g. "conflict" or "unknown-step".
62    pub code: &'static str,
63    pub message: String,
64    pub labels: Vec<Label>,
65    pub notes: Vec<String>,
66    pub help: Option<String>,
67}
68
69impl Diagnostic {
70    pub fn new(severity: Severity, code: &'static str, message: impl Into<String>) -> Self {
71        Diagnostic { severity, code, message: message.into(), labels: vec![], notes: vec![], help: None }
72    }
73
74    pub fn error(code: &'static str, message: impl Into<String>) -> Self {
75        Self::new(Severity::Error, code, message)
76    }
77
78    pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
79        Self::new(Severity::Warning, code, message)
80    }
81
82    pub fn note(code: &'static str, message: impl Into<String>) -> Self {
83        Self::new(Severity::Note, code, message)
84    }
85
86    pub fn at(mut self, span: &Span, message: impl Into<String>) -> Self {
87        self.labels.push(Label { span: span.clone(), message: message.into() });
88        self
89    }
90
91    pub fn note_that(mut self, note: impl Into<String>) -> Self {
92        self.notes.push(note.into());
93        self
94    }
95
96    pub fn help(mut self, help: impl Into<String>) -> Self {
97        self.help = Some(help.into());
98        self
99    }
100
101    pub fn is_error(&self) -> bool {
102        self.severity == Severity::Error
103    }
104
105    /// Where it sorts: by its first label's file and line.
106    pub fn sort_key(&self) -> (Option<&Span>, Severity) {
107        (self.labels.first().map(|l| &l.span), self.severity)
108    }
109
110    /// Rendered like a compiler message, quoting each labelled line.
111    pub fn render(&self, sources: &Sources) -> String {
112        let mut s = String::new();
113        let level = match self.severity {
114            Severity::Error => "error",
115            Severity::Warning => "warning",
116            Severity::Note => "note",
117        };
118        let _ = writeln!(s, "{level}[{}]: {}", self.code, self.message);
119        for label in &self.labels {
120            let _ = writeln!(s, "  --> {}", label.span);
121            if let Some(line) = sources.line(&label.span) {
122                let _ = writeln!(s, "   | {}", line.trim_end());
123            }
124            if !label.message.is_empty() {
125                let _ = writeln!(s, "   = {}", label.message);
126            }
127        }
128        for note in &self.notes {
129            let _ = writeln!(s, "   = note: {note}");
130        }
131        if let Some(help) = &self.help {
132            let _ = writeln!(s, "   = help: {help}");
133        }
134        s
135    }
136}
137
138pub fn has_errors(diagnostics: &[Diagnostic]) -> bool {
139    diagnostics.iter().any(Diagnostic::is_error)
140}
141
142/// All diagnostics rendered, errors and warnings sorted by position, with a count at the end.
143pub fn render_all(diagnostics: &[Diagnostic], sources: &Sources) -> String {
144    let mut sorted: Vec<&Diagnostic> = diagnostics.iter().collect();
145    sorted.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
146    let mut s = String::new();
147    for d in &sorted {
148        s.push_str(&d.render(sources));
149        s.push('\n');
150    }
151    let errors = diagnostics.iter().filter(|d| d.severity == Severity::Error).count();
152    let warnings = diagnostics.iter().filter(|d| d.severity == Severity::Warning).count();
153    let plural = |n: usize, w: &str| if n == 1 { format!("1 {w}") } else { format!("{n} {w}s") };
154    let _ = writeln!(s, "{}, {}", plural(errors, "error"), plural(warnings, "warning"));
155    s
156}