1use crate::Location;
2use crate::budget::BudgetBreach;
3use crate::de_snippet::fmt_snippet_window_offset_or_fallback;
4use crate::input_source::IncludeResolveError;
5use crate::localizer::{DEFAULT_ENGLISH_LOCALIZER, ExternalMessageSource, Localizer};
6use crate::location::Locations;
7use crate::parse_scalars::{
8 parse_int_signed, parse_yaml11_bool, parse_yaml12_float, scalar_is_nullish,
9};
10#[cfg(feature = "garde")]
11use crate::path_map::path_key_from_garde;
12use crate::properties_redaction::{
13 redact_custom_message, redact_dynamic_identifier, redact_dynamic_value,
14};
15use crate::tags::SfTag;
16#[cfg(any(feature = "garde", feature = "validator"))]
17use crate::{
18 localizer::ExternalMessage,
19 path_map::{PathKey, PathMap, format_path_with_resolved_leaf},
20};
21use annotate_snippets::Level;
22use saphyr_parser::{ScalarStyle, ScanError};
23use serde::de::{self};
24use std::borrow::Cow;
25use std::cell::Cell;
26use std::fmt;
27
28#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
29use crate::properties_redaction::{redact_with_ctxs, with_interp_redaction};
30
31#[cfg(feature = "validator")]
32use validator::{ValidationErrors, ValidationErrorsKind};
33
34pub trait MessageFormatter {
86 fn localizer(&self) -> &dyn Localizer {
91 &DEFAULT_ENGLISH_LOCALIZER
92 }
93
94 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str>;
99}
100
101#[derive(Debug, Default, Clone, Copy)]
113pub struct UserMessageFormatter;
114
115#[non_exhaustive]
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum SnippetMode {
119 Auto,
121 Off,
123}
124
125#[non_exhaustive]
148#[derive(Clone, Copy)]
149pub struct RenderOptions<'a> {
150 pub formatter: &'a dyn MessageFormatter,
152 pub snippets: SnippetMode,
154}
155
156impl<'a> Default for RenderOptions<'a> {
157 #[inline]
158 fn default() -> Self {
159 static DEFAULT_FMT: crate::message_formatters::DefaultMessageFormatter =
161 crate::message_formatters::DefaultMessageFormatter;
162
163 Self::new(&DEFAULT_FMT)
164 }
165}
166
167impl<'a> RenderOptions<'a> {
168 #[inline]
174 pub fn new(formatter: &'a dyn MessageFormatter) -> Self {
175 Self {
176 formatter,
177 snippets: SnippetMode::Auto,
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct CroppedRegion {
188 pub text: String,
190 pub source_name: String,
192 pub start_line: usize,
194 pub end_line: usize,
196 pub location: Location,
198}
199
200impl CroppedRegion {
201 fn covers_exact_source(&self, location: &Location) -> bool {
202 if location == &Location::UNKNOWN {
203 return false;
204 }
205 let source_id = location.source_id();
206 source_id != 0 && self.location.source_id() == source_id && self.covers_line(location)
207 }
208
209 fn covers_line(&self, location: &Location) -> bool {
210 let line = location.line as usize;
211 self.start_line <= line && line <= self.end_line
212 }
213
214 fn covers(&self, location: &Location) -> bool {
215 if location == &Location::UNKNOWN {
216 return false;
217 }
218 if !self.covers_line(location) {
219 return false;
220 }
221 let region_source_id = self.location.source_id();
222 let location_source_id = location.source_id();
223 region_source_id == 0 || location_source_id == 0 || region_source_id == location_source_id
224 }
225}
226
227fn line_count_including_trailing_empty_line(text: &str) -> usize {
228 let mut lines = text.split_terminator('\n').count().max(1);
229 if text.ends_with('\n') {
230 lines = lines.saturating_add(1);
231 }
232 lines
233}
234
235fn sanitize_snippet_source_name(name: &str) -> Cow<'_, str> {
236 if !name.chars().any(char::is_control) {
237 return Cow::Borrowed(name);
238 }
239
240 let sanitized: String = name
241 .chars()
242 .map(|ch| if ch.is_control() { ' ' } else { ch })
243 .collect();
244 Cow::Owned(sanitized)
245}
246
247#[cfg(any(feature = "garde", feature = "validator"))]
248#[derive(Debug, Clone)]
249pub struct ValidationIssue {
250 pub path: PathKey,
251 pub code: String,
252 pub message: Option<String>,
253 pub params: Vec<(String, String)>,
254}
255
256#[cfg(any(feature = "garde", feature = "validator"))]
257impl ValidationIssue {
258 pub(crate) fn display_entry(&self) -> String {
259 if let Some(msg) = &self.message {
260 return msg.clone();
261 }
262
263 if self.params.is_empty() {
264 return self.code.clone();
265 }
266
267 let mut params = String::new();
268 for (i, (k, v)) in self.params.iter().enumerate() {
269 if i > 0 {
270 params.push_str(", ");
271 }
272 params.push_str(k);
273 params.push('=');
274 params.push_str(v);
275 }
276 format!("{} ({params})", self.code)
277 }
278
279 pub(crate) fn display_entry_overridden(
280 &self,
281 l10n: &dyn Localizer,
282 source: ExternalMessageSource,
283 ) -> String {
284 let raw = self.display_entry();
285 let overridden = l10n
286 .override_external_message(ExternalMessage {
287 source,
288 original: raw.as_str(),
289 code: Some(self.code.as_str()),
290 params: &self.params,
291 })
292 .unwrap_or(Cow::Borrowed(raw.as_str()));
293 overridden.into_owned()
294 }
295}
296
297#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
298fn replace_known_effectives(
299 mut text: String,
300 ctxs: &[crate::properties_redaction::ScalarRedactionCtx],
301) -> String {
302 let mut pairs: Vec<&crate::properties_redaction::ScalarRedactionCtx> = ctxs
303 .iter()
304 .filter(|ctx| !ctx.effective.is_empty())
305 .collect();
306
307 pairs.sort_by_key(|ctx| std::cmp::Reverse(ctx.effective.len()));
308
309 for ctx in pairs {
310 if text.contains(&ctx.effective) {
311 text = text.replace(&ctx.effective, &ctx.raw);
312 }
313 }
314
315 text
316}
317
318#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
319pub(crate) fn redact_issue(mut issue: ValidationIssue) -> ValidationIssue {
320 with_interp_redaction(|pairs| {
321 if pairs.is_empty() {
322 return issue;
323 }
324
325 if let Some(msg) = issue.message.take() {
326 issue.message = Some(redact_with_ctxs(msg, pairs, "invalid interpolated value"));
327 }
328
329 issue.code = replace_known_effectives(std::mem::take(&mut issue.code), pairs);
330
331 for (key, value) in &mut issue.params {
332 *key = replace_known_effectives(std::mem::take(key), pairs);
333 *value = redact_with_ctxs(std::mem::take(value), pairs, "<redacted>");
334 }
335
336 issue
337 })
338}
339
340#[cfg(all(
341 not(feature = "properties"),
342 any(feature = "garde", feature = "validator")
343))]
344pub(crate) fn redact_issue(issue: ValidationIssue) -> ValidationIssue {
345 issue
346}
347
348thread_local! {
356 static MISSING_FIELD_FALLBACK: Cell<Option<Location>> = const { Cell::new(None) };
357}
358
359pub(crate) struct MissingFieldLocationGuard {
362 prev: Option<Location>,
363}
364
365impl MissingFieldLocationGuard {
366 pub(crate) fn new(location: Location) -> Self {
367 let prev = MISSING_FIELD_FALLBACK.with(|c| c.replace(Some(location)));
368 Self { prev }
369 }
370
371 pub(crate) fn replace_location(&mut self, location: Location) {
373 MISSING_FIELD_FALLBACK.with(|c| c.set(Some(location)));
374 }
375}
376
377impl Drop for MissingFieldLocationGuard {
378 fn drop(&mut self) {
379 MISSING_FIELD_FALLBACK.with(|c| c.set(self.prev));
380 }
381}
382
383#[non_exhaustive]
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub enum TransformReason {
390 EscapeSequence,
392 LineFolding,
394 MultiLineNormalization,
396 BlockScalarProcessing,
398 SingleQuoteEscape,
400 InputNotBorrowable,
404
405 ParserReturnedOwned,
411
412 VariableInterpolation,
414}
415
416impl fmt::Display for TransformReason {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 match self {
419 TransformReason::EscapeSequence => write!(f, "escape sequence processing"),
420 TransformReason::LineFolding => write!(f, "line folding"),
421 TransformReason::MultiLineNormalization => {
422 write!(f, "multi-line whitespace normalization")
423 }
424 TransformReason::BlockScalarProcessing => write!(f, "block scalar processing"),
425 TransformReason::SingleQuoteEscape => write!(f, "single-quote escape processing"),
426 TransformReason::InputNotBorrowable => {
427 write!(f, "input is not available for borrowing")
428 }
429 TransformReason::ParserReturnedOwned => write!(f, "parser returned an owned string"),
430 TransformReason::VariableInterpolation => write!(f, "variable interpolation"),
431 }
432 }
433}
434
435#[non_exhaustive]
437#[derive(Debug)]
438pub enum Error {
439 Message {
441 msg: String,
442 location: Location,
443 },
444
445 ExternalMessage {
450 source: ExternalMessageSource,
451 msg: String,
452 code: Option<String>,
454 params: Vec<(String, String)>,
456 location: Location,
457 },
458 Eof {
460 location: Location,
461 },
462 MultipleDocuments {
467 hint: &'static str,
469 location: Location,
470 },
471 Unexpected {
473 expected: &'static str,
474 location: Location,
475 },
476
477 MergeValueNotMapOrSeqOfMaps {
479 location: Location,
480 },
481
482 InvalidBinaryBase64 {
484 location: Location,
485 },
486
487 BinaryNotUtf8 {
489 location: Location,
490 },
491
492 TaggedScalarCannotDeserializeIntoString {
494 location: Location,
495 },
496
497 UnexpectedSequenceEnd {
499 location: Location,
500 },
501
502 UnexpectedMappingEnd {
504 location: Location,
505 },
506
507 InvalidBooleanStrict {
509 location: Location,
510 },
511
512 InvalidCharNull {
514 location: Location,
515 },
516
517 InvalidCharNotSingleScalar {
519 location: Location,
520 },
521
522 NullIntoString {
524 location: Location,
525 },
526
527 BytesNotSupportedMissingBinaryTag {
529 location: Location,
530 },
531
532 UnexpectedValueForUnit {
534 location: Location,
535 },
536
537 ExpectedEmptyMappingForUnitStruct {
539 location: Location,
540 },
541
542 UnexpectedContainerEndWhileSkippingNode {
544 location: Location,
545 },
546
547 InternalSeedReusedForMapKey {
549 location: Location,
550 },
551
552 ValueRequestedBeforeKey {
554 location: Location,
555 },
556
557 ExpectedStringKeyForExternallyTaggedEnum {
559 location: Location,
560 },
561
562 ExternallyTaggedEnumExpectedScalarOrMapping {
564 location: Location,
565 },
566
567 UnexpectedValueForUnitEnumVariant {
569 location: Location,
570 },
571
572 InvalidUtf8Input,
574
575 AliasReplayCounterOverflow {
577 location: Location,
578 },
579
580 AliasReplayLimitExceeded {
582 total_replayed_events: usize,
583 max_total_replayed_events: usize,
584 location: Location,
585 },
586
587 AliasExpansionLimitExceeded {
589 anchor_id: usize,
590 expansions: usize,
591 max_expansions_per_anchor: usize,
592 location: Location,
593 },
594
595 AliasReplayStackDepthExceeded {
597 depth: usize,
598 max_depth: usize,
599 location: Location,
600 },
601
602 FoldedBlockScalarMustIndentContent {
604 location: Location,
605 },
606
607 InternalDepthUnderflow {
609 location: Location,
610 },
611
612 InternalRecursionStackEmpty {
614 location: Location,
615 },
616
617 RecursiveReferencesRequireWeakTypes {
619 location: Location,
620 },
621
622 InvalidScalar {
624 ty: &'static str,
625 location: Location,
626 },
627
628 SerdeInvalidType {
630 unexpected: String,
631 expected: String,
632 location: Location,
633 },
634
635 SerdeInvalidValue {
637 unexpected: String,
638 expected: String,
639 location: Location,
640 },
641
642 SerdeUnknownVariant {
644 variant: String,
645 expected: Vec<&'static str>,
646 location: Location,
647 },
648
649 SerdeUnknownField {
651 field: String,
652 expected: Vec<&'static str>,
653 location: Location,
654 },
655
656 SerdeMissingField {
658 field: &'static str,
659 location: Location,
660 },
661
662 UnexpectedContainerEndWhileReadingKeyNode {
666 location: Location,
667 },
668
669 DuplicateMappingKey {
673 key: Option<String>,
674 location: Location,
675 },
676
677 TaggedEnumMismatch {
679 tagged: String,
680 target: &'static str,
681 location: Location,
682 },
683
684 SerdeVariantId {
686 msg: String,
687 location: Location,
688 },
689
690 ExpectedMappingEndAfterEnumVariantValue {
692 location: Location,
693 },
694 ContainerEndMismatch {
695 location: Location,
696 },
697 UnknownAnchor {
699 location: Location,
700 },
701 CyclicInclude {
703 id: String,
704 stack: Vec<String>,
705 location: Location,
706 },
707 UnsupportedIncludeForm {
709 location: Location,
710 },
711 ResolverError {
713 target: String,
714 error: IncludeResolveError,
715 stack: Vec<String>,
716 location: Location,
717 },
718 AliasError {
723 msg: String,
724 locations: Locations,
725 },
726 HookError {
729 msg: String,
730 location: Location,
731 },
732 UnresolvedProperty {
734 name: String,
736 location: Location,
737 },
738 InvalidPropertyName {
740 name: String,
742 location: Location,
743 },
744 Budget {
746 breach: BudgetBreach,
747 location: Location,
748 },
749 IOError {
751 cause: std::io::Error,
752 },
753 QuotingRequired {
756 value: String, location: Location,
758 },
759
760 CannotBorrowTransformedString {
766 reason: TransformReason,
768 location: Location,
769 },
770
771 IndentationError {
773 required: crate::indentation::RequireIndent,
775 actual: usize,
777 location: Location,
778 },
779
780 WithSnippet {
782 regions: Vec<CroppedRegion>,
787 crop_radius: usize,
788 error: Box<Error>,
789 },
790
791 #[cfg(feature = "garde")]
793 ValidationError {
794 issues: Vec<ValidationIssue>,
795 locations: PathMap,
796 },
797
798 #[cfg(feature = "garde")]
800 ValidationErrors {
801 errors: Vec<Error>,
802 },
803
804 #[cfg(feature = "validator")]
806 ValidatorError {
807 issues: Vec<ValidationIssue>,
808 locations: PathMap,
809 },
810
811 #[cfg(feature = "validator")]
813 ValidatorErrors {
814 errors: Vec<Error>,
815 },
816}
817
818impl Error {
819 #[cold]
820 #[inline(never)]
821 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
822 self.with_snippet_named(text, "<input>", crop_radius)
823 }
824
825 #[cold]
826 #[inline(never)]
827 pub(crate) fn with_snippet_named(
828 self,
829 text: &str,
830 source_name: &str,
831 crop_radius: usize,
832 ) -> Self {
833 let source_name = sanitize_snippet_source_name(source_name);
834
835 let inner = match self {
838 Error::WithSnippet { error, .. } => *error,
839 other => other,
840 };
841
842 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
844
845 fn push_region_for_location(
846 regions: &mut Vec<CroppedRegion>,
847 text: &str,
848 source_name: &str,
849 location: &Location,
850 mapping: crate::de_snippet::LineMapping,
851 crop_radius: usize,
852 ) {
853 if crop_radius == 0 || *location == Location::UNKNOWN {
854 return;
855 }
856 let (cropped, start_line) =
857 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
858 if cropped.is_empty() {
859 return;
860 }
861 let lines = line_count_including_trailing_empty_line(cropped.as_str());
862 let end_line = start_line.saturating_add(lines.saturating_sub(1));
863 regions.push(CroppedRegion {
864 text: cropped,
865 source_name: source_name.to_string(),
866 start_line,
867 end_line,
868 location: *location,
869 });
870 }
871
872 let mut regions: Vec<CroppedRegion> = Vec::new();
873 let mapping = crate::de_snippet::LineMapping::Identity;
874
875 #[cfg(feature = "garde")]
878 if let Error::ValidationError { issues, locations } = &inner {
879 for issue in issues {
880 let (locs, _) = locations
881 .search(&issue.path)
882 .unwrap_or((Locations::UNKNOWN, String::new()));
883 push_region_for_location(
884 &mut regions,
885 text,
886 source_name.as_ref(),
887 &locs.reference_location,
888 mapping,
889 crop_radius,
890 );
891 if locs.defined_location != locs.reference_location {
892 push_region_for_location(
893 &mut regions,
894 text,
895 source_name.as_ref(),
896 &locs.defined_location,
897 mapping,
898 crop_radius,
899 );
900 }
901 }
902 }
903 #[cfg(feature = "validator")]
904 if let Error::ValidatorError { issues, locations } = &inner {
905 for issue in issues {
906 let (locs, _) = locations
907 .search(&issue.path)
908 .unwrap_or((Locations::UNKNOWN, String::new()));
909 push_region_for_location(
910 &mut regions,
911 text,
912 source_name.as_ref(),
913 &locs.reference_location,
914 mapping,
915 crop_radius,
916 );
917 if locs.defined_location != locs.reference_location {
918 push_region_for_location(
919 &mut regions,
920 text,
921 source_name.as_ref(),
922 &locs.defined_location,
923 mapping,
924 crop_radius,
925 );
926 }
927 }
928 }
929
930 if regions.is_empty() {
933 if let Some(locs) = inner.locations() {
934 push_region_for_location(
935 &mut regions,
936 text,
937 source_name.as_ref(),
938 &locs.reference_location,
939 mapping,
940 crop_radius,
941 );
942 if locs.defined_location != locs.reference_location {
943 push_region_for_location(
944 &mut regions,
945 text,
946 source_name.as_ref(),
947 &locs.defined_location,
948 mapping,
949 crop_radius,
950 );
951 }
952 } else if let Some(loc) = inner.location() {
953 push_region_for_location(
954 &mut regions,
955 text,
956 source_name.as_ref(),
957 &loc,
958 mapping,
959 crop_radius,
960 );
961 }
962 }
963
964 Error::WithSnippet {
965 regions,
966 crop_radius,
967 error: Box::new(inner),
968 }
969 }
970
971 #[cfg(feature = "include")]
972 #[cold]
973 #[inline(never)]
974 pub(crate) fn with_additional_snippet_named(
975 mut self,
976 text: &str,
977 source_name: &str,
978 location: &Location,
979 crop_radius: usize,
980 ) -> Self {
981 let source_name = sanitize_snippet_source_name(source_name);
982
983 if crop_radius == 0 || *location == Location::UNKNOWN {
984 return self;
985 }
986
987 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
988 let mapping = crate::de_snippet::LineMapping::Identity;
989
990 let (cropped, start_line) =
991 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
992 if cropped.is_empty() {
993 return self;
994 }
995 let lines = line_count_including_trailing_empty_line(cropped.as_str());
996 let end_line = start_line.saturating_add(lines.saturating_sub(1));
997
998 let region = CroppedRegion {
999 text: cropped,
1000 source_name: source_name.into_owned(),
1001 start_line,
1002 end_line,
1003 location: *location,
1004 };
1005
1006 if let Error::WithSnippet {
1007 ref mut regions, ..
1008 } = self
1009 {
1010 regions.push(region);
1011 }
1012 self
1013 }
1014
1015 #[cfg(feature = "include")]
1016 #[cold]
1017 #[inline(never)]
1018 pub(crate) fn with_additional_snippet_offset_named(
1019 mut self,
1020 text: &str,
1021 start_line: usize,
1022 source_name: &str,
1023 location: &Location,
1024 crop_radius: usize,
1025 ) -> Self {
1026 let source_name = sanitize_snippet_source_name(source_name);
1027
1028 if crop_radius == 0 || *location == Location::UNKNOWN {
1029 return self;
1030 }
1031
1032 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1033 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1034
1035 let (cropped, start_line) =
1036 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
1037 if cropped.is_empty() {
1038 return self;
1039 }
1040 let lines = line_count_including_trailing_empty_line(cropped.as_str());
1041 let end_line = start_line.saturating_add(lines.saturating_sub(1));
1042
1043 let region = CroppedRegion {
1044 text: cropped,
1045 source_name: source_name.into_owned(),
1046 start_line,
1047 end_line,
1048 location: *location,
1049 };
1050
1051 if let Error::WithSnippet {
1052 ref mut regions, ..
1053 } = self
1054 {
1055 regions.push(region);
1056 }
1057 self
1058 }
1059
1060 #[cold]
1061 #[inline(never)]
1062 pub(crate) fn with_snippet_offset_named(
1063 self,
1064 text: &str,
1065 start_line: usize,
1066 source_name: &str,
1067 crop_radius: usize,
1068 ) -> Self {
1069 let source_name = sanitize_snippet_source_name(source_name);
1070
1071 let inner = match self {
1072 Error::WithSnippet { error, .. } => *error,
1073 other => other,
1074 };
1075
1076 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1078
1079 fn push_region_for_location(
1080 regions: &mut Vec<CroppedRegion>,
1081 text: &str,
1082 source_name: &str,
1083 location: &Location,
1084 mapping: crate::de_snippet::LineMapping,
1085 crop_radius: usize,
1086 ) {
1087 if crop_radius == 0 || *location == Location::UNKNOWN {
1088 return;
1089 }
1090 let (cropped, region_start_line) =
1091 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
1092 if cropped.is_empty() {
1093 return;
1094 }
1095 let lines = line_count_including_trailing_empty_line(cropped.as_str());
1096 let end_line = region_start_line.saturating_add(lines.saturating_sub(1));
1097 regions.push(CroppedRegion {
1098 text: cropped,
1099 source_name: source_name.to_string(),
1100 start_line: region_start_line,
1101 end_line,
1102 location: *location,
1103 });
1104 }
1105
1106 let mut regions: Vec<CroppedRegion> = Vec::new();
1107 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1108
1109 #[cfg(feature = "garde")]
1110 if let Error::ValidationError { issues, locations } = &inner {
1111 for issue in issues {
1112 let (locs, _) = locations
1113 .search(&issue.path)
1114 .unwrap_or((Locations::UNKNOWN, String::new()));
1115 push_region_for_location(
1116 &mut regions,
1117 text,
1118 source_name.as_ref(),
1119 &locs.reference_location,
1120 mapping,
1121 crop_radius,
1122 );
1123 if locs.defined_location != locs.reference_location {
1124 push_region_for_location(
1125 &mut regions,
1126 text,
1127 source_name.as_ref(),
1128 &locs.defined_location,
1129 mapping,
1130 crop_radius,
1131 );
1132 }
1133 }
1134 }
1135 #[cfg(feature = "validator")]
1136 if let Error::ValidatorError { issues, locations } = &inner {
1137 for issue in issues {
1138 let (locs, _) = locations
1139 .search(&issue.path)
1140 .unwrap_or((Locations::UNKNOWN, String::new()));
1141 push_region_for_location(
1142 &mut regions,
1143 text,
1144 source_name.as_ref(),
1145 &locs.reference_location,
1146 mapping,
1147 crop_radius,
1148 );
1149 if locs.defined_location != locs.reference_location {
1150 push_region_for_location(
1151 &mut regions,
1152 text,
1153 source_name.as_ref(),
1154 &locs.defined_location,
1155 mapping,
1156 crop_radius,
1157 );
1158 }
1159 }
1160 }
1161
1162 if regions.is_empty() {
1163 if let Some(locs) = inner.locations() {
1164 push_region_for_location(
1165 &mut regions,
1166 text,
1167 source_name.as_ref(),
1168 &locs.reference_location,
1169 mapping,
1170 crop_radius,
1171 );
1172 if locs.defined_location != locs.reference_location {
1173 push_region_for_location(
1174 &mut regions,
1175 text,
1176 source_name.as_ref(),
1177 &locs.defined_location,
1178 mapping,
1179 crop_radius,
1180 );
1181 }
1182 } else if let Some(loc) = inner.location() {
1183 push_region_for_location(
1184 &mut regions,
1185 text,
1186 source_name.as_ref(),
1187 &loc,
1188 mapping,
1189 crop_radius,
1190 );
1191 }
1192 }
1193
1194 Error::WithSnippet {
1195 regions,
1196 crop_radius,
1197 error: Box::new(inner),
1198 }
1199 }
1200
1201 pub fn without_snippet(&self) -> &Self {
1203 match self {
1204 Error::WithSnippet { error, .. } => error,
1205 other => other,
1206 }
1207 }
1208
1209 pub fn render(&self) -> String {
1215 self.render_with_options(RenderOptions::default())
1216 }
1217
1218 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
1220 self.render_with_options(RenderOptions {
1221 formatter,
1222 snippets: SnippetMode::Auto,
1223 })
1224 }
1225
1226 pub fn render_with_options(&self, options: RenderOptions<'_>) -> String {
1228 struct RenderDisplay<'a> {
1229 err: &'a Error,
1230 options: RenderOptions<'a>,
1231 }
1232
1233 impl fmt::Display for RenderDisplay<'_> {
1234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1235 fmt_error_rendered(f, self.err, self.options)
1236 }
1237 }
1238
1239 RenderDisplay { err: self, options }.to_string()
1240 }
1241
1242 #[cold]
1253 #[inline(never)]
1254 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
1255 Error::Message {
1256 msg: s.into(),
1257 location: Location::UNKNOWN,
1258 }
1259 }
1260
1261 #[cold]
1265 #[inline(never)]
1266 pub(crate) fn quoting_required(value: &str, interpolated: bool) -> Self {
1267 let location = Location::UNKNOWN;
1270 let value = if !interpolated
1271 && (parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
1272 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
1273 || parse_yaml11_bool(value).is_ok()
1274 || scalar_is_nullish(value, &ScalarStyle::Plain))
1275 {
1276 value.to_string()
1277 } else {
1278 String::new()
1279 };
1280 Error::QuotingRequired { value, location }
1281 }
1282
1283 #[cold]
1294 #[inline(never)]
1295 pub(crate) fn unexpected(what: &'static str) -> Self {
1296 Error::Unexpected {
1297 expected: what,
1298 location: Location::UNKNOWN,
1299 }
1300 }
1301
1302 #[cold]
1307 #[inline(never)]
1308 pub(crate) fn eof() -> Self {
1309 Error::Eof {
1310 location: Location::UNKNOWN,
1311 }
1312 }
1313
1314 #[cold]
1315 #[inline(never)]
1316 pub(crate) fn multiple_documents(hint: &'static str) -> Self {
1317 Error::MultipleDocuments {
1318 hint,
1319 location: Location::UNKNOWN,
1320 }
1321 }
1322
1323 #[cold]
1328 #[inline(never)]
1329 pub(crate) fn unknown_anchor() -> Self {
1330 Error::UnknownAnchor {
1331 location: Location::UNKNOWN,
1332 }
1333 }
1334
1335 #[cold]
1340 #[inline(never)]
1341 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
1342 Error::CannotBorrowTransformedString {
1343 reason,
1344 location: Location::UNKNOWN,
1345 }
1346 }
1347
1348 #[cold]
1359 #[inline(never)]
1360 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
1361 match &mut self {
1362 Error::Message { location, .. }
1363 | Error::ExternalMessage { location, .. }
1364 | Error::Eof { location }
1365 | Error::MultipleDocuments { location, .. }
1366 | Error::Unexpected { location, .. }
1367 | Error::MergeValueNotMapOrSeqOfMaps { location }
1368 | Error::InvalidBinaryBase64 { location }
1369 | Error::BinaryNotUtf8 { location }
1370 | Error::TaggedScalarCannotDeserializeIntoString { location }
1371 | Error::UnexpectedSequenceEnd { location }
1372 | Error::UnexpectedMappingEnd { location }
1373 | Error::InvalidBooleanStrict { location }
1374 | Error::InvalidCharNull { location }
1375 | Error::InvalidCharNotSingleScalar { location }
1376 | Error::NullIntoString { location }
1377 | Error::BytesNotSupportedMissingBinaryTag { location }
1378 | Error::UnexpectedValueForUnit { location }
1379 | Error::ExpectedEmptyMappingForUnitStruct { location }
1380 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1381 | Error::InternalSeedReusedForMapKey { location }
1382 | Error::ValueRequestedBeforeKey { location }
1383 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1384 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1385 | Error::UnexpectedValueForUnitEnumVariant { location }
1386 | Error::AliasReplayCounterOverflow { location }
1387 | Error::AliasReplayLimitExceeded { location, .. }
1388 | Error::AliasExpansionLimitExceeded { location, .. }
1389 | Error::AliasReplayStackDepthExceeded { location, .. }
1390 | Error::FoldedBlockScalarMustIndentContent { location }
1391 | Error::InternalDepthUnderflow { location }
1392 | Error::InternalRecursionStackEmpty { location }
1393 | Error::RecursiveReferencesRequireWeakTypes { location }
1394 | Error::InvalidScalar { location, .. }
1395 | Error::SerdeInvalidType { location, .. }
1396 | Error::SerdeInvalidValue { location, .. }
1397 | Error::SerdeUnknownVariant { location, .. }
1398 | Error::SerdeUnknownField { location, .. }
1399 | Error::SerdeMissingField { location, .. }
1400 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1401 | Error::DuplicateMappingKey { location, .. }
1402 | Error::TaggedEnumMismatch { location, .. }
1403 | Error::SerdeVariantId { location, .. }
1404 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1405 | Error::HookError { location, .. }
1406 | Error::UnresolvedProperty { location, .. }
1407 | Error::InvalidPropertyName { location, .. }
1408 | Error::ContainerEndMismatch { location, .. }
1409 | Error::UnknownAnchor { location, .. }
1410 | Error::CyclicInclude { location, .. }
1411 | Error::UnsupportedIncludeForm { location, .. }
1412 | Error::ResolverError { location, .. }
1413 | Error::QuotingRequired { location, .. }
1414 | Error::Budget { location, .. }
1415 | Error::CannotBorrowTransformedString { location, .. }
1416 | Error::IndentationError { location, .. } => {
1417 *location = set_location;
1418 }
1419 Error::InvalidUtf8Input => {}
1420 Error::IOError { .. } => {} Error::AliasError { .. } => {
1422 }
1424 Error::WithSnippet { error, .. } => {
1425 let inner = *std::mem::replace(error, Box::new(Error::eof()));
1426 **error = inner.with_location(set_location);
1427 }
1428 #[cfg(feature = "garde")]
1429 Error::ValidationError { .. } => {
1430 }
1432 #[cfg(feature = "garde")]
1433 Error::ValidationErrors { .. } => {
1434 }
1436 #[cfg(feature = "validator")]
1437 Error::ValidatorError { .. } => {
1438 }
1440 #[cfg(feature = "validator")]
1441 Error::ValidatorErrors { .. } => {
1442 }
1444 }
1445 self
1446 }
1447
1448 pub fn location(&self) -> Option<Location> {
1456 match self {
1457 Error::Message { location, .. }
1458 | Error::ExternalMessage { location, .. }
1459 | Error::Eof { location }
1460 | Error::MultipleDocuments { location, .. }
1461 | Error::Unexpected { location, .. }
1462 | Error::MergeValueNotMapOrSeqOfMaps { location }
1463 | Error::InvalidBinaryBase64 { location }
1464 | Error::BinaryNotUtf8 { location }
1465 | Error::TaggedScalarCannotDeserializeIntoString { location }
1466 | Error::UnexpectedSequenceEnd { location }
1467 | Error::UnexpectedMappingEnd { location }
1468 | Error::InvalidBooleanStrict { location }
1469 | Error::InvalidCharNull { location }
1470 | Error::InvalidCharNotSingleScalar { location }
1471 | Error::NullIntoString { location }
1472 | Error::BytesNotSupportedMissingBinaryTag { location }
1473 | Error::UnexpectedValueForUnit { location }
1474 | Error::ExpectedEmptyMappingForUnitStruct { location }
1475 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1476 | Error::InternalSeedReusedForMapKey { location }
1477 | Error::ValueRequestedBeforeKey { location }
1478 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1479 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1480 | Error::UnexpectedValueForUnitEnumVariant { location }
1481 | Error::AliasReplayCounterOverflow { location }
1482 | Error::AliasReplayLimitExceeded { location, .. }
1483 | Error::AliasExpansionLimitExceeded { location, .. }
1484 | Error::AliasReplayStackDepthExceeded { location, .. }
1485 | Error::FoldedBlockScalarMustIndentContent { location }
1486 | Error::InternalDepthUnderflow { location }
1487 | Error::InternalRecursionStackEmpty { location }
1488 | Error::RecursiveReferencesRequireWeakTypes { location }
1489 | Error::InvalidScalar { location, .. }
1490 | Error::SerdeInvalidType { location, .. }
1491 | Error::SerdeInvalidValue { location, .. }
1492 | Error::SerdeUnknownVariant { location, .. }
1493 | Error::SerdeUnknownField { location, .. }
1494 | Error::SerdeMissingField { location, .. }
1495 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1496 | Error::DuplicateMappingKey { location, .. }
1497 | Error::TaggedEnumMismatch { location, .. }
1498 | Error::SerdeVariantId { location, .. }
1499 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1500 | Error::HookError { location, .. }
1501 | Error::UnresolvedProperty { location, .. }
1502 | Error::InvalidPropertyName { location, .. }
1503 | Error::ContainerEndMismatch { location, .. }
1504 | Error::UnknownAnchor { location, .. }
1505 | Error::CyclicInclude { location, .. }
1506 | Error::UnsupportedIncludeForm { location, .. }
1507 | Error::ResolverError { location, .. }
1508 | Error::QuotingRequired { location, .. }
1509 | Error::Budget { location, .. }
1510 | Error::CannotBorrowTransformedString { location, .. }
1511 | Error::IndentationError { location, .. } => {
1512 if location != &Location::UNKNOWN {
1513 Some(*location)
1514 } else {
1515 None
1516 }
1517 }
1518 Error::InvalidUtf8Input => None,
1519 Error::IOError { cause: _ } => None,
1520 Error::AliasError { locations, .. } => Locations::primary_location(*locations),
1521 Error::WithSnippet { error, .. } => error.location(),
1522 #[cfg(feature = "garde")]
1523 Error::ValidationError { issues, locations } => issues.first().and_then(|issue| {
1524 let (locs, _) = locations.search(&issue.path)?;
1525 let loc = if locs.reference_location != Location::UNKNOWN {
1526 locs.reference_location
1527 } else {
1528 locs.defined_location
1529 };
1530 if loc != Location::UNKNOWN {
1531 Some(loc)
1532 } else {
1533 None
1534 }
1535 }),
1536 #[cfg(feature = "garde")]
1537 Error::ValidationErrors { errors } => errors.iter().find_map(|e| e.location()),
1538 #[cfg(feature = "validator")]
1539 Error::ValidatorError { issues, locations } => issues.first().and_then(|issue| {
1540 let (locs, _) = locations.search(&issue.path)?;
1541 let loc = if locs.reference_location != Location::UNKNOWN {
1542 locs.reference_location
1543 } else {
1544 locs.defined_location
1545 };
1546 if loc != Location::UNKNOWN {
1547 Some(loc)
1548 } else {
1549 None
1550 }
1551 }),
1552 #[cfg(feature = "validator")]
1553 Error::ValidatorErrors { errors } => errors.iter().find_map(|e| e.location()),
1554 }
1555 }
1556 pub fn locations(&self) -> Option<Locations> {
1566 match self {
1567 Error::Message { location, .. }
1568 | Error::ExternalMessage { location, .. }
1569 | Error::Eof { location }
1570 | Error::MultipleDocuments { location, .. }
1571 | Error::Unexpected { location, .. }
1572 | Error::MergeValueNotMapOrSeqOfMaps { location }
1573 | Error::InvalidBinaryBase64 { location }
1574 | Error::BinaryNotUtf8 { location }
1575 | Error::TaggedScalarCannotDeserializeIntoString { location }
1576 | Error::UnexpectedSequenceEnd { location }
1577 | Error::UnexpectedMappingEnd { location }
1578 | Error::InvalidBooleanStrict { location }
1579 | Error::InvalidCharNull { location }
1580 | Error::InvalidCharNotSingleScalar { location }
1581 | Error::NullIntoString { location }
1582 | Error::BytesNotSupportedMissingBinaryTag { location }
1583 | Error::UnexpectedValueForUnit { location }
1584 | Error::ExpectedEmptyMappingForUnitStruct { location }
1585 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1586 | Error::InternalSeedReusedForMapKey { location }
1587 | Error::ValueRequestedBeforeKey { location }
1588 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1589 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1590 | Error::UnexpectedValueForUnitEnumVariant { location }
1591 | Error::AliasReplayCounterOverflow { location }
1592 | Error::AliasReplayLimitExceeded { location, .. }
1593 | Error::AliasExpansionLimitExceeded { location, .. }
1594 | Error::AliasReplayStackDepthExceeded { location, .. }
1595 | Error::FoldedBlockScalarMustIndentContent { location }
1596 | Error::InternalDepthUnderflow { location }
1597 | Error::InternalRecursionStackEmpty { location }
1598 | Error::RecursiveReferencesRequireWeakTypes { location }
1599 | Error::InvalidScalar { location, .. }
1600 | Error::SerdeInvalidType { location, .. }
1601 | Error::SerdeInvalidValue { location, .. }
1602 | Error::SerdeUnknownVariant { location, .. }
1603 | Error::SerdeUnknownField { location, .. }
1604 | Error::SerdeMissingField { location, .. }
1605 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1606 | Error::DuplicateMappingKey { location, .. }
1607 | Error::TaggedEnumMismatch { location, .. }
1608 | Error::SerdeVariantId { location, .. }
1609 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1610 | Error::HookError { location, .. }
1611 | Error::UnresolvedProperty { location, .. }
1612 | Error::InvalidPropertyName { location, .. }
1613 | Error::ContainerEndMismatch { location, .. }
1614 | Error::UnknownAnchor { location, .. }
1615 | Error::CyclicInclude { location, .. }
1616 | Error::UnsupportedIncludeForm { location, .. }
1617 | Error::ResolverError { location, .. }
1618 | Error::QuotingRequired { location, .. }
1619 | Error::Budget { location, .. }
1620 | Error::CannotBorrowTransformedString { location, .. }
1621 | Error::IndentationError { location, .. } => Locations::same(location),
1622 Error::InvalidUtf8Input => None,
1623 Error::IOError { .. } => None,
1624 Error::AliasError { locations, .. } => Some(*locations),
1625 Error::WithSnippet { error, .. } => error.locations(),
1626 #[cfg(feature = "garde")]
1627 Error::ValidationError { issues, locations } => issues
1628 .first()
1629 .and_then(|issue| search_locations_with_ancestor_fallback(locations, &issue.path)),
1630 #[cfg(feature = "garde")]
1631 Error::ValidationErrors { errors } => errors.first().and_then(Error::locations),
1632 #[cfg(feature = "validator")]
1633 Error::ValidatorError { issues, locations } => issues
1634 .first()
1635 .and_then(|issue| locations.search(&issue.path).map(|(locs, _)| locs)),
1636 #[cfg(feature = "validator")]
1637 Error::ValidatorErrors { errors } => errors.first().and_then(Error::locations),
1638 }
1639 }
1640
1641 #[cold]
1646 #[inline(never)]
1647 pub(crate) fn from_scan_error(err: ScanError) -> Self {
1648 use crate::location::SpanIndex;
1649 let mark = err.marker();
1650 let location =
1651 Location::new(mark.line(), mark.col() + 1).with_span(crate::location::Span {
1652 offset: mark.index() as SpanIndex,
1653 len: 1,
1654 byte_info: (0, 0),
1655 });
1656
1657 let info = err.info();
1665 if info.to_ascii_lowercase().contains("unknown anchor") {
1666 return Error::UnknownAnchor { location };
1667 }
1668
1669 Error::ExternalMessage {
1670 source: ExternalMessageSource::SaphyrParser,
1671 msg: info.to_owned(),
1672 code: None,
1673 params: Vec::new(),
1674 location,
1675 }
1676 }
1677}
1678
1679fn fmt_error_plain_with_formatter(
1680 f: &mut fmt::Formatter<'_>,
1681 err: &Error,
1682 formatter: &dyn MessageFormatter,
1683) -> fmt::Result {
1684 let err = err.without_snippet();
1685
1686 let msg = formatter.format_message(err);
1687
1688 #[cfg(feature = "garde")]
1692 if matches!(err, Error::ValidationError { .. }) {
1693 return write!(f, "{msg}");
1694 }
1695 #[cfg(feature = "validator")]
1696 if matches!(err, Error::ValidatorError { .. }) {
1697 return write!(f, "{msg}");
1698 }
1699
1700 if let Some(loc) = err.location() {
1701 fmt_with_location(f, formatter.localizer(), msg.as_ref(), &loc)?;
1702 } else {
1703 write!(f, "{msg}")?;
1704 }
1705
1706 #[cfg(feature = "garde")]
1707 if let Error::ValidationErrors { errors } = err {
1708 for err in errors {
1709 writeln!(f)?;
1710 writeln!(f)?;
1711 fmt_error_plain_with_formatter(f, err, formatter)?;
1712 }
1713 }
1714
1715 #[cfg(feature = "validator")]
1716 if let Error::ValidatorErrors { errors } = err {
1717 for err in errors {
1718 writeln!(f)?;
1719 writeln!(f)?;
1720 fmt_error_plain_with_formatter(f, err, formatter)?;
1721 }
1722 }
1723
1724 Ok(())
1725}
1726
1727fn pick_cropped_region<'a>(
1728 regions: &'a [CroppedRegion],
1729 location: &Location,
1730) -> Option<&'a CroppedRegion> {
1731 let source_id = location.source_id();
1732
1733 if source_id != 0 {
1734 if let Some(region) = regions.iter().find(|r| r.covers_exact_source(location)) {
1735 return Some(region);
1736 }
1737 if let Some(region) = regions.iter().find(|r| r.location.source_id() == source_id) {
1738 return Some(region);
1739 }
1740 if let Some(region) = regions
1741 .iter()
1742 .find(|r| r.location.source_id() == 0 && r.covers(location))
1743 {
1744 return Some(region);
1745 }
1746 return None;
1747 }
1748
1749 regions
1750 .iter()
1751 .find(|r| r.covers(location))
1752 .or_else(|| regions.first())
1753}
1754
1755fn fmt_error_rendered(
1756 f: &mut fmt::Formatter<'_>,
1757 err: &Error,
1758 options: RenderOptions<'_>,
1759) -> fmt::Result {
1760 if options.snippets == SnippetMode::Off {
1761 return fmt_error_plain_with_formatter(f, err, options.formatter);
1762 }
1763
1764 match err {
1765 #[cfg(feature = "garde")]
1766 Error::ValidationErrors { errors } => {
1767 let msg = options.formatter.format_message(err);
1768 if !msg.is_empty() {
1769 writeln!(f, "{}", msg)?;
1770 }
1771 let mut first = true;
1772 for err in errors {
1773 if !first {
1774 writeln!(f)?;
1775 writeln!(f)?;
1776 }
1777 first = false;
1778 fmt_error_rendered(f, err, options)?;
1779 }
1780 Ok(())
1781 }
1782
1783 #[cfg(feature = "validator")]
1784 Error::ValidatorErrors { errors } => {
1785 let msg = options.formatter.format_message(err);
1786 if !msg.is_empty() {
1787 writeln!(f, "{}", msg)?;
1788 }
1789 let mut first = true;
1790 for err in errors {
1791 if !first {
1792 writeln!(f)?;
1793 writeln!(f)?;
1794 }
1795 first = false;
1796 fmt_error_rendered(f, err, options)?;
1797 }
1798 Ok(())
1799 }
1800
1801 Error::WithSnippet {
1802 regions,
1803 crop_radius,
1804 error,
1805 } => {
1806 if *crop_radius == 0 {
1807 return fmt_error_plain_with_formatter(f, error, options.formatter);
1809 }
1810
1811 if regions.is_empty() {
1812 return fmt_error_plain_with_formatter(f, error, options.formatter);
1813 }
1814
1815 #[cfg(feature = "garde")]
1818 if let Error::ValidationError { issues, locations } = error.as_ref() {
1819 return fmt_validation_error_with_snippets_offset(
1820 f,
1821 options.formatter.localizer(),
1822 issues,
1823 locations,
1824 regions,
1825 *crop_radius,
1826 );
1827 }
1828 #[cfg(feature = "garde")]
1829 if let Error::ValidationErrors { errors } = error.as_ref() {
1830 let msg = options.formatter.format_message(error);
1831 if !msg.is_empty() {
1832 writeln!(f, "{}", msg)?;
1833 }
1834 let mut first = true;
1835 for err in errors {
1836 if !first {
1837 writeln!(f)?;
1838 writeln!(f)?;
1839 }
1840 first = false;
1841 fmt_error_with_snippets_offset(
1842 f,
1843 err,
1844 regions,
1845 *crop_radius,
1846 options.formatter,
1847 )?;
1848 }
1849 return Ok(());
1850 }
1851
1852 #[cfg(feature = "validator")]
1853 if let Error::ValidatorError { issues, locations } = error.as_ref() {
1854 return fmt_validator_error_with_snippets_offset(
1855 f,
1856 options.formatter.localizer(),
1857 issues,
1858 locations,
1859 regions,
1860 *crop_radius,
1861 );
1862 }
1863 #[cfg(feature = "validator")]
1864 if let Error::ValidatorErrors { errors } = error.as_ref() {
1865 let msg = options.formatter.format_message(error);
1866 if !msg.is_empty() {
1867 writeln!(f, "{}", msg)?;
1868 }
1869 let mut first = true;
1870 for err in errors {
1871 if !first {
1872 writeln!(f)?;
1873 writeln!(f)?;
1874 }
1875 first = false;
1876 fmt_error_with_snippets_offset(
1877 f,
1878 err,
1879 regions,
1880 *crop_radius,
1881 options.formatter,
1882 )?;
1883 }
1884 return Ok(());
1885 }
1886
1887 let Some(location) = error.location() else {
1890 return fmt_error_plain_with_formatter(f, error, options.formatter);
1891 };
1892 if location == Location::UNKNOWN {
1893 return fmt_error_plain_with_formatter(f, error, options.formatter);
1894 }
1895
1896 let l10n = options.formatter.localizer();
1897
1898 let region = match pick_cropped_region(regions, &location) {
1899 Some(r) => r,
1900 None => return fmt_error_plain_with_formatter(f, error, options.formatter),
1901 };
1902
1903 let dual_locations = error.locations().filter(|locs| {
1905 locs.reference_location != Location::UNKNOWN
1906 && locs.defined_location != Location::UNKNOWN
1907 && locs.reference_location != locs.defined_location
1908 });
1909
1910 let mut msg = options.formatter.format_message(error);
1911
1912 if dual_locations.is_some()
1916 && let Error::AliasError { locations, .. } = error.as_ref()
1917 {
1918 let suffix = l10n.alias_defined_at(locations.defined_location);
1919 if let Some(stripped) = msg.as_ref().strip_suffix(&suffix) {
1920 msg = Cow::Owned(stripped.to_string());
1921 }
1922 }
1923
1924 if let Some(locs) = dual_locations {
1925 let ref_loc = locs.reference_location;
1926 let def_loc = locs.defined_location;
1927
1928 let used_region = pick_cropped_region(regions, &ref_loc).unwrap_or(region);
1929 let label = l10n.value_used_here();
1930 let ctx = crate::de_snippet::Snippet::new(
1931 used_region.text.as_str(),
1932 label.as_ref(),
1933 *crop_radius,
1934 )
1935 .with_offset(used_region.start_line);
1936 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &ref_loc)?;
1937
1938 let def_region = pick_cropped_region(regions, &def_loc).unwrap_or(region);
1939 writeln!(f)?;
1940 writeln!(f, "{}", l10n.value_comes_from_the_anchor(def_loc))?;
1941 fmt_snippet_window_offset_or_fallback(
1942 f,
1943 l10n,
1944 &def_loc,
1945 def_region.text.as_str(),
1946 def_region.start_line,
1947 l10n.defined_window().as_ref(),
1948 *crop_radius,
1949 )?;
1950 Ok(())
1951 } else {
1952 let ctx = crate::de_snippet::Snippet::new(
1954 region.text.as_str(),
1955 region.source_name.as_str(),
1956 *crop_radius,
1957 )
1958 .with_offset(region.start_line);
1959 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &location)?;
1960
1961 for extra_region in regions {
1962 if std::ptr::eq(extra_region, region) {
1963 continue;
1964 }
1965 writeln!(f)?;
1966 writeln!(f, "included from here:")?;
1967 let extra_ctx = crate::de_snippet::Snippet::new(
1968 extra_region.text.as_str(),
1969 extra_region.source_name.as_str(),
1970 *crop_radius,
1971 )
1972 .with_offset(extra_region.start_line);
1973 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
1974 }
1975 Ok(())
1976 }
1977 }
1978 _ => fmt_error_plain_with_formatter(f, err, options.formatter),
1979 }
1980}
1981
1982#[cfg(any(feature = "garde", feature = "validator"))]
1983fn search_locations_with_ancestor_fallback(
1984 locations: &PathMap,
1985 path: &PathKey,
1986) -> Option<Locations> {
1987 if let Some((locs, _)) = locations.search(path) {
1988 return Some(locs);
1989 }
1990
1991 let mut p = path.parent();
1992 while let Some(cur) = p {
1993 if let Some((locs, _)) = locations.search(&cur) {
1994 return Some(locs);
1995 }
1996 p = cur.parent();
1997 }
1998
1999 None
2000}
2001
2002impl fmt::Display for Error {
2003 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2004 fmt_error_rendered(f, self, RenderOptions::default())
2005 }
2006}
2007
2008#[cfg(feature = "garde")]
2009fn fmt_validation_error_with_snippets_offset(
2010 f: &mut fmt::Formatter<'_>,
2011 l10n: &dyn Localizer,
2012 issues: &[ValidationIssue],
2013 locations: &PathMap,
2014 regions: &[CroppedRegion],
2015 crop_radius: usize,
2016) -> fmt::Result {
2017 let mut first = true;
2018 for issue in issues {
2019 if !first {
2020 writeln!(f)?;
2021 }
2022 first = false;
2023
2024 let original_leaf = issue
2025 .path
2026 .leaf_string()
2027 .unwrap_or_else(|| l10n.root_path_label().into_owned());
2028
2029 let (locs, resolved_leaf) = locations
2030 .search(&issue.path)
2031 .unwrap_or((Locations::UNKNOWN, original_leaf));
2032
2033 let ref_loc = locs.reference_location;
2034 let def_loc = locs.defined_location;
2035
2036 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
2037 let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Garde);
2038 let base_msg = l10n.validation_base_message(&entry, &resolved_path);
2039
2040 let mut rendered_regions = Vec::new();
2041
2042 match (ref_loc, def_loc) {
2043 (Location::UNKNOWN, Location::UNKNOWN) => {
2044 write!(f, "{base_msg}")?;
2045 }
2046 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
2047 let label = l10n.defined();
2048 if let Some(region) = pick_cropped_region(regions, &r) {
2049 rendered_regions.push(region as *const _);
2050 let ctx = crate::de_snippet::Snippet::new(
2051 region.text.as_str(),
2052 label.as_ref(),
2053 crop_radius,
2054 )
2055 .with_offset(region.start_line);
2056 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
2057 } else {
2058 fmt_with_location(f, l10n, &base_msg, &r)?;
2059 }
2060 }
2061 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
2062 let label = l10n.defined_here();
2063 if let Some(region) = pick_cropped_region(regions, &d) {
2064 rendered_regions.push(region as *const _);
2065 let ctx = crate::de_snippet::Snippet::new(
2066 region.text.as_str(),
2067 label.as_ref(),
2068 crop_radius,
2069 )
2070 .with_offset(region.start_line);
2071 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
2072 } else {
2073 fmt_with_location(f, l10n, &base_msg, &d)?;
2074 }
2075 }
2076 (r, d) => {
2077 let label = l10n.value_used_here();
2078 let invalid_here = l10n.invalid_here(&base_msg);
2079 if let Some(region) = pick_cropped_region(regions, &r) {
2080 rendered_regions.push(region as *const _);
2081 let ctx = crate::de_snippet::Snippet::new(
2082 region.text.as_str(),
2083 label.as_ref(),
2084 crop_radius,
2085 )
2086 .with_offset(region.start_line);
2087 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &invalid_here, &r)?;
2088 } else {
2089 fmt_with_location(f, l10n, &invalid_here, &r)?;
2090 }
2091 writeln!(f)?;
2092 writeln!(f, "{}", l10n.value_comes_from_the_anchor(d))?;
2093 if let Some(region) = pick_cropped_region(regions, &d) {
2094 rendered_regions.push(region as *const _);
2095 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2096 f,
2097 l10n,
2098 &d,
2099 region.text.as_str(),
2100 region.start_line,
2101 l10n.defined_window().as_ref(),
2102 crop_radius,
2103 )?;
2104 } else {
2105 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
2106 }
2107 }
2108 }
2109
2110 for extra_region in regions {
2111 if rendered_regions.contains(&(extra_region as *const _)) {
2112 continue;
2113 }
2114 writeln!(f)?;
2115 writeln!(f, "included from here:")?;
2116 let extra_ctx = crate::de_snippet::Snippet::new(
2117 extra_region.text.as_str(),
2118 extra_region.source_name.as_str(),
2119 crop_radius,
2120 )
2121 .with_offset(extra_region.start_line);
2122 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2123 }
2124 }
2125 Ok(())
2126}
2127
2128#[cfg(feature = "validator")]
2129fn fmt_validator_error_with_snippets_offset(
2130 f: &mut fmt::Formatter<'_>,
2131 l10n: &dyn Localizer,
2132 issues: &[ValidationIssue],
2133 locations: &PathMap,
2134 regions: &[CroppedRegion],
2135 crop_radius: usize,
2136) -> fmt::Result {
2137 let mut first = true;
2138
2139 for issue in issues {
2140 if !first {
2141 writeln!(f)?;
2142 }
2143 first = false;
2144
2145 let original_leaf = issue
2146 .path
2147 .leaf_string()
2148 .unwrap_or_else(|| l10n.root_path_label().into_owned());
2149 let (locs, resolved_leaf) = locations
2150 .search(&issue.path)
2151 .unwrap_or((Locations::UNKNOWN, original_leaf));
2152
2153 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
2154 let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Validator);
2155 let base_msg = l10n.validation_base_message(&entry, &resolved_path);
2156
2157 let mut rendered_regions = Vec::new();
2158
2159 match (locs.reference_location, locs.defined_location) {
2160 (Location::UNKNOWN, Location::UNKNOWN) => {
2161 write!(f, "{base_msg}")?;
2162 }
2163 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
2164 let label = l10n.defined();
2165 if let Some(region) = pick_cropped_region(regions, &r) {
2166 rendered_regions.push(region as *const _);
2167 let ctx = crate::de_snippet::Snippet::new(
2168 region.text.as_str(),
2169 label.as_ref(),
2170 crop_radius,
2171 )
2172 .with_offset(region.start_line);
2173 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
2174 } else {
2175 fmt_with_location(f, l10n, &base_msg, &r)?;
2176 }
2177 }
2178 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
2179 let label = l10n.defined_here();
2180 if let Some(region) = pick_cropped_region(regions, &d) {
2181 rendered_regions.push(region as *const _);
2182 let ctx = crate::de_snippet::Snippet::new(
2183 region.text.as_str(),
2184 label.as_ref(),
2185 crop_radius,
2186 )
2187 .with_offset(region.start_line);
2188 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
2189 } else {
2190 fmt_with_location(f, l10n, &base_msg, &d)?;
2191 }
2192 }
2193 (r, d) => {
2194 let label = l10n.value_used_here();
2195 let invalid_here = l10n.invalid_here(&base_msg);
2196 if let Some(region) = pick_cropped_region(regions, &r) {
2197 rendered_regions.push(region as *const _);
2198 let ctx = crate::de_snippet::Snippet::new(
2199 region.text.as_str(),
2200 label.as_ref(),
2201 crop_radius,
2202 )
2203 .with_offset(region.start_line);
2204 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &invalid_here, &r)?;
2205 } else {
2206 fmt_with_location(f, l10n, &invalid_here, &r)?;
2207 }
2208 writeln!(f)?;
2209 writeln!(f, "{}", l10n.value_comes_from_the_anchor(d))?;
2210 if let Some(region) = pick_cropped_region(regions, &d) {
2211 rendered_regions.push(region as *const _);
2212 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2213 f,
2214 l10n,
2215 &d,
2216 region.text.as_str(),
2217 region.start_line,
2218 l10n.defined_window().as_ref(),
2219 crop_radius,
2220 )?;
2221 } else {
2222 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
2223 }
2224 }
2225 }
2226
2227 for extra_region in regions {
2228 if rendered_regions.contains(&(extra_region as *const _)) {
2229 continue;
2230 }
2231 writeln!(f)?;
2232 writeln!(f, "included from here:")?;
2233 let extra_ctx = crate::de_snippet::Snippet::new(
2234 extra_region.text.as_str(),
2235 extra_region.source_name.as_str(),
2236 crop_radius,
2237 )
2238 .with_offset(extra_region.start_line);
2239 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2240 }
2241 }
2242
2243 Ok(())
2244}
2245
2246#[cfg(any(feature = "garde", feature = "validator"))]
2247fn fmt_error_with_snippets_offset(
2248 f: &mut fmt::Formatter<'_>,
2249 err: &Error,
2250 regions: &[CroppedRegion],
2251 crop_radius: usize,
2252 formatter: &dyn MessageFormatter,
2253) -> fmt::Result {
2254 if crop_radius == 0 {
2255 return fmt_error_plain_with_formatter(f, err, formatter);
2256 }
2257
2258 if let Error::WithSnippet { .. } = err {
2260 return fmt_error_rendered(f, err, RenderOptions::new(formatter));
2261 }
2262
2263 #[cfg(feature = "garde")]
2264 if let Error::ValidationError { issues, locations } = err {
2265 return fmt_validation_error_with_snippets_offset(
2266 f,
2267 formatter.localizer(),
2268 issues,
2269 locations,
2270 regions,
2271 crop_radius,
2272 );
2273 }
2274
2275 #[cfg(feature = "validator")]
2276 if let Error::ValidatorError { issues, locations } = err {
2277 return fmt_validator_error_with_snippets_offset(
2278 f,
2279 formatter.localizer(),
2280 issues,
2281 locations,
2282 regions,
2283 crop_radius,
2284 );
2285 }
2286
2287 let msg = formatter.format_message(err);
2288 let Some(location) = err.location() else {
2289 return write!(f, "{msg}");
2290 };
2291 if location == Location::UNKNOWN {
2292 return write!(f, "{msg}");
2293 }
2294
2295 let Some(region) = pick_cropped_region(regions, &location) else {
2296 return fmt_with_location(f, formatter.localizer(), msg.as_ref(), &location);
2297 };
2298 let ctx = crate::de_snippet::Snippet::new(
2299 region.text.as_str(),
2300 region.source_name.as_str(),
2301 crop_radius,
2302 )
2303 .with_offset(region.start_line);
2304 ctx.fmt_or_fallback(
2305 f,
2306 Level::ERROR,
2307 formatter.localizer(),
2308 msg.as_ref(),
2309 &location,
2310 )
2311}
2312
2313#[cfg(feature = "validator")]
2314pub(crate) fn collect_validator_issues(errors: &ValidationErrors) -> Vec<ValidationIssue> {
2315 let mut out = Vec::new();
2316 let root = PathKey::empty();
2317 collect_validator_issues_inner(errors, &root, &mut out);
2318 out
2319}
2320
2321#[cfg(feature = "validator")]
2322fn collect_validator_issues_inner(
2323 errors: &ValidationErrors,
2324 path: &PathKey,
2325 out: &mut Vec<ValidationIssue>,
2326) {
2327 for (field, kind) in errors.errors() {
2328 let field_path = path.clone().join(field.as_ref());
2329 match kind {
2330 ValidationErrorsKind::Field(entries) => {
2331 for entry in entries {
2332 let mut params = Vec::new();
2333 for (k, v) in &entry.params {
2334 params.push((k.to_string(), v.to_string()));
2335 }
2336
2337 out.push(ValidationIssue {
2338 path: field_path.clone(),
2339 code: entry.code.to_string(),
2340 message: entry.message.as_ref().map(|m| m.to_string()),
2341 params,
2342 });
2343 }
2344 }
2345 ValidationErrorsKind::Struct(inner) => {
2346 collect_validator_issues_inner(inner, &field_path, out);
2347 }
2348 ValidationErrorsKind::List(list) => {
2349 for (idx, inner) in list {
2350 let index_path = field_path.clone().join(*idx);
2351 collect_validator_issues_inner(inner, &index_path, out);
2352 }
2353 }
2354 }
2355 }
2356}
2357
2358#[cfg(feature = "garde")]
2359pub(crate) fn collect_garde_issues(report: &garde::Report) -> Vec<ValidationIssue> {
2360 let mut out = Vec::new();
2361 for (path, entry) in report.iter() {
2362 out.push(ValidationIssue {
2363 path: path_key_from_garde(path),
2364 code: "garde".to_string(),
2365 message: Some(entry.message().to_string()),
2366 params: Vec::new(),
2367 });
2368 }
2369 out
2370}
2371impl std::error::Error for Error {}
2372
2373#[cold]
2375#[inline(never)]
2376fn maybe_attach_fallback_location(mut err: Error) -> Error {
2377 let loc = MISSING_FIELD_FALLBACK.with(|c| c.get());
2378 if let Some(loc) = loc
2379 && loc != Location::UNKNOWN
2380 {
2381 err = err.with_location(loc);
2382 }
2383 err
2384}
2385
2386impl de::Error for Error {
2387 #[cold]
2388 #[inline(never)]
2389 fn custom<T: fmt::Display>(msg: T) -> Self {
2390 Error::msg(redact_custom_message(msg.to_string()))
2394 }
2395
2396 #[cold]
2397 #[inline(never)]
2398 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2399 maybe_attach_fallback_location(Error::SerdeInvalidType {
2401 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2402 expected: exp.to_string(),
2403 location: Location::UNKNOWN,
2404 })
2405 }
2406
2407 #[cold]
2408 #[inline(never)]
2409 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2410 maybe_attach_fallback_location(Error::SerdeInvalidValue {
2411 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2412 expected: exp.to_string(),
2413 location: Location::UNKNOWN,
2414 })
2415 }
2416
2417 #[cold]
2418 #[inline(never)]
2419 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
2420 maybe_attach_fallback_location(Error::SerdeUnknownVariant {
2421 variant: redact_dynamic_identifier(variant, "an interpolated variant"),
2422 expected: expected.to_vec(),
2423 location: Location::UNKNOWN,
2424 })
2425 }
2426
2427 #[cold]
2428 #[inline(never)]
2429 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
2430 maybe_attach_fallback_location(Error::SerdeUnknownField {
2431 field: redact_dynamic_identifier(field, "an interpolated field"),
2432 expected: expected.to_vec(),
2433 location: Location::UNKNOWN,
2434 })
2435 }
2436
2437 #[cold]
2438 #[inline(never)]
2439 fn missing_field(field: &'static str) -> Self {
2440 maybe_attach_fallback_location(Error::SerdeMissingField {
2441 field,
2442 location: Location::UNKNOWN,
2443 })
2444 }
2445}
2446
2447#[cold]
2457#[inline(never)]
2458fn fmt_with_location(
2459 f: &mut fmt::Formatter<'_>,
2460 l10n: &dyn Localizer,
2461 msg: &str,
2462 location: &Location,
2463) -> fmt::Result {
2464 let out = l10n.attach_location(Cow::Borrowed(msg), *location);
2465 write!(f, "{out}")
2466}
2467
2468#[cold]
2479#[inline(never)]
2480pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
2481 Error::Budget {
2482 breach,
2483 location: Location::UNKNOWN,
2484 }
2485}
2486
2487#[cfg(test)]
2488mod tests {
2489 use super::*;
2490
2491 #[test]
2492 fn sanitize_snippet_source_name_replaces_control_chars() {
2493 let sanitized = sanitize_snippet_source_name("evil.yaml\nINJECTED:\u{001b}[31m");
2494 assert_eq!(sanitized, "evil.yaml INJECTED: [31m");
2495 }
2496
2497 #[test]
2498 fn with_snippet_named_sanitizes_source_name() {
2499 let err = Error::Message {
2500 msg: "oops".to_owned(),
2501 location: Location::new(1, 1),
2502 }
2503 .with_snippet_named("x: y\n", "evil.yaml\nINJECTED", 2);
2504
2505 let Error::WithSnippet { regions, .. } = err else {
2506 panic!("expected Error::WithSnippet");
2507 };
2508
2509 assert_eq!(regions.len(), 1);
2510 assert_eq!(regions[0].source_name, "evil.yaml INJECTED");
2511 }
2512
2513 #[test]
2514 fn locations_for_basic_error_duplicates_location() {
2515 let l = Location::new(3, 7);
2516 let err = Error::Message {
2517 msg: "x".to_owned(),
2518 location: l,
2519 };
2520 assert_eq!(
2521 err.locations(),
2522 Some(Locations {
2523 reference_location: l,
2524 defined_location: l,
2525 })
2526 );
2527 }
2528
2529 #[test]
2530 fn locations_for_io_error_is_unknown() {
2531 let err = Error::IOError {
2532 cause: std::io::Error::other("x"),
2533 };
2534 assert_eq!(err.locations(), None);
2535 }
2536
2537 #[test]
2538 fn alias_error_returns_both_locations() {
2539 let ref_loc = Location::new(5, 10);
2540 let def_loc = Location::new(2, 3);
2541 let err = Error::AliasError {
2542 msg: "test error".to_owned(),
2543 locations: Locations {
2544 reference_location: ref_loc,
2545 defined_location: def_loc,
2546 },
2547 };
2548
2549 assert_eq!(err.location(), Some(ref_loc));
2551
2552 assert_eq!(
2554 err.locations(),
2555 Some(Locations {
2556 reference_location: ref_loc,
2557 defined_location: def_loc,
2558 })
2559 );
2560 }
2561
2562 #[test]
2563 fn alias_error_display_shows_both_locations() {
2564 let ref_loc = Location::new(5, 10);
2565 let def_loc = Location::new(2, 3);
2566 let err = Error::AliasError {
2567 msg: "invalid value".to_owned(),
2568 locations: Locations {
2569 reference_location: ref_loc,
2570 defined_location: def_loc,
2571 },
2572 };
2573
2574 let display = err.to_string();
2575 assert!(display.contains("invalid value"));
2576 assert!(display.contains("line 5"));
2577 assert!(display.contains("column 10"));
2578 assert!(display.contains("line 2"));
2579 assert!(display.contains("column 3"));
2580 }
2581
2582 #[test]
2583 fn alias_error_display_with_same_locations() {
2584 let loc = Location::new(3, 7);
2585 let err = Error::AliasError {
2586 msg: "test".to_owned(),
2587 locations: Locations {
2588 reference_location: loc,
2589 defined_location: loc,
2590 },
2591 };
2592
2593 let display = err.to_string();
2594 assert!(display.contains("line 3"));
2596 assert!(display.contains("column 7"));
2597 assert!(!display.contains("defined at"));
2599 }
2600
2601 #[test]
2602 fn with_snippet_counts_trailing_empty_line_for_end_line() {
2603 let text = "a\n";
2605 let err = Error::Message {
2606 msg: "x".to_owned(),
2607 location: Location::new(2, 1),
2608 };
2609
2610 let wrapped = err.with_snippet(text, 50);
2611 let Error::WithSnippet { regions, .. } = wrapped else {
2612 panic!("expected WithSnippet wrapper");
2613 };
2614 assert_eq!(regions.len(), 1);
2615 assert_eq!(regions[0].start_line, 1);
2616 assert_eq!(regions[0].end_line, 2);
2617 }
2618
2619 #[test]
2620 fn with_snippet_offset_counts_trailing_empty_line_for_end_line() {
2621 let text = "a\n";
2623 let err = Error::Message {
2624 msg: "x".to_owned(),
2625 location: Location::new(11, 1),
2626 };
2627
2628 let wrapped = err.with_snippet_offset_named(text, 10, "<input>", 50);
2629 let Error::WithSnippet { regions, .. } = wrapped else {
2630 panic!("expected WithSnippet wrapper");
2631 };
2632 assert_eq!(regions.len(), 1);
2633 assert_eq!(regions[0].start_line, 10);
2634 assert_eq!(regions[0].end_line, 11);
2635 }
2636
2637 #[cfg(feature = "validator")]
2638 #[test]
2639 fn locations_for_validator_error_uses_first_entry() {
2640 use validator::Validate;
2641
2642 #[derive(Debug, Validate)]
2643 struct Cfg {
2644 #[validate(length(min = 2))]
2645 second_string: String,
2646 }
2647
2648 let cfg = Cfg {
2649 second_string: "x".to_owned(),
2650 };
2651 let errors = cfg.validate().expect_err("validation error expected");
2652
2653 let referenced_loc = Location::new(3, 15);
2654 let defined_loc = Location::new(2, 18);
2655
2656 let mut locations = PathMap::new();
2657 locations.insert(
2658 PathKey::empty().join("secondString"),
2659 Locations {
2660 reference_location: referenced_loc,
2661 defined_location: defined_loc,
2662 },
2663 );
2664
2665 let err = Error::ValidatorError {
2666 issues: crate::de_error::collect_validator_issues(&errors),
2667 locations,
2668 };
2669 assert_eq!(
2670 err.locations(),
2671 Some(Locations {
2672 reference_location: referenced_loc,
2673 defined_location: defined_loc,
2674 })
2675 );
2676 }
2677
2678 #[test]
2679 fn nested_snippet_preserves_custom_formatter() {
2680 struct Custom;
2681 impl MessageFormatter for Custom {
2682 fn localizer(&self) -> &dyn Localizer {
2683 &DEFAULT_ENGLISH_LOCALIZER
2684 }
2685 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
2686 match err {
2687 Error::Message { msg, .. } => Cow::Owned(format!("CUSTOM: {}", msg.as_str())),
2688 _ => Cow::Borrowed(""),
2689 }
2690 }
2691 }
2692 let loc = Location::new(1, 1);
2693 let base = Error::Message {
2694 msg: "original".to_string(),
2695 location: loc,
2696 };
2697 let text = "input";
2698 let start_line = 1;
2699 let radius = 1;
2700 let inner = base.with_snippet_offset_named(text, start_line, "<input>", radius);
2701 let outer = inner.with_snippet_offset_named(text, start_line, "<input>", radius);
2702 let rendered = outer.render_with_options(RenderOptions::new(&Custom));
2703 assert!(rendered.contains("CUSTOM: original"));
2704 }
2705
2706 #[test]
2707 fn alias_error_dual_snippet_rendering() {
2708 let yaml = r#"config:
2710 anchor: &myval 42
2711 other: stuff
2712 more: data
2713 use_it: *myval
2714"#;
2715 let ref_loc = Location::new(5, 11);
2717 let def_loc = Location::new(2, 11);
2719
2720 let err = Error::AliasError {
2721 msg: "invalid value type".to_owned(),
2722 locations: Locations {
2723 reference_location: ref_loc,
2724 defined_location: def_loc,
2725 },
2726 };
2727
2728 let wrapped = err.with_snippet(yaml, 5);
2730 let rendered = wrapped.render();
2731
2732 assert!(
2734 rendered.contains("invalid value type"),
2735 "rendered: {}",
2736 rendered
2737 );
2738
2739 assert!(
2742 !rendered.contains("(defined at line"),
2743 "did not expect alias defined-at suffix when secondary window is present: {}",
2744 rendered
2745 );
2746 assert!(
2748 rendered.contains("the value is used here") || rendered.contains("use_it"),
2749 "rendered should show reference location context: {}",
2750 rendered
2751 );
2752 assert!(
2754 rendered.contains("defined here") || rendered.contains("anchor"),
2755 "rendered should show defined location context: {}",
2756 rendered
2757 );
2758 assert!(
2760 rendered.contains("5") || rendered.contains("use_it"),
2761 "rendered should reference line 5: {}",
2762 rendered
2763 );
2764 assert!(
2765 rendered.contains("2") || rendered.contains("anchor"),
2766 "rendered should reference line 2: {}",
2767 rendered
2768 );
2769 }
2770
2771 #[test]
2772 fn alias_error_same_location_single_snippet() {
2773 let yaml = "value: &anchor 42\n";
2774 let loc = Location::new(1, 8);
2775
2776 let err = Error::AliasError {
2777 msg: "test error".to_owned(),
2778 locations: Locations {
2779 reference_location: loc,
2780 defined_location: loc,
2781 },
2782 };
2783
2784 let wrapped = err.with_snippet(yaml, 5);
2785 let rendered = wrapped.render();
2786
2787 assert!(rendered.contains("test error"), "rendered: {}", rendered);
2789 assert!(
2791 !rendered.contains("defined here"),
2792 "should not show 'defined here' when locations are same: {}",
2793 rendered
2794 );
2795 assert!(
2796 !rendered.contains("the value is used here"),
2797 "should not show 'value used here' when locations are same: {}",
2798 rendered
2799 );
2800 }
2801}