Skip to main content

device_driver_diagnostics/
lib.rs

1use std::{
2    borrow::Cow,
3    error::Error,
4    fmt::{Debug, Display, Write},
5    ops::Deref,
6};
7
8use annotate_snippets::{Group, Level, Renderer, Title, renderer::DecorStyle};
9use device_driver_common::span::Span;
10
11pub mod errors;
12
13#[derive(Debug)]
14pub struct Diagnostics {
15    diagnostics: Vec<Box<dyn Diagnostic>>,
16}
17
18impl Default for Diagnostics {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl Diagnostics {
25    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            diagnostics: Vec::new(),
29        }
30    }
31
32    pub fn add(&mut self, diagnostic: impl Diagnostic + 'static) {
33        self.diagnostics.push(Box::new(diagnostic));
34    }
35
36    #[must_use]
37    pub fn has_error(&self) -> bool {
38        self.diagnostics
39            .iter()
40            .any(|diagnostic| diagnostic.severity() == Severity::Error)
41    }
42
43    #[must_use]
44    pub fn is_empty(&self) -> bool {
45        self.diagnostics.is_empty()
46    }
47
48    pub fn iter(&self) -> impl Iterator<Item = &dyn Diagnostic> {
49        self.diagnostics.iter().map(Box::deref)
50    }
51
52    pub fn render_for_each<E>(
53        &self,
54        metadata: Metadata<'_>,
55        mut f: impl FnMut(&dyn Diagnostic, String) -> Result<(), E>,
56    ) -> Result<(), E> {
57        let renderer = metadata.get_renderer();
58
59        for diagnostic in self.iter() {
60            let mut rendered =
61                renderer.render(&diagnostic.as_report(metadata.source, metadata.source_path));
62
63            if !metadata.ansi {
64                rendered = strip_ansi_urls(&rendered);
65            }
66
67            f(diagnostic, rendered)?;
68        }
69
70        Ok(())
71    }
72
73    pub fn print_to<W: std::io::Write>(
74        self,
75        mut writer: W,
76        metadata: Metadata<'_>,
77    ) -> std::io::Result<()> {
78        self.render_for_each(metadata, |_, rendered| writeln!(writer, "{rendered}\n"))
79    }
80
81    pub fn print_to_fmt<W: std::fmt::Write>(
82        self,
83        mut writer: W,
84        metadata: Metadata<'_>,
85    ) -> std::fmt::Result {
86        self.render_for_each(metadata, |_, rendered| writeln!(writer, "{rendered}\n"))
87    }
88}
89
90pub struct Metadata<'s> {
91    /// The source code
92    pub source: &'s str,
93    /// The path to the source code
94    pub source_path: &'s str,
95    /// When Some, the specified width is used as the terminal width. If None, a reasonable default value is used.
96    pub term_width: Option<usize>,
97    /// When true, ansi escape codes are used to add color and OSC8 url links
98    pub ansi: bool,
99    /// When true, unicode styling is used. When false everything is plain ascii
100    pub unicode: bool,
101    /// When true, the line numbers will not be shown. This can be great for UI tests
102    pub anonymized_line_numbers: bool,
103}
104
105impl Metadata<'_> {
106    fn get_renderer(&self) -> Renderer {
107        if self.ansi {
108            Renderer::styled()
109        } else {
110            Renderer::plain()
111        }
112        .term_width(
113            self.term_width
114                .unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
115        )
116        .decor_style(if self.unicode {
117            DecorStyle::Unicode
118        } else {
119            DecorStyle::Ascii
120        })
121        .anonymized_snippet_line_numbers(self.anonymized_line_numbers)
122        .anonymized_origin_line_numbers(self.anonymized_line_numbers)
123    }
124}
125
126/// Encode links using OSC8: <https://github.com/Alhadis/OSC8-Adoption>
127pub fn encode_ansi_url(link: &str, name: &str) -> String {
128    format!("\x1b]8;;{link}\x1b\\{name}\x1b]8;;\x1b\\")
129}
130
131/// Probably not fully compliant, but will work for links generated from [`encode_ansi_url`]
132fn strip_ansi_urls(text: &str) -> String {
133    enum UrlStage {
134        None,
135        Start,
136        End,
137    }
138
139    let mut output = String::new();
140
141    let mut url_stage = UrlStage::None;
142
143    for split in text.split("\x1b]8;;") {
144        match url_stage {
145            UrlStage::None => {
146                output += split;
147                url_stage = UrlStage::Start;
148            }
149            UrlStage::Start => {
150                // Split contains <link>\x1b\\<name>
151                if let Some((link, name)) = split.split_once("\x1b\\") {
152                    output += name;
153                    let _ = write!(output, " ({link})");
154                    url_stage = UrlStage::End;
155                } else {
156                    // Something is unexpected!
157                    // TODO: Trace a warning
158                    return text.into();
159                }
160            }
161            UrlStage::End => {
162                // Same as None, but we have a \x1b\\ left
163                output += split.trim_start_matches("\x1b\\");
164                url_stage = UrlStage::Start;
165            }
166        }
167    }
168
169    output
170}
171
172pub trait Diagnostic: Debug + Send {
173    fn severity(&self) -> Severity;
174    fn as_report<'a>(&'a self, source: &'a str, path: &'a str) -> Vec<Group<'a>>;
175
176    fn primary_span(&self) -> Span;
177    fn title(&self) -> Cow<'static, str>;
178
179    fn title_snippet(&self) -> Title<'static> {
180        match self.severity() {
181            Severity::Error => Level::ERROR,
182            Severity::Warning => Level::WARNING,
183            Severity::Info => Level::INFO,
184            Severity::Note => Level::NOTE,
185            Severity::Help => Level::HELP,
186        }
187        .primary_title(self.title())
188    }
189}
190
191impl<E: Error + Send> Diagnostic for E {
192    fn severity(&self) -> Severity {
193        Severity::Error
194    }
195
196    fn as_report<'a>(&'a self, _source: &'a str, _file_path: &'a str) -> Vec<Group<'a>> {
197        let mut sources = Vec::new();
198        let mut source = self.source();
199
200        while let Some(current_source) = source {
201            sources.push(Level::NOTE.message(current_source.to_string()));
202            source = current_source.source();
203        }
204
205        vec![Group::with_title(Level::ERROR.primary_title(self.to_string())).elements(sources)]
206    }
207
208    fn primary_span(&self) -> Span {
209        Span::empty()
210    }
211
212    fn title(&self) -> Cow<'static, str> {
213        self.to_string().into()
214    }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
218pub enum Severity {
219    Error,
220    Warning,
221    Info,
222    Note,
223    Help,
224}
225
226#[derive(Debug)]
227pub struct DynError {
228    source: Option<Box<dyn Error + Send + 'static>>,
229    message: String,
230}
231
232impl DynError {
233    pub fn new(message: impl Display) -> Self {
234        Self {
235            source: None,
236            message: message.to_string(),
237        }
238    }
239
240    pub fn to_report_string(&self) -> String {
241        let mut string = String::new();
242        let report = self.as_report("", "");
243        let output = annotate_snippets::Renderer::styled().render(&report);
244        write!(&mut string, "{output}").unwrap();
245        string
246    }
247}
248
249impl Display for DynError {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        match (self.message.as_str(), self.source()) {
252            ("", Some(source)) => write!(f, "{source}"),
253            ("", None) => unreachable!(),
254            (message, _) if !f.alternate() => write!(f, "{message}"),
255            (message, None) => write!(f, "{message}"),
256            (message, Some(source)) => write!(f, "{message}\n| {source:#}"),
257        }
258    }
259}
260
261impl Error for DynError {
262    fn source(&self) -> Option<&(dyn Error + 'static)> {
263        if self.message.is_empty() {
264            // If message is empty, then the error *is* the source, so we don't have a deeper source
265            None
266        } else {
267            self.source.as_ref().map(|e| {
268                let x: &(dyn Error + Send) = Box::as_ref(e);
269                let y: &dyn Error = x;
270                y
271            })
272        }
273    }
274}
275
276pub trait ErrorExt: Error + Sized + Send + 'static {
277    fn with_message(self, message: impl Display) -> DynError {
278        DynError {
279            source: Some(Box::new(self)),
280            message: message.to_string(),
281        }
282    }
283
284    fn into_dyn_error(self) -> DynError {
285        DynError {
286            source: Some(Box::new(self)),
287            message: String::new(),
288        }
289    }
290}
291
292impl<E: Error + Send + Sized + 'static> ErrorExt for E {}
293
294pub trait ResultExt<T, E: ErrorExt> {
295    fn with_message<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError>;
296    fn into_dyn_result(self) -> Result<T, DynError>;
297}
298
299impl<T, E: ErrorExt> ResultExt<T, E> for Result<T, E> {
300    fn with_message<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError> {
301        self.map_err(|e| e.with_message(f()))
302    }
303
304    fn into_dyn_result(self) -> Result<T, DynError> {
305        self.map_err(ErrorExt::into_dyn_error)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use std::fmt::Display;
312
313    use super::*;
314
315    #[derive(Debug)]
316    struct DummyError;
317
318    impl Display for DummyError {
319        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320            write!(f, "Something went wrong!")
321        }
322    }
323
324    impl Error for DummyError {}
325
326    #[derive(Debug)]
327    struct DummyErrorWithSource(usize);
328
329    impl Display for DummyErrorWithSource {
330        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331            write!(f, "@level {} - Something deep down went wrong!", self.0)
332        }
333    }
334
335    impl Error for DummyErrorWithSource {
336        fn source(&self) -> Option<&(dyn Error + 'static)> {
337            if self.0 == 0 {
338                Some(&DummyError)
339            } else {
340                Some(Box::leak(Box::new(DummyErrorWithSource(self.0 - 1))))
341            }
342        }
343    }
344
345    #[test]
346    fn error_into_report() {
347        let report = DummyError.as_report("", "");
348        let output = annotate_snippets::Renderer::plain().render(&report);
349        pretty_assertions::assert_str_eq!("error: Something went wrong!", output);
350
351        let report = DummyErrorWithSource(5).as_report("", "");
352        let output = annotate_snippets::Renderer::plain().render(&report);
353        pretty_assertions::assert_str_eq!(
354            "error: @level 5 - Something deep down went wrong!
355 |
356 = note: @level 4 - Something deep down went wrong!
357 = note: @level 3 - Something deep down went wrong!
358 = note: @level 2 - Something deep down went wrong!
359 = note: @level 1 - Something deep down went wrong!
360 = note: @level 0 - Something deep down went wrong!
361 = note: Something went wrong!",
362            output
363        );
364    }
365
366    #[test]
367    fn dyn_error_context() {
368        let error = DummyError.with_message("Here's some context!");
369        let report = error.as_report("", "");
370        let output = annotate_snippets::Renderer::plain().render(&report);
371        pretty_assertions::assert_str_eq!(
372            "error: Here's some context!
373 |
374 = note: Something went wrong!",
375            output
376        );
377    }
378}