device_driver_diagnostics/
lib.rs1use 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 pub source: &'s str,
88 pub source_path: &'s str,
90 pub term_width: Option<usize>,
92 pub ansi: bool,
94 pub unicode: bool,
96 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
120pub fn encode_ansi_url(link: &str, name: &str) -> String {
122 format!("\x1b]8;;{link}\x1b\\{name}\x1b]8;;\x1b\\")
123}
124
125fn 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 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 return text.into();
153 }
154 }
155 UrlStage::End => {
156 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 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}