Skip to main content

device_driver_diagnostics/
lib.rs

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