lisbeth_error/
error.rs

1//! An error report with annotations
2//!
3//! The [`AnnotatedError`] type allows to construct error with annotations on it.
4
5use std::iter;
6
7use crate::{
8    reporter::Annotation as ReportedAnnotation,
9    span::{Position, Span},
10};
11
12/// An error report with annotations.
13///
14/// This error report is created with the precise span at which the error occurs
15/// and a simple message explaining the situation. It can then be improved by
16/// adding more information.
17///
18/// # Example
19///
20/// The following code shows how to create a simple error report.
21///
22/// ```rust
23/// use lisbeth_error::{
24///     error::AnnotatedError,
25///     span::{Span, SpannedStr},
26/// };
27///
28/// let file = SpannedStr::input_file("The cat are on the table.");
29///
30/// let cat = file.split_at(4).1.split_at(3).0;
31/// let are = file.split_at(8).1.split_at(3).0;
32///
33/// let report = AnnotatedError::new(are.span(), "Conjugation error")
34///     .with_annotation(cat.span(), "`cat` is singular,")
35///     .with_annotation(are.span(), "but `are` is used only for plural subject");
36/// ```
37#[derive(Clone, Debug, PartialEq)]
38pub struct AnnotatedError {
39    pub(crate) span: Span,
40    pub(crate) msg: String,
41    annotations: Vec<Annotation>,
42}
43
44impl AnnotatedError {
45    /// Constructs a new report.
46    ///
47    /// `span` represents the precise location at which the error is
48    /// encountered, `msg` describes the issue. `msg` can be either a static
49    /// string slice or a `String`.
50    pub fn new<Msg>(span: Span, msg: Msg) -> AnnotatedError
51    where
52        Msg: ToString,
53    {
54        let msg = msg.to_string();
55        AnnotatedError {
56            annotations: Vec::new(),
57            span,
58            msg,
59        }
60    }
61
62    /// Adds a new annotation at a given span to the report.
63    pub fn with_annotation<Msg>(mut self, span: Span, msg: Msg) -> AnnotatedError
64    where
65        Msg: ToString,
66    {
67        let content = msg.to_string();
68        let ann = Annotation { span, content };
69        self.annotations.push(ann);
70        self
71    }
72
73    /// Returns the span at which the error is encountered.
74    pub fn span(&self) -> Span {
75        self.span
76    }
77
78    fn all_spans<'a>(&'a self) -> impl Iterator<Item = Span> + 'a {
79        self.annotations
80            .iter()
81            .map(|a| a.span)
82            .chain(iter::once(self.span))
83    }
84
85    pub(crate) fn bounds(&self) -> (Position, Position) {
86        // These two unwraps won't panic because all_spans returns an iterator
87        // that contains at least the span at which the error happened.
88        let min = self.all_spans().map(Span::start).min().unwrap();
89        let max = self.all_spans().map(Span::end).max().unwrap();
90
91        (min, max)
92    }
93
94    pub(crate) fn error_matrix<'a>(&'a self) -> Vec<Vec<ReportedAnnotation<'a>>> {
95        let (start_pos, end_pos) = self.bounds();
96
97        let (first_line_number, last_line_number) =
98            (start_pos.line() as usize, end_pos.line() as usize);
99        let total_line_number = last_line_number - first_line_number + 1;
100
101        let mut matrix = (0..total_line_number)
102            .map(|_| Vec::new())
103            .collect::<Vec<_>>();
104
105        for annotation in self.annotations.iter() {
106            let line_idx = annotation.span.start().line() as usize - first_line_number;
107
108            assert_eq!(
109                annotation.span.start().line(),
110                annotation.span.end().line(),
111                "Multiline spans are not supported",
112            );
113
114            let col_number = annotation.span.start().col() as usize;
115            let length = annotation.span.end().col() as usize - col_number;
116            let text = annotation.content.as_str();
117
118            let ann = ReportedAnnotation {
119                col_number,
120                length,
121                text,
122            };
123            matrix[line_idx].push(ann);
124        }
125
126        matrix
127            .iter_mut()
128            .for_each(|anns| anns.sort_by_key(|a| a.col_number));
129
130        matrix
131    }
132}
133
134#[derive(Clone, Debug, PartialEq)]
135struct Annotation {
136    span: Span,
137    content: String,
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    mod annotated_error {
145        use super::*;
146
147        use crate::span::SpannedStr;
148
149        #[test]
150        fn bounds_double() {
151            let input = SpannedStr::input_file("ab");
152            let (a, b) = input.split_at(1);
153
154            let report = AnnotatedError::new(a.span(), "Some generic message")
155                .with_annotation(a.span(), "ann1")
156                .with_annotation(b.span(), "ann2");
157
158            let (start, end) = report.bounds();
159
160            assert_eq!(start.offset(), 0);
161            assert_eq!(end.offset(), 2);
162
163            assert_eq!(start.col(), 0);
164            assert_eq!(end.col(), 2);
165
166            assert_eq!(start.line(), 0);
167            assert_eq!(end.line(), 0);
168        }
169
170        #[test]
171        fn bounds_single() {
172            let input = SpannedStr::input_file("ab");
173
174            let report = AnnotatedError::new(input.span(), "Some generic message");
175
176            let (start, end) = report.bounds();
177
178            assert_eq!(start.offset(), 0);
179            assert_eq!(end.offset(), 2);
180
181            assert_eq!(start.col(), 0);
182            assert_eq!(end.col(), 2);
183
184            assert_eq!(start.line(), 0);
185            assert_eq!(end.line(), 0);
186        }
187
188        #[test]
189        fn error_matrix_for() {
190            // In this text, there is a line that gets ignored because it has
191            // no annotation in it, and two lines that contain one and two
192            // annotations respectively.
193
194            let input_file = SpannedStr::input_file("line 1\nline 2\nline3");
195
196            let l1 = input_file.split_at(6).0;
197            assert_eq!(l1.content(), "line 1");
198            let l2 = input_file.split_at(7).1.split_at(6).0;
199            assert_eq!(l2.content(), "line 2");
200            let num = input_file.split_at(12).1.split_at(1).0;
201            assert_eq!(num.content(), "2");
202
203            let report = AnnotatedError::new(l1.span(), "<insert general message here>")
204                .with_annotation(l1.span(), "first line")
205                .with_annotation(l2.span(), "second line")
206                .with_annotation(num.span(), "second line, but better");
207
208            let matrix = report.error_matrix();
209
210            assert_eq!(matrix.len(), 2);
211            assert_eq!(matrix[0].len(), 1);
212            assert_eq!(matrix[1].len(), 2);
213
214            assert!(matrix[1][0].col_number < matrix[1][1].col_number);
215        }
216    }
217}