1use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet};
8use proc_macro2::LineColumn;
9use std::str::FromStr;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct SourceSpan {
18 pub start: LineColumn,
20 pub end: LineColumn,
22}
23
24impl Default for SourceSpan {
25 fn default() -> Self {
26 SourceSpan {
27 start: LineColumn { line: 1, column: 0 },
28 end: LineColumn { line: 1, column: 0 },
29 }
30 }
31}
32
33impl SourceSpan {
34 pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
48 SourceSpan {
49 start: LineColumn {
50 line: start_line,
51 column: start_column,
52 },
53 end: LineColumn {
54 line: end_line,
55 column: end_column,
56 },
57 }
58 }
59
60 pub fn from_proc_macro2(span: proc_macro2::Span) -> Self {
76 SourceSpan {
77 start: span.start(),
78 end: span.end(),
79 }
80 }
81
82 pub fn from_proc_macro2_range(start: proc_macro2::Span, end: proc_macro2::Span) -> Self {
98 SourceSpan {
99 start: start.start(),
100 end: end.end(),
101 }
102 }
103
104 pub fn to_byte_range(&self, source: &str) -> std::ops::Range<usize> {
125 span_to_byte_range(source, *self)
126 }
127}
128
129#[derive(Clone, Debug)]
134pub struct CELError {
135 message: String,
136 span: SourceSpan,
137}
138
139pub(crate) fn span_to_byte_range(source: &str, span: SourceSpan) -> std::ops::Range<usize> {
144 debug_assert!(
145 span.start.line >= 1,
146 "`span.start.line` must be 1-based (≥ 1)"
147 );
148 debug_assert!(
149 span.end.line > span.start.line
150 || (span.end.line == span.start.line && span.end.column >= span.start.column),
151 "`span.end` must not precede `span.start`"
152 );
153 let start_line_byte: usize = source
154 .split_inclusive('\n')
155 .take(span.start.line - 1)
156 .map(str::len)
157 .sum();
158 let start_byte = start_line_byte
159 + source[start_line_byte..]
160 .chars()
161 .take(span.start.column)
162 .map(char::len_utf8)
163 .sum::<usize>();
164 let end_byte = if span.end.line == span.start.line {
165 start_byte
166 + source[start_byte..]
167 .chars()
168 .take(span.end.column - span.start.column)
169 .map(char::len_utf8)
170 .sum::<usize>()
171 } else {
172 let end_line_byte = start_byte
173 + source[start_byte..]
174 .split_inclusive('\n')
175 .take(span.end.line - span.start.line)
176 .map(str::len)
177 .sum::<usize>();
178 end_line_byte
179 + source[end_line_byte..]
180 .chars()
181 .take(span.end.column)
182 .map(char::len_utf8)
183 .sum::<usize>()
184 };
185 debug_assert!(
186 end_byte <= source.len(),
187 "computed end_byte exceeds source.len()"
188 );
189 start_byte..end_byte
190}
191
192#[derive(Clone, Debug)]
194pub struct SpanLabel {
195 pub span: SourceSpan,
197 pub label: String,
199}
200
201pub fn format_multi_span(
218 title: &str,
219 labels: &[SpanLabel],
220 source_code: &str,
221 filename: &str,
222 start_line: u32,
223 renderer: &Renderer,
224) -> String {
225 debug_assert!(!labels.is_empty(), "`labels` must be non-empty");
226 let mut snippet = Snippet::source(source_code)
227 .path(filename)
228 .line_start(start_line as usize);
229 for (i, l) in labels.iter().enumerate() {
230 let range = span_to_byte_range(source_code, l.span);
231 let kind = if i == 0 {
232 AnnotationKind::Primary
233 } else {
234 AnnotationKind::Context
235 };
236 snippet = snippet.annotation(kind.span(range).label(l.label.as_str()));
237 }
238 let report = [Group::with_title(Level::ERROR.primary_title(title)).element(snippet)];
239 renderer.render(&report)
240}
241
242fn matching_close(open: char) -> char {
246 match open {
247 '(' => ')',
248 '[' => ']',
249 '{' => '}',
250 _ => {
251 debug_assert!(false, "`open` must be one of `(`, `[`, `{{`");
252 open
253 }
254 }
255}
256
257fn innermost_open_delimiter(
270 source: &str,
271 close_span: proc_macro2::Span,
272) -> Option<(char, proc_macro2::Span)> {
273 let close_start = span_to_byte_range(source, SourceSpan::from_proc_macro2(close_span)).start;
274 let prefix_err = proc_macro2::TokenStream::from_str(&source[..close_start]).err()?;
275 let open_span = prefix_err.span();
276 let open_start = span_to_byte_range(source, SourceSpan::from_proc_macro2(open_span)).start;
277 match source[open_start..].chars().next()? {
278 ch @ ('(' | '[' | '{') => Some((ch, open_span)),
279 _ => None,
280 }
281}
282
283fn lex_failure_message(
292 source: &str,
293 span: proc_macro2::Span,
294) -> Option<(String, Option<SpanLabel>)> {
295 let byte_range = span_to_byte_range(source, SourceSpan::from_proc_macro2(span));
296 let rest = &source[byte_range.start..];
297 Some(match rest.chars().next()? {
298 '(' => (
299 "unclosed delimiter `(`: expected a matching `)`".to_string(),
300 None,
301 ),
302 '[' => (
303 "unclosed delimiter `[`: expected a matching `]`".to_string(),
304 None,
305 ),
306 '{' => (
307 "unclosed delimiter `{`: expected a matching `}`".to_string(),
308 None,
309 ),
310 ch @ (')' | ']' | '}') => match innermost_open_delimiter(source, span) {
311 Some((open_ch, open_span)) => (
312 format!(
313 "mismatched closing delimiter: found `{ch}`, expected `{}`",
314 matching_close(open_ch)
315 ),
316 Some(SpanLabel {
317 span: SourceSpan::from_proc_macro2(open_span),
318 label: format!("unclosed delimiter `{open_ch}`"),
319 }),
320 ),
321 None => (format!("unexpected closing delimiter `{ch}`"), None),
322 },
323 '"' => ("unterminated string literal".to_string(), None),
324 '\'' => (
325 "invalid or unterminated character literal".to_string(),
326 None,
327 ),
328 '`' => ("invalid character `` ` ``".to_string(), None),
329 '/' if rest.starts_with("/*") => ("unterminated block comment".to_string(), None),
330 ch => (format!("invalid character `{ch}`"), None),
331 })
332}
333
334impl CELError {
335 pub fn new(message: impl Into<String>, span: SourceSpan) -> Self {
348 CELError {
349 message: message.into(),
350 span,
351 }
352 }
353
354 pub fn message(&self) -> &str {
366 &self.message
367 }
368
369 pub fn span(&self) -> SourceSpan {
382 self.span
383 }
384
385 pub fn format_rustc_style(
414 &self,
415 source_code: &str,
416 filename: &str,
417 start_line: u32,
418 renderer: &Renderer,
419 ) -> String {
420 let byte_range = span_to_byte_range(source_code, self.span);
421 let report = [
422 Group::with_title(Level::ERROR.primary_title(self.message.as_str())).element(
423 Snippet::source(source_code)
424 .path(filename)
425 .line_start(start_line as usize)
426 .annotation(AnnotationKind::Primary.span(byte_range)),
427 ),
428 ];
429 renderer.render(&report)
430 }
431}
432
433impl std::fmt::Display for CELError {
434 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
435 f.write_str(&self.message)
436 }
437}
438
439impl std::error::Error for CELError {}
440
441#[derive(Clone, Debug)]
449pub struct ParseError {
450 message: String,
451 span: proc_macro2::Span,
452 end_span: Option<proc_macro2::Span>,
453 secondary: Vec<SpanLabel>,
454}
455
456impl ParseError {
457 pub fn new(message: impl Into<String>, span: proc_macro2::Span) -> Self {
469 ParseError {
470 message: message.into(),
471 span,
472 end_span: None,
473 secondary: Vec::new(),
474 }
475 }
476
477 pub fn new_range(
497 message: impl Into<String>,
498 start: proc_macro2::Span,
499 end: proc_macro2::Span,
500 ) -> Self {
501 ParseError {
502 message: message.into(),
503 span: start,
504 end_span: Some(end),
505 secondary: Vec::new(),
506 }
507 }
508
509 pub fn message(&self) -> &str {
511 &self.message
512 }
513
514 pub fn span(&self) -> proc_macro2::Span {
519 self.span
520 }
521
522 pub fn end_span(&self) -> Option<proc_macro2::Span> {
527 self.end_span
528 }
529
530 pub fn with_secondary(mut self, secondary: Vec<SpanLabel>) -> Self {
545 self.secondary = secondary;
546 self
547 }
548
549 pub fn secondary(&self) -> &[SpanLabel] {
564 &self.secondary
565 }
566
567 pub fn from_lex_error(source: &str, err: proc_macro2::LexError) -> Self {
590 let span = err.span();
591 match lex_failure_message(source, span) {
592 Some((message, Some(secondary))) => {
593 ParseError::new(message, span).with_secondary(vec![secondary])
594 }
595 Some((message, None)) => ParseError::new(message, span),
596 None => ParseError::new(err.to_string(), span),
597 }
598 }
599
600 pub fn format_rustc_style(
623 &self,
624 source_code: &str,
625 filename: &str,
626 start_line: u32,
627 renderer: &Renderer,
628 ) -> String {
629 let source_span = SourceSpan {
630 start: self.span.start(),
631 end: self.end_span.unwrap_or(self.span).end(),
632 };
633 if !self.secondary.is_empty() {
634 let mut labels = vec![SpanLabel {
635 span: source_span,
636 label: String::new(),
637 }];
638 labels.extend(self.secondary.iter().cloned());
639 return format_multi_span(
640 &self.message,
641 &labels,
642 source_code,
643 filename,
644 start_line,
645 renderer,
646 );
647 }
648 let byte_range = span_to_byte_range(source_code, source_span);
649 let report = [
650 Group::with_title(Level::ERROR.primary_title(self.message.as_str())).element(
651 Snippet::source(source_code)
652 .path(filename)
653 .line_start(start_line as usize)
654 .annotation(AnnotationKind::Primary.span(byte_range)),
655 ),
656 ];
657 renderer.render(&report)
658 }
659}
660
661impl From<ParseError> for CELError {
666 fn from(e: ParseError) -> Self {
667 let source_span = SourceSpan {
668 start: e.span.start(),
669 end: e.end_span.unwrap_or(e.span).end(),
670 };
671 CELError::new(e.message, source_span)
672 }
673}
674
675impl std::fmt::Display for ParseError {
676 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677 f.write_str(&self.message)
678 }
679}
680
681impl std::error::Error for ParseError {}
682
683pub struct SpanContext {
688 span: SourceSpan,
689}
690
691impl SpanContext {
692 pub fn new(span: SourceSpan) -> Self {
704 SpanContext { span }
705 }
706
707 pub fn span(&self) -> SourceSpan {
719 self.span
720 }
721
722 pub fn format_rustc_style(
739 &self,
740 message: &str,
741 source_code: &str,
742 filename: &str,
743 start_line: u32,
744 renderer: &Renderer,
745 ) -> String {
746 CELError::new(message, self.span).format_rustc_style(
747 source_code,
748 filename,
749 start_line,
750 renderer,
751 )
752 }
753}
754
755impl std::fmt::Display for SpanContext {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 write!(
758 f,
759 "at {}:{}-{}:{}",
760 self.span.start.line, self.span.start.column, self.span.end.line, self.span.end.column
761 )
762 }
763}
764
765impl std::fmt::Debug for SpanContext {
766 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
767 f.debug_struct("SpanContext")
768 .field("span", &self.span)
769 .finish()
770 }
771}
772
773impl std::error::Error for SpanContext {}
774
775pub trait FormatRustcStyle {
793 fn format_rustc_style(
798 &self,
799 source_code: &str,
800 filename: &str,
801 start_line: u32,
802 renderer: &Renderer,
803 ) -> String;
804}
805
806impl FormatRustcStyle for anyhow::Error {
807 fn format_rustc_style(
808 &self,
809 source_code: &str,
810 filename: &str,
811 start_line: u32,
812 renderer: &Renderer,
813 ) -> String {
814 if let Some(ctx) = self.downcast_ref::<SpanContext>() {
815 let message = self
819 .source()
820 .map(|e| e.to_string())
821 .unwrap_or_else(|| self.to_string());
822 ctx.format_rustc_style(&message, source_code, filename, start_line, renderer)
823 } else {
824 self.to_string()
825 }
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832 use annotate_snippets::Renderer;
833 use proc_macro2::Span;
834
835 #[test]
836 fn parse_error_message() {
837 let e = ParseError::new("bad token", Span::call_site());
838 assert_eq!(e.message(), "bad token");
839 }
840
841 #[test]
842 fn parse_error_display() {
843 let e = ParseError::new("bad token", Span::call_site());
844 assert_eq!(e.to_string(), "bad token");
845 }
846
847 #[test]
848 fn parse_error_into_cel_error() {
849 let e = ParseError::new("bad token", Span::call_site());
850 let cel: CELError = e.into();
851 assert_eq!(cel.message(), "bad token");
852 assert_eq!(cel.span(), SourceSpan::from_proc_macro2(Span::call_site()));
853 }
854
855 #[test]
856 fn parse_error_format_rustc_style() {
857 let source = "10 + 20 30";
858 let e = ParseError::new("unexpected token", Span::call_site());
859 let formatted = e.format_rustc_style(source, "test.cel", 1, &Renderer::plain());
860 assert!(formatted.contains("error: unexpected token"));
861 assert!(formatted.contains("test.cel"));
862 }
863
864 #[test]
865 fn parse_error_new_range_has_end_span() {
866 let start = Span::call_site();
867 let end = Span::call_site();
868 let e = ParseError::new_range("type mismatch", start, end);
869 assert_eq!(e.message(), "type mismatch");
870 assert!(e.end_span().is_some());
871 }
872
873 #[test]
874 fn from_lex_error_unclosed_paren_names_expected_close() {
875 let source = "(1 + 2";
876 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
877 let e = ParseError::from_lex_error(source, lex_err);
878 assert_eq!(
879 e.message(),
880 "unclosed delimiter `(`: expected a matching `)`"
881 );
882 }
883
884 #[test]
885 fn from_lex_error_unclosed_bracket_names_expected_close() {
886 let source = "[1, 2";
887 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
888 let e = ParseError::from_lex_error(source, lex_err);
889 assert_eq!(
890 e.message(),
891 "unclosed delimiter `[`: expected a matching `]`"
892 );
893 }
894
895 #[test]
896 fn from_lex_error_unclosed_brace_names_expected_close() {
897 let source = "{1: 2";
898 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
899 let e = ParseError::from_lex_error(source, lex_err);
900 assert_eq!(
901 e.message(),
902 "unclosed delimiter `{`: expected a matching `}`"
903 );
904 }
905
906 #[test]
907 fn from_lex_error_unexpected_closing_delimiter() {
908 let source = "1 + 2)";
909 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
910 let e = ParseError::from_lex_error(source, lex_err);
911 assert_eq!(e.message(), "unexpected closing delimiter `)`");
912 }
913
914 #[test]
915 fn from_lex_error_mismatched_delimiter_names_found_and_expected() {
916 let source = "(1 + 2]";
917 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
918 let e = ParseError::from_lex_error(source, lex_err);
919 assert_eq!(
920 e.message(),
921 "mismatched closing delimiter: found `]`, expected `)`"
922 );
923 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
924 assert!(
925 out.contains("unclosed delimiter `(`"),
926 "expected a secondary annotation on the unclosed `(`:\n{out}"
927 );
928 }
929
930 #[test]
931 fn from_lex_error_mismatched_delimiter_issue_77_example() {
932 let source = "{a: (1 + 2}";
935 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
936 let e = ParseError::from_lex_error(source, lex_err);
937 assert_eq!(
938 e.message(),
939 "mismatched closing delimiter: found `}`, expected `)`"
940 );
941 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
942 assert!(
943 out.contains("unclosed delimiter `(`"),
944 "expected a secondary annotation on the unclosed `(`:\n{out}"
945 );
946 }
947
948 #[test]
949 fn from_lex_error_mismatched_delimiter_reports_innermost_opener() {
950 let source = "([{1)]}";
952 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
953 let e = ParseError::from_lex_error(source, lex_err);
954 assert_eq!(
955 e.message(),
956 "mismatched closing delimiter: found `)`, expected `}`"
957 );
958 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
959 assert!(
960 out.contains("unclosed delimiter `{`"),
961 "expected a secondary annotation on the unclosed `{{`:\n{out}"
962 );
963 }
964
965 #[test]
966 fn from_lex_error_unterminated_string_literal() {
967 let source = "1 + \"abc";
968 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
969 let e = ParseError::from_lex_error(source, lex_err);
970 assert_eq!(e.message(), "unterminated string literal");
971 }
972
973 #[test]
974 fn from_lex_error_unterminated_block_comment() {
975 let source = "/* unterminated";
976 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
977 let e = ParseError::from_lex_error(source, lex_err);
978 assert_eq!(e.message(), "unterminated block comment");
979 }
980
981 #[test]
982 fn from_lex_error_invalid_character_names_the_character() {
983 let source = "1 + `";
984 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
985 let e = ParseError::from_lex_error(source, lex_err);
986 assert_eq!(e.message(), "invalid character `` ` ``");
987 }
988
989 #[test]
990 fn from_lex_error_preserves_lex_error_span() {
991 let source = "(1 + 2";
992 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
993 let expected_span = SourceSpan::from_proc_macro2(lex_err.span());
994 let e = ParseError::from_lex_error(source, lex_err);
995 assert_eq!(SourceSpan::from_proc_macro2(e.span()), expected_span);
996 }
997
998 #[test]
999 fn parse_error_new_range_cel_error_merges_spans() {
1000 let ts: proc_macro2::TokenStream = r#""Hello" 32.0"#.parse().unwrap();
1001 let mut iter = ts.into_iter();
1002 let start_tok = iter.next().unwrap();
1003 let end_tok = iter.next().unwrap();
1004 let start = start_tok.span();
1005 let end = end_tok.span();
1006
1007 let e = ParseError::new_range("type mismatch", start, end);
1008 let cel: CELError = e.into();
1009
1010 assert_eq!(cel.message(), "type mismatch");
1011 assert_eq!(
1012 cel.span().start,
1013 start.start(),
1014 "CELError span should start at expression start"
1015 );
1016 assert_eq!(
1017 cel.span().end,
1018 end.end(),
1019 "CELError span should end at expression end"
1020 );
1021 }
1022
1023 #[test]
1024 fn parse_error_new_has_no_end_span() {
1025 let e = ParseError::new("bad token", Span::call_site());
1026 assert!(e.end_span().is_none());
1027 }
1028
1029 #[test]
1030 fn span_to_byte_range_basic() {
1031 let source = "hello world";
1032 assert_eq!(
1033 span_to_byte_range(source, SourceSpan::new(1, 6, 1, 11)),
1034 6..11
1035 );
1036 }
1037
1038 #[test]
1039 fn span_to_byte_range_empty_span() {
1040 let source = "hello";
1041 assert_eq!(
1042 span_to_byte_range(source, SourceSpan::new(1, 2, 1, 2)),
1043 2..2
1044 );
1045 }
1046
1047 #[test]
1048 fn span_to_byte_range_start_past_end_of_source() {
1049 let source = "hi";
1050 let r = span_to_byte_range(source, SourceSpan::new(5, 0, 5, 3));
1051 assert_eq!(r, source.len()..source.len());
1052 }
1053
1054 #[test]
1055 fn span_to_byte_range_end_past_end_of_source() {
1056 let source = "hi";
1057 let r = span_to_byte_range(source, SourceSpan::new(1, 0, 1, 100));
1058 assert!(r.start <= r.end && r.end <= source.len());
1059 }
1060
1061 #[test]
1062 fn span_context_display_shows_span_location() {
1063 let span = SourceSpan::new(1, 0, 1, 5);
1064 let ctx = SpanContext::new(span);
1065 let s = ctx.to_string();
1066 assert!(!s.is_empty());
1067 }
1068
1069 #[test]
1070 fn span_context_span_roundtrip() {
1071 let span = SourceSpan::new(2, 3, 2, 7);
1072 let ctx = SpanContext::new(span);
1073 assert_eq!(ctx.span(), span);
1074 }
1075
1076 #[test]
1077 fn span_context_format_rustc_style_contains_message_and_location() {
1078 let source = "1i32 + 2i32";
1079 let span = SourceSpan::new(1, 5, 1, 6);
1080 let ctx = SpanContext::new(span);
1081 let output = ctx.format_rustc_style(
1082 "arithmetic overflow",
1083 source,
1084 "test.cel",
1085 1,
1086 &Renderer::plain(),
1087 );
1088 assert!(
1089 output.contains("arithmetic overflow"),
1090 "expected message in output:\n{output}"
1091 );
1092 assert!(
1093 output.contains("test.cel"),
1094 "expected filename in output:\n{output}"
1095 );
1096 }
1097
1098 #[test]
1099 fn format_rustc_style_with_span_context_uses_span() {
1100 let source = "1i32 + 2i32";
1101 let span = SourceSpan::new(1, 5, 1, 6);
1102 let inner = anyhow::anyhow!("arithmetic overflow");
1103 let wrapped = inner.context(SpanContext::new(span));
1104 let output = FormatRustcStyle::format_rustc_style(
1105 &wrapped,
1106 source,
1107 "test.cel",
1108 1,
1109 &Renderer::plain(),
1110 );
1111 assert!(
1112 output.contains("arithmetic overflow"),
1113 "expected message:\n{output}"
1114 );
1115 assert!(output.contains("test.cel"), "expected filename:\n{output}");
1116 }
1117
1118 #[test]
1119 fn format_rustc_style_without_span_context_falls_back_to_to_string() {
1120 let err = anyhow::anyhow!("something went wrong");
1121 let output = FormatRustcStyle::format_rustc_style(
1122 &err,
1123 "unused source",
1124 "unused.cel",
1125 1,
1126 &Renderer::plain(),
1127 );
1128 assert_eq!(output, "something went wrong");
1129 }
1130
1131 #[test]
1132 fn parse_error_with_secondary_renders_all_spans() {
1133 let source = "aaa bbb";
1134 let e = ParseError::new_range("bad", Span::call_site(), Span::call_site()).with_secondary(
1135 vec![SpanLabel {
1136 span: SourceSpan::new(1, 4, 1, 7),
1137 label: "also here".into(),
1138 }],
1139 );
1140 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
1142 assert!(out.contains("bad"), "{out}");
1143 assert!(out.contains("also here"), "{out}");
1144 }
1145
1146 #[test]
1147 fn parse_error_without_secondary_renders_single_span_as_before() {
1148 let e = ParseError::new("bad", Span::call_site());
1149 let out = e.format_rustc_style("10 + 20 30", "t.cel", 1, &Renderer::plain());
1150 assert!(out.contains("error: bad"), "{out}");
1151 }
1152
1153 #[test]
1154 fn format_multi_span_underlines_every_span_and_prints_labels() {
1155 let source = "aaa bbb ccc";
1156 let labels = vec![
1157 SpanLabel {
1158 span: SourceSpan::new(1, 0, 1, 3),
1159 label: "first".into(),
1160 },
1161 SpanLabel {
1162 span: SourceSpan::new(1, 8, 1, 11),
1163 label: "third".into(),
1164 },
1165 ];
1166 let out = format_multi_span("mismatch", &labels, source, "t.adm2", 1, &Renderer::plain());
1167 assert!(out.contains("mismatch"), "{out}");
1168 assert!(out.contains("first"), "{out}");
1169 assert!(out.contains("third"), "{out}");
1170 assert!(!out.contains('\u{1b}'), "plain renderer has no ANSI: {out}");
1171 }
1172
1173 #[test]
1174 fn to_byte_range_matches_manual_slice() {
1175 let source = "line one\nline two\nline three";
1176 let span = SourceSpan::new(2, 5, 3, 4);
1177 let range = span.to_byte_range(source);
1178 assert_eq!(&source[range], "two\nline");
1179 }
1180}