runic_kit/
error.rs

1//! This module provides error handling utilities.
2
3use colored::*;
4
5use crate::{
6    source::Source,
7    span::{Span, location_to_line_col},
8};
9
10/// Represents an advanced error.
11#[derive(Debug)]
12pub struct Error<'a> {
13    /// The error message describing the issue.
14    message: String,
15    /// The source code where the error occurred.
16    source: &'a Source<'a>,
17    /// The span in the source code where the error occurred.
18    span: Span,
19    /// The context of the error, if any.
20    context: Vec<String>,
21    /// Notes or additional information about the error.
22    notes: Vec<String>,
23}
24
25impl<'a> Error<'a> {
26    /// Creates a new `Error`
27    pub fn new(message: String, source: &'a Source<'a>, span: Span) -> Self {
28        Error {
29            message,
30            source,
31            span,
32            context: Vec::new(),
33            notes: Vec::new(),
34        }
35    }
36
37    /// Creates a new `Error`, adding the given context to the error.
38    pub fn with_context(mut self, context: String) -> Self {
39        self.context.push(context);
40        self
41    }
42
43    /// Creates a new `Error`, adding the given note to the error.
44    pub fn with_note(mut self, note: String) -> Self {
45        self.notes.push(note);
46        self
47    }
48
49    /// Displays the error in a human-readable format.
50    pub fn display(&self) {
51        let (start_line, start_col) = location_to_line_col(&self.source.code, self.span.start);
52        let (end_line, mut end_col) = location_to_line_col(&self.source.code, self.span.end);
53        end_col -= 1;
54
55        let number_of_spaces = start_line.max(end_line).to_string().len();
56
57        eprintln!(
58            "{}{} {}",
59            "error".red().bold(),
60            ":".bold(),
61            self.message.bold()
62        );
63
64        if start_line == end_line {
65            if start_col == end_col {
66                eprintln!(
67                    "{}{} {}:{}:{}",
68                    " ".repeat(number_of_spaces),
69                    "-->".cyan().bold(),
70                    self.source.filename,
71                    start_line,
72                    start_col
73                );
74            } else {
75                eprintln!(
76                    "{}{} {}:{}:{}-{}",
77                    " ".repeat(number_of_spaces),
78                    "-->".cyan().bold(),
79                    self.source.filename,
80                    start_line,
81                    start_col,
82                    end_col
83                );
84            }
85        } else {
86            eprintln!(
87                "{}{} {}:{}:{}-{}:{}",
88                " ".repeat(number_of_spaces),
89                "-->".cyan().bold(),
90                self.source.filename,
91                start_line,
92                start_col,
93                end_line,
94                end_col
95            );
96        }
97
98        let lines = self.source.code.lines().collect::<Vec<&str>>();
99        let lines = lines
100            .iter()
101            .skip(start_line - 1)
102            .take(end_line - start_line + 1);
103
104        eprintln!("{} {}", " ".repeat(number_of_spaces), "|".cyan().bold());
105
106        for (line_index, line) in lines.enumerate() {
107            let line_number = start_line + line_index;
108
109            if line_number == start_line && line_number == end_line {
110                eprintln!(
111                    "{}{} {} {}",
112                    line_number.to_string().cyan().bold(),
113                    " ".repeat(number_of_spaces - line_number.to_string().len()),
114                    "|".cyan().bold(),
115                    line
116                );
117                eprintln!(
118                    "{} {} {}{}",
119                    " ".repeat(number_of_spaces),
120                    "|".cyan().bold(),
121                    " ".repeat(start_col - 1),
122                    "^".repeat(end_col - start_col + 1).red().bold()
123                );
124                continue;
125            }
126
127            eprintln!(
128                "{}{} {} {}",
129                line_number.to_string().cyan().bold(),
130                " ".repeat(number_of_spaces - line_number.to_string().len()),
131                "|".cyan().bold(),
132                line
133            );
134
135            if line_number == start_line {
136                eprintln!(
137                    "{} {} {}{}",
138                    " ".repeat(number_of_spaces),
139                    "|".cyan().bold(),
140                    " ".repeat(start_col - 1),
141                    "^".repeat(line.len() - start_col + 1).red().bold()
142                );
143            } else if line_number == end_line {
144                eprintln!(
145                    "{} {} {}",
146                    " ".repeat(number_of_spaces),
147                    "|".cyan().bold(),
148                    "^".repeat(end_col + 1).red().bold()
149                );
150            } else {
151                eprintln!(
152                    "{} {} {}",
153                    " ".repeat(number_of_spaces),
154                    "|".cyan().bold(),
155                    "^".repeat(line.len()).red().bold()
156                );
157            }
158        }
159
160        if !self.context.is_empty() || !self.notes.is_empty() {
161            eprintln!("{} {}", " ".repeat(number_of_spaces), "|".cyan().bold());
162        }
163
164        for context in self.context.iter() {
165            eprintln!(
166                "{} {} {}",
167                " ".repeat(number_of_spaces),
168                "=".cyan().bold(),
169                context
170            );
171        }
172
173        for note in self.notes.iter() {
174            eprintln!(
175                "{} {} {} {}",
176                " ".repeat(number_of_spaces),
177                "=".cyan().bold(),
178                "note:".bold(),
179                note
180            );
181        }
182    }
183}
184
185/// Displays a basic error message.
186///
187/// # Usage
188///
189/// ```rust
190/// use runic_kit::error::basic_error;
191///
192/// basic_error("An error occurred".to_string()); // Displays: "error: An error occurred"
193/// ```
194pub fn basic_error(message: String) {
195    eprintln!("{}: {}", "error".red().bold(), message.bold());
196}
197
198#[cfg(test)]
199mod tests {
200    // TODO: check stdout for expected output
201
202    use super::*;
203    use crate::span::Span;
204
205    #[test]
206    fn test_error_display_single_line() {
207        let source = Source::from_str(
208            "test.rs",
209            "fn main() {\n    println!(\"Hello, world!\");\n}",
210        );
211        let span = Span { start: 12, end: 25 };
212        let error = Error::new("Syntax error".to_string(), &source, span)
213            .with_context("In function main".to_string())
214            .with_note("Check the syntax".to_string());
215        error.display();
216    }
217
218    #[test]
219    fn test_error_display_multi_line() {
220        let source = Source::from_str(
221            "test.rs",
222            "fn main() {\n    println!(\"Hello, world!\");\n}",
223        );
224        let span = Span { start: 12, end: 40 };
225        let error = Error::new("Syntax error".to_string(), &source, span)
226            .with_context("In function main".to_string())
227            .with_note("Check the syntax".to_string());
228        error.display();
229    }
230
231    #[test]
232    fn test_basic_error() {
233        let message = "An error occurred".to_string();
234        basic_error(message);
235    }
236}