regex_anre/
errorprinter.rs

1// Copyright (c) 2024 Hemashushu <hippospark@gmail.com>, All rights reserved.
2//
3// This Source Code Form is subject to the terms of
4// the Mozilla Public License version 2.0 and additional exceptions,
5// more details in file LICENSE, LICENSE.additional and CONTRIBUTING.
6
7use crate::AnreError;
8
9//                 /-- selection start
10//                 |                 /-- selection length
11//                 v                 v
12// prefix -->   ...snippet_source_text...  <-- suffix
13//                       ^^^^
14//                       |  \-- length
15//                       \----- offset
16struct SnippetRange {
17    prefix: bool,
18    suffix: bool,
19    selection_start: usize,
20    selection_length: usize,
21    offset: usize,
22    length: usize,
23}
24
25fn calculate_snippet_range(
26    original_selection_start: usize,
27    original_selection_length: usize,
28    source_total_length: usize,
29) -> SnippetRange {
30    const LEADING_LENGTH: usize = 15;
31    const SNIPPET_LENGTH: usize = 40;
32
33    let (prefix, selection_start, offset) =
34        if source_total_length < SNIPPET_LENGTH || original_selection_start < LEADING_LENGTH {
35            (false, 0, original_selection_start)
36        } else if original_selection_start + SNIPPET_LENGTH > source_total_length {
37            (
38                true,
39                source_total_length - SNIPPET_LENGTH,
40                original_selection_start - (source_total_length - SNIPPET_LENGTH),
41            )
42        } else {
43            (
44                true,
45                original_selection_start - LEADING_LENGTH,
46                LEADING_LENGTH,
47            )
48        };
49
50    let (suffix, selection_length) = if selection_start + SNIPPET_LENGTH >= source_total_length {
51        (false, source_total_length - selection_start)
52    } else {
53        (true, SNIPPET_LENGTH)
54    };
55
56    let length = if offset + original_selection_length > selection_length {
57        selection_length - offset
58    } else {
59        original_selection_length
60    };
61
62    SnippetRange {
63        prefix,
64        suffix,
65        selection_start,
66        selection_length,
67        offset,
68        length,
69    }
70}
71
72fn generate_snippet_and_indented_detail(
73    chars: &mut dyn Iterator<Item = char>,
74    snippet_range: &SnippetRange,
75    detail: &str,
76) -> (String, String) {
77    // build snippet
78    let mut snippet = String::new();
79    snippet.push_str("| ");
80    if snippet_range.prefix {
81        snippet.push_str("...");
82    }
83    let selection_chars = chars
84        .skip(snippet_range.selection_start)
85        .take(snippet_range.selection_length);
86    let selection_string = selection_chars
87        .map(|c| match c {
88            '\n' => ' ',
89            '\t' => ' ',
90            _ => c,
91        })
92        .collect::<String>();
93    snippet.push_str(&selection_string);
94    if snippet_range.suffix {
95        snippet.push_str("...");
96    }
97
98    // build indented detail
99    let mut indented_detail = String::new();
100    indented_detail.push_str("| ");
101    if snippet_range.prefix {
102        indented_detail.push_str("   ");
103    }
104    indented_detail.push_str(&" ".repeat(snippet_range.offset));
105    indented_detail.push('^');
106    if snippet_range.length > 0 {
107        indented_detail.push_str(&"^".repeat(snippet_range.length - 1));
108    } else {
109        indented_detail.push_str("____");
110    }
111    indented_detail.push(' ');
112    indented_detail.push_str(detail);
113
114    (snippet, indented_detail)
115}
116
117impl AnreError {
118    pub fn with_source(&self, source: &str) -> String {
119        // print human readable error message with the source
120
121        let source_total_length = source.chars().count();
122        let mut chars = source.chars();
123
124        // | leading length
125        // v
126        // |------|
127        // xxxxxxxx.xxxxxxxxxxx  <-- snippet text
128        // |------------------|
129        // ^
130        // | snippet length
131
132        match self {
133            AnreError::SyntaxIncorrect(msg) => msg.to_owned(),
134            AnreError::UnexpectedEndOfDocument(detail) => {
135                let msg = "Unexpected to reach the end of document.";
136                let snippet_range =
137                    calculate_snippet_range(source_total_length, 0, source_total_length);
138                let (snippet, indented_detail) =
139                    generate_snippet_and_indented_detail(&mut chars, &snippet_range, detail);
140                format!("{}\n{}\n{}", msg, snippet, indented_detail)
141            }
142            AnreError::MessageWithLocation(detail, location) => {
143                let msg = format!(
144                    "Error at line: {}, column: {}",
145                    location.line + 1,
146                    location.column + 1
147                );
148
149                let snippet_range =
150                    calculate_snippet_range(location.index, location.length, source_total_length);
151                let (snippet, indented_detail) =
152                    generate_snippet_and_indented_detail(&mut chars, &snippet_range, detail);
153                format!("{}\n{}\n{}", msg, snippet, indented_detail)
154            }
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161
162    use pretty_assertions::assert_eq;
163
164    use crate::{AnreError, location::Location};
165
166    #[test]
167    fn test_error_with_source() {
168        let source1 = "0123456789"; // 10 chars
169        let source2 = "012345678_b12345678_c12345678_d12345678_e123456789"; // 50 chars
170        let msg = "abcde";
171
172        assert_eq!(AnreError::SyntaxIncorrect(msg.to_owned()).with_source(source1), msg);
173        assert_eq!(AnreError::SyntaxIncorrect(msg.to_owned()).with_source(source2), msg);
174    }
175
176    #[test]
177    fn test_error_with_source_and_unexpected_end_of_document() {
178        let source1 = "0123456789"; // 10 chars
179        let source2 = "012345678_b12345678_c12345678_d12345678_e123456789"; // 50 chars
180        let msg = "abcde";
181
182        assert_eq!(
183            AnreError::UnexpectedEndOfDocument(msg.to_owned()).with_source(source1),
184            r#"Unexpected to reach the end of document.
185| 0123456789
186|           ^____ abcde"#
187        );
188
189        assert_eq!(
190            AnreError::UnexpectedEndOfDocument(msg.to_owned()).with_source(source2),
191            r#"Unexpected to reach the end of document.
192| ...b12345678_c12345678_d12345678_e123456789
193|                                            ^____ abcde"#
194        );
195    }
196
197    #[test]
198    fn test_error_with_source_and_location() {
199        let source1 = "0123456789"; // 10 chars
200        let source2 = "012345678_b12345678_c12345678_d12345678_e123456789"; // 50 chars
201        let msg = "abcde";
202
203        // first
204
205        assert_eq!(
206            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 0, 11, 13))
207                .with_source(source1),
208            r#"Error at line: 12, column: 14
209| 0123456789
210| ^____ abcde"#
211        );
212
213        assert_eq!(
214            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 0, 11, 13))
215                .with_source(source2),
216            r#"Error at line: 12, column: 14
217| 012345678_b12345678_c12345678_d12345678_...
218| ^____ abcde"#
219        );
220
221        // head
222
223        assert_eq!(
224            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 2, 11, 13))
225                .with_source(source1),
226            r#"Error at line: 12, column: 14
227| 0123456789
228|   ^____ abcde"#
229        );
230
231        assert_eq!(
232            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 15, 11, 13))
233                .with_source(source2),
234            r#"Error at line: 12, column: 14
235| ...b12345678_c12345678_d12345678_e123456789
236|         ^____ abcde"#
237        );
238
239        // middle
240
241        assert_eq!(
242            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 5, 11, 13))
243                .with_source(source1),
244            r#"Error at line: 12, column: 14
245| 0123456789
246|      ^____ abcde"#
247        );
248
249        assert_eq!(
250            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 25, 11, 13))
251                .with_source(source2),
252            r#"Error at line: 12, column: 14
253| ...b12345678_c12345678_d12345678_e123456789
254|                   ^____ abcde"#
255        );
256
257        // tail
258
259        assert_eq!(
260            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 8, 11, 13))
261                .with_source(source1),
262            r#"Error at line: 12, column: 14
263| 0123456789
264|         ^____ abcde"#
265        );
266
267        assert_eq!(
268            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 45, 11, 13))
269                .with_source(source2),
270            r#"Error at line: 12, column: 14
271| ...b12345678_c12345678_d12345678_e123456789
272|                                       ^____ abcde"#
273        );
274
275        // last
276
277        assert_eq!(
278            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 10, 11, 13))
279                .with_source(source1),
280            r#"Error at line: 12, column: 14
281| 0123456789
282|           ^____ abcde"#
283        );
284
285        assert_eq!(
286            AnreError::MessageWithLocation(msg.to_owned(), Location::new_position(/*0,*/ 50, 11, 13))
287                .with_source(source2),
288            r#"Error at line: 12, column: 14
289| ...b12345678_c12345678_d12345678_e123456789
290|                                            ^____ abcde"#
291        );
292    }
293
294    #[test]
295    fn test_error_with_source_and_range() {
296        let source1 = "0123456789"; // 10 chars
297        let source2 = "012345678_b12345678_c12345678_d12345678_e123456789"; // 50 chars
298        let msg = "abcde";
299
300        // first
301
302        assert_eq!(
303            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 0, 17, 19, 4))
304                .with_source(source1),
305            r#"Error at line: 18, column: 20
306| 0123456789
307| ^^^^ abcde"#
308        );
309
310        assert_eq!(
311            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 0, 17, 19, 8))
312                .with_source(source2),
313            r#"Error at line: 18, column: 20
314| 012345678_b12345678_c12345678_d12345678_...
315| ^^^^^^^^ abcde"#
316        );
317
318        // head
319
320        assert_eq!(
321            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 2, 17, 19, 4))
322                .with_source(source1),
323            r#"Error at line: 18, column: 20
324| 0123456789
325|   ^^^^ abcde"#
326        );
327
328        assert_eq!(
329            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 15, 17, 19, 8))
330                .with_source(source2),
331            r#"Error at line: 18, column: 20
332| ...b12345678_c12345678_d12345678_e123456789
333|         ^^^^^^^^ abcde"#
334        );
335
336        // middle
337
338        assert_eq!(
339            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 5, 17, 19, 4))
340                .with_source(source1),
341            r#"Error at line: 18, column: 20
342| 0123456789
343|      ^^^^ abcde"#
344        );
345
346        assert_eq!(
347            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 25, 17, 19, 8))
348                .with_source(source2),
349            r#"Error at line: 18, column: 20
350| ...b12345678_c12345678_d12345678_e123456789
351|                   ^^^^^^^^ abcde"#
352        );
353
354        // tail
355
356        assert_eq!(
357            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 8, 17, 19, 4))
358                .with_source(source1),
359            r#"Error at line: 18, column: 20
360| 0123456789
361|         ^^ abcde"#
362        );
363
364        assert_eq!(
365            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 45, 17, 19, 8))
366                .with_source(source2),
367            r#"Error at line: 18, column: 20
368| ...b12345678_c12345678_d12345678_e123456789
369|                                       ^^^^^ abcde"#
370        );
371
372        // last
373
374        assert_eq!(
375            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 10, 17, 19, 4))
376                .with_source(source1),
377            r#"Error at line: 18, column: 20
378| 0123456789
379|           ^____ abcde"#
380        );
381
382        assert_eq!(
383            AnreError::MessageWithLocation(msg.to_owned(), Location::new_range(/*0,*/ 50, 17, 19, 8))
384                .with_source(source2),
385            r#"Error at line: 18, column: 20
386| ...b12345678_c12345678_d12345678_e123456789
387|                                            ^____ abcde"#
388        );
389    }
390}