1 | // pest. The Elegant Parser |
2 | // Copyright (c) 2018 Dragoș Tiselice |
3 | // |
4 | // Licensed under the Apache License, Version 2.0 |
5 | // <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT |
6 | // license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
7 | // option. All files in the project carrying such notice may not be copied, |
8 | // modified, or distributed except according to those terms. |
9 | |
10 | //! Types for different kinds of parsing failures. |
11 | |
12 | use alloc::borrow::Cow; |
13 | use alloc::borrow::ToOwned; |
14 | use alloc::format; |
15 | use alloc::string::String; |
16 | use alloc::string::ToString; |
17 | use alloc::vec::Vec; |
18 | use core::cmp; |
19 | use core::fmt; |
20 | use core::mem; |
21 | |
22 | use crate::position::Position; |
23 | use crate::span::Span; |
24 | use crate::RuleType; |
25 | |
26 | /// Parse-related error type. |
27 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
28 | #[cfg_attr (feature = "std" , derive(thiserror::Error))] |
29 | pub struct Error<R> { |
30 | /// Variant of the error |
31 | pub variant: ErrorVariant<R>, |
32 | /// Location within the input string |
33 | pub location: InputLocation, |
34 | /// Line/column within the input string |
35 | pub line_col: LineColLocation, |
36 | path: Option<String>, |
37 | line: String, |
38 | continued_line: Option<String>, |
39 | } |
40 | |
41 | /// Different kinds of parsing errors. |
42 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
43 | #[cfg_attr (feature = "std" , derive(thiserror::Error))] |
44 | pub enum ErrorVariant<R> { |
45 | /// Generated parsing error with expected and unexpected `Rule`s |
46 | ParsingError { |
47 | /// Positive attempts |
48 | positives: Vec<R>, |
49 | /// Negative attempts |
50 | negatives: Vec<R>, |
51 | }, |
52 | /// Custom error with a message |
53 | CustomError { |
54 | /// Short explanation |
55 | message: String, |
56 | }, |
57 | } |
58 | |
59 | /// Where an `Error` has occurred. |
60 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
61 | pub enum InputLocation { |
62 | /// `Error` was created by `Error::new_from_pos` |
63 | Pos(usize), |
64 | /// `Error` was created by `Error::new_from_span` |
65 | Span((usize, usize)), |
66 | } |
67 | |
68 | /// Line/column where an `Error` has occurred. |
69 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
70 | pub enum LineColLocation { |
71 | /// Line/column pair if `Error` was created by `Error::new_from_pos` |
72 | Pos((usize, usize)), |
73 | /// Line/column pairs if `Error` was created by `Error::new_from_span` |
74 | Span((usize, usize), (usize, usize)), |
75 | } |
76 | |
77 | impl From<Position<'_>> for LineColLocation { |
78 | fn from(value: Position<'_>) -> Self { |
79 | Self::Pos(value.line_col()) |
80 | } |
81 | } |
82 | |
83 | impl From<Span<'_>> for LineColLocation { |
84 | fn from(value: Span<'_>) -> Self { |
85 | let (start: Position<'_>, end: Position<'_>) = value.split(); |
86 | Self::Span(start.line_col(), end.line_col()) |
87 | } |
88 | } |
89 | |
90 | impl<R: RuleType> Error<R> { |
91 | /// Creates `Error` from `ErrorVariant` and `Position`. |
92 | /// |
93 | /// # Examples |
94 | /// |
95 | /// ``` |
96 | /// # use pest::error::{Error, ErrorVariant}; |
97 | /// # use pest::Position; |
98 | /// # #[allow (non_camel_case_types)] |
99 | /// # #[allow (dead_code)] |
100 | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
101 | /// # enum Rule { |
102 | /// # open_paren, |
103 | /// # closed_paren |
104 | /// # } |
105 | /// # let input = "" ; |
106 | /// # let pos = Position::from_start(input); |
107 | /// let error = Error::new_from_pos( |
108 | /// ErrorVariant::ParsingError { |
109 | /// positives: vec![Rule::open_paren], |
110 | /// negatives: vec![Rule::closed_paren] |
111 | /// }, |
112 | /// pos |
113 | /// ); |
114 | /// |
115 | /// println!("{}" , error); |
116 | /// ``` |
117 | pub fn new_from_pos(variant: ErrorVariant<R>, pos: Position<'_>) -> Error<R> { |
118 | let visualize_ws = pos.match_char(' \n' ) || pos.match_char(' \r' ); |
119 | let line_of = pos.line_of(); |
120 | let line = if visualize_ws { |
121 | visualize_whitespace(line_of) |
122 | } else { |
123 | line_of.replace(&[' \r' , ' \n' ][..], "" ) |
124 | }; |
125 | Error { |
126 | variant, |
127 | location: InputLocation::Pos(pos.pos()), |
128 | path: None, |
129 | line, |
130 | continued_line: None, |
131 | line_col: LineColLocation::Pos(pos.line_col()), |
132 | } |
133 | } |
134 | |
135 | /// Creates `Error` from `ErrorVariant` and `Span`. |
136 | /// |
137 | /// # Examples |
138 | /// |
139 | /// ``` |
140 | /// # use pest::error::{Error, ErrorVariant}; |
141 | /// # use pest::{Position, Span}; |
142 | /// # #[allow (non_camel_case_types)] |
143 | /// # #[allow (dead_code)] |
144 | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
145 | /// # enum Rule { |
146 | /// # open_paren, |
147 | /// # closed_paren |
148 | /// # } |
149 | /// # let input = "" ; |
150 | /// # let start = Position::from_start(input); |
151 | /// # let end = start.clone(); |
152 | /// # let span = start.span(&end); |
153 | /// let error = Error::new_from_span( |
154 | /// ErrorVariant::ParsingError { |
155 | /// positives: vec![Rule::open_paren], |
156 | /// negatives: vec![Rule::closed_paren] |
157 | /// }, |
158 | /// span |
159 | /// ); |
160 | /// |
161 | /// println!("{}" , error); |
162 | /// ``` |
163 | pub fn new_from_span(variant: ErrorVariant<R>, span: Span<'_>) -> Error<R> { |
164 | let end = span.end_pos(); |
165 | let mut end_line_col = end.line_col(); |
166 | // end position is after a \n, so we want to point to the visual lf symbol |
167 | if end_line_col.1 == 1 { |
168 | let mut visual_end = end; |
169 | visual_end.skip_back(1); |
170 | let lc = visual_end.line_col(); |
171 | end_line_col = (lc.0, lc.1 + 1); |
172 | }; |
173 | |
174 | let mut line_iter = span.lines(); |
175 | let sl = line_iter.next().unwrap_or("" ); |
176 | let mut chars = span.as_str().chars(); |
177 | let visualize_ws = matches!(chars.next(), Some(' \n' ) | Some(' \r' )) |
178 | || matches!(chars.last(), Some(' \n' ) | Some(' \r' )); |
179 | let start_line = if visualize_ws { |
180 | visualize_whitespace(sl) |
181 | } else { |
182 | sl.to_owned().replace(&[' \r' , ' \n' ][..], "" ) |
183 | }; |
184 | let ll = line_iter.last(); |
185 | let continued_line = if visualize_ws { |
186 | ll.map(str::to_owned) |
187 | } else { |
188 | ll.map(visualize_whitespace) |
189 | }; |
190 | |
191 | Error { |
192 | variant, |
193 | location: InputLocation::Span((span.start(), end.pos())), |
194 | path: None, |
195 | line: start_line, |
196 | continued_line, |
197 | line_col: LineColLocation::Span(span.start_pos().line_col(), end_line_col), |
198 | } |
199 | } |
200 | |
201 | /// Returns `Error` variant with `path` which is shown when formatted with `Display`. |
202 | /// |
203 | /// # Examples |
204 | /// |
205 | /// ``` |
206 | /// # use pest::error::{Error, ErrorVariant}; |
207 | /// # use pest::Position; |
208 | /// # #[allow (non_camel_case_types)] |
209 | /// # #[allow (dead_code)] |
210 | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
211 | /// # enum Rule { |
212 | /// # open_paren, |
213 | /// # closed_paren |
214 | /// # } |
215 | /// # let input = "" ; |
216 | /// # let pos = Position::from_start(input); |
217 | /// Error::new_from_pos( |
218 | /// ErrorVariant::ParsingError { |
219 | /// positives: vec![Rule::open_paren], |
220 | /// negatives: vec![Rule::closed_paren] |
221 | /// }, |
222 | /// pos |
223 | /// ).with_path("file.rs" ); |
224 | /// ``` |
225 | pub fn with_path(mut self, path: &str) -> Error<R> { |
226 | self.path = Some(path.to_owned()); |
227 | |
228 | self |
229 | } |
230 | |
231 | /// Returns the path set using [`Error::with_path()`]. |
232 | /// |
233 | /// # Examples |
234 | /// |
235 | /// ``` |
236 | /// # use pest::error::{Error, ErrorVariant}; |
237 | /// # use pest::Position; |
238 | /// # #[allow (non_camel_case_types)] |
239 | /// # #[allow (dead_code)] |
240 | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
241 | /// # enum Rule { |
242 | /// # open_paren, |
243 | /// # closed_paren |
244 | /// # } |
245 | /// # let input = "" ; |
246 | /// # let pos = Position::from_start(input); |
247 | /// # let error = Error::new_from_pos( |
248 | /// # ErrorVariant::ParsingError { |
249 | /// # positives: vec![Rule::open_paren], |
250 | /// # negatives: vec![Rule::closed_paren] |
251 | /// # }, |
252 | /// # pos); |
253 | /// let error = error.with_path("file.rs" ); |
254 | /// assert_eq!(Some("file.rs" ), error.path()); |
255 | /// ``` |
256 | pub fn path(&self) -> Option<&str> { |
257 | self.path.as_deref() |
258 | } |
259 | |
260 | /// Returns the line that the error is on. |
261 | pub fn line(&self) -> &str { |
262 | self.line.as_str() |
263 | } |
264 | |
265 | /// Renames all `Rule`s if this is a [`ParsingError`]. It does nothing when called on a |
266 | /// [`CustomError`]. |
267 | /// |
268 | /// Useful in order to rename verbose rules or have detailed per-`Rule` formatting. |
269 | /// |
270 | /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError |
271 | /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError |
272 | /// |
273 | /// # Examples |
274 | /// |
275 | /// ``` |
276 | /// # use pest::error::{Error, ErrorVariant}; |
277 | /// # use pest::Position; |
278 | /// # #[allow (non_camel_case_types)] |
279 | /// # #[allow (dead_code)] |
280 | /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
281 | /// # enum Rule { |
282 | /// # open_paren, |
283 | /// # closed_paren |
284 | /// # } |
285 | /// # let input = "" ; |
286 | /// # let pos = Position::from_start(input); |
287 | /// Error::new_from_pos( |
288 | /// ErrorVariant::ParsingError { |
289 | /// positives: vec![Rule::open_paren], |
290 | /// negatives: vec![Rule::closed_paren] |
291 | /// }, |
292 | /// pos |
293 | /// ).renamed_rules(|rule| { |
294 | /// match *rule { |
295 | /// Rule::open_paren => "(" .to_owned(), |
296 | /// Rule::closed_paren => "closed paren" .to_owned() |
297 | /// } |
298 | /// }); |
299 | /// ``` |
300 | pub fn renamed_rules<F>(mut self, f: F) -> Error<R> |
301 | where |
302 | F: FnMut(&R) -> String, |
303 | { |
304 | let variant = match self.variant { |
305 | ErrorVariant::ParsingError { |
306 | positives, |
307 | negatives, |
308 | } => { |
309 | let message = Error::parsing_error_message(&positives, &negatives, f); |
310 | ErrorVariant::CustomError { message } |
311 | } |
312 | variant => variant, |
313 | }; |
314 | |
315 | self.variant = variant; |
316 | |
317 | self |
318 | } |
319 | |
320 | fn start(&self) -> (usize, usize) { |
321 | match self.line_col { |
322 | LineColLocation::Pos(line_col) => line_col, |
323 | LineColLocation::Span(start_line_col, _) => start_line_col, |
324 | } |
325 | } |
326 | |
327 | fn spacing(&self) -> String { |
328 | let line = match self.line_col { |
329 | LineColLocation::Pos((line, _)) => line, |
330 | LineColLocation::Span((start_line, _), (end_line, _)) => cmp::max(start_line, end_line), |
331 | }; |
332 | |
333 | let line_str_len = format!(" {}" , line).len(); |
334 | |
335 | let mut spacing = String::new(); |
336 | for _ in 0..line_str_len { |
337 | spacing.push(' ' ); |
338 | } |
339 | |
340 | spacing |
341 | } |
342 | |
343 | fn underline(&self) -> String { |
344 | let mut underline = String::new(); |
345 | |
346 | let mut start = self.start().1; |
347 | let end = match self.line_col { |
348 | LineColLocation::Span(_, (_, mut end)) => { |
349 | let inverted_cols = start > end; |
350 | if inverted_cols { |
351 | mem::swap(&mut start, &mut end); |
352 | start -= 1; |
353 | end += 1; |
354 | } |
355 | |
356 | Some(end) |
357 | } |
358 | _ => None, |
359 | }; |
360 | let offset = start - 1; |
361 | let line_chars = self.line.chars(); |
362 | |
363 | for c in line_chars.take(offset) { |
364 | match c { |
365 | ' \t' => underline.push(' \t' ), |
366 | _ => underline.push(' ' ), |
367 | } |
368 | } |
369 | |
370 | if let Some(end) = end { |
371 | underline.push('^' ); |
372 | if end - start > 1 { |
373 | for _ in 2..(end - start) { |
374 | underline.push('-' ); |
375 | } |
376 | underline.push('^' ); |
377 | } |
378 | } else { |
379 | underline.push_str("^---" ) |
380 | } |
381 | |
382 | underline |
383 | } |
384 | |
385 | fn message(&self) -> String { |
386 | self.variant.message().to_string() |
387 | } |
388 | |
389 | fn parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String |
390 | where |
391 | F: FnMut(&R) -> String, |
392 | { |
393 | match (negatives.is_empty(), positives.is_empty()) { |
394 | (false, false) => format!( |
395 | "unexpected {}; expected {}" , |
396 | Error::enumerate(negatives, &mut f), |
397 | Error::enumerate(positives, &mut f) |
398 | ), |
399 | (false, true) => format!("unexpected {}" , Error::enumerate(negatives, &mut f)), |
400 | (true, false) => format!("expected {}" , Error::enumerate(positives, &mut f)), |
401 | (true, true) => "unknown parsing error" .to_owned(), |
402 | } |
403 | } |
404 | |
405 | fn enumerate<F>(rules: &[R], f: &mut F) -> String |
406 | where |
407 | F: FnMut(&R) -> String, |
408 | { |
409 | match rules.len() { |
410 | 1 => f(&rules[0]), |
411 | 2 => format!(" {} or {}" , f(&rules[0]), f(&rules[1])), |
412 | l => { |
413 | let non_separated = f(&rules[l - 1]); |
414 | let separated = rules |
415 | .iter() |
416 | .take(l - 1) |
417 | .map(f) |
418 | .collect::<Vec<_>>() |
419 | .join(", " ); |
420 | format!(" {}, or {}" , separated, non_separated) |
421 | } |
422 | } |
423 | } |
424 | |
425 | pub(crate) fn format(&self) -> String { |
426 | let spacing = self.spacing(); |
427 | let path = self |
428 | .path |
429 | .as_ref() |
430 | .map(|path| format!(" {}:" , path)) |
431 | .unwrap_or_default(); |
432 | |
433 | let pair = (self.line_col.clone(), &self.continued_line); |
434 | if let (LineColLocation::Span(_, end), Some(ref continued_line)) = pair { |
435 | let has_line_gap = end.0 - self.start().0 > 1; |
436 | if has_line_gap { |
437 | format!( |
438 | " {s }--> {p}{ls}: {c}\n\ |
439 | {s } | \n\ |
440 | {ls:w$} | {line}\n\ |
441 | {s } | ... \n\ |
442 | {le:w$} | {continued_line}\n\ |
443 | {s } | {underline}\n\ |
444 | {s } | \n\ |
445 | {s } = {message}" , |
446 | s = spacing, |
447 | w = spacing.len(), |
448 | p = path, |
449 | ls = self.start().0, |
450 | le = end.0, |
451 | c = self.start().1, |
452 | line = self.line, |
453 | continued_line = continued_line, |
454 | underline = self.underline(), |
455 | message = self.message() |
456 | ) |
457 | } else { |
458 | format!( |
459 | " {s }--> {p}{ls}: {c}\n\ |
460 | {s } | \n\ |
461 | {ls:w$} | {line}\n\ |
462 | {le:w$} | {continued_line}\n\ |
463 | {s } | {underline}\n\ |
464 | {s } | \n\ |
465 | {s } = {message}" , |
466 | s = spacing, |
467 | w = spacing.len(), |
468 | p = path, |
469 | ls = self.start().0, |
470 | le = end.0, |
471 | c = self.start().1, |
472 | line = self.line, |
473 | continued_line = continued_line, |
474 | underline = self.underline(), |
475 | message = self.message() |
476 | ) |
477 | } |
478 | } else { |
479 | format!( |
480 | " {s}--> {p}{l}: {c}\n\ |
481 | {s} | \n\ |
482 | {l} | {line}\n\ |
483 | {s} | {underline}\n\ |
484 | {s} | \n\ |
485 | {s} = {message}" , |
486 | s = spacing, |
487 | p = path, |
488 | l = self.start().0, |
489 | c = self.start().1, |
490 | line = self.line, |
491 | underline = self.underline(), |
492 | message = self.message() |
493 | ) |
494 | } |
495 | } |
496 | } |
497 | |
498 | impl<R: RuleType> ErrorVariant<R> { |
499 | /// |
500 | /// Returns the error message for [`ErrorVariant`] |
501 | /// |
502 | /// If [`ErrorVariant`] is [`CustomError`], it returns a |
503 | /// [`Cow::Borrowed`] reference to [`message`]. If [`ErrorVariant`] is [`ParsingError`], a |
504 | /// [`Cow::Owned`] containing "expected [ErrorVariant::ParsingError::positives] [ErrorVariant::ParsingError::negatives]" is returned. |
505 | /// |
506 | /// [`ErrorVariant`]: enum.ErrorVariant.html |
507 | /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError |
508 | /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError |
509 | /// [`Cow::Owned`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Owned |
510 | /// [`Cow::Borrowed`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Borrowed |
511 | /// [`message`]: enum.ErrorVariant.html#variant.CustomError.field.message |
512 | /// # Examples |
513 | /// |
514 | /// ``` |
515 | /// # use pest::error::ErrorVariant; |
516 | /// let variant = ErrorVariant::<()>::CustomError { |
517 | /// message: String::from("unexpected error" ) |
518 | /// }; |
519 | /// |
520 | /// println!("{}" , variant.message()); |
521 | pub fn message(&self) -> Cow<'_, str> { |
522 | match self { |
523 | ErrorVariant::ParsingError { |
524 | ref positives, |
525 | ref negatives, |
526 | } => Cow::Owned(Error::parsing_error_message(positives, negatives, |r| { |
527 | format!(" {:?}" , r) |
528 | })), |
529 | ErrorVariant::CustomError { ref message } => Cow::Borrowed(message), |
530 | } |
531 | } |
532 | } |
533 | |
534 | impl<R: RuleType> fmt::Display for Error<R> { |
535 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
536 | write!(f, " {}" , self.format()) |
537 | } |
538 | } |
539 | |
540 | impl<R: RuleType> fmt::Display for ErrorVariant<R> { |
541 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
542 | match self { |
543 | ErrorVariant::ParsingError { .. } => write!(f, "parsing error: {}" , self.message()), |
544 | ErrorVariant::CustomError { .. } => write!(f, " {}" , self.message()), |
545 | } |
546 | } |
547 | } |
548 | |
549 | fn visualize_whitespace(input: &str) -> String { |
550 | input.to_owned().replace(' \r' , "␍" ).replace(from:' \n' , to:"␊" ) |
551 | } |
552 | |
553 | #[cfg (test)] |
554 | mod tests { |
555 | use super::super::position; |
556 | use super::*; |
557 | use alloc::vec; |
558 | |
559 | #[test ] |
560 | fn display_parsing_error_mixed() { |
561 | let input = "ab \ncd \nef" ; |
562 | let pos = position::Position::new(input, 4).unwrap(); |
563 | let error: Error<u32> = Error::new_from_pos( |
564 | ErrorVariant::ParsingError { |
565 | positives: vec![1, 2, 3], |
566 | negatives: vec![4, 5, 6], |
567 | }, |
568 | pos, |
569 | ); |
570 | |
571 | assert_eq!( |
572 | format!(" {}" , error), |
573 | vec![ |
574 | " --> 2:2" , |
575 | " |" , |
576 | "2 | cd" , |
577 | " | ^---" , |
578 | " |" , |
579 | " = unexpected 4, 5, or 6; expected 1, 2, or 3" , |
580 | ] |
581 | .join(" \n" ) |
582 | ); |
583 | } |
584 | |
585 | #[test ] |
586 | fn display_parsing_error_positives() { |
587 | let input = "ab \ncd \nef" ; |
588 | let pos = position::Position::new(input, 4).unwrap(); |
589 | let error: Error<u32> = Error::new_from_pos( |
590 | ErrorVariant::ParsingError { |
591 | positives: vec![1, 2], |
592 | negatives: vec![], |
593 | }, |
594 | pos, |
595 | ); |
596 | |
597 | assert_eq!( |
598 | format!(" {}" , error), |
599 | vec![ |
600 | " --> 2:2" , |
601 | " |" , |
602 | "2 | cd" , |
603 | " | ^---" , |
604 | " |" , |
605 | " = expected 1 or 2" , |
606 | ] |
607 | .join(" \n" ) |
608 | ); |
609 | } |
610 | |
611 | #[test ] |
612 | fn display_parsing_error_negatives() { |
613 | let input = "ab \ncd \nef" ; |
614 | let pos = position::Position::new(input, 4).unwrap(); |
615 | let error: Error<u32> = Error::new_from_pos( |
616 | ErrorVariant::ParsingError { |
617 | positives: vec![], |
618 | negatives: vec![4, 5, 6], |
619 | }, |
620 | pos, |
621 | ); |
622 | |
623 | assert_eq!( |
624 | format!(" {}" , error), |
625 | vec![ |
626 | " --> 2:2" , |
627 | " |" , |
628 | "2 | cd" , |
629 | " | ^---" , |
630 | " |" , |
631 | " = unexpected 4, 5, or 6" , |
632 | ] |
633 | .join(" \n" ) |
634 | ); |
635 | } |
636 | |
637 | #[test ] |
638 | fn display_parsing_error_unknown() { |
639 | let input = "ab \ncd \nef" ; |
640 | let pos = position::Position::new(input, 4).unwrap(); |
641 | let error: Error<u32> = Error::new_from_pos( |
642 | ErrorVariant::ParsingError { |
643 | positives: vec![], |
644 | negatives: vec![], |
645 | }, |
646 | pos, |
647 | ); |
648 | |
649 | assert_eq!( |
650 | format!(" {}" , error), |
651 | vec![ |
652 | " --> 2:2" , |
653 | " |" , |
654 | "2 | cd" , |
655 | " | ^---" , |
656 | " |" , |
657 | " = unknown parsing error" , |
658 | ] |
659 | .join(" \n" ) |
660 | ); |
661 | } |
662 | |
663 | #[test ] |
664 | fn display_custom_pos() { |
665 | let input = "ab \ncd \nef" ; |
666 | let pos = position::Position::new(input, 4).unwrap(); |
667 | let error: Error<u32> = Error::new_from_pos( |
668 | ErrorVariant::CustomError { |
669 | message: "error: big one" .to_owned(), |
670 | }, |
671 | pos, |
672 | ); |
673 | |
674 | assert_eq!( |
675 | format!(" {}" , error), |
676 | vec![ |
677 | " --> 2:2" , |
678 | " |" , |
679 | "2 | cd" , |
680 | " | ^---" , |
681 | " |" , |
682 | " = error: big one" , |
683 | ] |
684 | .join(" \n" ) |
685 | ); |
686 | } |
687 | |
688 | #[test ] |
689 | fn display_custom_span_two_lines() { |
690 | let input = "ab \ncd \nefgh" ; |
691 | let start = position::Position::new(input, 4).unwrap(); |
692 | let end = position::Position::new(input, 9).unwrap(); |
693 | let error: Error<u32> = Error::new_from_span( |
694 | ErrorVariant::CustomError { |
695 | message: "error: big one" .to_owned(), |
696 | }, |
697 | start.span(&end), |
698 | ); |
699 | |
700 | assert_eq!( |
701 | format!(" {}" , error), |
702 | vec![ |
703 | " --> 2:2" , |
704 | " |" , |
705 | "2 | cd" , |
706 | "3 | efgh" , |
707 | " | ^^" , |
708 | " |" , |
709 | " = error: big one" , |
710 | ] |
711 | .join(" \n" ) |
712 | ); |
713 | } |
714 | |
715 | #[test ] |
716 | fn display_custom_span_three_lines() { |
717 | let input = "ab \ncd \nefgh" ; |
718 | let start = position::Position::new(input, 1).unwrap(); |
719 | let end = position::Position::new(input, 9).unwrap(); |
720 | let error: Error<u32> = Error::new_from_span( |
721 | ErrorVariant::CustomError { |
722 | message: "error: big one" .to_owned(), |
723 | }, |
724 | start.span(&end), |
725 | ); |
726 | |
727 | assert_eq!( |
728 | format!(" {}" , error), |
729 | vec![ |
730 | " --> 1:2" , |
731 | " |" , |
732 | "1 | ab" , |
733 | " | ..." , |
734 | "3 | efgh" , |
735 | " | ^^" , |
736 | " |" , |
737 | " = error: big one" , |
738 | ] |
739 | .join(" \n" ) |
740 | ); |
741 | } |
742 | |
743 | #[test ] |
744 | fn display_custom_span_two_lines_inverted_cols() { |
745 | let input = "abcdef \ngh" ; |
746 | let start = position::Position::new(input, 5).unwrap(); |
747 | let end = position::Position::new(input, 8).unwrap(); |
748 | let error: Error<u32> = Error::new_from_span( |
749 | ErrorVariant::CustomError { |
750 | message: "error: big one" .to_owned(), |
751 | }, |
752 | start.span(&end), |
753 | ); |
754 | |
755 | assert_eq!( |
756 | format!(" {}" , error), |
757 | vec![ |
758 | " --> 1:6" , |
759 | " |" , |
760 | "1 | abcdef" , |
761 | "2 | gh" , |
762 | " | ^----^" , |
763 | " |" , |
764 | " = error: big one" , |
765 | ] |
766 | .join(" \n" ) |
767 | ); |
768 | } |
769 | |
770 | #[test ] |
771 | fn display_custom_span_end_after_newline() { |
772 | let input = "abcdef \n" ; |
773 | let start = position::Position::new(input, 0).unwrap(); |
774 | let end = position::Position::new(input, 7).unwrap(); |
775 | assert!(start.at_start()); |
776 | assert!(end.at_end()); |
777 | |
778 | let error: Error<u32> = Error::new_from_span( |
779 | ErrorVariant::CustomError { |
780 | message: "error: big one" .to_owned(), |
781 | }, |
782 | start.span(&end), |
783 | ); |
784 | |
785 | assert_eq!( |
786 | format!(" {}" , error), |
787 | vec![ |
788 | " --> 1:1" , |
789 | " |" , |
790 | "1 | abcdef␊" , |
791 | " | ^-----^" , |
792 | " |" , |
793 | " = error: big one" , |
794 | ] |
795 | .join(" \n" ) |
796 | ); |
797 | } |
798 | |
799 | #[test ] |
800 | fn display_custom_span_empty() { |
801 | let input = "" ; |
802 | let start = position::Position::new(input, 0).unwrap(); |
803 | let end = position::Position::new(input, 0).unwrap(); |
804 | assert!(start.at_start()); |
805 | assert!(end.at_end()); |
806 | |
807 | let error: Error<u32> = Error::new_from_span( |
808 | ErrorVariant::CustomError { |
809 | message: "error: empty" .to_owned(), |
810 | }, |
811 | start.span(&end), |
812 | ); |
813 | |
814 | assert_eq!( |
815 | format!(" {}" , error), |
816 | vec![ |
817 | " --> 1:1" , |
818 | " |" , |
819 | "1 | " , |
820 | " | ^" , |
821 | " |" , |
822 | " = error: empty" , |
823 | ] |
824 | .join(" \n" ) |
825 | ); |
826 | } |
827 | |
828 | #[test ] |
829 | fn mapped_parsing_error() { |
830 | let input = "ab \ncd \nef" ; |
831 | let pos = position::Position::new(input, 4).unwrap(); |
832 | let error: Error<u32> = Error::new_from_pos( |
833 | ErrorVariant::ParsingError { |
834 | positives: vec![1, 2, 3], |
835 | negatives: vec![4, 5, 6], |
836 | }, |
837 | pos, |
838 | ) |
839 | .renamed_rules(|n| format!(" {}" , n + 1)); |
840 | |
841 | assert_eq!( |
842 | format!(" {}" , error), |
843 | vec![ |
844 | " --> 2:2" , |
845 | " |" , |
846 | "2 | cd" , |
847 | " | ^---" , |
848 | " |" , |
849 | " = unexpected 5, 6, or 7; expected 2, 3, or 4" , |
850 | ] |
851 | .join(" \n" ) |
852 | ); |
853 | } |
854 | |
855 | #[test ] |
856 | fn error_with_path() { |
857 | let input = "ab \ncd \nef" ; |
858 | let pos = position::Position::new(input, 4).unwrap(); |
859 | let error: Error<u32> = Error::new_from_pos( |
860 | ErrorVariant::ParsingError { |
861 | positives: vec![1, 2, 3], |
862 | negatives: vec![4, 5, 6], |
863 | }, |
864 | pos, |
865 | ) |
866 | .with_path("file.rs" ); |
867 | |
868 | assert_eq!( |
869 | format!(" {}" , error), |
870 | vec![ |
871 | " --> file.rs:2:2" , |
872 | " |" , |
873 | "2 | cd" , |
874 | " | ^---" , |
875 | " |" , |
876 | " = unexpected 4, 5, or 6; expected 1, 2, or 3" , |
877 | ] |
878 | .join(" \n" ) |
879 | ); |
880 | } |
881 | |
882 | #[test ] |
883 | fn underline_with_tabs() { |
884 | let input = "a \txbc" ; |
885 | let pos = position::Position::new(input, 2).unwrap(); |
886 | let error: Error<u32> = Error::new_from_pos( |
887 | ErrorVariant::ParsingError { |
888 | positives: vec![1, 2, 3], |
889 | negatives: vec![4, 5, 6], |
890 | }, |
891 | pos, |
892 | ) |
893 | .with_path("file.rs" ); |
894 | |
895 | assert_eq!( |
896 | format!(" {}" , error), |
897 | vec![ |
898 | " --> file.rs:1:3" , |
899 | " |" , |
900 | "1 | a xbc" , |
901 | " | ^---" , |
902 | " |" , |
903 | " = unexpected 4, 5, or 6; expected 1, 2, or 3" , |
904 | ] |
905 | .join(" \n" ) |
906 | ); |
907 | } |
908 | |
909 | #[test ] |
910 | fn pos_to_lcl_conversion() { |
911 | let input = "input" ; |
912 | |
913 | let pos = Position::new(input, 2).unwrap(); |
914 | |
915 | assert_eq!(LineColLocation::Pos(pos.line_col()), pos.into()); |
916 | } |
917 | |
918 | #[test ] |
919 | fn span_to_lcl_conversion() { |
920 | let input = "input" ; |
921 | |
922 | let span = Span::new(input, 2, 4).unwrap(); |
923 | let (start, end) = span.split(); |
924 | |
925 | assert_eq!( |
926 | LineColLocation::Span(start.line_col(), end.line_col()), |
927 | span.into() |
928 | ); |
929 | } |
930 | } |
931 | |