1use std::{
49 fmt::{self, Display},
50 fs,
51 io::Error as IOError,
52};
53
54use crate::{
55 error::AnnotatedError,
56 span::{Position, Span, SpannedStr},
57};
58
59pub struct ErrorReporter {
70 path: Option<String>,
71 content: String,
72 span: Span,
73}
74
75impl ErrorReporter {
76 pub fn input_file(path: String, content: String) -> ErrorReporter {
80 let path = Some(path);
81 let span = Span::of_file(content.as_str());
82 ErrorReporter {
83 content,
84 path,
85 span,
86 }
87 }
88
89 pub fn non_file_input(content: String) -> ErrorReporter {
94 let path = None;
95 let span = Span::of_file(content.as_str());
96 ErrorReporter {
97 content,
98 path,
99 span,
100 }
101 }
102
103 pub fn from_path(path: String) -> Result<ErrorReporter, IOError> {
105 fs::read_to_string(path.as_str())
106 .map(|content| (Span::of_file(content.as_str()), content, Some(path)))
107 .map(|(span, content, path)| ErrorReporter {
108 content,
109 path,
110 span,
111 })
112 }
113
114 pub fn path(&self) -> Option<&str> {
116 self.path.as_deref()
117 }
118
119 pub fn spanned_str(&self) -> SpannedStr {
130 SpannedStr::assemble(self.content.as_str(), self.span)
132 }
133
134 fn code_snippet_for(&self, start_pos: Position, end_pos: Position) -> &str {
135 let (start_offset, end_offset) = (start_pos.offset() as usize, end_pos.offset() as usize);
136
137 let before_start = self.content.split_at(start_offset).0;
138 let after_end = self.content.split_at(end_offset).1;
139
140 let end_idx = end_offset as usize
141 + after_end
142 .char_indices()
143 .find(|(_, c)| *c == '\n')
144 .map(|(idx, _)| idx)
145 .unwrap_or_else(|| after_end.len());
146
147 let start_idx = before_start
148 .char_indices()
149 .rev()
150 .take_while(|(_, c)| *c != '\n')
151 .last()
152 .map(|(idx, _)| idx)
153 .unwrap_or_else(|| before_start.len());
154
155 self.content.split_at(end_idx).0.split_at(start_idx).1
156 }
157
158 pub fn format_error<'a>(&'a self, err: &'a AnnotatedError) -> FormattedError<'a> {
162 let (start_pos, end_pos) = err.bounds();
163 let stream_name = self.path();
164 let text = self.code_snippet_for(start_pos, end_pos);
165
166 let pos = err.span.start();
167 let general_msg = err.msg.as_str();
168
169 let errors = err.error_matrix();
170
171 let first_line_number = start_pos.line() as usize;
172
173 FormattedError {
174 pos,
175 first_line_number,
176 general_msg,
177 stream_name,
178 text,
179 errors,
180 }
181 }
182}
183
184#[derive(Clone, Debug, PartialEq)]
189pub struct FormattedError<'a> {
190 pos: Position,
191 general_msg: &'a str,
192 stream_name: Option<&'a str>,
193 first_line_number: usize,
194 text: &'a str,
196 errors: Vec<Vec<Annotation<'a>>>,
197}
198
199impl<'a> FormattedError<'a> {
200 fn write_general_message(&self, f: &mut fmt::Formatter) -> fmt::Result {
201 writeln!(f, "Error: {}", self.general_msg)
202 }
203
204 fn write_position(&self, f: &mut fmt::Formatter) -> fmt::Result {
205 let (line, col) = (self.pos.line() + 1, self.pos.col() + 1);
206 match self.stream_name {
207 Some(name) => writeln!(f, " --> {}:{}:{}", name, line, col),
208 None => writeln!(f, " --> {}:{}", line, col),
209 }
210 }
211
212 fn write_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
213 self.write_general_message(f)?;
214 self.write_position(f)
215 }
216
217 fn spacing(&self) -> usize {
218 self.errors
219 .iter()
220 .flatten()
221 .map(|ann| ann.text.len())
222 .max()
223 .unwrap_or(0)
224 }
225
226 fn write_line(
227 content: &str,
228 spacing: usize,
229 number: usize,
230 f: &mut fmt::Formatter,
231 ) -> fmt::Result {
232 writeln!(f, " {:>3} | {} {}", number, " ".repeat(spacing), content)
233 }
234
235 fn write_underlines(
236 errs: &[Annotation<'_>],
237 spacing: usize,
238 f: &mut fmt::Formatter,
239 ) -> fmt::Result {
240 write!(f, " | {} ", " ".repeat(spacing))?;
241
242 let mut current_col_number = 0;
243 for annotation in errs {
244 let delta = annotation.col_number - current_col_number;
245 let length = usize::max(1, annotation.length);
246 let chr = if length == 1 { "|" } else { "^" };
247
248 write!(f, "{}{}", " ".repeat(delta), chr.repeat(length))?;
249
250 current_col_number += delta + length;
251 }
252
253 writeln!(f)
254 }
255
256 fn write_error_line(
257 annotation: &Annotation,
258 spacing: usize,
259 other_annotations: &[Annotation],
260 f: &mut fmt::Formatter,
261 ) -> fmt::Result {
262 let pipe_len = spacing - annotation.text.len() + annotation.col_number + 1;
263
264 write!(f, " | {}{}'", annotation.text, "-".repeat(pipe_len))?;
265
266 let mut current_col_number = annotation.col_number;
267
268 for annotation in other_annotations {
269 let delta = annotation.col_number - current_col_number - 1;
270 write!(f, "{}|", " ".repeat(delta))?;
271
272 current_col_number = annotation.col_number;
273 }
274
275 writeln!(f)
276 }
277
278 fn write_errors(
279 annotations: &[Annotation<'_>],
280 spacing: usize,
281 f: &mut fmt::Formatter,
282 ) -> fmt::Result {
283 Self::write_underlines(annotations, spacing, f)?;
284
285 for idx in 0..annotations.len() {
286 let annotation = &annotations[idx];
287 let annotations = &annotations[idx + 1..];
288
289 Self::write_error_line(annotation, spacing, annotations, f)?;
290 }
291
292 Ok(())
293 }
294}
295
296impl<'a> Display for FormattedError<'a> {
297 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
298 self.write_header(f)?;
299
300 let spacing = self.spacing();
301
302 writeln!(f, " |")?;
303
304 for (idx, (line, errs)) in self.text.lines().zip(self.errors.iter()).enumerate() {
305 Self::write_line(line, spacing, idx + self.first_line_number + 1, f)?;
306 Self::write_errors(errs, spacing, f)?;
307
308 writeln!(f, " |")?;
309 }
310
311 Ok(())
312 }
313}
314
315#[derive(Clone, Debug, PartialEq)]
316pub(crate) struct Annotation<'a> {
317 pub(crate) col_number: usize,
318 pub(crate) length: usize,
319 pub(crate) text: &'a str,
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 mod reporting {
327 use super::*;
329
330 #[test]
331 fn reporting_simple() {
332 let input_file = ErrorReporter::non_file_input("hello, world".to_string());
333
334 let hello = input_file.spanned_str().split_at(5).0;
335 let comma = input_file.spanned_str().split_at(5).1.split_at(1).0;
336 let world = input_file.spanned_str().split_at(7).1;
337
338 let report = AnnotatedError::new(comma.span(), "Don't you recognize me?")
339 .with_annotation(hello.span(), "Hi sweetie")
340 .with_annotation(world.span(), "I am not a world!")
341 .with_annotation(comma.span(), "Such cute, very comma");
342
343 let left = input_file.format_error(&report).to_string();
344
345 let right = "\
346 Error: Don't you recognize me?\n \
347 --> 1:6\n \
348 |\n \
349 1 | hello, world\n \
350 | ^^^^^| ^^^^^\n \
351 | Hi sweetie------------' | |\n \
352 | Such cute, very comma------' |\n \
353 | I am not a world!------------'\n \
354 |\n\
355 ";
356
357 assert_eq!(left, right);
358 }
359
360 #[test]
361 fn conjugaison_error() {
362 let reporter = ErrorReporter::input_file(
363 "docs.txt".to_string(),
364 "The cat are on the table.".to_string(),
365 );
366 let file = reporter.spanned_str();
367
368 let cat = file.split_at(4).1.split_at(3).0;
369 let are = file.split_at(8).1.split_at(3).0;
370
371 let report = AnnotatedError::new(are.span(), "Conjugation error")
372 .with_annotation(cat.span(), "`cat` is singular,")
373 .with_annotation(are.span(), "but `are` is used only for plural subject");
374
375 let left = reporter.format_error(&report).to_string();
376
377 let right = "\
378 Error: Conjugation error\n \
379 --> docs.txt:1:9\n \
380 |\n \
381 1 | The cat are on the table.\n \
382 | ^^^ ^^^\n \
383 | `cat` is singular,----------------------------' |\n \
384 | but `are` is used only for plural subject---------'\n \
385 |\n\
386 ";
387
388 assert_eq!(left, right);
389 }
390
391 #[test]
392 fn multiline_simple() {
393 let reporter = ErrorReporter::non_file_input("Hello\nWorld".into());
394 let content = reporter.spanned_str();
395
396 let hello = content.split_at(5).0;
397 let world = content.split_at(6).1;
398
399 let report = AnnotatedError::new(hello.span(), "Foo")
400 .with_annotation(hello.span(), "bar")
401 .with_annotation(world.span(), "baz");
402
403 let left = reporter.format_error(&report).to_string();
404
405 let right = "\
406 Error: Foo\n \
407 --> 1:1\n \
408 |\n \
409 1 | Hello\n \
410 | ^^^^^\n \
411 | bar-'\n \
412 |\n \
413 2 | World\n \
414 | ^^^^^\n \
415 | baz-'\n \
416 |\n\
417 ";
418
419 assert_eq!(left, right);
420 }
421 }
422
423 mod error_reporter {
424 use super::*;
425
426 #[test]
427 fn code_snippet_for_single_line() {
428 let foobar = "foo bar";
429 let input_file = ErrorReporter::non_file_input(foobar.to_string());
430
431 let foo = input_file.spanned_str().split_at(3).0;
432 let bar = input_file.spanned_str().split_at(4).1;
433
434 let report = AnnotatedError::new(foo.span(), "Common word found")
435 .with_annotation(foo.span(), "This happens to be a common word")
436 .with_annotation(bar.span(), "This too by the way");
437
438 let (start, end) = report.bounds();
439
440 let selected_text = input_file.code_snippet_for(start, end);
441
442 assert_eq!(selected_text, "foo bar");
443 }
444
445 #[test]
446 fn code_snippet_for_select_specific() {
447 let input_text = "foo bar\nbarbar\nbazbaz";
448 let input_file = ErrorReporter::non_file_input(input_text.to_string());
449
450 let barbar = input_file.spanned_str().split_at(8).1.split_at(6).0;
451 assert_eq!(barbar.content(), "barbar");
452 let report = AnnotatedError::new(barbar.span(), "Found a non-existant word");
453
454 let (start, end) = report.bounds();
455
456 let selected_text = input_file.code_snippet_for(start, end);
457
458 assert_eq!(selected_text, "barbar");
459 }
460 }
461}