heraclitus_compiler/compiling/failing/
logger.rs

1//! This is a logger module which is used by compiler to log errors, warnings and info messages
2
3use colored::{Colorize, Color};
4use pad::PadStr;
5use crate::compiling::failing::position_info::PositionInfo;
6use crate::compiling::failing::message::MessageType;
7use crate::prelude::Position;
8
9#[cfg(feature = "serde")]
10use serde::{Serialize, Deserialize};
11
12/// This is a logger that is used to log messages to the user
13/// The logger is being used internally by the Message struct
14/// when invoking the `show` method
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct Logger {
17    kind: MessageType,
18    trace: Vec<PositionInfo>
19}
20
21impl Logger {
22    /// Create a new Displayer instance
23    pub fn new(kind: MessageType, trace: &[PositionInfo]) -> Self {
24        Logger {
25            kind,
26            trace: trace.to_vec()
27        }
28    }
29
30    fn kind_to_color(&self) -> Color {
31        match self.kind {
32            MessageType::Error => Color::Red,
33            MessageType::Warning => Color::Yellow,
34            MessageType::Info => Color::Blue
35        }
36    }
37
38    /// Render header of your information
39    pub fn header(self, kind: MessageType) -> Self {
40        let name = match kind {
41            MessageType::Error => " ERROR ".to_string(),
42            MessageType::Warning => " WARN ".to_string(),
43            MessageType::Info => " INFO ".to_string()
44        };
45        let formatted = name
46            .black()
47            .bold()
48            .on_color(self.kind_to_color());
49        eprint!("{formatted} ");
50        self
51    }
52
53    /// Render text with supplied coloring
54    pub fn text(self, text: Option<String>) -> Self {
55        if let Some(text) = text {
56            eprint!("{}", text.color(self.kind_to_color()));
57        }
58        self
59    }
60
61    /// Render text with supplied coloring and end it with a newline
62    pub fn line(self, text: Option<String>) -> Self {
63        if let Some(text) = text {
64            eprintln!("{}", text.color(self.kind_to_color()));
65        }
66        self
67    }
68
69    /// Render padded text with a newline, applying the supplied coloring, and end it with another newline
70    pub fn padded_line(self, text: Option<String>) -> Self {
71        if let Some(text) = text {
72            eprintln!("\n{}", text.color(self.kind_to_color()));
73        }
74        self
75    }
76
77    /// Render location details with supplied coloring
78    pub fn path(self) -> Self {
79        let get_row_col = |pos: &PositionInfo| match pos.position {
80            Position::Pos(row, col) => format!("{}:{}", row, col),
81            Position::EOF => " end of file".to_string()
82        };
83        let path = match self.trace.first() {
84            Some(pos) => {
85                [
86                    format!("at {}:{}", pos.get_path(), get_row_col(pos)),
87                    self.trace.iter()
88                        .skip(1)
89                        .map(|pos| format!("in {}:{}", pos.get_path(), get_row_col(pos)))
90                        .collect::<Vec<String>>()
91                        .join("\n")
92                ].join("\n")
93            },
94            None => {
95                "at [unknown]:0:0".to_string()
96            }
97        }.trim_end().to_string();
98        eprintln!("{}", path.color(self.kind_to_color()).dimmed());
99        self
100    }
101
102    // Returns last row, column and it's length
103    fn get_row_col_len(&self) -> Option<(usize, usize, usize)> {
104        match self.trace.first() {
105            Some(pos) => match pos.position {
106                Position::Pos(row, col) => Some((row, col, pos.len)),
107                Position::EOF => None
108            },
109            None => None
110        }
111    }
112
113    // Get max padding size (for line numbering)
114    fn get_max_pad_size(&self, length: usize) -> Option<usize> {
115        let (row, _, _) = self.get_row_col_len()?;
116        if row < length - 1 {
117            Some(format!("{}", row + 1).len())
118        }
119        else {
120            Some(format!("{}", row).len())
121        }
122    }
123
124    // Returns chopped string where fisrt and third part are supposed
125    // to be left as is but the second one is supposed to be highlighted
126    fn get_highlighted_part(&self, line: &str) -> Option<[String;3]> {
127        let (_row, col, len) = self.get_row_col_len()?;
128        let begin = col - 1;
129        let end = begin + len;
130        let mut results: [String; 3] = Default::default();
131        for (index, letter) in line.chars().enumerate() {
132            if index < begin {
133                results[0].push(letter);
134            }
135            else if index >= end {
136                results[2].push(letter);
137            }
138            else {
139                results[1].push(letter);
140            }
141        }
142        Some(results)
143    }
144
145    // Return requested row with appropriate coloring
146    fn get_snippet_row(&self, code: &Vec<String>, index: usize, offset: i8, overflow: &mut usize) -> Option<String> {
147        let (row, col, len) = self.get_row_col_len()?;
148        let max_pad = self.get_max_pad_size(code.len())?;
149        let index = index as i32 + offset as i32;
150        let row = row as i32 + offset as i32;
151        let code = code.get(index as usize)?.clone();
152        let line = format!("{row}").pad_to_width(max_pad);
153        // Case if we are in the same line as the error (or message)
154        if offset == 0 {
155            let slices = self.get_highlighted_part(&code)?;
156            let formatted = format!("{}{}{}", slices[0], slices[1].color(self.kind_to_color()), slices[2]);
157            let end = col.checked_add(len).unwrap_or(len);
158            // If we are at the end of the code snippet and there is still some
159            if end - 1 > code.chars().count() {
160                // We substract here 2 because 1 is the offset of col (starts at 1)
161                // and other 1 is the new line character that we do not display
162                *overflow = (end - 2).checked_sub(code.chars().count()).unwrap_or(0);
163            }
164            Some(format!("{line}| {formatted}"))
165        }
166        // Case if we are in a different line than the error (or message)
167        else {
168            // If there is some overflow value - display it as well
169            if *overflow > 0 {
170                // Case if all line is highlighted
171                if *overflow > code.chars().count() {
172                    Some(format!("{line}| {}", code.color(self.kind_to_color())).dimmed().to_string())
173                }
174                // Case if some line is highlighted
175                else {
176                    let err = code.get(0..*overflow).unwrap().to_string().color(self.kind_to_color());
177                    let rest = code.get(*overflow..).unwrap().to_string();
178                    Some(format!("{line}| {err}{rest}").dimmed().to_string())
179                }
180            }
181            // Case if no overflow
182            else {
183                Some(format!("{line}| {code}").dimmed().to_string())
184            }
185        }
186    }
187
188    /// Render snippet of the code if the message is contextual to it
189    pub fn snippet<T: AsRef<str>>(self, code: Option<T>) -> Self {
190        if let Some(pos) = self.trace.first() {
191            if let Ok(code) = std::fs::read_to_string(pos.get_path()) {
192                self.snippet_from_code(code);
193                return self;
194            }
195        }
196        if let Some(code) = code {
197            self.snippet_from_code(code.as_ref().to_string());
198        }
199        self
200    }
201
202    /// Render snippet of the code based on the code data
203    fn snippet_from_code(&self, code: String) -> Option<()> {
204        let (row, _, _) = self.get_row_col_len()?;
205        let mut overflow = 0;
206        let index = row - 1;
207        let code = code.split('\n')
208            .map(|item| item.trim_end().to_string())
209            .collect::<Vec<String>>();
210        eprintln!();
211        // Show additional code above the snippet
212        if let Some(line) = self.get_snippet_row(&code, index, -1, &mut overflow) {
213            eprintln!("{}", line);
214        }
215        // Show the current line of code
216        eprintln!("{}", self.get_snippet_row(&code, index, 0, &mut overflow)?);
217        // Show additional code below the snippet
218        eprintln!("{}", self.get_snippet_row(&code, index, 1, &mut overflow)?);
219        Some(())
220    }
221}
222
223#[cfg(test)]
224mod test {
225    #![allow(unused_imports)]
226    use std::time::Duration;
227    use std::thread::sleep;
228
229    use crate::prelude::{DefaultMetadata, Metadata, MessageType, PositionInfo, Token};
230    #[allow(unused_variables)]
231
232    #[test]
233    fn test_displayer() {
234        let code = vec![
235            "let a = 12",
236            "value = 'this",
237            "is mutltiline",
238            "code'"
239        ].join("\n");
240        // Uncomment to see the error message
241        sleep(Duration::from_secs(1));
242        let trace = [
243            PositionInfo::at_pos(Some("/path/to/bar".to_string()), (3, 4), 10),
244            PositionInfo::at_pos(Some("/path/to/foo".to_string()), (2, 9), 24),
245        ];
246        super::Logger::new(MessageType::Error, &trace)
247            .header(MessageType::Error)
248            .line(Some(format!("Cannot call function \"foobar\" on a number")))
249            .path()
250            .snippet(Some(code));
251    }
252
253    #[test]
254    fn test_end_of_line_displayer() {
255        let code = vec![
256            "hello"
257        ].join("\n");
258        // Uncomment to see the error message
259        sleep(Duration::from_secs(1));
260        let trace = [
261            PositionInfo::at_pos(Some("/path/to/foo".to_string()), (2, 6), 1)
262        ];
263        super::Logger::new(MessageType::Error, &trace)
264            .header(MessageType::Error)
265            .line(Some(format!("Cannot call function \"foobar\" on a number")))
266            .path()
267            .snippet(Some(code));
268    }
269
270    #[test]
271    fn test_between_tokens() {
272        let code = vec![
273            "foo(12 + 24)"
274        ].join("\n");
275        // Uncomment to see the error message
276        sleep(Duration::from_secs(1));
277        let begin = Token { word: "12".to_string(), pos: (1, 5), start: 4 };
278        let end = Token { word: ")".to_string(), pos: (1, 12), start: 11 };
279        let mut meta = DefaultMetadata::new(vec![], Some("/path/to/foo".to_string()), Some(code.clone()));
280        let trace = [
281            PositionInfo::from_between_tokens(&mut meta, Some(begin), Some(end))
282        ];
283        super::Logger::new(MessageType::Error, &trace)
284            .header(MessageType::Error)
285            .line(Some(format!("Cannot call function \"foobar\" on a number")))
286            .path()
287            .snippet(Some(code));
288    }
289}