1use crate::Location;
2use crate::budget::BudgetBreach;
3use crate::de_snippet::{
4 fmt_snippet_window_offset_or_fallback, snippet_window_frame_prefix_offset,
5};
6use crate::input_source::IncludeResolveError;
7use crate::localizer::{DEFAULT_ENGLISH_LOCALIZER, ExternalMessageSource, Localizer};
8use crate::location::Locations;
9use crate::parse_scalars::{
10 parse_int_signed, parse_yaml11_bool, parse_yaml12_float, scalar_is_nullish,
11};
12#[cfg(feature = "garde")]
13use crate::path_map::path_key_from_garde;
14use crate::properties_redaction::{
15 redact_custom_message, redact_dynamic_identifier, redact_dynamic_value,
16};
17use crate::tags::SfTag;
18#[cfg(any(feature = "garde", feature = "validator"))]
19use crate::{
20 localizer::ExternalMessage,
21 path_map::{PathKey, PathMap, format_path_with_resolved_leaf},
22};
23use annotate_snippets::Level;
24use granit_parser::{ErrorKind, ScalarStyle, ScanError};
25use serde_core::de::{self};
26use std::borrow::Cow;
27use std::cell::Cell;
28use std::fmt;
29
30#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
31use crate::properties_redaction::{redact_with_ctxs, with_interp_redaction};
32
33#[cfg(feature = "validator")]
34use validator::{ValidationErrors, ValidationErrorsKind};
35
36pub trait MessageFormatter {
88 fn localizer(&self) -> &dyn Localizer {
93 &DEFAULT_ENGLISH_LOCALIZER
94 }
95
96 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str>;
109}
110
111pub(crate) fn sanitize_message_text(text: Cow<'_, str>) -> Cow<'_, str> {
116 let Some(first_unsafe) = text
117 .char_indices()
118 .find_map(|(offset, ch)| message_char_needs_escape(ch).then_some(offset))
119 else {
120 return text;
121 };
122
123 let mut sanitized = String::with_capacity(text.len());
124 sanitized.push_str(&text[..first_unsafe]);
125 for ch in text[first_unsafe..].chars() {
126 if message_char_needs_escape(ch) {
127 sanitized.extend(ch.escape_debug());
128 } else {
129 sanitized.push(ch);
130 }
131 }
132 Cow::Owned(sanitized)
133}
134
135#[inline]
136fn message_char_needs_escape(ch: char) -> bool {
137 ch.is_control() || matches!(ch, '\u{2028}' | '\u{2029}')
138}
139
140#[inline]
141pub(crate) fn render_message_text<'a>(
142 formatter: &dyn MessageFormatter,
143 err: &'a Error,
144) -> Cow<'a, str> {
145 sanitize_message_text(formatter.format_message(err))
146}
147
148#[derive(Debug, Default, Clone, Copy)]
160pub struct UserMessageFormatter;
161
162#[non_exhaustive]
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum SnippetMode {
166 Auto,
168 Off,
170}
171
172#[non_exhaustive]
195#[derive(Clone, Copy)]
196pub struct RenderOptions<'a> {
197 pub formatter: &'a dyn MessageFormatter,
199 pub snippets: SnippetMode,
201}
202
203impl Default for RenderOptions<'_> {
204 #[inline]
205 fn default() -> Self {
206 static DEFAULT_FMT: crate::message_formatters::DefaultMessageFormatter =
208 crate::message_formatters::DefaultMessageFormatter;
209
210 Self::new(&DEFAULT_FMT)
211 }
212}
213
214impl<'a> RenderOptions<'a> {
215 #[inline]
221 #[must_use]
222 pub fn new(formatter: &'a dyn MessageFormatter) -> Self {
223 Self {
224 formatter,
225 snippets: SnippetMode::Auto,
226 }
227 }
228}
229
230#[non_exhaustive]
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct CroppedRegion {
237 pub text: String,
239 pub source_name: String,
241 pub start_line: usize,
243 pub end_line: usize,
245 pub location: Location,
247}
248
249impl CroppedRegion {
250 #[must_use]
252 pub fn new(
253 text: impl Into<String>,
254 source_name: impl Into<String>,
255 start_line: usize,
256 end_line: usize,
257 location: Location,
258 ) -> Self {
259 Self {
260 text: text.into(),
261 source_name: source_name.into(),
262 start_line,
263 end_line,
264 location,
265 }
266 }
267
268 fn covers_exact_source(&self, location: &Location) -> bool {
269 if location == &Location::UNKNOWN {
270 return false;
271 }
272 let source_id = location.source_id();
273 source_id != 0 && self.location.source_id() == source_id && self.covers_line(location)
274 }
275
276 fn covers_line(&self, location: &Location) -> bool {
277 let line = location.line as usize;
278 self.start_line <= line && line <= self.end_line
279 }
280
281 fn covers(&self, location: &Location) -> bool {
282 if location == &Location::UNKNOWN {
283 return false;
284 }
285 if !self.covers_line(location) {
286 return false;
287 }
288 let region_source_id = self.location.source_id();
289 let location_source_id = location.source_id();
290 region_source_id == 0 || location_source_id == 0 || region_source_id == location_source_id
291 }
292}
293
294fn line_count_including_trailing_empty_line(text: &str) -> usize {
295 let mut lines = text.split_terminator('\n').count().max(1);
296 if text.ends_with('\n') {
297 lines = lines.saturating_add(1);
298 }
299 lines
300}
301
302fn sanitize_snippet_source_name(name: &str) -> Cow<'_, str> {
303 if !name.chars().any(char::is_control) {
304 return Cow::Borrowed(name);
305 }
306
307 let sanitized: String = name
308 .chars()
309 .map(|ch| if ch.is_control() { ' ' } else { ch })
310 .collect();
311 Cow::Owned(sanitized)
312}
313
314fn cropped_region_for_location(
315 text: &str,
316 source_name: &str,
317 location: &Location,
318 mapping: crate::de_snippet::LineMapping,
319 crop_radius: usize,
320) -> Option<CroppedRegion> {
321 if crop_radius == 0 || *location == Location::UNKNOWN {
322 return None;
323 }
324
325 let (cropped, start_line) =
326 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
327 if cropped.is_empty() {
328 return None;
329 }
330
331 let lines = line_count_including_trailing_empty_line(cropped.as_str());
332 let end_line = start_line.saturating_add(lines.saturating_sub(1));
333 Some(CroppedRegion {
334 text: cropped,
335 source_name: source_name.to_string(),
336 start_line,
337 end_line,
338 location: *location,
339 })
340}
341
342fn push_region_for_location(
343 regions: &mut Vec<CroppedRegion>,
344 text: &str,
345 source_name: &str,
346 location: &Location,
347 mapping: crate::de_snippet::LineMapping,
348 crop_radius: usize,
349) {
350 if let Some(region) =
351 cropped_region_for_location(text, source_name, location, mapping, crop_radius)
352 {
353 regions.push(region);
354 }
355}
356
357fn push_regions_for_locations(
358 regions: &mut Vec<CroppedRegion>,
359 text: &str,
360 source_name: &str,
361 locations: Locations,
362 mapping: crate::de_snippet::LineMapping,
363 crop_radius: usize,
364) {
365 push_region_for_location(
366 regions,
367 text,
368 source_name,
369 &locations.reference_location,
370 mapping,
371 crop_radius,
372 );
373 if locations.defined_location != locations.reference_location {
374 push_region_for_location(
375 regions,
376 text,
377 source_name,
378 &locations.defined_location,
379 mapping,
380 crop_radius,
381 );
382 }
383}
384
385#[cfg(any(feature = "garde", feature = "validator"))]
386fn push_validation_issue_regions(
387 regions: &mut Vec<CroppedRegion>,
388 issues: &[ValidationIssue],
389 locations: &PathMap,
390 text: &str,
391 source_name: &str,
392 mapping: crate::de_snippet::LineMapping,
393 crop_radius: usize,
394) {
395 for issue in issues {
396 let (locs, _) = locations
397 .search_with_ancestor_fallback(&issue.path)
398 .unwrap_or((Locations::UNKNOWN, String::new()));
399 push_regions_for_locations(regions, text, source_name, locs, mapping, crop_radius);
400 }
401}
402
403fn collect_snippet_regions(
404 inner: &Error,
405 text: &str,
406 source_name: &str,
407 mapping: crate::de_snippet::LineMapping,
408 crop_radius: usize,
409) -> Vec<CroppedRegion> {
410 let mut regions = Vec::new();
411
412 #[cfg(any(feature = "garde", feature = "validator"))]
415 if let Error::ValidationError {
416 issues, locations, ..
417 } = inner
418 {
419 push_validation_issue_regions(
420 &mut regions,
421 issues,
422 locations,
423 text,
424 source_name,
425 mapping,
426 crop_radius,
427 );
428 }
429
430 if regions.is_empty() {
433 if let Some(locs) = inner.locations() {
434 push_regions_for_locations(&mut regions, text, source_name, locs, mapping, crop_radius);
435 } else if let Some(loc) = inner.location() {
436 push_region_for_location(&mut regions, text, source_name, &loc, mapping, crop_radius);
437 }
438 }
439
440 regions
441}
442
443#[cfg(any(feature = "garde", feature = "validator"))]
444#[non_exhaustive]
449#[derive(Debug, Clone)]
450pub struct ValidationIssue {
451 pub path: PathKey,
453 pub code: String,
455 pub message: Option<String>,
457 pub params: Vec<(String, String)>,
459}
460
461#[cfg(any(feature = "garde", feature = "validator"))]
462#[non_exhaustive]
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub enum ValidationSource {
465 Garde,
466 Validator,
467}
468
469#[cfg(any(feature = "garde", feature = "validator"))]
470impl ValidationSource {
471 pub(crate) fn external_message_source(self) -> ExternalMessageSource {
472 match self {
473 ValidationSource::Garde => ExternalMessageSource::Garde,
474 ValidationSource::Validator => ExternalMessageSource::Validator,
475 }
476 }
477}
478
479#[cfg(any(feature = "garde", feature = "validator"))]
480impl ValidationIssue {
481 #[must_use]
483 pub fn new(path: PathKey, code: impl Into<String>) -> Self {
484 Self {
485 path,
486 code: code.into(),
487 message: None,
488 params: Vec::new(),
489 }
490 }
491
492 #[must_use]
494 pub fn with_message(mut self, message: impl Into<String>) -> Self {
495 self.message = Some(message.into());
496 self
497 }
498
499 #[must_use]
501 pub fn with_params(mut self, params: Vec<(String, String)>) -> Self {
502 self.params = params;
503 self
504 }
505
506 pub(crate) fn display_entry(&self) -> String {
507 if let Some(msg) = &self.message {
508 return msg.clone();
509 }
510
511 if self.params.is_empty() {
512 return self.code.clone();
513 }
514
515 let mut params = String::new();
516 for (i, (k, v)) in self.params.iter().enumerate() {
517 if i > 0 {
518 params.push_str(", ");
519 }
520 params.push_str(k);
521 params.push('=');
522 params.push_str(v);
523 }
524 format!("{} ({params})", self.code)
525 }
526
527 pub(crate) fn display_entry_overridden(
528 &self,
529 l10n: &dyn Localizer,
530 source: ExternalMessageSource,
531 ) -> String {
532 let raw = self.display_entry();
533 let overridden = l10n
534 .override_external_message(ExternalMessage {
535 source,
536 original: raw.as_str(),
537 code: Some(self.code.as_str()),
538 params: &self.params,
539 })
540 .unwrap_or(Cow::Borrowed(raw.as_str()));
541 overridden.into_owned()
542 }
543}
544
545#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
546fn replace_known_effectives(
547 mut text: String,
548 ctxs: &[crate::properties_redaction::ScalarRedactionCtx],
549) -> String {
550 let mut pairs: Vec<&crate::properties_redaction::ScalarRedactionCtx> = ctxs
551 .iter()
552 .filter(|ctx| !ctx.effective.is_empty())
553 .collect();
554
555 pairs.sort_by_key(|ctx| std::cmp::Reverse(ctx.effective.len()));
556
557 for ctx in pairs {
558 if text.contains(&ctx.effective) {
559 text = text.replace(&ctx.effective, &ctx.raw);
560 }
561 }
562
563 text
564}
565
566#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
567pub(crate) fn redact_issue(mut issue: ValidationIssue) -> ValidationIssue {
568 with_interp_redaction(|pairs| {
569 if pairs.is_empty() {
570 return issue;
571 }
572
573 if let Some(msg) = issue.message.take() {
574 issue.message = Some(redact_with_ctxs(msg, pairs, "invalid interpolated value"));
575 }
576
577 issue.code = replace_known_effectives(std::mem::take(&mut issue.code), pairs);
578
579 for (key, value) in &mut issue.params {
580 *key = replace_known_effectives(std::mem::take(key), pairs);
581 *value = redact_with_ctxs(std::mem::take(value), pairs, "<redacted>");
582 }
583
584 issue
585 })
586}
587
588#[cfg(all(
589 not(feature = "properties"),
590 any(feature = "garde", feature = "validator")
591))]
592pub(crate) fn redact_issue(issue: ValidationIssue) -> ValidationIssue {
593 issue
594}
595
596thread_local! {
604 static MISSING_FIELD_FALLBACK: Cell<Option<Location>> = const { Cell::new(None) };
605}
606
607pub(crate) struct MissingFieldLocationGuard {
610 prev: Option<Location>,
611}
612
613impl MissingFieldLocationGuard {
614 pub(crate) fn new(location: Location) -> Self {
615 let prev = MISSING_FIELD_FALLBACK.with(|c| c.replace(Some(location)));
616 Self { prev }
617 }
618
619 #[allow(clippy::unused_self)] pub(crate) fn replace_location(&mut self, location: Location) {
622 MISSING_FIELD_FALLBACK.with(|c| c.set(Some(location)));
623 }
624}
625
626impl Drop for MissingFieldLocationGuard {
627 fn drop(&mut self) {
628 MISSING_FIELD_FALLBACK.with(|c| c.set(self.prev));
629 }
630}
631
632#[non_exhaustive]
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
638pub enum TransformReason {
639 EscapeSequence,
641 LineFolding,
643 MultiLineNormalization,
645 BlockScalarProcessing,
647 SingleQuoteEscape,
649 InputNotBorrowable,
653
654 ParserReturnedOwned,
660
661 VariableInterpolation,
663}
664
665impl fmt::Display for TransformReason {
666 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667 match self {
668 TransformReason::EscapeSequence => write!(f, "escape sequence processing"),
669 TransformReason::LineFolding => write!(f, "line folding"),
670 TransformReason::MultiLineNormalization => {
671 write!(f, "multi-line whitespace normalization")
672 }
673 TransformReason::BlockScalarProcessing => write!(f, "block scalar processing"),
674 TransformReason::SingleQuoteEscape => write!(f, "single-quote escape processing"),
675 TransformReason::InputNotBorrowable => {
676 write!(f, "input is not available for borrowing")
677 }
678 TransformReason::ParserReturnedOwned => write!(f, "parser returned an owned string"),
679 TransformReason::VariableInterpolation => write!(f, "variable interpolation"),
680 }
681 }
682}
683
684#[non_exhaustive]
686pub enum Error {
687 Message {
689 msg: String,
690 location: Location,
691 },
692
693 InvalidOptions {
695 msg: String,
696 location: Location,
697 },
698
699 ExternalMessage {
704 source: Box<ExternalMessageSource>,
708 msg: String,
709 code: Option<String>,
711 params: Vec<(String, String)>,
713 location: Location,
714 },
715 Eof {
717 location: Location,
718 },
719 MultipleDocuments {
724 hint: &'static str,
726 location: Location,
727 },
728 Unexpected {
730 expected: &'static str,
731 location: Location,
732 },
733
734 MergeValueNotMapOrSeqOfMaps {
736 location: Location,
737 },
738
739 MergeKeyNotAllowed {
741 location: Location,
742 },
743
744 InvalidBinaryBase64 {
746 location: Location,
747 },
748
749 BinaryNotUtf8 {
751 location: Location,
752 },
753
754 TaggedScalarCannotDeserializeIntoString {
756 location: Location,
757 },
758
759 UnexpectedSequenceEnd {
761 location: Location,
762 },
763
764 UnexpectedMappingEnd {
766 location: Location,
767 },
768
769 InvalidBooleanStrict {
771 location: Location,
772 },
773
774 InvalidCharNull {
776 location: Location,
777 },
778
779 InvalidCharNotSingleScalar {
781 location: Location,
782 },
783
784 NullIntoString {
786 location: Location,
787 },
788
789 BytesNotSupportedMissingBinaryTag {
791 location: Location,
792 },
793
794 UnexpectedValueForUnit {
796 location: Location,
797 },
798
799 ExpectedEmptyMappingForUnitStruct {
801 location: Location,
802 },
803
804 UnexpectedContainerEndWhileSkippingNode {
806 location: Location,
807 },
808
809 InternalSeedReusedForMapKey {
811 location: Location,
812 },
813
814 ValueRequestedBeforeKey {
816 location: Location,
817 },
818
819 ExpectedStringKeyForExternallyTaggedEnum {
821 location: Location,
822 },
823
824 ExternallyTaggedEnumExpectedScalarOrMapping {
826 location: Location,
827 },
828
829 UnexpectedValueForUnitEnumVariant {
831 location: Location,
832 },
833
834 InvalidUtf8Input,
836
837 AliasReplayCounterOverflow {
839 location: Location,
840 },
841
842 AliasReplayLimitExceeded {
844 total_replayed_events: usize,
845 max_total_replayed_events: usize,
846 location: Location,
847 },
848
849 AliasExpansionLimitExceeded {
851 anchor_id: usize,
852 expansions: usize,
853 max_expansions_per_anchor: usize,
854 location: Location,
855 },
856
857 AliasReplayStackDepthExceeded {
859 depth: usize,
860 max_depth: usize,
861 location: Location,
862 },
863
864 FoldedBlockScalarMustIndentContent {
866 location: Location,
867 },
868
869 InternalDepthUnderflow {
871 location: Location,
872 },
873
874 InternalRecursionStackEmpty {
876 location: Location,
877 },
878
879 RecursiveReferencesRequireWeakTypes {
881 location: Location,
882 },
883
884 InvalidScalar {
886 ty: &'static str,
887 location: Location,
888 },
889
890 NonFiniteFloat {
896 value: String,
898 location: Location,
899 },
900
901 SerdeInvalidType {
903 unexpected: String,
904 expected: String,
905 location: Location,
906 },
907
908 SerdeInvalidValue {
910 unexpected: String,
911 expected: String,
912 location: Location,
913 },
914
915 SerdeUnknownVariant {
917 variant: String,
918 expected: Vec<&'static str>,
919 location: Location,
920 },
921
922 SerdeUnknownField {
924 field: String,
925 expected: Vec<&'static str>,
926 location: Location,
927 },
928
929 SerdeMissingField {
931 field: &'static str,
932 location: Location,
933 },
934
935 UnexpectedContainerEndWhileReadingKeyNode {
939 location: Location,
940 },
941
942 DuplicateMappingKey {
946 key: Option<String>,
947 location: Location,
948 },
949
950 TaggedEnumMismatch {
952 tagged: String,
953 target: &'static str,
954 location: Location,
955 },
956
957 SerdeVariantId {
959 msg: String,
960 location: Location,
961 },
962
963 ExpectedMappingEndAfterEnumVariantValue {
965 location: Location,
966 },
967 ContainerEndMismatch {
968 location: Location,
969 },
970 UnknownAnchor {
972 location: Location,
973 },
974 CyclicInclude {
976 id: String,
977 stack: Vec<String>,
978 location: Location,
979 },
980 UnsupportedIncludeForm {
982 location: Location,
983 },
984 ResolverError {
986 target: String,
987 error: IncludeResolveError,
988 stack: Vec<String>,
989 location: Location,
990 },
991 AliasError {
996 msg: String,
997 locations: Locations,
998 },
999 HookError {
1002 msg: String,
1003 location: Location,
1004 },
1005 UnresolvedProperty {
1007 name: String,
1009 location: Location,
1010 },
1011 InvalidPropertyName {
1013 name: String,
1015 location: Location,
1016 },
1017 PropertyRequiredButUnset {
1020 name: String,
1021 message: String,
1022 location: Location,
1023 },
1024 PropertyRequiredButEmpty {
1027 name: String,
1028 message: String,
1029 location: Location,
1030 },
1031 Budget {
1033 breach: BudgetBreach,
1034 location: Location,
1035 },
1036 IOError {
1038 cause: std::io::Error,
1039 },
1040 QuotingRequired {
1043 value: String, location: Location,
1045 },
1046
1047 CannotBorrowTransformedString {
1053 reason: TransformReason,
1055 location: Location,
1056 },
1057
1058 IndentationError {
1060 required: crate::indentation::RequireIndent,
1062 actual: usize,
1064 location: Location,
1065 },
1066
1067 WithSnippet {
1069 regions: Vec<CroppedRegion>,
1074 crop_radius: usize,
1075 error: Box<Error>,
1076 },
1077
1078 #[cfg(any(feature = "garde", feature = "validator"))]
1080 ValidationError {
1081 source: ValidationSource,
1082 issues: Vec<ValidationIssue>,
1083 locations: PathMap,
1084 },
1085
1086 #[cfg(any(feature = "garde", feature = "validator"))]
1088 ValidationErrors {
1089 source: ValidationSource,
1090 errors: Vec<Error>,
1091 },
1092
1093 UnsupportedTag {
1095 tag: String,
1096 location: Location,
1097 },
1098}
1099
1100impl Error {
1101 #[cold]
1102 #[inline(never)]
1103 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
1104 self.with_snippet_named(text, "<input>", crop_radius)
1105 }
1106
1107 #[cold]
1108 #[inline(never)]
1109 pub(crate) fn with_snippet_named(
1110 self,
1111 text: &str,
1112 source_name: &str,
1113 crop_radius: usize,
1114 ) -> Self {
1115 let source_name = sanitize_snippet_source_name(source_name);
1116
1117 let inner = match self {
1120 Error::WithSnippet { error, .. } => *error,
1121 other => other,
1122 };
1123
1124 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1126
1127 let regions = collect_snippet_regions(
1128 &inner,
1129 text,
1130 source_name.as_ref(),
1131 crate::de_snippet::LineMapping::Identity,
1132 crop_radius,
1133 );
1134
1135 Error::WithSnippet {
1136 regions,
1137 crop_radius,
1138 error: Box::new(inner),
1139 }
1140 }
1141
1142 #[cfg(feature = "include")]
1143 #[cold]
1144 #[inline(never)]
1145 pub(crate) fn with_additional_snippet_named(
1146 mut self,
1147 text: &str,
1148 source_name: &str,
1149 location: &Location,
1150 crop_radius: usize,
1151 ) -> Self {
1152 let source_name = sanitize_snippet_source_name(source_name);
1153
1154 if crop_radius == 0 || *location == Location::UNKNOWN {
1155 return self;
1156 }
1157
1158 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1159 let mapping = crate::de_snippet::LineMapping::Identity;
1160
1161 let Some(region) =
1162 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1163 else {
1164 return self;
1165 };
1166
1167 if let Error::WithSnippet {
1168 ref mut regions, ..
1169 } = self
1170 {
1171 regions.push(region);
1172 }
1173 self
1174 }
1175
1176 #[cfg(feature = "include")]
1177 #[cold]
1178 #[inline(never)]
1179 pub(crate) fn with_additional_snippet_offset_named(
1180 mut self,
1181 text: &str,
1182 start_line: usize,
1183 source_name: &str,
1184 location: &Location,
1185 crop_radius: usize,
1186 ) -> Self {
1187 let source_name = sanitize_snippet_source_name(source_name);
1188
1189 if crop_radius == 0 || *location == Location::UNKNOWN {
1190 return self;
1191 }
1192
1193 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1194 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1195
1196 let Some(region) =
1197 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1198 else {
1199 return self;
1200 };
1201
1202 if let Error::WithSnippet {
1203 ref mut regions, ..
1204 } = self
1205 {
1206 regions.push(region);
1207 }
1208 self
1209 }
1210
1211 #[cold]
1212 #[inline(never)]
1213 pub(crate) fn with_snippet_offset_named(
1214 self,
1215 text: &str,
1216 start_line: usize,
1217 source_name: &str,
1218 crop_radius: usize,
1219 ) -> Self {
1220 let source_name = sanitize_snippet_source_name(source_name);
1221
1222 let inner = match self {
1223 Error::WithSnippet { error, .. } => *error,
1224 other => other,
1225 };
1226
1227 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1229
1230 let regions = collect_snippet_regions(
1231 &inner,
1232 text,
1233 source_name.as_ref(),
1234 crate::de_snippet::LineMapping::Offset { start_line },
1235 crop_radius,
1236 );
1237
1238 Error::WithSnippet {
1239 regions,
1240 crop_radius,
1241 error: Box::new(inner),
1242 }
1243 }
1244
1245 #[must_use]
1247 pub fn without_snippet(&self) -> &Self {
1248 match self {
1249 Error::WithSnippet { error, .. } => error,
1250 other => other,
1251 }
1252 }
1253
1254 #[must_use]
1260 pub fn render(&self) -> String {
1261 self.render_with_options(RenderOptions::default())
1262 }
1263
1264 #[must_use]
1266 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
1267 self.render_with_options(RenderOptions {
1268 formatter,
1269 snippets: SnippetMode::Auto,
1270 })
1271 }
1272
1273 #[must_use]
1275 pub fn render_with_options(&self, options: RenderOptions<'_>) -> String {
1276 struct RenderDisplay<'a> {
1277 err: &'a Error,
1278 options: RenderOptions<'a>,
1279 }
1280
1281 impl fmt::Display for RenderDisplay<'_> {
1282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1283 fmt_error_rendered(f, self.err, self.options)
1284 }
1285 }
1286
1287 RenderDisplay { err: self, options }.to_string()
1288 }
1289
1290 #[cold]
1301 #[inline(never)]
1302 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
1303 Error::Message {
1304 msg: s.into(),
1305 location: Location::UNKNOWN,
1306 }
1307 }
1308
1309 #[cold]
1311 #[inline(never)]
1312 pub(crate) fn invalid_options<S: Into<String>>(s: S) -> Self {
1313 Error::InvalidOptions {
1314 msg: s.into(),
1315 location: Location::UNKNOWN,
1316 }
1317 }
1318
1319 #[cold]
1323 #[inline(never)]
1324 pub(crate) fn quoting_required(value: &str, interpolated: bool) -> Self {
1325 let location = Location::UNKNOWN;
1328 let value = if !interpolated
1329 && (parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
1330 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
1331 || parse_yaml11_bool(value).is_ok()
1332 || scalar_is_nullish(value, &ScalarStyle::Plain))
1333 {
1334 value.to_string()
1335 } else {
1336 String::new()
1337 };
1338 Error::QuotingRequired { value, location }
1339 }
1340
1341 #[cold]
1352 #[inline(never)]
1353 pub(crate) fn unexpected(what: &'static str) -> Self {
1354 Error::Unexpected {
1355 expected: what,
1356 location: Location::UNKNOWN,
1357 }
1358 }
1359
1360 #[cold]
1365 #[inline(never)]
1366 pub(crate) fn eof() -> Self {
1367 Error::Eof {
1368 location: Location::UNKNOWN,
1369 }
1370 }
1371
1372 #[cold]
1373 #[inline(never)]
1374 pub(crate) fn multiple_documents(hint: &'static str) -> Self {
1375 Error::MultipleDocuments {
1376 hint,
1377 location: Location::UNKNOWN,
1378 }
1379 }
1380
1381 #[cfg(any(feature = "garde", feature = "validator"))]
1382 pub(crate) fn validation_error(
1383 source: ValidationSource,
1384 issues: Vec<ValidationIssue>,
1385 locations: PathMap,
1386 ) -> Self {
1387 Error::ValidationError {
1388 source,
1389 issues,
1390 locations,
1391 }
1392 }
1393
1394 #[cfg(any(feature = "garde", feature = "validator"))]
1395 pub(crate) fn validation_errors(source: ValidationSource, errors: Vec<Error>) -> Self {
1396 Error::ValidationErrors { source, errors }
1397 }
1398
1399 #[cfg(any(feature = "garde", feature = "validator"))]
1400 pub(crate) fn is_validation_error(&self) -> bool {
1401 matches!(self, Error::ValidationError { .. })
1402 }
1403
1404 #[cold]
1409 #[inline(never)]
1410 pub(crate) fn unknown_anchor() -> Self {
1411 Error::UnknownAnchor {
1412 location: Location::UNKNOWN,
1413 }
1414 }
1415
1416 #[cold]
1421 #[inline(never)]
1422 #[must_use]
1423 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
1424 Error::CannotBorrowTransformedString {
1425 reason,
1426 location: Location::UNKNOWN,
1427 }
1428 }
1429
1430 #[cold]
1441 #[inline(never)]
1442 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
1443 match &mut self {
1444 Error::Message { location, .. }
1445 | Error::InvalidOptions { location, .. }
1446 | Error::ExternalMessage { location, .. }
1447 | Error::Eof { location }
1448 | Error::MultipleDocuments { location, .. }
1449 | Error::Unexpected { location, .. }
1450 | Error::MergeValueNotMapOrSeqOfMaps { location }
1451 | Error::MergeKeyNotAllowed { location }
1452 | Error::UnsupportedTag { location, .. }
1453 | Error::InvalidBinaryBase64 { location }
1454 | Error::BinaryNotUtf8 { location }
1455 | Error::TaggedScalarCannotDeserializeIntoString { location }
1456 | Error::UnexpectedSequenceEnd { location }
1457 | Error::UnexpectedMappingEnd { location }
1458 | Error::InvalidBooleanStrict { location }
1459 | Error::InvalidCharNull { location }
1460 | Error::InvalidCharNotSingleScalar { location }
1461 | Error::NullIntoString { location }
1462 | Error::BytesNotSupportedMissingBinaryTag { location }
1463 | Error::UnexpectedValueForUnit { location }
1464 | Error::ExpectedEmptyMappingForUnitStruct { location }
1465 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1466 | Error::InternalSeedReusedForMapKey { location }
1467 | Error::ValueRequestedBeforeKey { location }
1468 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1469 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1470 | Error::UnexpectedValueForUnitEnumVariant { location }
1471 | Error::AliasReplayCounterOverflow { location }
1472 | Error::AliasReplayLimitExceeded { location, .. }
1473 | Error::AliasExpansionLimitExceeded { location, .. }
1474 | Error::AliasReplayStackDepthExceeded { location, .. }
1475 | Error::FoldedBlockScalarMustIndentContent { location }
1476 | Error::InternalDepthUnderflow { location }
1477 | Error::InternalRecursionStackEmpty { location }
1478 | Error::RecursiveReferencesRequireWeakTypes { location }
1479 | Error::InvalidScalar { location, .. }
1480 | Error::NonFiniteFloat { location, .. }
1481 | Error::SerdeInvalidType { location, .. }
1482 | Error::SerdeInvalidValue { location, .. }
1483 | Error::SerdeUnknownVariant { location, .. }
1484 | Error::SerdeUnknownField { location, .. }
1485 | Error::SerdeMissingField { location, .. }
1486 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1487 | Error::DuplicateMappingKey { location, .. }
1488 | Error::TaggedEnumMismatch { location, .. }
1489 | Error::SerdeVariantId { location, .. }
1490 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1491 | Error::HookError { location, .. }
1492 | Error::UnresolvedProperty { location, .. }
1493 | Error::InvalidPropertyName { location, .. }
1494 | Error::PropertyRequiredButUnset { location, .. }
1495 | Error::PropertyRequiredButEmpty { location, .. }
1496 | Error::ContainerEndMismatch { location, .. }
1497 | Error::UnknownAnchor { location, .. }
1498 | Error::CyclicInclude { location, .. }
1499 | Error::UnsupportedIncludeForm { location, .. }
1500 | Error::ResolverError { location, .. }
1501 | Error::QuotingRequired { location, .. }
1502 | Error::Budget { location, .. }
1503 | Error::CannotBorrowTransformedString { location, .. }
1504 | Error::IndentationError { location, .. } => {
1505 *location = set_location;
1506 }
1507 Error::InvalidUtf8Input => {}
1508 Error::IOError { .. } => {} Error::AliasError { .. } => {
1510 }
1512 Error::WithSnippet { error, .. } => {
1513 let inner = *std::mem::replace(error, Box::new(Error::eof()));
1514 **error = inner.with_location(set_location);
1515 }
1516 #[cfg(any(feature = "garde", feature = "validator"))]
1517 Error::ValidationError { .. } => {
1518 }
1520 #[cfg(any(feature = "garde", feature = "validator"))]
1521 Error::ValidationErrors { .. } => {
1522 }
1524 }
1525 self
1526 }
1527
1528 #[must_use]
1536 pub fn location(&self) -> Option<Location> {
1537 #[cfg(any(feature = "garde", feature = "validator"))]
1538 if let Error::ValidationErrors { errors, .. } = self {
1539 return errors.iter().find_map(Error::location);
1542 }
1543
1544 self.locations().and_then(Locations::primary_location)
1545 }
1546 #[must_use]
1556 pub fn locations(&self) -> Option<Locations> {
1557 match self {
1558 Error::Message { location, .. }
1559 | Error::InvalidOptions { location, .. }
1560 | Error::ExternalMessage { location, .. }
1561 | Error::Eof { location }
1562 | Error::MultipleDocuments { location, .. }
1563 | Error::Unexpected { location, .. }
1564 | Error::MergeValueNotMapOrSeqOfMaps { location }
1565 | Error::MergeKeyNotAllowed { location }
1566 | Error::UnsupportedTag { location, .. }
1567 | Error::InvalidBinaryBase64 { location }
1568 | Error::BinaryNotUtf8 { location }
1569 | Error::TaggedScalarCannotDeserializeIntoString { location }
1570 | Error::UnexpectedSequenceEnd { location }
1571 | Error::UnexpectedMappingEnd { location }
1572 | Error::InvalidBooleanStrict { location }
1573 | Error::InvalidCharNull { location }
1574 | Error::InvalidCharNotSingleScalar { location }
1575 | Error::NullIntoString { location }
1576 | Error::BytesNotSupportedMissingBinaryTag { location }
1577 | Error::UnexpectedValueForUnit { location }
1578 | Error::ExpectedEmptyMappingForUnitStruct { location }
1579 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1580 | Error::InternalSeedReusedForMapKey { location }
1581 | Error::ValueRequestedBeforeKey { location }
1582 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1583 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1584 | Error::UnexpectedValueForUnitEnumVariant { location }
1585 | Error::AliasReplayCounterOverflow { location }
1586 | Error::AliasReplayLimitExceeded { location, .. }
1587 | Error::AliasExpansionLimitExceeded { location, .. }
1588 | Error::AliasReplayStackDepthExceeded { location, .. }
1589 | Error::FoldedBlockScalarMustIndentContent { location }
1590 | Error::InternalDepthUnderflow { location }
1591 | Error::InternalRecursionStackEmpty { location }
1592 | Error::RecursiveReferencesRequireWeakTypes { location }
1593 | Error::InvalidScalar { location, .. }
1594 | Error::NonFiniteFloat { location, .. }
1595 | Error::SerdeInvalidType { location, .. }
1596 | Error::SerdeInvalidValue { location, .. }
1597 | Error::SerdeUnknownVariant { location, .. }
1598 | Error::SerdeUnknownField { location, .. }
1599 | Error::SerdeMissingField { location, .. }
1600 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1601 | Error::DuplicateMappingKey { location, .. }
1602 | Error::TaggedEnumMismatch { location, .. }
1603 | Error::SerdeVariantId { location, .. }
1604 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1605 | Error::HookError { location, .. }
1606 | Error::UnresolvedProperty { location, .. }
1607 | Error::InvalidPropertyName { location, .. }
1608 | Error::PropertyRequiredButUnset { location, .. }
1609 | Error::PropertyRequiredButEmpty { location, .. }
1610 | Error::ContainerEndMismatch { location, .. }
1611 | Error::UnknownAnchor { location, .. }
1612 | Error::CyclicInclude { location, .. }
1613 | Error::UnsupportedIncludeForm { location, .. }
1614 | Error::ResolverError { location, .. }
1615 | Error::QuotingRequired { location, .. }
1616 | Error::Budget { location, .. }
1617 | Error::CannotBorrowTransformedString { location, .. }
1618 | Error::IndentationError { location, .. } => Locations::same(location),
1619 Error::InvalidUtf8Input => None,
1620 Error::IOError { .. } => None,
1621 Error::AliasError { locations, .. } => Some(*locations),
1622 Error::WithSnippet { error, .. } => error.locations(),
1623 #[cfg(any(feature = "garde", feature = "validator"))]
1624 Error::ValidationError {
1625 issues, locations, ..
1626 } => issues
1627 .first()
1628 .and_then(|issue| locations.search_with_ancestor_fallback(&issue.path))
1629 .map(|(locs, _)| locs),
1630 #[cfg(any(feature = "garde", feature = "validator"))]
1631 Error::ValidationErrors { errors, .. } => errors.first().and_then(Error::locations),
1632 }
1633 }
1634
1635 #[cold]
1640 #[inline(never)]
1641 pub(crate) fn from_scan_error(err: ScanError) -> Self {
1642 let err = match err.try_into_input_io_error() {
1643 Ok(error) => {
1644 let cause = match error.try_into_io_error() {
1645 Ok(error) => error,
1646 Err(error) => {
1647 let kind = error
1648 .io_error()
1649 .map_or(std::io::ErrorKind::Other, std::io::Error::kind);
1650 std::io::Error::new(kind, error)
1651 }
1652 };
1653 return Error::IOError { cause };
1654 }
1655 Err(err) => err,
1656 };
1657
1658 let mark = err.marker();
1659 let location = Location::new(mark.line(), mark.col() + 1)
1660 .with_span(crate::Span::new(mark.index() as u64, 1));
1661
1662 match err.kind() {
1663 ErrorKind::InputDecoding { message } => {
1664 return Error::IOError {
1665 cause: std::io::Error::new(std::io::ErrorKind::InvalidData, message.clone()),
1666 };
1667 }
1668 ErrorKind::InputByteLimitExceeded { limit } => {
1669 return Error::IOError {
1670 cause: std::io::Error::new(
1671 std::io::ErrorKind::FileTooLarge,
1672 format!("input size limit of {limit} bytes exceeded"),
1673 ),
1674 };
1675 }
1676 ErrorKind::MultipleDocumentsUnsupported => {
1677 return Error::MultipleDocuments {
1678 hint: "only one document is supported in this context",
1679 location,
1680 };
1681 }
1682 ErrorKind::UnknownAnchor => return Error::UnknownAnchor { location },
1683 _ => {}
1684 }
1685
1686 let message = err.info();
1687 Error::ExternalMessage {
1688 source: Box::new(ExternalMessageSource::Parser(err)),
1689 msg: message,
1690 code: None,
1691 params: Vec::new(),
1692 location,
1693 }
1694 }
1695}
1696
1697fn fmt_error_plain_with_formatter(
1698 f: &mut fmt::Formatter<'_>,
1699 err: &Error,
1700 formatter: &dyn MessageFormatter,
1701) -> fmt::Result {
1702 let err = err.without_snippet();
1703
1704 let msg = render_message_text(formatter, err);
1705
1706 #[cfg(any(feature = "garde", feature = "validator"))]
1710 if matches!(err, Error::ValidationError { .. }) {
1711 return write!(f, "{msg}");
1712 }
1713
1714 if let Some(loc) = err.location() {
1715 fmt_with_location(f, formatter.localizer(), msg.as_ref(), &loc)?;
1716 } else {
1717 write!(f, "{msg}")?;
1718 }
1719
1720 #[cfg(any(feature = "garde", feature = "validator"))]
1721 if let Error::ValidationErrors { errors, .. } = err {
1722 for err in errors {
1723 writeln!(f)?;
1724 writeln!(f)?;
1725 fmt_error_plain_with_formatter(f, err, formatter)?;
1726 }
1727 }
1728
1729 Ok(())
1730}
1731
1732fn pick_cropped_region<'a>(
1733 regions: &'a [CroppedRegion],
1734 location: &Location,
1735) -> Option<&'a CroppedRegion> {
1736 let source_id = location.source_id();
1737
1738 if source_id != 0 {
1739 if let Some(region) = regions.iter().find(|r| r.covers_exact_source(location)) {
1740 return Some(region);
1741 }
1742 if let Some(region) = regions.iter().find(|r| r.location.source_id() == source_id) {
1743 return Some(region);
1744 }
1745 if let Some(region) = regions
1746 .iter()
1747 .find(|r| r.location.source_id() == 0 && r.covers(location))
1748 {
1749 return Some(region);
1750 }
1751 return None;
1752 }
1753
1754 regions
1755 .iter()
1756 .find(|r| r.covers(location))
1757 .or_else(|| regions.first())
1758}
1759
1760fn writeln_anchor_intro(
1761 f: &mut fmt::Formatter<'_>,
1762 l10n: &dyn Localizer,
1763 def_loc: Location,
1764 def_region: &CroppedRegion,
1765) -> fmt::Result {
1766 let line = sanitize_message_text(Cow::Owned(l10n.value_comes_from_the_anchor(def_loc)));
1767 let Some(prefix) = snippet_window_frame_prefix_offset(
1768 def_region.text.as_str(),
1769 def_region.start_line,
1770 &def_loc,
1771 ) else {
1772 return writeln!(f, "{line}");
1773 };
1774
1775 match line.strip_prefix(" |") {
1776 Some(rest) => writeln!(f, "{prefix}{rest}"),
1777 None => writeln!(f, "{line}"),
1778 }
1779}
1780
1781fn fmt_error_rendered(
1782 f: &mut fmt::Formatter<'_>,
1783 err: &Error,
1784 options: RenderOptions<'_>,
1785) -> fmt::Result {
1786 if options.snippets == SnippetMode::Off {
1787 return fmt_error_plain_with_formatter(f, err, options.formatter);
1788 }
1789
1790 match err {
1791 #[cfg(any(feature = "garde", feature = "validator"))]
1792 Error::ValidationErrors { errors, .. } => {
1793 let msg = render_message_text(options.formatter, err);
1794 if !msg.is_empty() {
1795 writeln!(f, "{msg}")?;
1796 }
1797 let mut first = true;
1798 for err in errors {
1799 if !first {
1800 writeln!(f)?;
1801 writeln!(f)?;
1802 }
1803 first = false;
1804 fmt_error_rendered(f, err, options)?;
1805 }
1806 Ok(())
1807 }
1808
1809 Error::WithSnippet {
1810 regions,
1811 crop_radius,
1812 error,
1813 } => {
1814 if *crop_radius == 0 {
1815 return fmt_error_plain_with_formatter(f, error, options.formatter);
1817 }
1818
1819 if regions.is_empty() {
1820 return fmt_error_plain_with_formatter(f, error, options.formatter);
1821 }
1822
1823 #[cfg(any(feature = "garde", feature = "validator"))]
1826 if let Error::ValidationError {
1827 source,
1828 issues,
1829 locations,
1830 } = error.as_ref()
1831 {
1832 return fmt_validation_error_with_snippets_offset(
1833 f,
1834 options.formatter.localizer(),
1835 &source.external_message_source(),
1836 issues,
1837 locations,
1838 regions,
1839 *crop_radius,
1840 );
1841 }
1842 #[cfg(any(feature = "garde", feature = "validator"))]
1843 if let Error::ValidationErrors { errors, .. } = error.as_ref() {
1844 let msg = render_message_text(options.formatter, error);
1845 if !msg.is_empty() {
1846 writeln!(f, "{msg}")?;
1847 }
1848 let mut first = true;
1849 for err in errors {
1850 if !first {
1851 writeln!(f)?;
1852 writeln!(f)?;
1853 }
1854 first = false;
1855 fmt_error_with_snippets_offset(
1856 f,
1857 err,
1858 regions,
1859 *crop_radius,
1860 options.formatter,
1861 )?;
1862 }
1863 return Ok(());
1864 }
1865
1866 let Some(location) = error.location() else {
1869 return fmt_error_plain_with_formatter(f, error, options.formatter);
1870 };
1871 if location == Location::UNKNOWN {
1872 return fmt_error_plain_with_formatter(f, error, options.formatter);
1873 }
1874
1875 let l10n = options.formatter.localizer();
1876
1877 let Some(region) = pick_cropped_region(regions, &location) else {
1878 return fmt_error_plain_with_formatter(f, error, options.formatter);
1879 };
1880
1881 let dual_locations = error.locations().filter(|locs| {
1883 locs.reference_location != Location::UNKNOWN
1884 && locs.defined_location != Location::UNKNOWN
1885 && locs.reference_location != locs.defined_location
1886 });
1887
1888 let mut msg = render_message_text(options.formatter, error);
1889
1890 if dual_locations.is_some()
1894 && let Error::AliasError { locations, .. } = error.as_ref()
1895 {
1896 let suffix = sanitize_message_text(Cow::Owned(
1897 l10n.alias_defined_at(locations.defined_location),
1898 ));
1899 if let Some(stripped) = msg.as_ref().strip_suffix(suffix.as_ref()) {
1900 msg = Cow::Owned(stripped.to_string());
1901 }
1902 }
1903
1904 if let Some(locs) = dual_locations {
1905 let ref_loc = locs.reference_location;
1906 let def_loc = locs.defined_location;
1907
1908 let used_region = pick_cropped_region(regions, &ref_loc).unwrap_or(region);
1909 let label = l10n.value_used_here();
1910 let ctx = crate::de_snippet::Snippet::new(
1911 used_region.text.as_str(),
1912 used_region.source_name.as_str(),
1913 *crop_radius,
1914 )
1915 .with_offset(used_region.start_line);
1916 ctx.fmt_or_fallback_with_label(
1917 f,
1918 Level::ERROR,
1919 l10n,
1920 msg.as_ref(),
1921 label.as_ref(),
1922 &ref_loc,
1923 )?;
1924
1925 let def_region = pick_cropped_region(regions, &def_loc).unwrap_or(region);
1926 writeln!(f)?;
1927 writeln_anchor_intro(f, l10n, def_loc, def_region)?;
1928 fmt_snippet_window_offset_or_fallback(
1929 f,
1930 l10n,
1931 &def_loc,
1932 def_region.text.as_str(),
1933 def_region.start_line,
1934 l10n.defined_window().as_ref(),
1935 *crop_radius,
1936 )?;
1937 Ok(())
1938 } else {
1939 let ctx = crate::de_snippet::Snippet::new(
1941 region.text.as_str(),
1942 region.source_name.as_str(),
1943 *crop_radius,
1944 )
1945 .with_offset(region.start_line);
1946 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &location)?;
1947
1948 for extra_region in regions {
1949 if std::ptr::eq(extra_region, region) {
1950 continue;
1951 }
1952 writeln!(f)?;
1953 writeln!(f, "included from here:")?;
1954 let extra_ctx = crate::de_snippet::Snippet::new(
1955 extra_region.text.as_str(),
1956 extra_region.source_name.as_str(),
1957 *crop_radius,
1958 )
1959 .with_offset(extra_region.start_line);
1960 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
1961 }
1962 Ok(())
1963 }
1964 }
1965 _ => fmt_error_plain_with_formatter(f, err, options.formatter),
1966 }
1967}
1968
1969impl fmt::Display for Error {
1970 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1971 fmt_error_rendered(f, self, RenderOptions::default())
1972 }
1973}
1974
1975impl fmt::Debug for Error {
1976 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1977 fmt::Display::fmt(self, f)
1978 }
1979}
1980
1981#[cfg(any(feature = "garde", feature = "validator"))]
1982fn fmt_validation_error_with_snippets_offset(
1983 f: &mut fmt::Formatter<'_>,
1984 l10n: &dyn Localizer,
1985 source: &ExternalMessageSource,
1986 issues: &[ValidationIssue],
1987 locations: &PathMap,
1988 regions: &[CroppedRegion],
1989 crop_radius: usize,
1990) -> fmt::Result {
1991 let mut first = true;
1992 for issue in issues {
1993 if !first {
1994 writeln!(f)?;
1995 }
1996 first = false;
1997
1998 let original_leaf = issue
1999 .path
2000 .leaf_string()
2001 .unwrap_or_else(|| l10n.root_path_label().into_owned());
2002
2003 let (locs, resolved_leaf) = locations
2004 .search_with_ancestor_fallback(&issue.path)
2005 .unwrap_or((Locations::UNKNOWN, original_leaf));
2006
2007 let ref_loc = locs.reference_location;
2008 let def_loc = locs.defined_location;
2009
2010 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
2011 let entry = issue.display_entry_overridden(l10n, (*source).clone());
2012 let base_msg = sanitize_message_text(Cow::Owned(
2013 l10n.validation_base_message(&entry, &resolved_path),
2014 ));
2015
2016 let mut rendered_regions = Vec::new();
2017
2018 match (ref_loc, def_loc) {
2019 (Location::UNKNOWN, Location::UNKNOWN) => {
2020 write!(f, "{base_msg}")?;
2021 }
2022 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
2023 let label = l10n.defined();
2024 if let Some(region) = pick_cropped_region(regions, &r) {
2025 rendered_regions.push(std::ptr::from_ref(region));
2026 let ctx = crate::de_snippet::Snippet::new(
2027 region.text.as_str(),
2028 label.as_ref(),
2029 crop_radius,
2030 )
2031 .with_offset(region.start_line);
2032 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
2033 } else {
2034 fmt_with_location(f, l10n, &base_msg, &r)?;
2035 }
2036 }
2037 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
2038 let label = l10n.defined_here();
2039 if let Some(region) = pick_cropped_region(regions, &d) {
2040 rendered_regions.push(std::ptr::from_ref(region));
2041 let ctx = crate::de_snippet::Snippet::new(
2042 region.text.as_str(),
2043 label.as_ref(),
2044 crop_radius,
2045 )
2046 .with_offset(region.start_line);
2047 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
2048 } else {
2049 fmt_with_location(f, l10n, &base_msg, &d)?;
2050 }
2051 }
2052 (r, d) => {
2053 let label = l10n.value_used_here();
2054 let invalid_here = l10n.invalid_here(&base_msg);
2055 if let Some(region) = pick_cropped_region(regions, &r) {
2056 rendered_regions.push(std::ptr::from_ref(region));
2057 let ctx = crate::de_snippet::Snippet::new(
2058 region.text.as_str(),
2059 region.source_name.as_str(),
2060 crop_radius,
2061 )
2062 .with_offset(region.start_line);
2063 ctx.fmt_or_fallback_with_label(
2064 f,
2065 Level::ERROR,
2066 l10n,
2067 &invalid_here,
2068 label.as_ref(),
2069 &r,
2070 )?;
2071 } else {
2072 fmt_with_location(f, l10n, &invalid_here, &r)?;
2073 }
2074 writeln!(f)?;
2075 if let Some(region) = pick_cropped_region(regions, &d) {
2076 writeln_anchor_intro(f, l10n, d, region)?;
2077 rendered_regions.push(std::ptr::from_ref(region));
2078 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2079 f,
2080 l10n,
2081 &d,
2082 region.text.as_str(),
2083 region.start_line,
2084 l10n.defined_window().as_ref(),
2085 crop_radius,
2086 )?;
2087 } else {
2088 let anchor_intro =
2089 sanitize_message_text(Cow::Owned(l10n.value_comes_from_the_anchor(d)));
2090 writeln!(f, "{anchor_intro}")?;
2091 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
2092 }
2093 }
2094 }
2095
2096 for extra_region in regions {
2097 if rendered_regions.contains(&std::ptr::from_ref(extra_region)) {
2098 continue;
2099 }
2100 writeln!(f)?;
2101 writeln!(f, "included from here:")?;
2102 let extra_ctx = crate::de_snippet::Snippet::new(
2103 extra_region.text.as_str(),
2104 extra_region.source_name.as_str(),
2105 crop_radius,
2106 )
2107 .with_offset(extra_region.start_line);
2108 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2109 }
2110 }
2111 Ok(())
2112}
2113
2114#[cfg(any(feature = "garde", feature = "validator"))]
2115fn fmt_error_with_snippets_offset(
2116 f: &mut fmt::Formatter<'_>,
2117 err: &Error,
2118 regions: &[CroppedRegion],
2119 crop_radius: usize,
2120 formatter: &dyn MessageFormatter,
2121) -> fmt::Result {
2122 if crop_radius == 0 {
2123 return fmt_error_plain_with_formatter(f, err, formatter);
2124 }
2125
2126 if let Error::WithSnippet { .. } = err {
2128 return fmt_error_rendered(f, err, RenderOptions::new(formatter));
2129 }
2130
2131 #[cfg(any(feature = "garde", feature = "validator"))]
2132 if let Error::ValidationError {
2133 source,
2134 issues,
2135 locations,
2136 } = err
2137 {
2138 return fmt_validation_error_with_snippets_offset(
2139 f,
2140 formatter.localizer(),
2141 &source.external_message_source(),
2142 issues,
2143 locations,
2144 regions,
2145 crop_radius,
2146 );
2147 }
2148
2149 let msg = render_message_text(formatter, err);
2150 let Some(location) = err.location() else {
2151 return write!(f, "{msg}");
2152 };
2153 if location == Location::UNKNOWN {
2154 return write!(f, "{msg}");
2155 }
2156
2157 let Some(region) = pick_cropped_region(regions, &location) else {
2158 return fmt_with_location(f, formatter.localizer(), msg.as_ref(), &location);
2159 };
2160 let ctx = crate::de_snippet::Snippet::new(
2161 region.text.as_str(),
2162 region.source_name.as_str(),
2163 crop_radius,
2164 )
2165 .with_offset(region.start_line);
2166 ctx.fmt_or_fallback(
2167 f,
2168 Level::ERROR,
2169 formatter.localizer(),
2170 msg.as_ref(),
2171 &location,
2172 )
2173}
2174
2175#[cfg(feature = "validator")]
2176pub(crate) fn collect_validator_issues(errors: &ValidationErrors) -> Vec<ValidationIssue> {
2177 let mut out = Vec::new();
2178 let root = PathKey::empty();
2179 collect_validator_issues_inner(errors, &root, &mut out);
2180 out
2181}
2182
2183#[cfg(feature = "validator")]
2184fn collect_validator_issues_inner(
2185 errors: &ValidationErrors,
2186 path: &PathKey,
2187 out: &mut Vec<ValidationIssue>,
2188) {
2189 for (field, kind) in errors.errors() {
2190 let field_path = path.clone().join(field.as_ref());
2191 match kind {
2192 ValidationErrorsKind::Field(entries) => {
2193 for entry in entries {
2194 let mut params = Vec::new();
2195 for (k, v) in &entry.params {
2196 params.push((k.to_string(), v.to_string()));
2197 }
2198
2199 out.push(ValidationIssue {
2200 path: field_path.clone(),
2201 code: entry.code.to_string(),
2202 message: entry.message.as_ref().map(std::string::ToString::to_string),
2203 params,
2204 });
2205 }
2206 }
2207 ValidationErrorsKind::Struct(inner) => {
2208 collect_validator_issues_inner(inner, &field_path, out);
2209 }
2210 ValidationErrorsKind::List(list) => {
2211 for (idx, inner) in list {
2212 let index_path = field_path.clone().join(*idx);
2213 collect_validator_issues_inner(inner, &index_path, out);
2214 }
2215 }
2216 }
2217 }
2218}
2219
2220#[cfg(feature = "garde")]
2221pub(crate) fn collect_garde_issues(report: &garde::Report) -> Vec<ValidationIssue> {
2222 let mut out = Vec::new();
2223 for (path, entry) in report.iter() {
2224 out.push(ValidationIssue {
2225 path: path_key_from_garde(path),
2226 code: "garde".to_string(),
2227 message: Some(entry.message().to_string()),
2228 params: Vec::new(),
2229 });
2230 }
2231 out
2232}
2233impl std::error::Error for Error {}
2234
2235#[cold]
2237#[inline(never)]
2238fn maybe_attach_fallback_location(mut err: Error) -> Error {
2239 let loc = MISSING_FIELD_FALLBACK.with(std::cell::Cell::get);
2240 if let Some(loc) = loc
2241 && loc != Location::UNKNOWN
2242 {
2243 err = err.with_location(loc);
2244 }
2245 err
2246}
2247
2248impl de::Error for Error {
2249 #[cold]
2250 #[inline(never)]
2251 fn custom<T: fmt::Display>(msg: T) -> Self {
2252 Error::msg(redact_custom_message(msg.to_string()))
2256 }
2257
2258 #[cold]
2259 #[inline(never)]
2260 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2261 maybe_attach_fallback_location(Error::SerdeInvalidType {
2263 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2264 expected: exp.to_string(),
2265 location: Location::UNKNOWN,
2266 })
2267 }
2268
2269 #[cold]
2270 #[inline(never)]
2271 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2272 maybe_attach_fallback_location(Error::SerdeInvalidValue {
2273 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2274 expected: exp.to_string(),
2275 location: Location::UNKNOWN,
2276 })
2277 }
2278
2279 #[cold]
2280 #[inline(never)]
2281 fn invalid_length(len: usize, exp: &dyn de::Expected) -> Self {
2282 maybe_attach_fallback_location(Error::msg(format!("invalid length {len}, expected {exp}")))
2283 }
2284
2285 #[cold]
2286 #[inline(never)]
2287 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
2288 maybe_attach_fallback_location(Error::SerdeUnknownVariant {
2289 variant: redact_dynamic_identifier(variant, "an interpolated variant"),
2290 expected: expected.to_vec(),
2291 location: Location::UNKNOWN,
2292 })
2293 }
2294
2295 #[cold]
2296 #[inline(never)]
2297 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
2298 maybe_attach_fallback_location(Error::SerdeUnknownField {
2299 field: redact_dynamic_identifier(field, "an interpolated field"),
2300 expected: expected.to_vec(),
2301 location: Location::UNKNOWN,
2302 })
2303 }
2304
2305 #[cold]
2306 #[inline(never)]
2307 fn missing_field(field: &'static str) -> Self {
2308 maybe_attach_fallback_location(Error::SerdeMissingField {
2309 field,
2310 location: Location::UNKNOWN,
2311 })
2312 }
2313}
2314
2315#[cold]
2325#[inline(never)]
2326fn fmt_with_location(
2327 f: &mut fmt::Formatter<'_>,
2328 l10n: &dyn Localizer,
2329 msg: &str,
2330 location: &Location,
2331) -> fmt::Result {
2332 let out = sanitize_message_text(l10n.attach_location(Cow::Borrowed(msg), *location));
2333 write!(f, "{out}")
2334}
2335
2336#[cold]
2347#[inline(never)]
2348pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
2349 Error::Budget {
2350 breach,
2351 location: Location::UNKNOWN,
2352 }
2353}
2354
2355#[cfg(test)]
2356mod tests {
2357 use super::*;
2358
2359 #[test]
2360 fn message_sanitizer_is_selective_and_idempotent() {
2361 let clean = Cow::Borrowed("quotes: '\"', slash: \\, Unicode: ☃");
2362 let sanitized = sanitize_message_text(clean.clone());
2363 assert!(matches!(sanitized, Cow::Borrowed(_)));
2364 assert_eq!(sanitized, clean);
2365
2366 let unsafe_text = Cow::Borrowed(
2367 "nul:\0 tab:\t line:\n esc:\u{1b} del:\u{7f} c1:\u{9b} ls:\u{2028} ps:\u{2029}",
2368 );
2369 let sanitized = sanitize_message_text(unsafe_text);
2370 assert_eq!(
2371 sanitized,
2372 r"nul:\0 tab:\t line:\n esc:\u{1b} del:\u{7f} c1:\u{9b} ls:\u{2028} ps:\u{2029}"
2373 );
2374 assert_eq!(
2375 sanitize_message_text(Cow::Owned(sanitized.clone().into_owned())),
2376 sanitized
2377 );
2378 }
2379
2380 #[test]
2381 fn formatter_newlines_are_semantic_text_not_renderer_layout() {
2382 let error = Error::CyclicInclude {
2383 id: "child.yaml".to_owned(),
2384 stack: vec!["root.yaml".to_owned(), "parent.yaml".to_owned()],
2385 location: Location::UNKNOWN,
2386 };
2387 let formatter = crate::message_formatters::DefaultMessageFormatter;
2388
2389 assert!(formatter.format_message(&error).contains('\n'));
2390 let rendered = error.render_with_formatter(&formatter);
2391 assert_eq!(
2392 rendered,
2393 r"cyclic include detected: child.yaml\nwhile processing include from root.yaml -> parent.yaml"
2394 );
2395 assert!(!rendered.contains('\n'));
2396 }
2397
2398 #[cfg(any(feature = "garde", feature = "validator"))]
2399 #[test]
2400 fn validation_messages_cross_the_same_sanitization_boundary() {
2401 let path = PathKey::empty().join("field\n\u{1b}");
2402 let mut locations = PathMap::new();
2403 locations.insert(
2404 path.clone(),
2405 Locations {
2406 reference_location: Location::new(1, 1),
2407 defined_location: Location::new(1, 1),
2408 },
2409 );
2410 let error = Error::ValidationError {
2411 source: ValidationSource::Validator,
2412 issues: vec![
2413 ValidationIssue::new(path, "bad\u{9b}")
2414 .with_message("invalid\nvalue\u{1b}]0;owned\u{7}"),
2415 ],
2416 locations,
2417 };
2418
2419 let plain = error.render();
2420 assert!(
2421 plain.contains(r"invalid\nvalue\u{1b}]0;owned\u{7}"),
2422 "{plain:?}"
2423 );
2424 assert!(plain.contains(r"field\n\u{1b}"), "{plain:?}");
2425 assert!(!plain.contains("invalid\nvalue\u{1b}"), "{plain:?}");
2426
2427 let snippet = error.with_snippet("field: bad\n", 20).render();
2428 assert!(
2429 snippet.contains(r"invalid\nvalue\u{1b}]0;owned\u{7}"),
2430 "{snippet:?}"
2431 );
2432 assert!(snippet.contains(r"field\n\u{1b}"), "{snippet:?}");
2433 assert!(
2434 snippet.contains('\n'),
2435 "renderer-owned snippet layout should retain newlines: {snippet:?}"
2436 );
2437 assert!(!snippet.contains("invalid\nvalue\u{1b}"), "{snippet:?}");
2438 }
2439
2440 #[test]
2441 fn message_only_input_io_scan_error_uses_portable_fallback() {
2442 let input = core::iter::once(Err::<char, _>(ErrorKind::InputIo {
2443 error: granit_parser::InputIoError::from_message("portable reader failure"),
2444 }));
2445 let scan_error = granit_parser::Parser::new_from_fallible_iter(input)
2446 .find_map(Result::err)
2447 .expect("the source error should be reported");
2448 let error = Error::from_scan_error(scan_error);
2449
2450 match error {
2451 Error::IOError { cause } => {
2452 assert_eq!(cause.kind(), std::io::ErrorKind::Other);
2453 assert_eq!(cause.to_string(), "portable reader failure");
2454 }
2455 other => panic!("expected reader I/O error, got {other:?}"),
2456 }
2457 }
2458
2459 #[rstest::rstest]
2460 #[case::unknown_anchor("while parsing node, found unknown anchor")]
2461 #[case::multiple_documents("multiple documents not supported here")]
2462 fn custom_scan_error_messages_are_not_reclassified_as_builtin_kinds(#[case] message: &str) {
2463 let scan_error = ScanError::new(granit_parser::Marker::new(0, 1, 0), message);
2464 let mapped = Error::from_scan_error(scan_error);
2465
2466 assert!(matches!(
2467 mapped,
2468 Error::ExternalMessage {
2469 ref source,
2470 ref msg,
2471 ..
2472 } if msg == message
2473 && matches!(source.as_ref(), ExternalMessageSource::Parser(error)
2474 if matches!(error.kind(), ErrorKind::Custom(_)))
2475 ));
2476 }
2477
2478 #[test]
2479 fn sanitize_snippet_source_name_replaces_control_chars() {
2480 let sanitized = sanitize_snippet_source_name("evil.yaml\nINJECTED:\u{001b}[31m");
2481 assert_eq!(sanitized, "evil.yaml INJECTED: [31m");
2482 }
2483
2484 #[test]
2485 fn with_snippet_named_sanitizes_source_name() {
2486 let err = Error::Message {
2487 msg: "oops".to_owned(),
2488 location: Location::new(1, 1),
2489 }
2490 .with_snippet_named("x: y\n", "evil.yaml\nINJECTED", 2);
2491
2492 let Error::WithSnippet { regions, .. } = err else {
2493 panic!("expected Error::WithSnippet");
2494 };
2495
2496 assert_eq!(regions.len(), 1);
2497 assert_eq!(regions[0].source_name, "evil.yaml INJECTED");
2498 }
2499
2500 #[test]
2501 fn locations_for_basic_error_duplicates_location() {
2502 let l = Location::new(3, 7);
2503 let err = Error::Message {
2504 msg: "x".to_owned(),
2505 location: l,
2506 };
2507 assert_eq!(
2508 err.locations(),
2509 Some(Locations {
2510 reference_location: l,
2511 defined_location: l,
2512 })
2513 );
2514 }
2515
2516 #[test]
2517 fn merge_key_not_allowed_location_helpers() {
2518 let l = Location::new(4, 2);
2519 let err = Error::MergeKeyNotAllowed { location: l };
2520 assert_eq!(err.location(), Some(l));
2521 assert_eq!(
2522 err.locations(),
2523 Some(Locations {
2524 reference_location: l,
2525 defined_location: l,
2526 })
2527 );
2528
2529 let updated = err.with_location(Location::new(5, 9));
2530 assert_eq!(updated.location(), Some(Location::new(5, 9)));
2531 }
2532
2533 #[test]
2534 fn serde_invalid_type_location_helpers() {
2535 let l = Location::new(4, 2);
2536 let err = Error::SerdeInvalidType {
2537 unexpected: "string".to_owned(),
2538 expected: "an integer".to_owned(),
2539 location: l,
2540 };
2541 assert_eq!(err.location(), Some(l));
2542 assert_eq!(
2543 err.locations(),
2544 Some(Locations {
2545 reference_location: l,
2546 defined_location: l,
2547 })
2548 );
2549
2550 let updated = err.with_location(Location::new(5, 9));
2551 assert_eq!(updated.location(), Some(Location::new(5, 9)));
2552 }
2553
2554 #[test]
2555 fn locations_for_io_error_is_unknown() {
2556 let err = Error::IOError {
2557 cause: std::io::Error::other("x"),
2558 };
2559 assert_eq!(err.locations(), None);
2560 }
2561
2562 #[test]
2563 fn alias_error_returns_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: "test error".to_owned(),
2568 locations: Locations {
2569 reference_location: ref_loc,
2570 defined_location: def_loc,
2571 },
2572 };
2573
2574 assert_eq!(err.location(), Some(ref_loc));
2576
2577 assert_eq!(
2579 err.locations(),
2580 Some(Locations {
2581 reference_location: ref_loc,
2582 defined_location: def_loc,
2583 })
2584 );
2585 }
2586
2587 #[test]
2588 fn alias_error_display_shows_both_locations() {
2589 let ref_loc = Location::new(5, 10);
2590 let def_loc = Location::new(2, 3);
2591 let err = Error::AliasError {
2592 msg: "invalid value".to_owned(),
2593 locations: Locations {
2594 reference_location: ref_loc,
2595 defined_location: def_loc,
2596 },
2597 };
2598
2599 let display = err.to_string();
2600 assert!(display.contains("invalid value"));
2601 assert!(display.contains("line 5"));
2602 assert!(display.contains("column 10"));
2603 assert!(display.contains("line 2"));
2604 assert!(display.contains("column 3"));
2605 }
2606
2607 #[test]
2608 fn alias_error_display_with_same_locations() {
2609 let loc = Location::new(3, 7);
2610 let err = Error::AliasError {
2611 msg: "test".to_owned(),
2612 locations: Locations {
2613 reference_location: loc,
2614 defined_location: loc,
2615 },
2616 };
2617
2618 let display = err.to_string();
2619 assert!(display.contains("line 3"));
2621 assert!(display.contains("column 7"));
2622 assert!(!display.contains("defined at"));
2624 }
2625
2626 #[test]
2627 fn with_snippet_counts_trailing_empty_line_for_end_line() {
2628 let text = "a\n";
2630 let err = Error::Message {
2631 msg: "x".to_owned(),
2632 location: Location::new(2, 1),
2633 };
2634
2635 let wrapped = err.with_snippet(text, 50);
2636 let Error::WithSnippet { regions, .. } = wrapped else {
2637 panic!("expected WithSnippet wrapper");
2638 };
2639 assert_eq!(regions.len(), 1);
2640 assert_eq!(regions[0].start_line, 1);
2641 assert_eq!(regions[0].end_line, 2);
2642 }
2643
2644 #[test]
2645 fn with_snippet_offset_counts_trailing_empty_line_for_end_line() {
2646 let text = "a\n";
2648 let err = Error::Message {
2649 msg: "x".to_owned(),
2650 location: Location::new(11, 1),
2651 };
2652
2653 let wrapped = err.with_snippet_offset_named(text, 10, "<input>", 50);
2654 let Error::WithSnippet { regions, .. } = wrapped else {
2655 panic!("expected WithSnippet wrapper");
2656 };
2657 assert_eq!(regions.len(), 1);
2658 assert_eq!(regions[0].start_line, 10);
2659 assert_eq!(regions[0].end_line, 11);
2660 }
2661
2662 #[cfg(feature = "validator")]
2663 #[test]
2664 fn locations_for_validator_error_uses_first_entry() {
2665 use validator::Validate;
2666
2667 #[derive(Debug, Validate)]
2668 struct Cfg {
2669 #[validate(length(min = 2))]
2670 second_string: String,
2671 }
2672
2673 let cfg = Cfg {
2674 second_string: "x".to_owned(),
2675 };
2676 let errors = cfg.validate().expect_err("validation error expected");
2677
2678 let referenced_loc = Location::new(3, 15);
2679 let defined_loc = Location::new(2, 18);
2680
2681 let mut locations = PathMap::new();
2682 locations.insert(
2683 PathKey::empty().join("secondString"),
2684 Locations {
2685 reference_location: referenced_loc,
2686 defined_location: defined_loc,
2687 },
2688 );
2689
2690 let err = Error::ValidationError {
2691 source: ValidationSource::Validator,
2692 issues: crate::de_error::collect_validator_issues(&errors),
2693 locations,
2694 };
2695 assert_eq!(
2696 err.locations(),
2697 Some(Locations {
2698 reference_location: referenced_loc,
2699 defined_location: defined_loc,
2700 })
2701 );
2702 }
2703
2704 #[cfg(feature = "validator")]
2705 #[test]
2706 fn validator_error_uses_ancestor_path_location_fallback() {
2707 let yaml = "parent:\n child: bad\n";
2708 let referenced_loc = Location::new(1, 1);
2709 let defined_loc = Location::new(1, 1);
2710
2711 let mut locations = PathMap::new();
2712 locations.insert(
2713 PathKey::empty().join("parent"),
2714 Locations {
2715 reference_location: referenced_loc,
2716 defined_location: defined_loc,
2717 },
2718 );
2719
2720 let err = Error::ValidationError {
2721 source: ValidationSource::Validator,
2722 issues: vec![ValidationIssue {
2723 path: PathKey::empty().join("parent").join("child").join("value"),
2724 code: "custom".to_owned(),
2725 message: Some("custom validation failed".to_owned()),
2726 params: Vec::new(),
2727 }],
2728 locations,
2729 };
2730
2731 assert_eq!(err.location(), Some(referenced_loc));
2732 assert_eq!(
2733 err.locations(),
2734 Some(Locations {
2735 reference_location: referenced_loc,
2736 defined_location: defined_loc,
2737 })
2738 );
2739
2740 let rendered = err.with_snippet(yaml, 20).render();
2741 assert!(
2742 rendered.contains("custom validation failed"),
2743 "expected validation message, got: {rendered}"
2744 );
2745 assert!(
2746 rendered.contains("for `parent.child.value`"),
2747 "expected original validation path, got: {rendered}"
2748 );
2749 assert!(
2750 rendered.contains("line 1 column 1"),
2751 "expected ancestor location, got: {rendered}"
2752 );
2753 assert!(
2754 rendered.contains("1 | parent:"),
2755 "expected snippet around ancestor path, got: {rendered}"
2756 );
2757 }
2758
2759 #[test]
2760 fn nested_snippet_preserves_custom_formatter() {
2761 struct Custom;
2762 impl MessageFormatter for Custom {
2763 fn localizer(&self) -> &dyn Localizer {
2764 &DEFAULT_ENGLISH_LOCALIZER
2765 }
2766 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
2767 match err {
2768 Error::Message { msg, .. } => Cow::Owned(format!("CUSTOM: {}", msg.as_str())),
2769 _ => Cow::Borrowed(""),
2770 }
2771 }
2772 }
2773 let loc = Location::new(1, 1);
2774 let base = Error::Message {
2775 msg: "original".to_string(),
2776 location: loc,
2777 };
2778 let text = "input";
2779 let start_line = 1;
2780 let radius = 1;
2781 let inner = base.with_snippet_offset_named(text, start_line, "<input>", radius);
2782 let outer = inner.with_snippet_offset_named(text, start_line, "<input>", radius);
2783 let rendered = outer.render_with_options(RenderOptions::new(&Custom));
2784 assert!(rendered.contains("CUSTOM: original"));
2785 }
2786
2787 #[test]
2788 fn alias_error_dual_snippet_rendering() {
2789 let yaml = r#"config:
2791 anchor: &myval 42
2792 other: stuff
2793 more: data
2794 use_it: *myval
2795"#;
2796 let ref_loc = Location::new(5, 11);
2798 let def_loc = Location::new(2, 11);
2800
2801 let err = Error::AliasError {
2802 msg: "invalid value type".to_owned(),
2803 locations: Locations {
2804 reference_location: ref_loc,
2805 defined_location: def_loc,
2806 },
2807 };
2808
2809 let wrapped = err.with_snippet(yaml, 5);
2811 let rendered = wrapped.render();
2812
2813 assert!(
2815 rendered.contains("invalid value type"),
2816 "rendered: {}",
2817 rendered
2818 );
2819
2820 assert!(
2823 !rendered.contains("(defined at line"),
2824 "did not expect alias defined-at suffix when secondary window is present: {}",
2825 rendered
2826 );
2827 assert!(
2829 rendered.contains("the value is used here") || rendered.contains("use_it"),
2830 "rendered should show reference location context: {}",
2831 rendered
2832 );
2833 assert!(
2835 rendered.contains("defined here") || rendered.contains("anchor"),
2836 "rendered should show defined location context: {}",
2837 rendered
2838 );
2839 assert!(
2841 rendered.contains('5') || rendered.contains("use_it"),
2842 "rendered should reference line 5: {}",
2843 rendered
2844 );
2845 assert!(
2846 rendered.contains('2') || rendered.contains("anchor"),
2847 "rendered should reference line 2: {}",
2848 rendered
2849 );
2850 }
2851
2852 #[test]
2853 fn alias_error_same_location_single_snippet() {
2854 let yaml = "value: &anchor 42\n";
2855 let loc = Location::new(1, 8);
2856
2857 let err = Error::AliasError {
2858 msg: "test error".to_owned(),
2859 locations: Locations {
2860 reference_location: loc,
2861 defined_location: loc,
2862 },
2863 };
2864
2865 let wrapped = err.with_snippet(yaml, 5);
2866 let rendered = wrapped.render();
2867
2868 assert!(rendered.contains("test error"), "rendered: {}", rendered);
2870 assert!(
2872 !rendered.contains("defined here"),
2873 "should not show 'defined here' when locations are same: {}",
2874 rendered
2875 );
2876 assert!(
2877 !rendered.contains("the value is used here"),
2878 "should not show 'value used here' when locations are same: {}",
2879 rendered
2880 );
2881 }
2882}