1use crate::parser_state::{ParseAttempts, ParsingToken, RulesCallStack};
13use alloc::borrow::Cow;
14use alloc::borrow::ToOwned;
15use alloc::boxed::Box;
16use alloc::collections::{BTreeMap, BTreeSet};
17use alloc::format;
18use alloc::string::String;
19use alloc::string::ToString;
20use alloc::vec;
21use alloc::vec::Vec;
22use core::cmp;
23use core::fmt;
24use core::mem;
25
26use crate::position::Position;
27use crate::span::Span;
28use crate::RuleType;
29
30#[derive(Clone, Debug, Eq, Hash, PartialEq)]
32pub struct Error<R> {
33 pub variant: ErrorVariant<R>,
35 pub location: InputLocation,
37 pub line_col: LineColLocation,
39 inner: Box<ErrorInner<R>>,
40}
41
42#[derive(Clone, Debug, Eq, Hash, PartialEq)]
44struct ErrorInner<R> {
45 path: Option<String>,
46 line: String,
47 continued_line: Option<String>,
48 parse_attempts: Option<ParseAttempts<R>>,
49}
50
51impl<R: RuleType> core::error::Error for Error<R> {}
52
53#[derive(Clone, Debug, Eq, Hash, PartialEq)]
55pub enum ErrorVariant<R> {
56 ParsingError {
58 positives: Vec<R>,
60 negatives: Vec<R>,
62 },
63 CustomError {
65 message: String,
67 },
68}
69
70impl<R: RuleType> core::error::Error for ErrorVariant<R> {}
71
72#[derive(Clone, Debug, Eq, Hash, PartialEq)]
74pub enum InputLocation {
75 Pos(usize),
77 Span((usize, usize)),
79}
80
81#[derive(Clone, Debug, Eq, Hash, PartialEq)]
83pub enum LineColLocation {
84 Pos((usize, usize)),
86 Span((usize, usize), (usize, usize)),
88}
89
90impl From<Position<'_>> for LineColLocation {
91 fn from(value: Position<'_>) -> Self {
92 Self::Pos(value.line_col())
93 }
94}
95
96impl From<Span<'_>> for LineColLocation {
97 fn from(value: Span<'_>) -> Self {
98 let (start, end) = value.split();
99 Self::Span(start.line_col(), end.line_col())
100 }
101}
102
103pub type RuleToMessageFn<R> = Box<dyn Fn(&R) -> Option<String>>;
105pub type IsWhitespaceFn = Box<dyn Fn(String) -> bool>;
107
108impl ParsingToken {
109 pub fn is_whitespace(&self, is_whitespace: &IsWhitespaceFn) -> bool {
110 match self {
111 ParsingToken::Sensitive { token } => is_whitespace(token.clone()),
112 ParsingToken::Insensitive { token } => is_whitespace(token.clone()),
113 ParsingToken::Range { .. } => false,
114 ParsingToken::BuiltInRule => false,
115 }
116 }
117}
118
119impl<R: RuleType> ParseAttempts<R> {
120 fn tokens_helper_messages(
124 &self,
125 is_whitespace_fn: &IsWhitespaceFn,
126 spacing: &str,
127 ) -> Vec<String> {
128 let mut helper_messages = Vec::new();
129 let tokens_header_pairs = vec![
130 (self.expected_tokens(), "expected"),
131 (self.unexpected_tokens(), "unexpected"),
132 ];
133
134 for (tokens, header) in &tokens_header_pairs {
135 if tokens.is_empty() {
136 continue;
137 }
138
139 let mut helper_tokens_message = format!("{spacing}note: {header} ");
140 helper_tokens_message.push_str(if tokens.len() == 1 {
141 "token: "
142 } else {
143 "one of tokens: "
144 });
145
146 let expected_tokens_set: BTreeSet<String> = tokens
147 .iter()
148 .map(|token| {
149 if token.is_whitespace(is_whitespace_fn) {
150 String::from("WHITESPACE")
151 } else {
152 format!("`{token}`")
153 }
154 })
155 .collect();
156
157 helper_tokens_message.push_str(
158 &expected_tokens_set
159 .iter()
160 .cloned()
161 .collect::<Vec<String>>()
162 .join(", "),
163 );
164 helper_messages.push(helper_tokens_message);
165 }
166
167 helper_messages
168 }
169}
170
171impl<R: RuleType> Error<R> {
172 pub fn new_from_pos(variant: ErrorVariant<R>, pos: Position<'_>) -> Error<R> {
199 let visualize_ws = pos.match_char('\n') || pos.match_char('\r');
200 let line_of = pos.line_of();
201 let line = if visualize_ws {
202 visualize_whitespace(line_of)
203 } else {
204 line_of.replace(&['\r', '\n'][..], "")
205 };
206 Error {
207 variant,
208 location: InputLocation::Pos(pos.pos()),
209 line_col: LineColLocation::Pos(pos.line_col()),
210 inner: Box::new(ErrorInner {
211 path: None,
212 line,
213 continued_line: None,
214 parse_attempts: None,
215 }),
216 }
217 }
218
219 pub(crate) fn new_from_pos_with_parsing_attempts(
222 variant: ErrorVariant<R>,
223 pos: Position<'_>,
224 parse_attempts: ParseAttempts<R>,
225 ) -> Error<R> {
226 let mut error = Self::new_from_pos(variant, pos);
227 error.inner.parse_attempts = Some(parse_attempts);
228 error
229 }
230
231 pub fn new_from_span(variant: ErrorVariant<R>, span: Span<'_>) -> Error<R> {
260 let end = span.end_pos();
261 let mut end_line_col = end.line_col();
262 if end_line_col.1 == 1 {
264 let mut visual_end = end;
265 visual_end.skip_back(1);
266 let lc = visual_end.line_col();
267 end_line_col = (lc.0, lc.1 + 1);
268 };
269
270 let mut line_iter = span.lines();
271 let sl = line_iter.next().unwrap_or("");
272 let mut chars = span.as_str().chars();
273 let visualize_ws = matches!(chars.next(), Some('\n') | Some('\r'))
274 || matches!(chars.last(), Some('\n') | Some('\r'));
275 let start_line = if visualize_ws {
276 visualize_whitespace(sl)
277 } else {
278 sl.to_owned().replace(&['\r', '\n'][..], "")
279 };
280 let ll = line_iter.last();
281 let continued_line = if visualize_ws {
282 ll.map(str::to_owned)
283 } else {
284 ll.map(visualize_whitespace)
285 };
286
287 Error {
288 variant,
289 location: InputLocation::Span((span.start(), end.pos())),
290 line_col: LineColLocation::Span(span.start_pos().line_col(), end_line_col),
291 inner: Box::new(ErrorInner {
292 path: None,
293 line: start_line,
294 continued_line,
295 parse_attempts: None,
296 }),
297 }
298 }
299
300 pub fn with_path(mut self, path: &str) -> Error<R> {
325 self.inner.path = Some(path.to_owned());
326
327 self
328 }
329
330 pub fn path(&self) -> Option<&str> {
356 self.inner.path.as_deref()
357 }
358
359 pub fn line(&self) -> &str {
361 self.inner.line.as_str()
362 }
363
364 pub fn renamed_rules<F>(mut self, f: F) -> Error<R>
400 where
401 F: FnMut(&R) -> String,
402 {
403 let variant = match self.variant {
404 ErrorVariant::ParsingError {
405 positives,
406 negatives,
407 } => {
408 let message = Error::parsing_error_message(&positives, &negatives, f);
409 ErrorVariant::CustomError { message }
410 }
411 variant => variant,
412 };
413
414 self.variant = variant;
415
416 self
417 }
418
419 pub fn parse_attempts(&self) -> Option<ParseAttempts<R>> {
422 self.inner.parse_attempts.clone()
423 }
424
425 pub fn parse_attempts_error(
428 &self,
429 input: &str,
430 rule_to_message: &RuleToMessageFn<R>,
431 is_whitespace: &IsWhitespaceFn,
432 ) -> Option<Error<R>> {
433 #[allow(clippy::question_mark)]
434 let attempts = if let Some(ref parse_attempts) = self.inner.parse_attempts {
435 parse_attempts.clone()
436 } else {
437 return None;
438 };
439
440 let spacing = self.spacing() + " ";
441 let error_position = attempts.max_position;
442 let message = {
443 let mut help_lines: Vec<String> = Vec::new();
444 help_lines.push(String::from("error: parsing error occurred."));
445
446 for tokens_helper_message in attempts.tokens_helper_messages(is_whitespace, &spacing) {
448 help_lines.push(tokens_helper_message);
449 }
450
451 let call_stacks = attempts.call_stacks();
452 let mut call_stacks_parents_groups: BTreeMap<Option<R>, Vec<RulesCallStack<R>>> =
455 BTreeMap::new();
456 for call_stack in call_stacks {
457 call_stacks_parents_groups
458 .entry(call_stack.parent)
459 .or_default()
460 .push(call_stack);
461 }
462
463 for (group_parent, group) in call_stacks_parents_groups {
464 if let Some(parent_rule) = group_parent {
465 let mut contains_meaningful_info = false;
466 help_lines.push(format!(
467 "{spacing}help: {}",
468 if let Some(message) = rule_to_message(&parent_rule) {
469 contains_meaningful_info = true;
470 message
471 } else {
472 String::from("[Unknown parent rule]")
473 }
474 ));
475 for call_stack in group {
476 if let Some(r) = call_stack.deepest.get_rule() {
477 if let Some(message) = rule_to_message(r) {
478 contains_meaningful_info = true;
479 help_lines.push(format!("{spacing} - {message}"));
480 }
481 }
482 }
483 if !contains_meaningful_info {
484 help_lines.pop();
486 }
487 } else {
488 for call_stack in group {
489 if let Some(r) = call_stack.deepest.get_rule() {
493 let helper_message = rule_to_message(r);
494 if let Some(helper_message) = helper_message {
495 help_lines.push(format!("{spacing}help: {helper_message}"));
496 }
497 }
498 }
499 }
500 }
501
502 help_lines.join("\n")
503 };
504 let error = Error::new_from_pos(
505 ErrorVariant::CustomError { message },
506 Position::new_internal(input, error_position),
507 );
508 Some(error)
509 }
510
511 fn start(&self) -> (usize, usize) {
512 match self.line_col {
513 LineColLocation::Pos(line_col) => line_col,
514 LineColLocation::Span(start_line_col, _) => start_line_col,
515 }
516 }
517
518 fn spacing(&self) -> String {
519 let line = match self.line_col {
520 LineColLocation::Pos((line, _)) => line,
521 LineColLocation::Span((start_line, _), (end_line, _)) => cmp::max(start_line, end_line),
522 };
523
524 let line_str_len = format!("{line}").len();
525
526 let mut spacing = String::new();
527 for _ in 0..line_str_len {
528 spacing.push(' ');
529 }
530
531 spacing
532 }
533
534 fn underline(&self) -> String {
535 let mut underline = String::new();
536
537 let mut start = self.start().1;
538 let end = match self.line_col {
539 LineColLocation::Span(_, (_, mut end)) => {
540 let inverted_cols = start > end;
541 if inverted_cols {
542 mem::swap(&mut start, &mut end);
543 start -= 1;
544 end += 1;
545 }
546
547 Some(end)
548 }
549 _ => None,
550 };
551 let offset = start - 1;
552 let line_chars = self.inner.line.chars();
553
554 for c in line_chars.take(offset) {
555 match c {
556 '\t' => underline.push('\t'),
557 _ => underline.push(' '),
558 }
559 }
560
561 if let Some(end) = end {
562 underline.push('^');
563 if end - start > 1 {
564 for _ in 2..(end - start) {
565 underline.push('-');
566 }
567 underline.push('^');
568 }
569 } else {
570 underline.push_str("^---")
571 }
572
573 underline
574 }
575
576 fn message(&self) -> String {
577 self.variant.message().to_string()
578 }
579
580 fn parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String
581 where
582 F: FnMut(&R) -> String,
583 {
584 match (negatives.is_empty(), positives.is_empty()) {
585 (false, false) => format!(
586 "unexpected {}; expected {}",
587 Error::enumerate(negatives, &mut f),
588 Error::enumerate(positives, &mut f)
589 ),
590 (false, true) => format!("unexpected {}", Error::enumerate(negatives, &mut f)),
591 (true, false) => format!("expected {}", Error::enumerate(positives, &mut f)),
592 (true, true) => "unknown parsing error".to_owned(),
593 }
594 }
595
596 fn enumerate<F>(rules: &[R], f: &mut F) -> String
597 where
598 F: FnMut(&R) -> String,
599 {
600 match rules.len() {
601 1 => f(&rules[0]),
602 2 => format!("{} or {}", f(&rules[0]), f(&rules[1])),
603 l => {
604 let non_separated = f(&rules[l - 1]);
605 let separated = rules
606 .iter()
607 .take(l - 1)
608 .map(f)
609 .collect::<Vec<_>>()
610 .join(", ");
611 format!("{separated}, or {non_separated}")
612 }
613 }
614 }
615
616 pub(crate) fn format(&self) -> String {
617 let spacing = self.spacing();
618 let path = self
619 .inner
620 .path
621 .as_ref()
622 .map(|path| format!("{path}:"))
623 .unwrap_or_default();
624
625 let pair = (self.line_col.clone(), &self.inner.continued_line);
626 if let (LineColLocation::Span(_, end), Some(ref continued_line)) = pair {
627 let has_line_gap = end.0 - self.start().0 > 1;
628 if has_line_gap {
629 format!(
630 "{s }--> {p}{ls}:{c}\n\
631 {s } |\n\
632 {ls:w$} | {line}\n\
633 {s } | ...\n\
634 {le:w$} | {continued_line}\n\
635 {s } | {underline}\n\
636 {s } |\n\
637 {s } = {message}",
638 s = spacing,
639 w = spacing.len(),
640 p = path,
641 ls = self.start().0,
642 le = end.0,
643 c = self.start().1,
644 line = self.inner.line,
645 continued_line = continued_line,
646 underline = self.underline(),
647 message = self.message()
648 )
649 } else {
650 format!(
651 "{s }--> {p}{ls}:{c}\n\
652 {s } |\n\
653 {ls:w$} | {line}\n\
654 {le:w$} | {continued_line}\n\
655 {s } | {underline}\n\
656 {s } |\n\
657 {s } = {message}",
658 s = spacing,
659 w = spacing.len(),
660 p = path,
661 ls = self.start().0,
662 le = end.0,
663 c = self.start().1,
664 line = self.inner.line,
665 continued_line = continued_line,
666 underline = self.underline(),
667 message = self.message()
668 )
669 }
670 } else {
671 format!(
672 "{s}--> {p}{l}:{c}\n\
673 {s} |\n\
674 {l} | {line}\n\
675 {s} | {underline}\n\
676 {s} |\n\
677 {s} = {message}",
678 s = spacing,
679 p = path,
680 l = self.start().0,
681 c = self.start().1,
682 line = self.inner.line,
683 underline = self.underline(),
684 message = self.message()
685 )
686 }
687 }
688
689 #[cfg(feature = "miette-error")]
690 pub fn into_miette(self) -> impl ::miette::Diagnostic {
692 miette_adapter::MietteAdapter(self)
693 }
694}
695
696impl<R: RuleType> ErrorVariant<R> {
697 pub fn message(&self) -> Cow<'_, str> {
720 match self {
721 ErrorVariant::ParsingError {
722 ref positives,
723 ref negatives,
724 } => Cow::Owned(Error::parsing_error_message(positives, negatives, |r| {
725 format!("{r:?}")
726 })),
727 ErrorVariant::CustomError { ref message } => Cow::Borrowed(message),
728 }
729 }
730}
731
732impl<R: RuleType> fmt::Display for Error<R> {
733 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
734 write!(f, "{}", self.format())
735 }
736}
737
738impl<R: RuleType> fmt::Display for ErrorVariant<R> {
739 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740 match self {
741 ErrorVariant::ParsingError { .. } => write!(f, "parsing error: {}", self.message()),
742 ErrorVariant::CustomError { .. } => write!(f, "{}", self.message()),
743 }
744 }
745}
746
747fn visualize_whitespace(input: &str) -> String {
748 input.to_owned().replace('\r', "␍").replace('\n', "␊")
749}
750
751#[cfg(feature = "miette-error")]
752mod miette_adapter {
753 use alloc::string::ToString;
754 use core::fmt;
755 use std::boxed::Box;
756
757 use crate::error::LineColLocation;
758
759 use super::{Error, RuleType};
760
761 use miette::{Diagnostic, LabeledSpan, SourceCode};
762
763 #[derive(Debug)]
764 pub(crate) struct MietteAdapter<R: RuleType>(pub(crate) Error<R>);
765
766 impl<R: RuleType> Diagnostic for MietteAdapter<R> {
767 fn source_code(&self) -> Option<&dyn SourceCode> {
768 Some(&self.0.inner.line)
769 }
770
771 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan>>> {
772 let message = self.0.variant.message().to_string();
773
774 let (offset, length) = match self.0.line_col {
775 LineColLocation::Pos((_, c)) => (c - 1, 1),
776 LineColLocation::Span((_, start_c), (_, end_c)) => {
777 (start_c - 1, end_c - start_c + 1)
778 }
779 };
780
781 let span = LabeledSpan::new(Some(message), offset, length);
782
783 Some(Box::new(std::iter::once(span)))
784 }
785
786 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
787 Some(Box::new(self.0.message()))
788 }
789 }
790
791 impl<R: RuleType> fmt::Display for MietteAdapter<R> {
792 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
793 write!(f, "Failure to parse at {:?}", self.0.line_col)
794 }
795 }
796
797 impl<R> core::error::Error for MietteAdapter<R>
798 where
799 R: RuleType,
800 Self: fmt::Debug + fmt::Display,
801 {
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use alloc::vec;
809
810 #[test]
811 fn display_parsing_error_mixed() {
812 let input = "ab\ncd\nef";
813 let pos = Position::new(input, 4).unwrap();
814 let error: Error<u32> = Error::new_from_pos(
815 ErrorVariant::ParsingError {
816 positives: vec![1, 2, 3],
817 negatives: vec![4, 5, 6],
818 },
819 pos,
820 );
821
822 assert_eq!(
823 format!("{error}"),
824 [
825 " --> 2:2",
826 " |",
827 "2 | cd",
828 " | ^---",
829 " |",
830 " = unexpected 4, 5, or 6; expected 1, 2, or 3"
831 ]
832 .join("\n")
833 );
834 }
835
836 #[test]
837 fn display_parsing_error_positives() {
838 let input = "ab\ncd\nef";
839 let pos = Position::new(input, 4).unwrap();
840 let error: Error<u32> = Error::new_from_pos(
841 ErrorVariant::ParsingError {
842 positives: vec![1, 2],
843 negatives: vec![],
844 },
845 pos,
846 );
847
848 assert_eq!(
849 format!("{error}"),
850 [
851 " --> 2:2",
852 " |",
853 "2 | cd",
854 " | ^---",
855 " |",
856 " = expected 1 or 2"
857 ]
858 .join("\n")
859 );
860 }
861
862 #[test]
863 fn display_parsing_error_negatives() {
864 let input = "ab\ncd\nef";
865 let pos = Position::new(input, 4).unwrap();
866 let error: Error<u32> = Error::new_from_pos(
867 ErrorVariant::ParsingError {
868 positives: vec![],
869 negatives: vec![4, 5, 6],
870 },
871 pos,
872 );
873
874 assert_eq!(
875 format!("{error}"),
876 [
877 " --> 2:2",
878 " |",
879 "2 | cd",
880 " | ^---",
881 " |",
882 " = unexpected 4, 5, or 6"
883 ]
884 .join("\n")
885 );
886 }
887
888 #[test]
889 fn display_parsing_error_unknown() {
890 let input = "ab\ncd\nef";
891 let pos = Position::new(input, 4).unwrap();
892 let error: Error<u32> = Error::new_from_pos(
893 ErrorVariant::ParsingError {
894 positives: vec![],
895 negatives: vec![],
896 },
897 pos,
898 );
899
900 assert_eq!(
901 format!("{error}"),
902 [
903 " --> 2:2",
904 " |",
905 "2 | cd",
906 " | ^---",
907 " |",
908 " = unknown parsing error"
909 ]
910 .join("\n")
911 );
912 }
913
914 #[test]
915 fn display_custom_pos() {
916 let input = "ab\ncd\nef";
917 let pos = Position::new(input, 4).unwrap();
918 let error: Error<u32> = Error::new_from_pos(
919 ErrorVariant::CustomError {
920 message: "error: big one".to_owned(),
921 },
922 pos,
923 );
924
925 assert_eq!(
926 format!("{error}"),
927 [
928 " --> 2:2",
929 " |",
930 "2 | cd",
931 " | ^---",
932 " |",
933 " = error: big one"
934 ]
935 .join("\n")
936 );
937 }
938
939 #[test]
940 fn display_custom_span_two_lines() {
941 let input = "ab\ncd\nefgh";
942 let start = Position::new(input, 4).unwrap();
943 let end = Position::new(input, 9).unwrap();
944 let error: Error<u32> = Error::new_from_span(
945 ErrorVariant::CustomError {
946 message: "error: big one".to_owned(),
947 },
948 start.span(&end),
949 );
950
951 assert_eq!(
952 format!("{error}"),
953 [
954 " --> 2:2",
955 " |",
956 "2 | cd",
957 "3 | efgh",
958 " | ^^",
959 " |",
960 " = error: big one"
961 ]
962 .join("\n")
963 );
964 }
965
966 #[test]
967 fn display_custom_span_three_lines() {
968 let input = "ab\ncd\nefgh";
969 let start = Position::new(input, 1).unwrap();
970 let end = Position::new(input, 9).unwrap();
971 let error: Error<u32> = Error::new_from_span(
972 ErrorVariant::CustomError {
973 message: "error: big one".to_owned(),
974 },
975 start.span(&end),
976 );
977
978 assert_eq!(
979 format!("{error}"),
980 [
981 " --> 1:2",
982 " |",
983 "1 | ab",
984 " | ...",
985 "3 | efgh",
986 " | ^^",
987 " |",
988 " = error: big one"
989 ]
990 .join("\n")
991 );
992 }
993
994 #[test]
995 fn display_custom_span_two_lines_inverted_cols() {
996 let input = "abcdef\ngh";
997 let start = Position::new(input, 5).unwrap();
998 let end = Position::new(input, 8).unwrap();
999 let error: Error<u32> = Error::new_from_span(
1000 ErrorVariant::CustomError {
1001 message: "error: big one".to_owned(),
1002 },
1003 start.span(&end),
1004 );
1005
1006 assert_eq!(
1007 format!("{error}"),
1008 [
1009 " --> 1:6",
1010 " |",
1011 "1 | abcdef",
1012 "2 | gh",
1013 " | ^----^",
1014 " |",
1015 " = error: big one"
1016 ]
1017 .join("\n")
1018 );
1019 }
1020
1021 #[test]
1022 fn display_custom_span_end_after_newline() {
1023 let input = "abcdef\n";
1024 let start = Position::new(input, 0).unwrap();
1025 let end = Position::new(input, 7).unwrap();
1026 assert!(start.at_start());
1027 assert!(end.at_end());
1028
1029 let error: Error<u32> = Error::new_from_span(
1030 ErrorVariant::CustomError {
1031 message: "error: big one".to_owned(),
1032 },
1033 start.span(&end),
1034 );
1035
1036 assert_eq!(
1037 format!("{error}"),
1038 [
1039 " --> 1:1",
1040 " |",
1041 "1 | abcdef␊",
1042 " | ^-----^",
1043 " |",
1044 " = error: big one"
1045 ]
1046 .join("\n")
1047 );
1048 }
1049
1050 #[test]
1051 fn display_custom_span_end_after_inner_newline() {
1052 let input = "abcdef\nghi";
1053 let start = Position::new(input, 0).unwrap();
1054 let end = Position::new(input, 7).unwrap();
1055 assert!(start.at_start());
1056 assert!(!end.at_end());
1057
1058 let error: Error<u32> = Error::new_from_span(
1059 ErrorVariant::CustomError {
1060 message: "error: big one".to_owned(),
1061 },
1062 start.span(&end),
1063 );
1064
1065 assert_eq!(
1066 format!("{error}"),
1067 [
1068 " --> 1:1",
1069 " |",
1070 "1 | abcdef␊",
1071 " | ^-----^",
1072 " |",
1073 " = error: big one"
1074 ]
1075 .join("\n")
1076 );
1077 }
1078
1079 #[test]
1080 fn display_custom_span_empty() {
1081 let input = "";
1082 let start = Position::new(input, 0).unwrap();
1083 let end = Position::new(input, 0).unwrap();
1084 assert!(start.at_start());
1085 assert!(end.at_end());
1086
1087 let error: Error<u32> = Error::new_from_span(
1088 ErrorVariant::CustomError {
1089 message: "error: empty".to_owned(),
1090 },
1091 start.span(&end),
1092 );
1093
1094 assert_eq!(
1095 format!("{error}"),
1096 [
1097 " --> 1:1",
1098 " |",
1099 "1 | ",
1100 " | ^",
1101 " |",
1102 " = error: empty"
1103 ]
1104 .join("\n")
1105 );
1106 }
1107
1108 #[test]
1109 fn mapped_parsing_error() {
1110 let input = "ab\ncd\nef";
1111 let pos = Position::new(input, 4).unwrap();
1112 let error: Error<u32> = Error::new_from_pos(
1113 ErrorVariant::ParsingError {
1114 positives: vec![1, 2, 3],
1115 negatives: vec![4, 5, 6],
1116 },
1117 pos,
1118 )
1119 .renamed_rules(|n| format!("{}", n + 1));
1120
1121 assert_eq!(
1122 format!("{error}"),
1123 [
1124 " --> 2:2",
1125 " |",
1126 "2 | cd",
1127 " | ^---",
1128 " |",
1129 " = unexpected 5, 6, or 7; expected 2, 3, or 4"
1130 ]
1131 .join("\n")
1132 );
1133 }
1134
1135 #[test]
1136 fn error_with_path() {
1137 let input = "ab\ncd\nef";
1138 let pos = Position::new(input, 4).unwrap();
1139 let error: Error<u32> = Error::new_from_pos(
1140 ErrorVariant::ParsingError {
1141 positives: vec![1, 2, 3],
1142 negatives: vec![4, 5, 6],
1143 },
1144 pos,
1145 )
1146 .with_path("file.rs");
1147
1148 assert_eq!(
1149 format!("{error}"),
1150 [
1151 " --> file.rs:2:2",
1152 " |",
1153 "2 | cd",
1154 " | ^---",
1155 " |",
1156 " = unexpected 4, 5, or 6; expected 1, 2, or 3"
1157 ]
1158 .join("\n")
1159 );
1160 }
1161
1162 #[test]
1163 fn underline_with_tabs() {
1164 let input = "a\txbc";
1165 let pos = Position::new(input, 2).unwrap();
1166 let error: Error<u32> = Error::new_from_pos(
1167 ErrorVariant::ParsingError {
1168 positives: vec![1, 2, 3],
1169 negatives: vec![4, 5, 6],
1170 },
1171 pos,
1172 )
1173 .with_path("file.rs");
1174
1175 assert_eq!(
1176 format!("{error}"),
1177 [
1178 " --> file.rs:1:3",
1179 " |",
1180 "1 | a xbc",
1181 " | ^---",
1182 " |",
1183 " = unexpected 4, 5, or 6; expected 1, 2, or 3"
1184 ]
1185 .join("\n")
1186 );
1187 }
1188
1189 #[test]
1190 fn pos_to_lcl_conversion() {
1191 let input = "input";
1192
1193 let pos = Position::new(input, 2).unwrap();
1194
1195 assert_eq!(LineColLocation::Pos(pos.line_col()), pos.into());
1196 }
1197
1198 #[test]
1199 fn span_to_lcl_conversion() {
1200 let input = "input";
1201
1202 let span = Span::new(input, 2, 4).unwrap();
1203 let (start, end) = span.split();
1204
1205 assert_eq!(
1206 LineColLocation::Span(start.line_col(), end.line_col()),
1207 span.into()
1208 );
1209 }
1210
1211 #[cfg(feature = "miette-error")]
1212 #[test]
1213 fn miette_error() {
1214 let prev_force_color = std::env::var_os("FORCE_COLOR");
1216 std::env::set_var("FORCE_COLOR", "1");
1217
1218 let input = "abc\ndef";
1219 let pos = Position::new(input, 4).unwrap();
1220 let error: Error<u32> = Error::new_from_pos(
1221 ErrorVariant::ParsingError {
1222 positives: vec![1, 2, 3],
1223 negatives: vec![4, 5, 6],
1224 },
1225 pos,
1226 );
1227
1228 let miette_error = miette::Error::new(error.into_miette());
1229
1230 assert_eq!(
1231 format!("{miette_error:?}"),
1232 [
1233 " \u{1b}[31m×\u{1b}[0m Failure to parse at Pos((2, 1))",
1234 " ╭────",
1235 " \u{1b}[2m1\u{1b}[0m │ def",
1236 " · \u{1b}[35;1m┬\u{1b}[0m",
1237 " · \u{1b}[35;1m╰── \u{1b}[35;1munexpected 4, 5, or 6; expected 1, 2, or 3\u{1b}[0m\u{1b}[0m",
1238 " ╰────",
1239 "\u{1b}[36m help: \u{1b}[0munexpected 4, 5, or 6; expected 1, 2, or 3\n"
1240 ]
1241 .join("\n")
1242 );
1243
1244 if let Some(prev) = prev_force_color {
1246 std::env::set_var("FORCE_COLOR", prev);
1247 } else {
1248 std::env::remove_var("FORCE_COLOR");
1249 }
1250 }
1251}