uiua_parser/
error.rs

1use std::fmt;
2
3use colored::{Color, Colorize};
4
5use crate::{InputSrc, Inputs, Span};
6
7/// A message to be displayed to the user that is not an error
8#[derive(Debug, Clone)]
9pub struct Diagnostic {
10    /// The span of the message
11    pub span: Span,
12    /// The message itself
13    pub message: String,
14    /// What kind of diagnostic this is
15    pub kind: DiagnosticKind,
16    /// The inputs of the program
17    pub inputs: Inputs,
18}
19
20impl PartialEq for Diagnostic {
21    fn eq(&self, other: &Self) -> bool {
22        self.span == other.span && self.message == other.message
23    }
24}
25
26impl Eq for Diagnostic {}
27
28impl PartialOrd for Diagnostic {
29    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
30        Some(self.cmp(other))
31    }
32}
33
34impl Ord for Diagnostic {
35    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
36        self.span.cmp(&other.span)
37    }
38}
39
40/// Kinds of non-error diagnostics
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum DiagnosticKind {
43    /// Informational message
44    Info,
45    /// Bad code style
46    Style,
47    /// Something that should be fixed for performance reasons
48    Advice,
49    /// Something that really needs to be fixed
50    Warning,
51}
52
53impl fmt::Display for Diagnostic {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        self.message.fmt(f)
56    }
57}
58
59impl Diagnostic {
60    /// Create a new diagnostic
61    pub fn new(
62        message: String,
63        span: impl Into<Span>,
64        kind: DiagnosticKind,
65        inputs: Inputs,
66    ) -> Self {
67        Self {
68            message,
69            span: span.into(),
70            kind,
71            inputs,
72        }
73    }
74    /// Get a rich-text report for the diagnostic
75    pub fn report(&self) -> Report {
76        Report::new_multi(
77            ReportKind::Diagnostic(self.kind),
78            &self.inputs,
79            [(&self.message, self.span.clone())],
80        )
81    }
82}
83
84/// Kinds of reports
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ReportKind {
87    /// An error
88    Error,
89    /// A diagnostic
90    Diagnostic(DiagnosticKind),
91}
92
93impl From<DiagnosticKind> for ReportKind {
94    fn from(kind: DiagnosticKind) -> Self {
95        ReportKind::Diagnostic(kind)
96    }
97}
98
99impl ReportKind {
100    /// Get the string that prefixes the formatted report
101    pub fn str(&self) -> &'static str {
102        match self {
103            ReportKind::Error => "Error",
104            ReportKind::Diagnostic(DiagnosticKind::Warning) => "Warning",
105            ReportKind::Diagnostic(DiagnosticKind::Advice) => "Advice",
106            ReportKind::Diagnostic(DiagnosticKind::Style) => "Style",
107            ReportKind::Diagnostic(DiagnosticKind::Info) => "Info",
108        }
109    }
110}
111
112/// A text fragment of a report
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum ReportFragment {
115    /// Just plain text
116    Plain(String),
117    /// Text colored according to the report kind
118    Colored(String, ReportKind),
119    /// Faint text
120    Faint(String),
121    /// Even fainter text
122    Fainter(String),
123    /// A newline
124    Newline,
125}
126
127/// A rich-text error/diagnostic report
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct Report {
130    /// The rich-text fragments of the report
131    pub fragments: Vec<ReportFragment>,
132    /// Whether to color the report with ANSI escape codes when converting it to a string
133    ///
134    /// Defaults to `true`
135    pub color: bool,
136}
137
138impl Report {
139    /// Change whether to color the report with ANSI escape codes when converting it to a string
140    ///
141    /// Defaults to `true`
142    pub fn color(mut self, color: bool) -> Self {
143        self.color = color;
144        self
145    }
146    /// Create a new report
147    pub fn new(kind: ReportKind, message: impl Into<String>) -> Self {
148        let message = message.into();
149        let mut fragments = vec![
150            ReportFragment::Colored(kind.str().into(), kind),
151            ReportFragment::Plain(": ".into()),
152        ];
153        if message.lines().count() > 1 {
154            fragments.push(ReportFragment::Newline);
155        }
156        fragments.push(ReportFragment::Plain(message));
157        Self {
158            fragments,
159            color: true,
160        }
161    }
162    /// Create a new report with multiple messages
163    pub fn new_multi<I, T>(kind: ReportKind, inputs: &Inputs, errors: I) -> Self
164    where
165        I: IntoIterator<Item = (T, Span)>,
166        T: fmt::Display,
167    {
168        let mut fragments = Vec::new();
169        for (i, (message, span)) in errors.into_iter().enumerate() {
170            if i > 0 {
171                fragments.push(ReportFragment::Newline);
172            }
173            fragments.push(ReportFragment::Colored(kind.str().into(), kind));
174            fragments.push(ReportFragment::Plain(": ".into()));
175            let message = message.to_string();
176            for (i, line) in message.lines().enumerate() {
177                if i > 0 || message.lines().count() > 1 {
178                    fragments.push(ReportFragment::Newline);
179                    fragments.push(ReportFragment::Plain("  ".into()));
180                }
181                fragments.push(ReportFragment::Plain(line.into()));
182            }
183            if let Span::Code(mut span) = span {
184                while let InputSrc::Macro(inner) = span.src {
185                    span = *inner;
186                }
187                fragments.push(ReportFragment::Newline);
188                fragments.push(ReportFragment::Fainter("  at ".into()));
189                if let InputSrc::File(path) = &span.src {
190                    fragments.push(ReportFragment::Fainter(format!("{}:", path.display())));
191                }
192                fragments.push(ReportFragment::Fainter(format!(
193                    "{}:{}",
194                    span.start.line, span.start.col
195                )));
196                fragments.push(ReportFragment::Newline);
197                let line_prefix = format!("{} | ", span.start.line);
198                fragments.push(ReportFragment::Plain(line_prefix.clone()));
199                let input = inputs.get(&span.src);
200                let line = input
201                    .lines()
202                    .nth(span.start.line as usize - 1)
203                    .unwrap_or("");
204                let start_char_pos = span.start.col - 1;
205                let end_char_pos = if span.start.line == span.end.line {
206                    span.end.col - 1
207                } else {
208                    line.chars().count() as u16
209                };
210                let pre_color: String = line.chars().take(start_char_pos as usize).collect();
211                let color: String = line
212                    .chars()
213                    .skip(start_char_pos as usize)
214                    .take(end_char_pos.saturating_sub(start_char_pos).max(1) as usize)
215                    .collect();
216                let post_color: String = line.chars().skip(end_char_pos as usize).collect();
217                fragments.push(ReportFragment::Faint(pre_color));
218                fragments.push(ReportFragment::Colored(color, kind));
219                fragments.push(ReportFragment::Faint(post_color));
220                fragments.push(ReportFragment::Newline);
221                fragments.push(ReportFragment::Plain(
222                    " ".repeat(line_prefix.chars().count()),
223                ));
224                fragments.push(ReportFragment::Plain(" ".repeat(start_char_pos as usize)));
225                fragments.push(ReportFragment::Colored(
226                    "─".repeat(end_char_pos.saturating_sub(start_char_pos).max(1) as usize),
227                    kind,
228                ));
229            }
230        }
231        Self {
232            fragments,
233            color: true,
234        }
235    }
236    /// A report that tests have finished
237    pub fn tests(successes: usize, failures: usize, not_run: usize) -> Self {
238        let mut fragments = if successes == 0 && not_run == 0 {
239            if failures == 0 {
240                vec![]
241            } else {
242                vec![ReportFragment::Colored(
243                    match failures {
244                        1 => "Test failed".into(),
245                        2 => "Both tests failed".into(),
246                        _ => format!("All {failures} tests failed"),
247                    },
248                    ReportKind::Error,
249                )]
250            }
251        } else {
252            let mut fragments = vec![ReportFragment::Colored(
253                match (successes, failures) {
254                    (1, 0) if not_run == 0 => "Test passed".into(),
255                    (2, 0) if not_run == 0 => "Both tests passed".into(),
256                    (suc, 0) if not_run == 0 => format!("All {suc} tests passed"),
257                    (suc, _) => {
258                        format!("{suc} test{} passed", if suc == 1 { "" } else { "s" })
259                    }
260                },
261                DiagnosticKind::Info.into(),
262            )];
263            if failures > 0 {
264                fragments.extend([
265                    ReportFragment::Plain(", ".into()),
266                    ReportFragment::Colored(format!("{failures} failed"), ReportKind::Error),
267                ])
268            }
269            fragments
270        };
271        if not_run > 0 {
272            if fragments.is_empty() {
273                fragments.push(ReportFragment::Colored(
274                    format!("0 of {not_run} tests ran"),
275                    ReportKind::Error,
276                ));
277            } else {
278                fragments.push(ReportFragment::Plain(", ".into()));
279                fragments.push(ReportFragment::Colored(
280                    format!("{not_run} didn't run"),
281                    DiagnosticKind::Warning.into(),
282                ));
283            }
284        }
285        Report {
286            fragments,
287            color: true,
288        }
289    }
290}
291
292impl fmt::Display for Report {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        for frag in &self.fragments {
295            match frag {
296                ReportFragment::Plain(s)
297                | ReportFragment::Faint(s)
298                | ReportFragment::Fainter(s) => write!(f, "{s}")?,
299                ReportFragment::Colored(s, kind) => {
300                    if self.color {
301                        let s = s.color(match kind {
302                            ReportKind::Error => Color::Red,
303                            ReportKind::Diagnostic(DiagnosticKind::Warning) => Color::Yellow,
304                            ReportKind::Diagnostic(DiagnosticKind::Style) => Color::Green,
305                            ReportKind::Diagnostic(DiagnosticKind::Advice) => Color::TrueColor {
306                                r: 50,
307                                g: 150,
308                                b: 255,
309                            },
310                            ReportKind::Diagnostic(DiagnosticKind::Info) => Color::BrightCyan,
311                        });
312                        write!(f, "{s}")?
313                    } else {
314                        write!(f, "{s}")?
315                    }
316                }
317                ReportFragment::Newline => writeln!(f)?,
318            }
319        }
320        Ok(())
321    }
322}