roan_error/
diagnostic.rs

1use crate::{error::RoanError, span::TextSpan};
2use colored::Colorize;
3use log::Level;
4use std::io::{BufWriter, Stderr, Write};
5
6/// Represents a diagnostic message, which includes information about an error or warning
7/// and can be pretty-printed to the console.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Diagnostic {
10    /// The title or summary of the diagnostic message.
11    pub title: String,
12    /// An optional detailed description of the diagnostic message.
13    pub text: Option<String>,
14    /// The severity level of the diagnostic message (e.g., Error, Warning).
15    pub level: Level,
16    /// The location in the source code where the error or warning occurred, represented as a `TextSpan`.
17    pub location: Option<TextSpan>,
18    /// An optional hint that provides additional guidance on resolving the issue.
19    pub hint: Option<String>,
20    /// The content of the source code related to the diagnostic.
21    pub content: Option<String>,
22}
23
24impl Diagnostic {
25    /// Logs the diagnostic in a human-readable format to the provided buffer.
26    ///
27    /// The message is colored according to its severity level, and the source code around
28    /// the error location (if available) is highlighted.
29    ///
30    /// # Arguments
31    ///
32    /// * `buff` - A mutable reference to a `BufWriter` that writes to `stderr`.
33    ///
34    /// # Example
35    ///
36    /// ```rust ignore
37    /// use std::io::BufWriter;
38    /// use log::Level;
39    /// use roan_error::{Diagnostic, Position, TextSpan};
40    /// let diagnostic = Diagnostic {
41    ///     title: "Syntax Error".to_string(),
42    ///     text: None,
43    ///     level: Level::Error,
44    ///     location: Some(TextSpan::new(Position::new(1, 1, 0), Position::new(1, 5, 4), "test".to_string())),
45    ///     hint: None,
46    ///     content: Some("let x = ;".to_string()),
47    /// };
48    ///
49    /// let mut buff = BufWriter::new(std::io::stderr());
50    /// diagnostic.log_pretty(&mut buff);
51    /// ```
52    pub fn log_pretty(&self, buff: &mut BufWriter<Stderr>) {
53        writeln!(
54            buff,
55            "{}{}{}",
56            self.level.to_string().to_lowercase().bright_red(),
57            ": ".dimmed(),
58            self.title
59        )
60        .expect("Error writing level");
61
62        if let Some(location) = &self.location {
63            if let Some(content) = &self.content {
64                let line_number = location.start.line;
65                let line = content
66                    .lines()
67                    .nth((line_number - 1) as usize)
68                    .unwrap_or("");
69                let column = location.start.column;
70                let line_content = line.trim_end();
71                let decoration =
72                    "^".repeat(location.end.column as usize - location.start.column as usize);
73
74                writeln!(buff, "{} {}:{}", "--->".cyan(), line_number, column)
75                    .expect("Error writing line number");
76
77                if line_number > 1 {
78                    let line_before = format!("{} |", line_number - 1);
79                    writeln!(buff, "{}", line_before.cyan()).expect("Error writing line number");
80                }
81
82                let line_current = format!("{} |", line_number);
83                write!(buff, "{}", line_current.cyan()).expect("Error writing line number");
84                writeln!(buff, "    {}", line_content).expect("Error writing content");
85
86                let padding_left =
87                    " ".repeat((column + 6 + line_number.to_string().len() as u32) as usize);
88                writeln!(buff, "{}{}", padding_left, decoration.bright_red())
89                    .expect("Error writing decoration");
90
91                if line_number > 1 {
92                    let line_after = format!("{} |", line_number + 1);
93                    writeln!(buff, "{}", line_after.cyan()).expect("Error writing line number");
94                }
95            }
96        }
97
98        if let Some(text) = &self.text {
99            writeln!(buff, "{}", text).expect("Error writing text");
100        }
101
102        self.print_hint(buff);
103    }
104
105    /// Prints a hint message (if available) to the provided buffer.
106    ///
107    /// # Arguments
108    ///
109    /// * `buff` - A mutable reference to a `BufWriter` that writes to `stderr`.
110    pub fn print_hint(&self, buff: &mut BufWriter<Stderr>) {
111        if let Some(hint) = &self.hint {
112            writeln!(buff, "{}{}", "Hint: ".bright_cyan(), hint.bright_cyan())
113                .expect("Error writing hint");
114        }
115    }
116}
117
118/// Prints a diagnostic message based on the provided error. The function matches
119/// the error type with corresponding diagnostics and logs it prettily.
120///
121/// # Arguments
122///
123/// * `err` - An `anyhow::Error` object that encapsulates the actual error.
124/// * `content` - An optional string slice containing the source code related to the error.
125///
126/// # Example
127///
128/// ```rust ignore
129/// use roan_error::error::PulseError;
130/// use roan_error::print_diagnostic;
131/// let err = PulseError::SemanticError("Unexpected token".to_string(), span);
132/// print_diagnostic(anyhow::Error::new(err), Some(source_code));
133/// ```
134pub fn print_diagnostic(err: anyhow::Error, content: Option<String>) {
135    let pulse_error = err.downcast_ref::<RoanError>();
136
137    if let Some(err) = pulse_error {
138        let err_str = err.to_string();
139
140        if let RoanError::Throw(content, frames) = err {
141            let mut buff = BufWriter::new(std::io::stderr());
142            write!(buff, "{}{}", "error".bright_red(), ": ".dimmed()).expect("Error writing level");
143            writeln!(buff, "{}", content).expect("Error writing text");
144            for frame in frames {
145                writeln!(buff, "{:?}", frame).expect("Error writing text");
146            }
147            return;
148        }
149
150        let diagnostic = match err {
151            RoanError::Io(_) => Diagnostic {
152                title: "IO error".to_string(),
153                text: Some(err_str),
154                level: Level::Error,
155                location: None,
156                hint: None,
157                content: None,
158            },
159            RoanError::RestParameterNotLast(span)
160            | RoanError::RestParameterNotLastPosition(span)
161            | RoanError::MultipleRestParameters(span)
162            | RoanError::SelfParameterCannotBeRest(span)
163            | RoanError::SelfParameterNotFirst(span)
164            | RoanError::MultipleSelfParameters(span)
165            | RoanError::StaticContext(span)
166            | RoanError::StaticMemberAccess(span)
167            | RoanError::StaticMemberAssignment(span) => Diagnostic {
168                title: err_str,
169                text: None,
170                level: Level::Error,
171                location: Some(span.clone()),
172                hint: None,
173                content,
174            },
175            RoanError::InvalidToken(_, span)
176            | RoanError::SemanticError(_, span)
177            | RoanError::UnexpectedToken(_, span)
178            | RoanError::InvalidEscapeSequence(_, span)
179            | RoanError::NonBooleanCondition(_, span)
180            | RoanError::StructNotFoundError(_, span)
181            | RoanError::TraitNotFoundError(_, span) => Diagnostic {
182                title: err_str,
183                text: None,
184                level: Level::Error,
185                location: Some(span.clone()),
186                hint: None,
187                content,
188            },
189            RoanError::TraitMethodNotImplemented(name, methods, span) => Diagnostic {
190                title: format!(
191                    "Trait {name} doesn't implement these methods: {}",
192                    methods.join(", ")
193                ),
194                text: None,
195                level: Level::Error,
196                location: Some(span.clone()),
197                hint: Some("Method not implemented".to_string()),
198                content,
199            },
200            RoanError::StructAlreadyImplementsTrait(_, _, span) => Diagnostic {
201                title: err_str,
202                text: None,
203                level: Level::Error,
204                location: Some(span.clone()),
205                hint: Some("Struct already implements this trait".to_string()),
206                content,
207            },
208            RoanError::ExpectedToken(expected, hint, span) => Diagnostic {
209                title: format!("Expected {}", expected),
210                text: None,
211                level: Level::Error,
212                location: Some(span.clone()),
213                hint: Some(hint.clone()),
214                content,
215            },
216            RoanError::FailedToImportModule(_, _, span) => Diagnostic {
217                title: err_str,
218                text: None,
219                level: Level::Error,
220                location: Some(span.clone()),
221                hint: None,
222                content,
223            },
224            RoanError::InvalidType(_, _, span) => Diagnostic {
225                title: err_str,
226                text: None,
227                level: Level::Error,
228                location: Some(span.clone()),
229                hint: None,
230                content,
231            },
232            RoanError::ResolverError(_) => Diagnostic {
233                title: err_str,
234                text: None,
235                level: Level::Error,
236                location: None,
237                hint: None,
238                content: None,
239            },
240            RoanError::ModuleError(_) => Diagnostic {
241                title: err_str,
242                text: None,
243                level: Level::Error,
244                location: None,
245                hint: None,
246                content: None,
247            },
248            | RoanError::UndefinedFunctionError(_, span)
249            | RoanError::VariableNotFoundError(_, span)
250            | RoanError::ImportError(_, span)
251            | RoanError::PropertyNotFoundError(_, span)
252            | RoanError::TypeMismatch(_, span)
253            | RoanError::InvalidAssignment(_, span)
254            | RoanError::MissingParameter(_, span)
255            | RoanError::InvalidUnaryOperation(_, span) => Diagnostic {
256                title: err_str,
257                text: None,
258                level: Level::Error,
259                location: Some(span.clone()),
260                hint: None,
261                content,
262            },
263            RoanError::InvalidBreakOrContinue(span) => Diagnostic {
264                title: err_str,
265                text: None,
266                level: Level::Error,
267                location: Some(span.clone()),
268                hint: Some(
269                    "Break and continue statements can only be used inside loops".to_string(),
270                ),
271                content,
272            },
273            RoanError::LoopBreak(span) | RoanError::LoopContinue(span) => Diagnostic {
274                title: err_str,
275                text: None,
276                level: Level::Error,
277                location: Some(span.clone()),
278                hint: Some(
279                    "Break and continue statements can only be used inside loops".to_string(),
280                ),
281                content,
282            },
283            RoanError::InvalidSpread(span) => Diagnostic {
284                title: err_str,
285                text: None,
286                level: Level::Error,
287                location: Some(span.clone()),
288                hint: Some(
289                    "Spread operator can only be used in function calls or vectors".to_string(),
290                ),
291                content,
292            },
293            RoanError::InvalidPropertyAccess(span) => Diagnostic {
294                title: err_str,
295                text: None,
296                level: Level::Error,
297                location: Some(span.clone()),
298                hint: Some("Only string literals or call expressions are allowed".to_string()),
299                content,
300            },
301            RoanError::IndexOutOfBounds(_, _, span) => Diagnostic {
302                title: err_str,
303                text: None,
304                level: Level::Error,
305                location: Some(span.clone()),
306                hint: None,
307                content,
308            },
309            _ => {
310                log::error!("{:?}", err);
311                return;
312            }
313        };
314
315        let mut buff = BufWriter::new(std::io::stderr());
316        diagnostic.log_pretty(&mut buff);
317    } else {
318        log::error!("{:?}", err);
319    }
320}