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 MergeKeyNotAllowed {
484 location: Location,
485 },
486
487 InvalidBinaryBase64 {
489 location: Location,
490 },
491
492 BinaryNotUtf8 {
494 location: Location,
495 },
496
497 TaggedScalarCannotDeserializeIntoString {
499 location: Location,
500 },
501
502 UnexpectedSequenceEnd {
504 location: Location,
505 },
506
507 UnexpectedMappingEnd {
509 location: Location,
510 },
511
512 InvalidBooleanStrict {
514 location: Location,
515 },
516
517 InvalidCharNull {
519 location: Location,
520 },
521
522 InvalidCharNotSingleScalar {
524 location: Location,
525 },
526
527 NullIntoString {
529 location: Location,
530 },
531
532 BytesNotSupportedMissingBinaryTag {
534 location: Location,
535 },
536
537 UnexpectedValueForUnit {
539 location: Location,
540 },
541
542 ExpectedEmptyMappingForUnitStruct {
544 location: Location,
545 },
546
547 UnexpectedContainerEndWhileSkippingNode {
549 location: Location,
550 },
551
552 InternalSeedReusedForMapKey {
554 location: Location,
555 },
556
557 ValueRequestedBeforeKey {
559 location: Location,
560 },
561
562 ExpectedStringKeyForExternallyTaggedEnum {
564 location: Location,
565 },
566
567 ExternallyTaggedEnumExpectedScalarOrMapping {
569 location: Location,
570 },
571
572 UnexpectedValueForUnitEnumVariant {
574 location: Location,
575 },
576
577 InvalidUtf8Input,
579
580 AliasReplayCounterOverflow {
582 location: Location,
583 },
584
585 AliasReplayLimitExceeded {
587 total_replayed_events: usize,
588 max_total_replayed_events: usize,
589 location: Location,
590 },
591
592 AliasExpansionLimitExceeded {
594 anchor_id: usize,
595 expansions: usize,
596 max_expansions_per_anchor: usize,
597 location: Location,
598 },
599
600 AliasReplayStackDepthExceeded {
602 depth: usize,
603 max_depth: usize,
604 location: Location,
605 },
606
607 FoldedBlockScalarMustIndentContent {
609 location: Location,
610 },
611
612 InternalDepthUnderflow {
614 location: Location,
615 },
616
617 InternalRecursionStackEmpty {
619 location: Location,
620 },
621
622 RecursiveReferencesRequireWeakTypes {
624 location: Location,
625 },
626
627 InvalidScalar {
629 ty: &'static str,
630 location: Location,
631 },
632
633 SerdeInvalidType {
635 unexpected: String,
636 expected: String,
637 location: Location,
638 },
639
640 SerdeInvalidValue {
642 unexpected: String,
643 expected: String,
644 location: Location,
645 },
646
647 SerdeUnknownVariant {
649 variant: String,
650 expected: Vec<&'static str>,
651 location: Location,
652 },
653
654 SerdeUnknownField {
656 field: String,
657 expected: Vec<&'static str>,
658 location: Location,
659 },
660
661 SerdeMissingField {
663 field: &'static str,
664 location: Location,
665 },
666
667 UnexpectedContainerEndWhileReadingKeyNode {
671 location: Location,
672 },
673
674 DuplicateMappingKey {
678 key: Option<String>,
679 location: Location,
680 },
681
682 TaggedEnumMismatch {
684 tagged: String,
685 target: &'static str,
686 location: Location,
687 },
688
689 SerdeVariantId {
691 msg: String,
692 location: Location,
693 },
694
695 ExpectedMappingEndAfterEnumVariantValue {
697 location: Location,
698 },
699 ContainerEndMismatch {
700 location: Location,
701 },
702 UnknownAnchor {
704 location: Location,
705 },
706 CyclicInclude {
708 id: String,
709 stack: Vec<String>,
710 location: Location,
711 },
712 UnsupportedIncludeForm {
714 location: Location,
715 },
716 ResolverError {
718 target: String,
719 error: IncludeResolveError,
720 stack: Vec<String>,
721 location: Location,
722 },
723 AliasError {
728 msg: String,
729 locations: Locations,
730 },
731 HookError {
734 msg: String,
735 location: Location,
736 },
737 UnresolvedProperty {
739 name: String,
741 location: Location,
742 },
743 InvalidPropertyName {
745 name: String,
747 location: Location,
748 },
749 Budget {
751 breach: BudgetBreach,
752 location: Location,
753 },
754 IOError {
756 cause: std::io::Error,
757 },
758 QuotingRequired {
761 value: String, location: Location,
763 },
764
765 CannotBorrowTransformedString {
771 reason: TransformReason,
773 location: Location,
774 },
775
776 IndentationError {
778 required: crate::indentation::RequireIndent,
780 actual: usize,
782 location: Location,
783 },
784
785 WithSnippet {
787 regions: Vec<CroppedRegion>,
792 crop_radius: usize,
793 error: Box<Error>,
794 },
795
796 #[cfg(feature = "garde")]
798 ValidationError {
799 issues: Vec<ValidationIssue>,
800 locations: PathMap,
801 },
802
803 #[cfg(feature = "garde")]
805 ValidationErrors {
806 errors: Vec<Error>,
807 },
808
809 #[cfg(feature = "validator")]
811 ValidatorError {
812 issues: Vec<ValidationIssue>,
813 locations: PathMap,
814 },
815
816 #[cfg(feature = "validator")]
818 ValidatorErrors {
819 errors: Vec<Error>,
820 },
821}
822
823impl Error {
824 #[cold]
825 #[inline(never)]
826 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
827 self.with_snippet_named(text, "<input>", crop_radius)
828 }
829
830 #[cold]
831 #[inline(never)]
832 pub(crate) fn with_snippet_named(
833 self,
834 text: &str,
835 source_name: &str,
836 crop_radius: usize,
837 ) -> Self {
838 let source_name = sanitize_snippet_source_name(source_name);
839
840 let inner = match self {
843 Error::WithSnippet { error, .. } => *error,
844 other => other,
845 };
846
847 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
849
850 fn push_region_for_location(
851 regions: &mut Vec<CroppedRegion>,
852 text: &str,
853 source_name: &str,
854 location: &Location,
855 mapping: crate::de_snippet::LineMapping,
856 crop_radius: usize,
857 ) {
858 if crop_radius == 0 || *location == Location::UNKNOWN {
859 return;
860 }
861 let (cropped, start_line) =
862 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
863 if cropped.is_empty() {
864 return;
865 }
866 let lines = line_count_including_trailing_empty_line(cropped.as_str());
867 let end_line = start_line.saturating_add(lines.saturating_sub(1));
868 regions.push(CroppedRegion {
869 text: cropped,
870 source_name: source_name.to_string(),
871 start_line,
872 end_line,
873 location: *location,
874 });
875 }
876
877 let mut regions: Vec<CroppedRegion> = Vec::new();
878 let mapping = crate::de_snippet::LineMapping::Identity;
879
880 #[cfg(feature = "garde")]
883 if let Error::ValidationError { issues, locations } = &inner {
884 for issue in issues {
885 let (locs, _) = locations
886 .search(&issue.path)
887 .unwrap_or((Locations::UNKNOWN, String::new()));
888 push_region_for_location(
889 &mut regions,
890 text,
891 source_name.as_ref(),
892 &locs.reference_location,
893 mapping,
894 crop_radius,
895 );
896 if locs.defined_location != locs.reference_location {
897 push_region_for_location(
898 &mut regions,
899 text,
900 source_name.as_ref(),
901 &locs.defined_location,
902 mapping,
903 crop_radius,
904 );
905 }
906 }
907 }
908 #[cfg(feature = "validator")]
909 if let Error::ValidatorError { issues, locations } = &inner {
910 for issue in issues {
911 let (locs, _) = locations
912 .search(&issue.path)
913 .unwrap_or((Locations::UNKNOWN, String::new()));
914 push_region_for_location(
915 &mut regions,
916 text,
917 source_name.as_ref(),
918 &locs.reference_location,
919 mapping,
920 crop_radius,
921 );
922 if locs.defined_location != locs.reference_location {
923 push_region_for_location(
924 &mut regions,
925 text,
926 source_name.as_ref(),
927 &locs.defined_location,
928 mapping,
929 crop_radius,
930 );
931 }
932 }
933 }
934
935 if regions.is_empty() {
938 if let Some(locs) = inner.locations() {
939 push_region_for_location(
940 &mut regions,
941 text,
942 source_name.as_ref(),
943 &locs.reference_location,
944 mapping,
945 crop_radius,
946 );
947 if locs.defined_location != locs.reference_location {
948 push_region_for_location(
949 &mut regions,
950 text,
951 source_name.as_ref(),
952 &locs.defined_location,
953 mapping,
954 crop_radius,
955 );
956 }
957 } else if let Some(loc) = inner.location() {
958 push_region_for_location(
959 &mut regions,
960 text,
961 source_name.as_ref(),
962 &loc,
963 mapping,
964 crop_radius,
965 );
966 }
967 }
968
969 Error::WithSnippet {
970 regions,
971 crop_radius,
972 error: Box::new(inner),
973 }
974 }
975
976 #[cfg(feature = "include")]
977 #[cold]
978 #[inline(never)]
979 pub(crate) fn with_additional_snippet_named(
980 mut self,
981 text: &str,
982 source_name: &str,
983 location: &Location,
984 crop_radius: usize,
985 ) -> Self {
986 let source_name = sanitize_snippet_source_name(source_name);
987
988 if crop_radius == 0 || *location == Location::UNKNOWN {
989 return self;
990 }
991
992 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
993 let mapping = crate::de_snippet::LineMapping::Identity;
994
995 let (cropped, start_line) =
996 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
997 if cropped.is_empty() {
998 return self;
999 }
1000 let lines = line_count_including_trailing_empty_line(cropped.as_str());
1001 let end_line = start_line.saturating_add(lines.saturating_sub(1));
1002
1003 let region = CroppedRegion {
1004 text: cropped,
1005 source_name: source_name.into_owned(),
1006 start_line,
1007 end_line,
1008 location: *location,
1009 };
1010
1011 if let Error::WithSnippet {
1012 ref mut regions, ..
1013 } = self
1014 {
1015 regions.push(region);
1016 }
1017 self
1018 }
1019
1020 #[cfg(feature = "include")]
1021 #[cold]
1022 #[inline(never)]
1023 pub(crate) fn with_additional_snippet_offset_named(
1024 mut self,
1025 text: &str,
1026 start_line: usize,
1027 source_name: &str,
1028 location: &Location,
1029 crop_radius: usize,
1030 ) -> Self {
1031 let source_name = sanitize_snippet_source_name(source_name);
1032
1033 if crop_radius == 0 || *location == Location::UNKNOWN {
1034 return self;
1035 }
1036
1037 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1038 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1039
1040 let (cropped, start_line) =
1041 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
1042 if cropped.is_empty() {
1043 return self;
1044 }
1045 let lines = line_count_including_trailing_empty_line(cropped.as_str());
1046 let end_line = start_line.saturating_add(lines.saturating_sub(1));
1047
1048 let region = CroppedRegion {
1049 text: cropped,
1050 source_name: source_name.into_owned(),
1051 start_line,
1052 end_line,
1053 location: *location,
1054 };
1055
1056 if let Error::WithSnippet {
1057 ref mut regions, ..
1058 } = self
1059 {
1060 regions.push(region);
1061 }
1062 self
1063 }
1064
1065 #[cold]
1066 #[inline(never)]
1067 pub(crate) fn with_snippet_offset_named(
1068 self,
1069 text: &str,
1070 start_line: usize,
1071 source_name: &str,
1072 crop_radius: usize,
1073 ) -> Self {
1074 let source_name = sanitize_snippet_source_name(source_name);
1075
1076 let inner = match self {
1077 Error::WithSnippet { error, .. } => *error,
1078 other => other,
1079 };
1080
1081 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1083
1084 fn push_region_for_location(
1085 regions: &mut Vec<CroppedRegion>,
1086 text: &str,
1087 source_name: &str,
1088 location: &Location,
1089 mapping: crate::de_snippet::LineMapping,
1090 crop_radius: usize,
1091 ) {
1092 if crop_radius == 0 || *location == Location::UNKNOWN {
1093 return;
1094 }
1095 let (cropped, region_start_line) =
1096 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
1097 if cropped.is_empty() {
1098 return;
1099 }
1100 let lines = line_count_including_trailing_empty_line(cropped.as_str());
1101 let end_line = region_start_line.saturating_add(lines.saturating_sub(1));
1102 regions.push(CroppedRegion {
1103 text: cropped,
1104 source_name: source_name.to_string(),
1105 start_line: region_start_line,
1106 end_line,
1107 location: *location,
1108 });
1109 }
1110
1111 let mut regions: Vec<CroppedRegion> = Vec::new();
1112 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1113
1114 #[cfg(feature = "garde")]
1115 if let Error::ValidationError { issues, locations } = &inner {
1116 for issue in issues {
1117 let (locs, _) = locations
1118 .search(&issue.path)
1119 .unwrap_or((Locations::UNKNOWN, String::new()));
1120 push_region_for_location(
1121 &mut regions,
1122 text,
1123 source_name.as_ref(),
1124 &locs.reference_location,
1125 mapping,
1126 crop_radius,
1127 );
1128 if locs.defined_location != locs.reference_location {
1129 push_region_for_location(
1130 &mut regions,
1131 text,
1132 source_name.as_ref(),
1133 &locs.defined_location,
1134 mapping,
1135 crop_radius,
1136 );
1137 }
1138 }
1139 }
1140 #[cfg(feature = "validator")]
1141 if let Error::ValidatorError { issues, locations } = &inner {
1142 for issue in issues {
1143 let (locs, _) = locations
1144 .search(&issue.path)
1145 .unwrap_or((Locations::UNKNOWN, String::new()));
1146 push_region_for_location(
1147 &mut regions,
1148 text,
1149 source_name.as_ref(),
1150 &locs.reference_location,
1151 mapping,
1152 crop_radius,
1153 );
1154 if locs.defined_location != locs.reference_location {
1155 push_region_for_location(
1156 &mut regions,
1157 text,
1158 source_name.as_ref(),
1159 &locs.defined_location,
1160 mapping,
1161 crop_radius,
1162 );
1163 }
1164 }
1165 }
1166
1167 if regions.is_empty() {
1168 if let Some(locs) = inner.locations() {
1169 push_region_for_location(
1170 &mut regions,
1171 text,
1172 source_name.as_ref(),
1173 &locs.reference_location,
1174 mapping,
1175 crop_radius,
1176 );
1177 if locs.defined_location != locs.reference_location {
1178 push_region_for_location(
1179 &mut regions,
1180 text,
1181 source_name.as_ref(),
1182 &locs.defined_location,
1183 mapping,
1184 crop_radius,
1185 );
1186 }
1187 } else if let Some(loc) = inner.location() {
1188 push_region_for_location(
1189 &mut regions,
1190 text,
1191 source_name.as_ref(),
1192 &loc,
1193 mapping,
1194 crop_radius,
1195 );
1196 }
1197 }
1198
1199 Error::WithSnippet {
1200 regions,
1201 crop_radius,
1202 error: Box::new(inner),
1203 }
1204 }
1205
1206 pub fn without_snippet(&self) -> &Self {
1208 match self {
1209 Error::WithSnippet { error, .. } => error,
1210 other => other,
1211 }
1212 }
1213
1214 pub fn render(&self) -> String {
1220 self.render_with_options(RenderOptions::default())
1221 }
1222
1223 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
1225 self.render_with_options(RenderOptions {
1226 formatter,
1227 snippets: SnippetMode::Auto,
1228 })
1229 }
1230
1231 pub fn render_with_options(&self, options: RenderOptions<'_>) -> String {
1233 struct RenderDisplay<'a> {
1234 err: &'a Error,
1235 options: RenderOptions<'a>,
1236 }
1237
1238 impl fmt::Display for RenderDisplay<'_> {
1239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1240 fmt_error_rendered(f, self.err, self.options)
1241 }
1242 }
1243
1244 RenderDisplay { err: self, options }.to_string()
1245 }
1246
1247 #[cold]
1258 #[inline(never)]
1259 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
1260 Error::Message {
1261 msg: s.into(),
1262 location: Location::UNKNOWN,
1263 }
1264 }
1265
1266 #[cold]
1270 #[inline(never)]
1271 pub(crate) fn quoting_required(value: &str, interpolated: bool) -> Self {
1272 let location = Location::UNKNOWN;
1275 let value = if !interpolated
1276 && (parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
1277 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
1278 || parse_yaml11_bool(value).is_ok()
1279 || scalar_is_nullish(value, &ScalarStyle::Plain))
1280 {
1281 value.to_string()
1282 } else {
1283 String::new()
1284 };
1285 Error::QuotingRequired { value, location }
1286 }
1287
1288 #[cold]
1299 #[inline(never)]
1300 pub(crate) fn unexpected(what: &'static str) -> Self {
1301 Error::Unexpected {
1302 expected: what,
1303 location: Location::UNKNOWN,
1304 }
1305 }
1306
1307 #[cold]
1312 #[inline(never)]
1313 pub(crate) fn eof() -> Self {
1314 Error::Eof {
1315 location: Location::UNKNOWN,
1316 }
1317 }
1318
1319 #[cold]
1320 #[inline(never)]
1321 pub(crate) fn multiple_documents(hint: &'static str) -> Self {
1322 Error::MultipleDocuments {
1323 hint,
1324 location: Location::UNKNOWN,
1325 }
1326 }
1327
1328 #[cold]
1333 #[inline(never)]
1334 pub(crate) fn unknown_anchor() -> Self {
1335 Error::UnknownAnchor {
1336 location: Location::UNKNOWN,
1337 }
1338 }
1339
1340 #[cold]
1345 #[inline(never)]
1346 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
1347 Error::CannotBorrowTransformedString {
1348 reason,
1349 location: Location::UNKNOWN,
1350 }
1351 }
1352
1353 #[cold]
1364 #[inline(never)]
1365 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
1366 match &mut self {
1367 Error::Message { location, .. }
1368 | Error::ExternalMessage { location, .. }
1369 | Error::Eof { location }
1370 | Error::MultipleDocuments { location, .. }
1371 | Error::Unexpected { location, .. }
1372 | Error::MergeValueNotMapOrSeqOfMaps { location }
1373 | Error::MergeKeyNotAllowed { location }
1374 | Error::InvalidBinaryBase64 { location }
1375 | Error::BinaryNotUtf8 { location }
1376 | Error::TaggedScalarCannotDeserializeIntoString { location }
1377 | Error::UnexpectedSequenceEnd { location }
1378 | Error::UnexpectedMappingEnd { location }
1379 | Error::InvalidBooleanStrict { location }
1380 | Error::InvalidCharNull { location }
1381 | Error::InvalidCharNotSingleScalar { location }
1382 | Error::NullIntoString { location }
1383 | Error::BytesNotSupportedMissingBinaryTag { location }
1384 | Error::UnexpectedValueForUnit { location }
1385 | Error::ExpectedEmptyMappingForUnitStruct { location }
1386 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1387 | Error::InternalSeedReusedForMapKey { location }
1388 | Error::ValueRequestedBeforeKey { location }
1389 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1390 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1391 | Error::UnexpectedValueForUnitEnumVariant { location }
1392 | Error::AliasReplayCounterOverflow { location }
1393 | Error::AliasReplayLimitExceeded { location, .. }
1394 | Error::AliasExpansionLimitExceeded { location, .. }
1395 | Error::AliasReplayStackDepthExceeded { location, .. }
1396 | Error::FoldedBlockScalarMustIndentContent { location }
1397 | Error::InternalDepthUnderflow { location }
1398 | Error::InternalRecursionStackEmpty { location }
1399 | Error::RecursiveReferencesRequireWeakTypes { location }
1400 | Error::InvalidScalar { location, .. }
1401 | Error::SerdeInvalidType { location, .. }
1402 | Error::SerdeInvalidValue { location, .. }
1403 | Error::SerdeUnknownVariant { location, .. }
1404 | Error::SerdeUnknownField { location, .. }
1405 | Error::SerdeMissingField { location, .. }
1406 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1407 | Error::DuplicateMappingKey { location, .. }
1408 | Error::TaggedEnumMismatch { location, .. }
1409 | Error::SerdeVariantId { location, .. }
1410 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1411 | Error::HookError { location, .. }
1412 | Error::UnresolvedProperty { location, .. }
1413 | Error::InvalidPropertyName { location, .. }
1414 | Error::ContainerEndMismatch { location, .. }
1415 | Error::UnknownAnchor { location, .. }
1416 | Error::CyclicInclude { location, .. }
1417 | Error::UnsupportedIncludeForm { location, .. }
1418 | Error::ResolverError { location, .. }
1419 | Error::QuotingRequired { location, .. }
1420 | Error::Budget { location, .. }
1421 | Error::CannotBorrowTransformedString { location, .. }
1422 | Error::IndentationError { location, .. } => {
1423 *location = set_location;
1424 }
1425 Error::InvalidUtf8Input => {}
1426 Error::IOError { .. } => {} Error::AliasError { .. } => {
1428 }
1430 Error::WithSnippet { error, .. } => {
1431 let inner = *std::mem::replace(error, Box::new(Error::eof()));
1432 **error = inner.with_location(set_location);
1433 }
1434 #[cfg(feature = "garde")]
1435 Error::ValidationError { .. } => {
1436 }
1438 #[cfg(feature = "garde")]
1439 Error::ValidationErrors { .. } => {
1440 }
1442 #[cfg(feature = "validator")]
1443 Error::ValidatorError { .. } => {
1444 }
1446 #[cfg(feature = "validator")]
1447 Error::ValidatorErrors { .. } => {
1448 }
1450 }
1451 self
1452 }
1453
1454 pub fn location(&self) -> Option<Location> {
1462 match self {
1463 Error::Message { location, .. }
1464 | Error::ExternalMessage { location, .. }
1465 | Error::Eof { location }
1466 | Error::MultipleDocuments { location, .. }
1467 | Error::Unexpected { location, .. }
1468 | Error::MergeValueNotMapOrSeqOfMaps { location }
1469 | Error::MergeKeyNotAllowed { location }
1470 | Error::InvalidBinaryBase64 { location }
1471 | Error::BinaryNotUtf8 { location }
1472 | Error::TaggedScalarCannotDeserializeIntoString { location }
1473 | Error::UnexpectedSequenceEnd { location }
1474 | Error::UnexpectedMappingEnd { location }
1475 | Error::InvalidBooleanStrict { location }
1476 | Error::InvalidCharNull { location }
1477 | Error::InvalidCharNotSingleScalar { location }
1478 | Error::NullIntoString { location }
1479 | Error::BytesNotSupportedMissingBinaryTag { location }
1480 | Error::UnexpectedValueForUnit { location }
1481 | Error::ExpectedEmptyMappingForUnitStruct { location }
1482 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1483 | Error::InternalSeedReusedForMapKey { location }
1484 | Error::ValueRequestedBeforeKey { location }
1485 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1486 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1487 | Error::UnexpectedValueForUnitEnumVariant { location }
1488 | Error::AliasReplayCounterOverflow { location }
1489 | Error::AliasReplayLimitExceeded { location, .. }
1490 | Error::AliasExpansionLimitExceeded { location, .. }
1491 | Error::AliasReplayStackDepthExceeded { location, .. }
1492 | Error::FoldedBlockScalarMustIndentContent { location }
1493 | Error::InternalDepthUnderflow { location }
1494 | Error::InternalRecursionStackEmpty { location }
1495 | Error::RecursiveReferencesRequireWeakTypes { location }
1496 | Error::InvalidScalar { location, .. }
1497 | Error::SerdeInvalidType { location, .. }
1498 | Error::SerdeInvalidValue { location, .. }
1499 | Error::SerdeUnknownVariant { location, .. }
1500 | Error::SerdeUnknownField { location, .. }
1501 | Error::SerdeMissingField { location, .. }
1502 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1503 | Error::DuplicateMappingKey { location, .. }
1504 | Error::TaggedEnumMismatch { location, .. }
1505 | Error::SerdeVariantId { location, .. }
1506 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1507 | Error::HookError { location, .. }
1508 | Error::UnresolvedProperty { location, .. }
1509 | Error::InvalidPropertyName { location, .. }
1510 | Error::ContainerEndMismatch { location, .. }
1511 | Error::UnknownAnchor { location, .. }
1512 | Error::CyclicInclude { location, .. }
1513 | Error::UnsupportedIncludeForm { location, .. }
1514 | Error::ResolverError { location, .. }
1515 | Error::QuotingRequired { location, .. }
1516 | Error::Budget { location, .. }
1517 | Error::CannotBorrowTransformedString { location, .. }
1518 | Error::IndentationError { location, .. } => {
1519 if location != &Location::UNKNOWN {
1520 Some(*location)
1521 } else {
1522 None
1523 }
1524 }
1525 Error::InvalidUtf8Input => None,
1526 Error::IOError { cause: _ } => None,
1527 Error::AliasError { locations, .. } => Locations::primary_location(*locations),
1528 Error::WithSnippet { error, .. } => error.location(),
1529 #[cfg(feature = "garde")]
1530 Error::ValidationError { issues, locations } => issues.first().and_then(|issue| {
1531 let (locs, _) = locations.search(&issue.path)?;
1532 let loc = if locs.reference_location != Location::UNKNOWN {
1533 locs.reference_location
1534 } else {
1535 locs.defined_location
1536 };
1537 if loc != Location::UNKNOWN {
1538 Some(loc)
1539 } else {
1540 None
1541 }
1542 }),
1543 #[cfg(feature = "garde")]
1544 Error::ValidationErrors { errors } => errors.iter().find_map(|e| e.location()),
1545 #[cfg(feature = "validator")]
1546 Error::ValidatorError { issues, locations } => issues.first().and_then(|issue| {
1547 let (locs, _) = locations.search(&issue.path)?;
1548 let loc = if locs.reference_location != Location::UNKNOWN {
1549 locs.reference_location
1550 } else {
1551 locs.defined_location
1552 };
1553 if loc != Location::UNKNOWN {
1554 Some(loc)
1555 } else {
1556 None
1557 }
1558 }),
1559 #[cfg(feature = "validator")]
1560 Error::ValidatorErrors { errors } => errors.iter().find_map(|e| e.location()),
1561 }
1562 }
1563 pub fn locations(&self) -> Option<Locations> {
1573 match self {
1574 Error::Message { location, .. }
1575 | Error::ExternalMessage { location, .. }
1576 | Error::Eof { location }
1577 | Error::MultipleDocuments { location, .. }
1578 | Error::Unexpected { location, .. }
1579 | Error::MergeValueNotMapOrSeqOfMaps { location }
1580 | Error::MergeKeyNotAllowed { location }
1581 | Error::InvalidBinaryBase64 { location }
1582 | Error::BinaryNotUtf8 { location }
1583 | Error::TaggedScalarCannotDeserializeIntoString { location }
1584 | Error::UnexpectedSequenceEnd { location }
1585 | Error::UnexpectedMappingEnd { location }
1586 | Error::InvalidBooleanStrict { location }
1587 | Error::InvalidCharNull { location }
1588 | Error::InvalidCharNotSingleScalar { location }
1589 | Error::NullIntoString { location }
1590 | Error::BytesNotSupportedMissingBinaryTag { location }
1591 | Error::UnexpectedValueForUnit { location }
1592 | Error::ExpectedEmptyMappingForUnitStruct { location }
1593 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1594 | Error::InternalSeedReusedForMapKey { location }
1595 | Error::ValueRequestedBeforeKey { location }
1596 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1597 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1598 | Error::UnexpectedValueForUnitEnumVariant { location }
1599 | Error::AliasReplayCounterOverflow { location }
1600 | Error::AliasReplayLimitExceeded { location, .. }
1601 | Error::AliasExpansionLimitExceeded { location, .. }
1602 | Error::AliasReplayStackDepthExceeded { location, .. }
1603 | Error::FoldedBlockScalarMustIndentContent { location }
1604 | Error::InternalDepthUnderflow { location }
1605 | Error::InternalRecursionStackEmpty { location }
1606 | Error::RecursiveReferencesRequireWeakTypes { location }
1607 | Error::InvalidScalar { location, .. }
1608 | Error::SerdeInvalidType { location, .. }
1609 | Error::SerdeInvalidValue { location, .. }
1610 | Error::SerdeUnknownVariant { location, .. }
1611 | Error::SerdeUnknownField { location, .. }
1612 | Error::SerdeMissingField { location, .. }
1613 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1614 | Error::DuplicateMappingKey { location, .. }
1615 | Error::TaggedEnumMismatch { location, .. }
1616 | Error::SerdeVariantId { location, .. }
1617 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1618 | Error::HookError { location, .. }
1619 | Error::UnresolvedProperty { location, .. }
1620 | Error::InvalidPropertyName { location, .. }
1621 | Error::ContainerEndMismatch { location, .. }
1622 | Error::UnknownAnchor { location, .. }
1623 | Error::CyclicInclude { location, .. }
1624 | Error::UnsupportedIncludeForm { location, .. }
1625 | Error::ResolverError { location, .. }
1626 | Error::QuotingRequired { location, .. }
1627 | Error::Budget { location, .. }
1628 | Error::CannotBorrowTransformedString { location, .. }
1629 | Error::IndentationError { location, .. } => Locations::same(location),
1630 Error::InvalidUtf8Input => None,
1631 Error::IOError { .. } => None,
1632 Error::AliasError { locations, .. } => Some(*locations),
1633 Error::WithSnippet { error, .. } => error.locations(),
1634 #[cfg(feature = "garde")]
1635 Error::ValidationError { issues, locations } => issues
1636 .first()
1637 .and_then(|issue| search_locations_with_ancestor_fallback(locations, &issue.path)),
1638 #[cfg(feature = "garde")]
1639 Error::ValidationErrors { errors } => errors.first().and_then(Error::locations),
1640 #[cfg(feature = "validator")]
1641 Error::ValidatorError { issues, locations } => issues
1642 .first()
1643 .and_then(|issue| locations.search(&issue.path).map(|(locs, _)| locs)),
1644 #[cfg(feature = "validator")]
1645 Error::ValidatorErrors { errors } => errors.first().and_then(Error::locations),
1646 }
1647 }
1648
1649 #[cold]
1654 #[inline(never)]
1655 pub(crate) fn from_scan_error(err: ScanError) -> Self {
1656 let mark = err.marker();
1657 let location = Location::new(mark.line(), mark.col() + 1)
1658 .with_span(crate::Span::new(mark.index() as u64, 1));
1659
1660 let info = err.info();
1668 if info.to_ascii_lowercase().contains("unknown anchor") {
1669 return Error::UnknownAnchor { location };
1670 }
1671
1672 Error::ExternalMessage {
1673 source: ExternalMessageSource::SaphyrParser,
1674 msg: info.to_owned(),
1675 code: None,
1676 params: Vec::new(),
1677 location,
1678 }
1679 }
1680}
1681
1682fn fmt_error_plain_with_formatter(
1683 f: &mut fmt::Formatter<'_>,
1684 err: &Error,
1685 formatter: &dyn MessageFormatter,
1686) -> fmt::Result {
1687 let err = err.without_snippet();
1688
1689 let msg = formatter.format_message(err);
1690
1691 #[cfg(feature = "garde")]
1695 if matches!(err, Error::ValidationError { .. }) {
1696 return write!(f, "{msg}");
1697 }
1698 #[cfg(feature = "validator")]
1699 if matches!(err, Error::ValidatorError { .. }) {
1700 return write!(f, "{msg}");
1701 }
1702
1703 if let Some(loc) = err.location() {
1704 fmt_with_location(f, formatter.localizer(), msg.as_ref(), &loc)?;
1705 } else {
1706 write!(f, "{msg}")?;
1707 }
1708
1709 #[cfg(feature = "garde")]
1710 if let Error::ValidationErrors { errors } = err {
1711 for err in errors {
1712 writeln!(f)?;
1713 writeln!(f)?;
1714 fmt_error_plain_with_formatter(f, err, formatter)?;
1715 }
1716 }
1717
1718 #[cfg(feature = "validator")]
1719 if let Error::ValidatorErrors { errors } = err {
1720 for err in errors {
1721 writeln!(f)?;
1722 writeln!(f)?;
1723 fmt_error_plain_with_formatter(f, err, formatter)?;
1724 }
1725 }
1726
1727 Ok(())
1728}
1729
1730fn pick_cropped_region<'a>(
1731 regions: &'a [CroppedRegion],
1732 location: &Location,
1733) -> Option<&'a CroppedRegion> {
1734 let source_id = location.source_id();
1735
1736 if source_id != 0 {
1737 if let Some(region) = regions.iter().find(|r| r.covers_exact_source(location)) {
1738 return Some(region);
1739 }
1740 if let Some(region) = regions.iter().find(|r| r.location.source_id() == source_id) {
1741 return Some(region);
1742 }
1743 if let Some(region) = regions
1744 .iter()
1745 .find(|r| r.location.source_id() == 0 && r.covers(location))
1746 {
1747 return Some(region);
1748 }
1749 return None;
1750 }
1751
1752 regions
1753 .iter()
1754 .find(|r| r.covers(location))
1755 .or_else(|| regions.first())
1756}
1757
1758fn fmt_error_rendered(
1759 f: &mut fmt::Formatter<'_>,
1760 err: &Error,
1761 options: RenderOptions<'_>,
1762) -> fmt::Result {
1763 if options.snippets == SnippetMode::Off {
1764 return fmt_error_plain_with_formatter(f, err, options.formatter);
1765 }
1766
1767 match err {
1768 #[cfg(feature = "garde")]
1769 Error::ValidationErrors { errors } => {
1770 let msg = options.formatter.format_message(err);
1771 if !msg.is_empty() {
1772 writeln!(f, "{}", msg)?;
1773 }
1774 let mut first = true;
1775 for err in errors {
1776 if !first {
1777 writeln!(f)?;
1778 writeln!(f)?;
1779 }
1780 first = false;
1781 fmt_error_rendered(f, err, options)?;
1782 }
1783 Ok(())
1784 }
1785
1786 #[cfg(feature = "validator")]
1787 Error::ValidatorErrors { errors } => {
1788 let msg = options.formatter.format_message(err);
1789 if !msg.is_empty() {
1790 writeln!(f, "{}", msg)?;
1791 }
1792 let mut first = true;
1793 for err in errors {
1794 if !first {
1795 writeln!(f)?;
1796 writeln!(f)?;
1797 }
1798 first = false;
1799 fmt_error_rendered(f, err, options)?;
1800 }
1801 Ok(())
1802 }
1803
1804 Error::WithSnippet {
1805 regions,
1806 crop_radius,
1807 error,
1808 } => {
1809 if *crop_radius == 0 {
1810 return fmt_error_plain_with_formatter(f, error, options.formatter);
1812 }
1813
1814 if regions.is_empty() {
1815 return fmt_error_plain_with_formatter(f, error, options.formatter);
1816 }
1817
1818 #[cfg(feature = "garde")]
1821 if let Error::ValidationError { issues, locations } = error.as_ref() {
1822 return fmt_validation_error_with_snippets_offset(
1823 f,
1824 options.formatter.localizer(),
1825 issues,
1826 locations,
1827 regions,
1828 *crop_radius,
1829 );
1830 }
1831 #[cfg(feature = "garde")]
1832 if let Error::ValidationErrors { errors } = error.as_ref() {
1833 let msg = options.formatter.format_message(error);
1834 if !msg.is_empty() {
1835 writeln!(f, "{}", msg)?;
1836 }
1837 let mut first = true;
1838 for err in errors {
1839 if !first {
1840 writeln!(f)?;
1841 writeln!(f)?;
1842 }
1843 first = false;
1844 fmt_error_with_snippets_offset(
1845 f,
1846 err,
1847 regions,
1848 *crop_radius,
1849 options.formatter,
1850 )?;
1851 }
1852 return Ok(());
1853 }
1854
1855 #[cfg(feature = "validator")]
1856 if let Error::ValidatorError { issues, locations } = error.as_ref() {
1857 return fmt_validator_error_with_snippets_offset(
1858 f,
1859 options.formatter.localizer(),
1860 issues,
1861 locations,
1862 regions,
1863 *crop_radius,
1864 );
1865 }
1866 #[cfg(feature = "validator")]
1867 if let Error::ValidatorErrors { errors } = error.as_ref() {
1868 let msg = options.formatter.format_message(error);
1869 if !msg.is_empty() {
1870 writeln!(f, "{}", msg)?;
1871 }
1872 let mut first = true;
1873 for err in errors {
1874 if !first {
1875 writeln!(f)?;
1876 writeln!(f)?;
1877 }
1878 first = false;
1879 fmt_error_with_snippets_offset(
1880 f,
1881 err,
1882 regions,
1883 *crop_radius,
1884 options.formatter,
1885 )?;
1886 }
1887 return Ok(());
1888 }
1889
1890 let Some(location) = error.location() else {
1893 return fmt_error_plain_with_formatter(f, error, options.formatter);
1894 };
1895 if location == Location::UNKNOWN {
1896 return fmt_error_plain_with_formatter(f, error, options.formatter);
1897 }
1898
1899 let l10n = options.formatter.localizer();
1900
1901 let region = match pick_cropped_region(regions, &location) {
1902 Some(r) => r,
1903 None => return fmt_error_plain_with_formatter(f, error, options.formatter),
1904 };
1905
1906 let dual_locations = error.locations().filter(|locs| {
1908 locs.reference_location != Location::UNKNOWN
1909 && locs.defined_location != Location::UNKNOWN
1910 && locs.reference_location != locs.defined_location
1911 });
1912
1913 let mut msg = options.formatter.format_message(error);
1914
1915 if dual_locations.is_some()
1919 && let Error::AliasError { locations, .. } = error.as_ref()
1920 {
1921 let suffix = l10n.alias_defined_at(locations.defined_location);
1922 if let Some(stripped) = msg.as_ref().strip_suffix(&suffix) {
1923 msg = Cow::Owned(stripped.to_string());
1924 }
1925 }
1926
1927 if let Some(locs) = dual_locations {
1928 let ref_loc = locs.reference_location;
1929 let def_loc = locs.defined_location;
1930
1931 let used_region = pick_cropped_region(regions, &ref_loc).unwrap_or(region);
1932 let label = l10n.value_used_here();
1933 let ctx = crate::de_snippet::Snippet::new(
1934 used_region.text.as_str(),
1935 label.as_ref(),
1936 *crop_radius,
1937 )
1938 .with_offset(used_region.start_line);
1939 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &ref_loc)?;
1940
1941 let def_region = pick_cropped_region(regions, &def_loc).unwrap_or(region);
1942 writeln!(f)?;
1943 writeln!(f, "{}", l10n.value_comes_from_the_anchor(def_loc))?;
1944 fmt_snippet_window_offset_or_fallback(
1945 f,
1946 l10n,
1947 &def_loc,
1948 def_region.text.as_str(),
1949 def_region.start_line,
1950 l10n.defined_window().as_ref(),
1951 *crop_radius,
1952 )?;
1953 Ok(())
1954 } else {
1955 let ctx = crate::de_snippet::Snippet::new(
1957 region.text.as_str(),
1958 region.source_name.as_str(),
1959 *crop_radius,
1960 )
1961 .with_offset(region.start_line);
1962 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &location)?;
1963
1964 for extra_region in regions {
1965 if std::ptr::eq(extra_region, region) {
1966 continue;
1967 }
1968 writeln!(f)?;
1969 writeln!(f, "included from here:")?;
1970 let extra_ctx = crate::de_snippet::Snippet::new(
1971 extra_region.text.as_str(),
1972 extra_region.source_name.as_str(),
1973 *crop_radius,
1974 )
1975 .with_offset(extra_region.start_line);
1976 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
1977 }
1978 Ok(())
1979 }
1980 }
1981 _ => fmt_error_plain_with_formatter(f, err, options.formatter),
1982 }
1983}
1984
1985#[cfg(any(feature = "garde", feature = "validator"))]
1986fn search_locations_with_ancestor_fallback(
1987 locations: &PathMap,
1988 path: &PathKey,
1989) -> Option<Locations> {
1990 if let Some((locs, _)) = locations.search(path) {
1991 return Some(locs);
1992 }
1993
1994 let mut p = path.parent();
1995 while let Some(cur) = p {
1996 if let Some((locs, _)) = locations.search(&cur) {
1997 return Some(locs);
1998 }
1999 p = cur.parent();
2000 }
2001
2002 None
2003}
2004
2005impl fmt::Display for Error {
2006 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2007 fmt_error_rendered(f, self, RenderOptions::default())
2008 }
2009}
2010
2011#[cfg(feature = "garde")]
2012fn fmt_validation_error_with_snippets_offset(
2013 f: &mut fmt::Formatter<'_>,
2014 l10n: &dyn Localizer,
2015 issues: &[ValidationIssue],
2016 locations: &PathMap,
2017 regions: &[CroppedRegion],
2018 crop_radius: usize,
2019) -> fmt::Result {
2020 let mut first = true;
2021 for issue in issues {
2022 if !first {
2023 writeln!(f)?;
2024 }
2025 first = false;
2026
2027 let original_leaf = issue
2028 .path
2029 .leaf_string()
2030 .unwrap_or_else(|| l10n.root_path_label().into_owned());
2031
2032 let (locs, resolved_leaf) = locations
2033 .search(&issue.path)
2034 .unwrap_or((Locations::UNKNOWN, original_leaf));
2035
2036 let ref_loc = locs.reference_location;
2037 let def_loc = locs.defined_location;
2038
2039 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
2040 let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Garde);
2041 let base_msg = l10n.validation_base_message(&entry, &resolved_path);
2042
2043 let mut rendered_regions = Vec::new();
2044
2045 match (ref_loc, def_loc) {
2046 (Location::UNKNOWN, Location::UNKNOWN) => {
2047 write!(f, "{base_msg}")?;
2048 }
2049 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
2050 let label = l10n.defined();
2051 if let Some(region) = pick_cropped_region(regions, &r) {
2052 rendered_regions.push(region as *const _);
2053 let ctx = crate::de_snippet::Snippet::new(
2054 region.text.as_str(),
2055 label.as_ref(),
2056 crop_radius,
2057 )
2058 .with_offset(region.start_line);
2059 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
2060 } else {
2061 fmt_with_location(f, l10n, &base_msg, &r)?;
2062 }
2063 }
2064 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
2065 let label = l10n.defined_here();
2066 if let Some(region) = pick_cropped_region(regions, &d) {
2067 rendered_regions.push(region as *const _);
2068 let ctx = crate::de_snippet::Snippet::new(
2069 region.text.as_str(),
2070 label.as_ref(),
2071 crop_radius,
2072 )
2073 .with_offset(region.start_line);
2074 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
2075 } else {
2076 fmt_with_location(f, l10n, &base_msg, &d)?;
2077 }
2078 }
2079 (r, d) => {
2080 let label = l10n.value_used_here();
2081 let invalid_here = l10n.invalid_here(&base_msg);
2082 if let Some(region) = pick_cropped_region(regions, &r) {
2083 rendered_regions.push(region as *const _);
2084 let ctx = crate::de_snippet::Snippet::new(
2085 region.text.as_str(),
2086 label.as_ref(),
2087 crop_radius,
2088 )
2089 .with_offset(region.start_line);
2090 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &invalid_here, &r)?;
2091 } else {
2092 fmt_with_location(f, l10n, &invalid_here, &r)?;
2093 }
2094 writeln!(f)?;
2095 writeln!(f, "{}", l10n.value_comes_from_the_anchor(d))?;
2096 if let Some(region) = pick_cropped_region(regions, &d) {
2097 rendered_regions.push(region as *const _);
2098 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2099 f,
2100 l10n,
2101 &d,
2102 region.text.as_str(),
2103 region.start_line,
2104 l10n.defined_window().as_ref(),
2105 crop_radius,
2106 )?;
2107 } else {
2108 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
2109 }
2110 }
2111 }
2112
2113 for extra_region in regions {
2114 if rendered_regions.contains(&(extra_region as *const _)) {
2115 continue;
2116 }
2117 writeln!(f)?;
2118 writeln!(f, "included from here:")?;
2119 let extra_ctx = crate::de_snippet::Snippet::new(
2120 extra_region.text.as_str(),
2121 extra_region.source_name.as_str(),
2122 crop_radius,
2123 )
2124 .with_offset(extra_region.start_line);
2125 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2126 }
2127 }
2128 Ok(())
2129}
2130
2131#[cfg(feature = "validator")]
2132fn fmt_validator_error_with_snippets_offset(
2133 f: &mut fmt::Formatter<'_>,
2134 l10n: &dyn Localizer,
2135 issues: &[ValidationIssue],
2136 locations: &PathMap,
2137 regions: &[CroppedRegion],
2138 crop_radius: usize,
2139) -> fmt::Result {
2140 let mut first = true;
2141
2142 for issue in issues {
2143 if !first {
2144 writeln!(f)?;
2145 }
2146 first = false;
2147
2148 let original_leaf = issue
2149 .path
2150 .leaf_string()
2151 .unwrap_or_else(|| l10n.root_path_label().into_owned());
2152 let (locs, resolved_leaf) = locations
2153 .search(&issue.path)
2154 .unwrap_or((Locations::UNKNOWN, original_leaf));
2155
2156 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
2157 let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Validator);
2158 let base_msg = l10n.validation_base_message(&entry, &resolved_path);
2159
2160 let mut rendered_regions = Vec::new();
2161
2162 match (locs.reference_location, locs.defined_location) {
2163 (Location::UNKNOWN, Location::UNKNOWN) => {
2164 write!(f, "{base_msg}")?;
2165 }
2166 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
2167 let label = l10n.defined();
2168 if let Some(region) = pick_cropped_region(regions, &r) {
2169 rendered_regions.push(region as *const _);
2170 let ctx = crate::de_snippet::Snippet::new(
2171 region.text.as_str(),
2172 label.as_ref(),
2173 crop_radius,
2174 )
2175 .with_offset(region.start_line);
2176 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
2177 } else {
2178 fmt_with_location(f, l10n, &base_msg, &r)?;
2179 }
2180 }
2181 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
2182 let label = l10n.defined_here();
2183 if let Some(region) = pick_cropped_region(regions, &d) {
2184 rendered_regions.push(region as *const _);
2185 let ctx = crate::de_snippet::Snippet::new(
2186 region.text.as_str(),
2187 label.as_ref(),
2188 crop_radius,
2189 )
2190 .with_offset(region.start_line);
2191 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
2192 } else {
2193 fmt_with_location(f, l10n, &base_msg, &d)?;
2194 }
2195 }
2196 (r, d) => {
2197 let label = l10n.value_used_here();
2198 let invalid_here = l10n.invalid_here(&base_msg);
2199 if let Some(region) = pick_cropped_region(regions, &r) {
2200 rendered_regions.push(region as *const _);
2201 let ctx = crate::de_snippet::Snippet::new(
2202 region.text.as_str(),
2203 label.as_ref(),
2204 crop_radius,
2205 )
2206 .with_offset(region.start_line);
2207 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &invalid_here, &r)?;
2208 } else {
2209 fmt_with_location(f, l10n, &invalid_here, &r)?;
2210 }
2211 writeln!(f)?;
2212 writeln!(f, "{}", l10n.value_comes_from_the_anchor(d))?;
2213 if let Some(region) = pick_cropped_region(regions, &d) {
2214 rendered_regions.push(region as *const _);
2215 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2216 f,
2217 l10n,
2218 &d,
2219 region.text.as_str(),
2220 region.start_line,
2221 l10n.defined_window().as_ref(),
2222 crop_radius,
2223 )?;
2224 } else {
2225 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
2226 }
2227 }
2228 }
2229
2230 for extra_region in regions {
2231 if rendered_regions.contains(&(extra_region as *const _)) {
2232 continue;
2233 }
2234 writeln!(f)?;
2235 writeln!(f, "included from here:")?;
2236 let extra_ctx = crate::de_snippet::Snippet::new(
2237 extra_region.text.as_str(),
2238 extra_region.source_name.as_str(),
2239 crop_radius,
2240 )
2241 .with_offset(extra_region.start_line);
2242 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2243 }
2244 }
2245
2246 Ok(())
2247}
2248
2249#[cfg(any(feature = "garde", feature = "validator"))]
2250fn fmt_error_with_snippets_offset(
2251 f: &mut fmt::Formatter<'_>,
2252 err: &Error,
2253 regions: &[CroppedRegion],
2254 crop_radius: usize,
2255 formatter: &dyn MessageFormatter,
2256) -> fmt::Result {
2257 if crop_radius == 0 {
2258 return fmt_error_plain_with_formatter(f, err, formatter);
2259 }
2260
2261 if let Error::WithSnippet { .. } = err {
2263 return fmt_error_rendered(f, err, RenderOptions::new(formatter));
2264 }
2265
2266 #[cfg(feature = "garde")]
2267 if let Error::ValidationError { issues, locations } = err {
2268 return fmt_validation_error_with_snippets_offset(
2269 f,
2270 formatter.localizer(),
2271 issues,
2272 locations,
2273 regions,
2274 crop_radius,
2275 );
2276 }
2277
2278 #[cfg(feature = "validator")]
2279 if let Error::ValidatorError { issues, locations } = err {
2280 return fmt_validator_error_with_snippets_offset(
2281 f,
2282 formatter.localizer(),
2283 issues,
2284 locations,
2285 regions,
2286 crop_radius,
2287 );
2288 }
2289
2290 let msg = formatter.format_message(err);
2291 let Some(location) = err.location() else {
2292 return write!(f, "{msg}");
2293 };
2294 if location == Location::UNKNOWN {
2295 return write!(f, "{msg}");
2296 }
2297
2298 let Some(region) = pick_cropped_region(regions, &location) else {
2299 return fmt_with_location(f, formatter.localizer(), msg.as_ref(), &location);
2300 };
2301 let ctx = crate::de_snippet::Snippet::new(
2302 region.text.as_str(),
2303 region.source_name.as_str(),
2304 crop_radius,
2305 )
2306 .with_offset(region.start_line);
2307 ctx.fmt_or_fallback(
2308 f,
2309 Level::ERROR,
2310 formatter.localizer(),
2311 msg.as_ref(),
2312 &location,
2313 )
2314}
2315
2316#[cfg(feature = "validator")]
2317pub(crate) fn collect_validator_issues(errors: &ValidationErrors) -> Vec<ValidationIssue> {
2318 let mut out = Vec::new();
2319 let root = PathKey::empty();
2320 collect_validator_issues_inner(errors, &root, &mut out);
2321 out
2322}
2323
2324#[cfg(feature = "validator")]
2325fn collect_validator_issues_inner(
2326 errors: &ValidationErrors,
2327 path: &PathKey,
2328 out: &mut Vec<ValidationIssue>,
2329) {
2330 for (field, kind) in errors.errors() {
2331 let field_path = path.clone().join(field.as_ref());
2332 match kind {
2333 ValidationErrorsKind::Field(entries) => {
2334 for entry in entries {
2335 let mut params = Vec::new();
2336 for (k, v) in &entry.params {
2337 params.push((k.to_string(), v.to_string()));
2338 }
2339
2340 out.push(ValidationIssue {
2341 path: field_path.clone(),
2342 code: entry.code.to_string(),
2343 message: entry.message.as_ref().map(|m| m.to_string()),
2344 params,
2345 });
2346 }
2347 }
2348 ValidationErrorsKind::Struct(inner) => {
2349 collect_validator_issues_inner(inner, &field_path, out);
2350 }
2351 ValidationErrorsKind::List(list) => {
2352 for (idx, inner) in list {
2353 let index_path = field_path.clone().join(*idx);
2354 collect_validator_issues_inner(inner, &index_path, out);
2355 }
2356 }
2357 }
2358 }
2359}
2360
2361#[cfg(feature = "garde")]
2362pub(crate) fn collect_garde_issues(report: &garde::Report) -> Vec<ValidationIssue> {
2363 let mut out = Vec::new();
2364 for (path, entry) in report.iter() {
2365 out.push(ValidationIssue {
2366 path: path_key_from_garde(path),
2367 code: "garde".to_string(),
2368 message: Some(entry.message().to_string()),
2369 params: Vec::new(),
2370 });
2371 }
2372 out
2373}
2374impl std::error::Error for Error {}
2375
2376#[cold]
2378#[inline(never)]
2379fn maybe_attach_fallback_location(mut err: Error) -> Error {
2380 let loc = MISSING_FIELD_FALLBACK.with(|c| c.get());
2381 if let Some(loc) = loc
2382 && loc != Location::UNKNOWN
2383 {
2384 err = err.with_location(loc);
2385 }
2386 err
2387}
2388
2389impl de::Error for Error {
2390 #[cold]
2391 #[inline(never)]
2392 fn custom<T: fmt::Display>(msg: T) -> Self {
2393 Error::msg(redact_custom_message(msg.to_string()))
2397 }
2398
2399 #[cold]
2400 #[inline(never)]
2401 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2402 maybe_attach_fallback_location(Error::SerdeInvalidType {
2404 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2405 expected: exp.to_string(),
2406 location: Location::UNKNOWN,
2407 })
2408 }
2409
2410 #[cold]
2411 #[inline(never)]
2412 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2413 maybe_attach_fallback_location(Error::SerdeInvalidValue {
2414 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2415 expected: exp.to_string(),
2416 location: Location::UNKNOWN,
2417 })
2418 }
2419
2420 #[cold]
2421 #[inline(never)]
2422 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
2423 maybe_attach_fallback_location(Error::SerdeUnknownVariant {
2424 variant: redact_dynamic_identifier(variant, "an interpolated variant"),
2425 expected: expected.to_vec(),
2426 location: Location::UNKNOWN,
2427 })
2428 }
2429
2430 #[cold]
2431 #[inline(never)]
2432 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
2433 maybe_attach_fallback_location(Error::SerdeUnknownField {
2434 field: redact_dynamic_identifier(field, "an interpolated field"),
2435 expected: expected.to_vec(),
2436 location: Location::UNKNOWN,
2437 })
2438 }
2439
2440 #[cold]
2441 #[inline(never)]
2442 fn missing_field(field: &'static str) -> Self {
2443 maybe_attach_fallback_location(Error::SerdeMissingField {
2444 field,
2445 location: Location::UNKNOWN,
2446 })
2447 }
2448}
2449
2450#[cold]
2460#[inline(never)]
2461fn fmt_with_location(
2462 f: &mut fmt::Formatter<'_>,
2463 l10n: &dyn Localizer,
2464 msg: &str,
2465 location: &Location,
2466) -> fmt::Result {
2467 let out = l10n.attach_location(Cow::Borrowed(msg), *location);
2468 write!(f, "{out}")
2469}
2470
2471#[cold]
2482#[inline(never)]
2483pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
2484 Error::Budget {
2485 breach,
2486 location: Location::UNKNOWN,
2487 }
2488}
2489
2490#[cfg(test)]
2491mod tests {
2492 use super::*;
2493
2494 #[test]
2495 fn sanitize_snippet_source_name_replaces_control_chars() {
2496 let sanitized = sanitize_snippet_source_name("evil.yaml\nINJECTED:\u{001b}[31m");
2497 assert_eq!(sanitized, "evil.yaml INJECTED: [31m");
2498 }
2499
2500 #[test]
2501 fn with_snippet_named_sanitizes_source_name() {
2502 let err = Error::Message {
2503 msg: "oops".to_owned(),
2504 location: Location::new(1, 1),
2505 }
2506 .with_snippet_named("x: y\n", "evil.yaml\nINJECTED", 2);
2507
2508 let Error::WithSnippet { regions, .. } = err else {
2509 panic!("expected Error::WithSnippet");
2510 };
2511
2512 assert_eq!(regions.len(), 1);
2513 assert_eq!(regions[0].source_name, "evil.yaml INJECTED");
2514 }
2515
2516 #[test]
2517 fn locations_for_basic_error_duplicates_location() {
2518 let l = Location::new(3, 7);
2519 let err = Error::Message {
2520 msg: "x".to_owned(),
2521 location: l,
2522 };
2523 assert_eq!(
2524 err.locations(),
2525 Some(Locations {
2526 reference_location: l,
2527 defined_location: l,
2528 })
2529 );
2530 }
2531
2532 #[test]
2533 fn merge_key_not_allowed_location_helpers() {
2534 let l = Location::new(4, 2);
2535 let err = Error::MergeKeyNotAllowed { location: l };
2536 assert_eq!(err.location(), Some(l));
2537 assert_eq!(
2538 err.locations(),
2539 Some(Locations {
2540 reference_location: l,
2541 defined_location: l,
2542 })
2543 );
2544
2545 let updated = err.with_location(Location::new(5, 9));
2546 assert_eq!(updated.location(), Some(Location::new(5, 9)));
2547 }
2548
2549 #[test]
2550 fn locations_for_io_error_is_unknown() {
2551 let err = Error::IOError {
2552 cause: std::io::Error::other("x"),
2553 };
2554 assert_eq!(err.locations(), None);
2555 }
2556
2557 #[test]
2558 fn alias_error_returns_both_locations() {
2559 let ref_loc = Location::new(5, 10);
2560 let def_loc = Location::new(2, 3);
2561 let err = Error::AliasError {
2562 msg: "test error".to_owned(),
2563 locations: Locations {
2564 reference_location: ref_loc,
2565 defined_location: def_loc,
2566 },
2567 };
2568
2569 assert_eq!(err.location(), Some(ref_loc));
2571
2572 assert_eq!(
2574 err.locations(),
2575 Some(Locations {
2576 reference_location: ref_loc,
2577 defined_location: def_loc,
2578 })
2579 );
2580 }
2581
2582 #[test]
2583 fn alias_error_display_shows_both_locations() {
2584 let ref_loc = Location::new(5, 10);
2585 let def_loc = Location::new(2, 3);
2586 let err = Error::AliasError {
2587 msg: "invalid value".to_owned(),
2588 locations: Locations {
2589 reference_location: ref_loc,
2590 defined_location: def_loc,
2591 },
2592 };
2593
2594 let display = err.to_string();
2595 assert!(display.contains("invalid value"));
2596 assert!(display.contains("line 5"));
2597 assert!(display.contains("column 10"));
2598 assert!(display.contains("line 2"));
2599 assert!(display.contains("column 3"));
2600 }
2601
2602 #[test]
2603 fn alias_error_display_with_same_locations() {
2604 let loc = Location::new(3, 7);
2605 let err = Error::AliasError {
2606 msg: "test".to_owned(),
2607 locations: Locations {
2608 reference_location: loc,
2609 defined_location: loc,
2610 },
2611 };
2612
2613 let display = err.to_string();
2614 assert!(display.contains("line 3"));
2616 assert!(display.contains("column 7"));
2617 assert!(!display.contains("defined at"));
2619 }
2620
2621 #[test]
2622 fn with_snippet_counts_trailing_empty_line_for_end_line() {
2623 let text = "a\n";
2625 let err = Error::Message {
2626 msg: "x".to_owned(),
2627 location: Location::new(2, 1),
2628 };
2629
2630 let wrapped = err.with_snippet(text, 50);
2631 let Error::WithSnippet { regions, .. } = wrapped else {
2632 panic!("expected WithSnippet wrapper");
2633 };
2634 assert_eq!(regions.len(), 1);
2635 assert_eq!(regions[0].start_line, 1);
2636 assert_eq!(regions[0].end_line, 2);
2637 }
2638
2639 #[test]
2640 fn with_snippet_offset_counts_trailing_empty_line_for_end_line() {
2641 let text = "a\n";
2643 let err = Error::Message {
2644 msg: "x".to_owned(),
2645 location: Location::new(11, 1),
2646 };
2647
2648 let wrapped = err.with_snippet_offset_named(text, 10, "<input>", 50);
2649 let Error::WithSnippet { regions, .. } = wrapped else {
2650 panic!("expected WithSnippet wrapper");
2651 };
2652 assert_eq!(regions.len(), 1);
2653 assert_eq!(regions[0].start_line, 10);
2654 assert_eq!(regions[0].end_line, 11);
2655 }
2656
2657 #[cfg(feature = "validator")]
2658 #[test]
2659 fn locations_for_validator_error_uses_first_entry() {
2660 use validator::Validate;
2661
2662 #[derive(Debug, Validate)]
2663 struct Cfg {
2664 #[validate(length(min = 2))]
2665 second_string: String,
2666 }
2667
2668 let cfg = Cfg {
2669 second_string: "x".to_owned(),
2670 };
2671 let errors = cfg.validate().expect_err("validation error expected");
2672
2673 let referenced_loc = Location::new(3, 15);
2674 let defined_loc = Location::new(2, 18);
2675
2676 let mut locations = PathMap::new();
2677 locations.insert(
2678 PathKey::empty().join("secondString"),
2679 Locations {
2680 reference_location: referenced_loc,
2681 defined_location: defined_loc,
2682 },
2683 );
2684
2685 let err = Error::ValidatorError {
2686 issues: crate::de_error::collect_validator_issues(&errors),
2687 locations,
2688 };
2689 assert_eq!(
2690 err.locations(),
2691 Some(Locations {
2692 reference_location: referenced_loc,
2693 defined_location: defined_loc,
2694 })
2695 );
2696 }
2697
2698 #[test]
2699 fn nested_snippet_preserves_custom_formatter() {
2700 struct Custom;
2701 impl MessageFormatter for Custom {
2702 fn localizer(&self) -> &dyn Localizer {
2703 &DEFAULT_ENGLISH_LOCALIZER
2704 }
2705 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
2706 match err {
2707 Error::Message { msg, .. } => Cow::Owned(format!("CUSTOM: {}", msg.as_str())),
2708 _ => Cow::Borrowed(""),
2709 }
2710 }
2711 }
2712 let loc = Location::new(1, 1);
2713 let base = Error::Message {
2714 msg: "original".to_string(),
2715 location: loc,
2716 };
2717 let text = "input";
2718 let start_line = 1;
2719 let radius = 1;
2720 let inner = base.with_snippet_offset_named(text, start_line, "<input>", radius);
2721 let outer = inner.with_snippet_offset_named(text, start_line, "<input>", radius);
2722 let rendered = outer.render_with_options(RenderOptions::new(&Custom));
2723 assert!(rendered.contains("CUSTOM: original"));
2724 }
2725
2726 #[test]
2727 fn alias_error_dual_snippet_rendering() {
2728 let yaml = r#"config:
2730 anchor: &myval 42
2731 other: stuff
2732 more: data
2733 use_it: *myval
2734"#;
2735 let ref_loc = Location::new(5, 11);
2737 let def_loc = Location::new(2, 11);
2739
2740 let err = Error::AliasError {
2741 msg: "invalid value type".to_owned(),
2742 locations: Locations {
2743 reference_location: ref_loc,
2744 defined_location: def_loc,
2745 },
2746 };
2747
2748 let wrapped = err.with_snippet(yaml, 5);
2750 let rendered = wrapped.render();
2751
2752 assert!(
2754 rendered.contains("invalid value type"),
2755 "rendered: {}",
2756 rendered
2757 );
2758
2759 assert!(
2762 !rendered.contains("(defined at line"),
2763 "did not expect alias defined-at suffix when secondary window is present: {}",
2764 rendered
2765 );
2766 assert!(
2768 rendered.contains("the value is used here") || rendered.contains("use_it"),
2769 "rendered should show reference location context: {}",
2770 rendered
2771 );
2772 assert!(
2774 rendered.contains("defined here") || rendered.contains("anchor"),
2775 "rendered should show defined location context: {}",
2776 rendered
2777 );
2778 assert!(
2780 rendered.contains("5") || rendered.contains("use_it"),
2781 "rendered should reference line 5: {}",
2782 rendered
2783 );
2784 assert!(
2785 rendered.contains("2") || rendered.contains("anchor"),
2786 "rendered should reference line 2: {}",
2787 rendered
2788 );
2789 }
2790
2791 #[test]
2792 fn alias_error_same_location_single_snippet() {
2793 let yaml = "value: &anchor 42\n";
2794 let loc = Location::new(1, 8);
2795
2796 let err = Error::AliasError {
2797 msg: "test error".to_owned(),
2798 locations: Locations {
2799 reference_location: loc,
2800 defined_location: loc,
2801 },
2802 };
2803
2804 let wrapped = err.with_snippet(yaml, 5);
2805 let rendered = wrapped.render();
2806
2807 assert!(rendered.contains("test error"), "rendered: {}", rendered);
2809 assert!(
2811 !rendered.contains("defined here"),
2812 "should not show 'defined here' when locations are same: {}",
2813 rendered
2814 );
2815 assert!(
2816 !rendered.contains("the value is used here"),
2817 "should not show 'value used here' when locations are same: {}",
2818 rendered
2819 );
2820 }
2821}