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::{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>;
101}
102
103#[derive(Debug, Default, Clone, Copy)]
115pub struct UserMessageFormatter;
116
117#[non_exhaustive]
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum SnippetMode {
121 Auto,
123 Off,
125}
126
127#[non_exhaustive]
150#[derive(Clone, Copy)]
151pub struct RenderOptions<'a> {
152 pub formatter: &'a dyn MessageFormatter,
154 pub snippets: SnippetMode,
156}
157
158impl<'a> Default for RenderOptions<'a> {
159 #[inline]
160 fn default() -> Self {
161 static DEFAULT_FMT: crate::message_formatters::DefaultMessageFormatter =
163 crate::message_formatters::DefaultMessageFormatter;
164
165 Self::new(&DEFAULT_FMT)
166 }
167}
168
169impl<'a> RenderOptions<'a> {
170 #[inline]
176 pub fn new(formatter: &'a dyn MessageFormatter) -> Self {
177 Self {
178 formatter,
179 snippets: SnippetMode::Auto,
180 }
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct CroppedRegion {
190 pub text: String,
192 pub source_name: String,
194 pub start_line: usize,
196 pub end_line: usize,
198 pub location: Location,
200}
201
202impl CroppedRegion {
203 fn covers_exact_source(&self, location: &Location) -> bool {
204 if location == &Location::UNKNOWN {
205 return false;
206 }
207 let source_id = location.source_id();
208 source_id != 0 && self.location.source_id() == source_id && self.covers_line(location)
209 }
210
211 fn covers_line(&self, location: &Location) -> bool {
212 let line = location.line as usize;
213 self.start_line <= line && line <= self.end_line
214 }
215
216 fn covers(&self, location: &Location) -> bool {
217 if location == &Location::UNKNOWN {
218 return false;
219 }
220 if !self.covers_line(location) {
221 return false;
222 }
223 let region_source_id = self.location.source_id();
224 let location_source_id = location.source_id();
225 region_source_id == 0 || location_source_id == 0 || region_source_id == location_source_id
226 }
227}
228
229fn line_count_including_trailing_empty_line(text: &str) -> usize {
230 let mut lines = text.split_terminator('\n').count().max(1);
231 if text.ends_with('\n') {
232 lines = lines.saturating_add(1);
233 }
234 lines
235}
236
237fn sanitize_snippet_source_name(name: &str) -> Cow<'_, str> {
238 if !name.chars().any(char::is_control) {
239 return Cow::Borrowed(name);
240 }
241
242 let sanitized: String = name
243 .chars()
244 .map(|ch| if ch.is_control() { ' ' } else { ch })
245 .collect();
246 Cow::Owned(sanitized)
247}
248
249fn cropped_region_for_location(
250 text: &str,
251 source_name: &str,
252 location: &Location,
253 mapping: crate::de_snippet::LineMapping,
254 crop_radius: usize,
255) -> Option<CroppedRegion> {
256 if crop_radius == 0 || *location == Location::UNKNOWN {
257 return None;
258 }
259
260 let (cropped, start_line) =
261 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
262 if cropped.is_empty() {
263 return None;
264 }
265
266 let lines = line_count_including_trailing_empty_line(cropped.as_str());
267 let end_line = start_line.saturating_add(lines.saturating_sub(1));
268 Some(CroppedRegion {
269 text: cropped,
270 source_name: source_name.to_string(),
271 start_line,
272 end_line,
273 location: *location,
274 })
275}
276
277fn push_region_for_location(
278 regions: &mut Vec<CroppedRegion>,
279 text: &str,
280 source_name: &str,
281 location: &Location,
282 mapping: crate::de_snippet::LineMapping,
283 crop_radius: usize,
284) {
285 if let Some(region) =
286 cropped_region_for_location(text, source_name, location, mapping, crop_radius)
287 {
288 regions.push(region);
289 }
290}
291
292fn push_regions_for_locations(
293 regions: &mut Vec<CroppedRegion>,
294 text: &str,
295 source_name: &str,
296 locations: Locations,
297 mapping: crate::de_snippet::LineMapping,
298 crop_radius: usize,
299) {
300 push_region_for_location(
301 regions,
302 text,
303 source_name,
304 &locations.reference_location,
305 mapping,
306 crop_radius,
307 );
308 if locations.defined_location != locations.reference_location {
309 push_region_for_location(
310 regions,
311 text,
312 source_name,
313 &locations.defined_location,
314 mapping,
315 crop_radius,
316 );
317 }
318}
319
320#[cfg(any(feature = "garde", feature = "validator"))]
321fn push_validation_issue_regions(
322 regions: &mut Vec<CroppedRegion>,
323 issues: &[ValidationIssue],
324 locations: &PathMap,
325 text: &str,
326 source_name: &str,
327 mapping: crate::de_snippet::LineMapping,
328 crop_radius: usize,
329) {
330 for issue in issues {
331 let (locs, _) = locations
332 .search_with_ancestor_fallback(&issue.path)
333 .unwrap_or((Locations::UNKNOWN, String::new()));
334 push_regions_for_locations(regions, text, source_name, locs, mapping, crop_radius);
335 }
336}
337
338fn collect_snippet_regions(
339 inner: &Error,
340 text: &str,
341 source_name: &str,
342 mapping: crate::de_snippet::LineMapping,
343 crop_radius: usize,
344) -> Vec<CroppedRegion> {
345 let mut regions = Vec::new();
346
347 #[cfg(any(feature = "garde", feature = "validator"))]
350 if let Error::ValidationError {
351 issues, locations, ..
352 } = inner
353 {
354 push_validation_issue_regions(
355 &mut regions,
356 issues,
357 locations,
358 text,
359 source_name,
360 mapping,
361 crop_radius,
362 );
363 }
364
365 if regions.is_empty() {
368 if let Some(locs) = inner.locations() {
369 push_regions_for_locations(&mut regions, text, source_name, locs, mapping, crop_radius);
370 } else if let Some(loc) = inner.location() {
371 push_region_for_location(&mut regions, text, source_name, &loc, mapping, crop_radius);
372 }
373 }
374
375 regions
376}
377
378#[cfg(any(feature = "garde", feature = "validator"))]
379#[derive(Debug, Clone)]
380pub struct ValidationIssue {
381 pub path: PathKey,
382 pub code: String,
383 pub message: Option<String>,
384 pub params: Vec<(String, String)>,
385}
386
387#[cfg(any(feature = "garde", feature = "validator"))]
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub enum ValidationSource {
390 Garde,
391 Validator,
392}
393
394#[cfg(any(feature = "garde", feature = "validator"))]
395impl ValidationSource {
396 pub(crate) fn external_message_source(self) -> ExternalMessageSource {
397 match self {
398 ValidationSource::Garde => ExternalMessageSource::Garde,
399 ValidationSource::Validator => ExternalMessageSource::Validator,
400 }
401 }
402}
403
404#[cfg(any(feature = "garde", feature = "validator"))]
405impl ValidationIssue {
406 pub(crate) fn display_entry(&self) -> String {
407 if let Some(msg) = &self.message {
408 return msg.clone();
409 }
410
411 if self.params.is_empty() {
412 return self.code.clone();
413 }
414
415 let mut params = String::new();
416 for (i, (k, v)) in self.params.iter().enumerate() {
417 if i > 0 {
418 params.push_str(", ");
419 }
420 params.push_str(k);
421 params.push('=');
422 params.push_str(v);
423 }
424 format!("{} ({params})", self.code)
425 }
426
427 pub(crate) fn display_entry_overridden(
428 &self,
429 l10n: &dyn Localizer,
430 source: ExternalMessageSource,
431 ) -> String {
432 let raw = self.display_entry();
433 let overridden = l10n
434 .override_external_message(ExternalMessage {
435 source,
436 original: raw.as_str(),
437 code: Some(self.code.as_str()),
438 params: &self.params,
439 })
440 .unwrap_or(Cow::Borrowed(raw.as_str()));
441 overridden.into_owned()
442 }
443}
444
445#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
446fn replace_known_effectives(
447 mut text: String,
448 ctxs: &[crate::properties_redaction::ScalarRedactionCtx],
449) -> String {
450 let mut pairs: Vec<&crate::properties_redaction::ScalarRedactionCtx> = ctxs
451 .iter()
452 .filter(|ctx| !ctx.effective.is_empty())
453 .collect();
454
455 pairs.sort_by_key(|ctx| std::cmp::Reverse(ctx.effective.len()));
456
457 for ctx in pairs {
458 if text.contains(&ctx.effective) {
459 text = text.replace(&ctx.effective, &ctx.raw);
460 }
461 }
462
463 text
464}
465
466#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
467pub(crate) fn redact_issue(mut issue: ValidationIssue) -> ValidationIssue {
468 with_interp_redaction(|pairs| {
469 if pairs.is_empty() {
470 return issue;
471 }
472
473 if let Some(msg) = issue.message.take() {
474 issue.message = Some(redact_with_ctxs(msg, pairs, "invalid interpolated value"));
475 }
476
477 issue.code = replace_known_effectives(std::mem::take(&mut issue.code), pairs);
478
479 for (key, value) in &mut issue.params {
480 *key = replace_known_effectives(std::mem::take(key), pairs);
481 *value = redact_with_ctxs(std::mem::take(value), pairs, "<redacted>");
482 }
483
484 issue
485 })
486}
487
488#[cfg(all(
489 not(feature = "properties"),
490 any(feature = "garde", feature = "validator")
491))]
492pub(crate) fn redact_issue(issue: ValidationIssue) -> ValidationIssue {
493 issue
494}
495
496thread_local! {
504 static MISSING_FIELD_FALLBACK: Cell<Option<Location>> = const { Cell::new(None) };
505}
506
507pub(crate) struct MissingFieldLocationGuard {
510 prev: Option<Location>,
511}
512
513impl MissingFieldLocationGuard {
514 pub(crate) fn new(location: Location) -> Self {
515 let prev = MISSING_FIELD_FALLBACK.with(|c| c.replace(Some(location)));
516 Self { prev }
517 }
518
519 pub(crate) fn replace_location(&mut self, location: Location) {
521 MISSING_FIELD_FALLBACK.with(|c| c.set(Some(location)));
522 }
523}
524
525impl Drop for MissingFieldLocationGuard {
526 fn drop(&mut self) {
527 MISSING_FIELD_FALLBACK.with(|c| c.set(self.prev));
528 }
529}
530
531#[non_exhaustive]
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537pub enum TransformReason {
538 EscapeSequence,
540 LineFolding,
542 MultiLineNormalization,
544 BlockScalarProcessing,
546 SingleQuoteEscape,
548 InputNotBorrowable,
552
553 ParserReturnedOwned,
559
560 VariableInterpolation,
562}
563
564impl fmt::Display for TransformReason {
565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566 match self {
567 TransformReason::EscapeSequence => write!(f, "escape sequence processing"),
568 TransformReason::LineFolding => write!(f, "line folding"),
569 TransformReason::MultiLineNormalization => {
570 write!(f, "multi-line whitespace normalization")
571 }
572 TransformReason::BlockScalarProcessing => write!(f, "block scalar processing"),
573 TransformReason::SingleQuoteEscape => write!(f, "single-quote escape processing"),
574 TransformReason::InputNotBorrowable => {
575 write!(f, "input is not available for borrowing")
576 }
577 TransformReason::ParserReturnedOwned => write!(f, "parser returned an owned string"),
578 TransformReason::VariableInterpolation => write!(f, "variable interpolation"),
579 }
580 }
581}
582
583#[non_exhaustive]
585#[derive(Debug)]
586pub enum Error {
587 Message {
589 msg: String,
590 location: Location,
591 },
592
593 ExternalMessage {
598 source: ExternalMessageSource,
599 msg: String,
600 code: Option<String>,
602 params: Vec<(String, String)>,
604 location: Location,
605 },
606 Eof {
608 location: Location,
609 },
610 MultipleDocuments {
615 hint: &'static str,
617 location: Location,
618 },
619 Unexpected {
621 expected: &'static str,
622 location: Location,
623 },
624
625 MergeValueNotMapOrSeqOfMaps {
627 location: Location,
628 },
629
630 MergeKeyNotAllowed {
632 location: Location,
633 },
634
635 InvalidBinaryBase64 {
637 location: Location,
638 },
639
640 BinaryNotUtf8 {
642 location: Location,
643 },
644
645 TaggedScalarCannotDeserializeIntoString {
647 location: Location,
648 },
649
650 UnexpectedSequenceEnd {
652 location: Location,
653 },
654
655 UnexpectedMappingEnd {
657 location: Location,
658 },
659
660 InvalidBooleanStrict {
662 location: Location,
663 },
664
665 InvalidCharNull {
667 location: Location,
668 },
669
670 InvalidCharNotSingleScalar {
672 location: Location,
673 },
674
675 NullIntoString {
677 location: Location,
678 },
679
680 BytesNotSupportedMissingBinaryTag {
682 location: Location,
683 },
684
685 UnexpectedValueForUnit {
687 location: Location,
688 },
689
690 ExpectedEmptyMappingForUnitStruct {
692 location: Location,
693 },
694
695 UnexpectedContainerEndWhileSkippingNode {
697 location: Location,
698 },
699
700 InternalSeedReusedForMapKey {
702 location: Location,
703 },
704
705 ValueRequestedBeforeKey {
707 location: Location,
708 },
709
710 ExpectedStringKeyForExternallyTaggedEnum {
712 location: Location,
713 },
714
715 ExternallyTaggedEnumExpectedScalarOrMapping {
717 location: Location,
718 },
719
720 UnexpectedValueForUnitEnumVariant {
722 location: Location,
723 },
724
725 InvalidUtf8Input,
727
728 AliasReplayCounterOverflow {
730 location: Location,
731 },
732
733 AliasReplayLimitExceeded {
735 total_replayed_events: usize,
736 max_total_replayed_events: usize,
737 location: Location,
738 },
739
740 AliasExpansionLimitExceeded {
742 anchor_id: usize,
743 expansions: usize,
744 max_expansions_per_anchor: usize,
745 location: Location,
746 },
747
748 AliasReplayStackDepthExceeded {
750 depth: usize,
751 max_depth: usize,
752 location: Location,
753 },
754
755 FoldedBlockScalarMustIndentContent {
757 location: Location,
758 },
759
760 InternalDepthUnderflow {
762 location: Location,
763 },
764
765 InternalRecursionStackEmpty {
767 location: Location,
768 },
769
770 RecursiveReferencesRequireWeakTypes {
772 location: Location,
773 },
774
775 InvalidScalar {
777 ty: &'static str,
778 location: Location,
779 },
780
781 SerdeInvalidType {
783 unexpected: String,
784 expected: String,
785 location: Location,
786 },
787
788 SerdeInvalidValue {
790 unexpected: String,
791 expected: String,
792 location: Location,
793 },
794
795 SerdeUnknownVariant {
797 variant: String,
798 expected: Vec<&'static str>,
799 location: Location,
800 },
801
802 SerdeUnknownField {
804 field: String,
805 expected: Vec<&'static str>,
806 location: Location,
807 },
808
809 SerdeMissingField {
811 field: &'static str,
812 location: Location,
813 },
814
815 UnexpectedContainerEndWhileReadingKeyNode {
819 location: Location,
820 },
821
822 DuplicateMappingKey {
826 key: Option<String>,
827 location: Location,
828 },
829
830 TaggedEnumMismatch {
832 tagged: String,
833 target: &'static str,
834 location: Location,
835 },
836
837 SerdeVariantId {
839 msg: String,
840 location: Location,
841 },
842
843 ExpectedMappingEndAfterEnumVariantValue {
845 location: Location,
846 },
847 ContainerEndMismatch {
848 location: Location,
849 },
850 UnknownAnchor {
852 location: Location,
853 },
854 CyclicInclude {
856 id: String,
857 stack: Vec<String>,
858 location: Location,
859 },
860 UnsupportedIncludeForm {
862 location: Location,
863 },
864 ResolverError {
866 target: String,
867 error: IncludeResolveError,
868 stack: Vec<String>,
869 location: Location,
870 },
871 AliasError {
876 msg: String,
877 locations: Locations,
878 },
879 HookError {
882 msg: String,
883 location: Location,
884 },
885 UnresolvedProperty {
887 name: String,
889 location: Location,
890 },
891 InvalidPropertyName {
893 name: String,
895 location: Location,
896 },
897 PropertyRequiredButUnset {
900 name: String,
901 message: String,
902 location: Location,
903 },
904 PropertyRequiredButEmpty {
907 name: String,
908 message: String,
909 location: Location,
910 },
911 Budget {
913 breach: BudgetBreach,
914 location: Location,
915 },
916 IOError {
918 cause: std::io::Error,
919 },
920 QuotingRequired {
923 value: String, location: Location,
925 },
926
927 CannotBorrowTransformedString {
933 reason: TransformReason,
935 location: Location,
936 },
937
938 IndentationError {
940 required: crate::indentation::RequireIndent,
942 actual: usize,
944 location: Location,
945 },
946
947 WithSnippet {
949 regions: Vec<CroppedRegion>,
954 crop_radius: usize,
955 error: Box<Error>,
956 },
957
958 #[cfg(any(feature = "garde", feature = "validator"))]
960 ValidationError {
961 source: ValidationSource,
962 issues: Vec<ValidationIssue>,
963 locations: PathMap,
964 },
965
966 #[cfg(any(feature = "garde", feature = "validator"))]
968 ValidationErrors {
969 source: ValidationSource,
970 errors: Vec<Error>,
971 },
972}
973
974impl Error {
975 #[cold]
976 #[inline(never)]
977 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
978 self.with_snippet_named(text, "<input>", crop_radius)
979 }
980
981 #[cold]
982 #[inline(never)]
983 pub(crate) fn with_snippet_named(
984 self,
985 text: &str,
986 source_name: &str,
987 crop_radius: usize,
988 ) -> Self {
989 let source_name = sanitize_snippet_source_name(source_name);
990
991 let inner = match self {
994 Error::WithSnippet { error, .. } => *error,
995 other => other,
996 };
997
998 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1000
1001 let regions = collect_snippet_regions(
1002 &inner,
1003 text,
1004 source_name.as_ref(),
1005 crate::de_snippet::LineMapping::Identity,
1006 crop_radius,
1007 );
1008
1009 Error::WithSnippet {
1010 regions,
1011 crop_radius,
1012 error: Box::new(inner),
1013 }
1014 }
1015
1016 #[cfg(feature = "include")]
1017 #[cold]
1018 #[inline(never)]
1019 pub(crate) fn with_additional_snippet_named(
1020 mut self,
1021 text: &str,
1022 source_name: &str,
1023 location: &Location,
1024 crop_radius: usize,
1025 ) -> Self {
1026 let source_name = sanitize_snippet_source_name(source_name);
1027
1028 if crop_radius == 0 || *location == Location::UNKNOWN {
1029 return self;
1030 }
1031
1032 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1033 let mapping = crate::de_snippet::LineMapping::Identity;
1034
1035 let Some(region) =
1036 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1037 else {
1038 return self;
1039 };
1040
1041 if let Error::WithSnippet {
1042 ref mut regions, ..
1043 } = self
1044 {
1045 regions.push(region);
1046 }
1047 self
1048 }
1049
1050 #[cfg(feature = "include")]
1051 #[cold]
1052 #[inline(never)]
1053 pub(crate) fn with_additional_snippet_offset_named(
1054 mut self,
1055 text: &str,
1056 start_line: usize,
1057 source_name: &str,
1058 location: &Location,
1059 crop_radius: usize,
1060 ) -> Self {
1061 let source_name = sanitize_snippet_source_name(source_name);
1062
1063 if crop_radius == 0 || *location == Location::UNKNOWN {
1064 return self;
1065 }
1066
1067 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1068 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1069
1070 let Some(region) =
1071 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1072 else {
1073 return self;
1074 };
1075
1076 if let Error::WithSnippet {
1077 ref mut regions, ..
1078 } = self
1079 {
1080 regions.push(region);
1081 }
1082 self
1083 }
1084
1085 #[cold]
1086 #[inline(never)]
1087 pub(crate) fn with_snippet_offset_named(
1088 self,
1089 text: &str,
1090 start_line: usize,
1091 source_name: &str,
1092 crop_radius: usize,
1093 ) -> Self {
1094 let source_name = sanitize_snippet_source_name(source_name);
1095
1096 let inner = match self {
1097 Error::WithSnippet { error, .. } => *error,
1098 other => other,
1099 };
1100
1101 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1103
1104 let regions = collect_snippet_regions(
1105 &inner,
1106 text,
1107 source_name.as_ref(),
1108 crate::de_snippet::LineMapping::Offset { start_line },
1109 crop_radius,
1110 );
1111
1112 Error::WithSnippet {
1113 regions,
1114 crop_radius,
1115 error: Box::new(inner),
1116 }
1117 }
1118
1119 pub fn without_snippet(&self) -> &Self {
1121 match self {
1122 Error::WithSnippet { error, .. } => error,
1123 other => other,
1124 }
1125 }
1126
1127 pub fn render(&self) -> String {
1133 self.render_with_options(RenderOptions::default())
1134 }
1135
1136 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
1138 self.render_with_options(RenderOptions {
1139 formatter,
1140 snippets: SnippetMode::Auto,
1141 })
1142 }
1143
1144 pub fn render_with_options(&self, options: RenderOptions<'_>) -> String {
1146 struct RenderDisplay<'a> {
1147 err: &'a Error,
1148 options: RenderOptions<'a>,
1149 }
1150
1151 impl fmt::Display for RenderDisplay<'_> {
1152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1153 fmt_error_rendered(f, self.err, self.options)
1154 }
1155 }
1156
1157 RenderDisplay { err: self, options }.to_string()
1158 }
1159
1160 #[cold]
1171 #[inline(never)]
1172 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
1173 Error::Message {
1174 msg: s.into(),
1175 location: Location::UNKNOWN,
1176 }
1177 }
1178
1179 #[cold]
1183 #[inline(never)]
1184 pub(crate) fn quoting_required(value: &str, interpolated: bool) -> Self {
1185 let location = Location::UNKNOWN;
1188 let value = if !interpolated
1189 && (parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
1190 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
1191 || parse_yaml11_bool(value).is_ok()
1192 || scalar_is_nullish(value, &ScalarStyle::Plain))
1193 {
1194 value.to_string()
1195 } else {
1196 String::new()
1197 };
1198 Error::QuotingRequired { value, location }
1199 }
1200
1201 #[cold]
1212 #[inline(never)]
1213 pub(crate) fn unexpected(what: &'static str) -> Self {
1214 Error::Unexpected {
1215 expected: what,
1216 location: Location::UNKNOWN,
1217 }
1218 }
1219
1220 #[cold]
1225 #[inline(never)]
1226 pub(crate) fn eof() -> Self {
1227 Error::Eof {
1228 location: Location::UNKNOWN,
1229 }
1230 }
1231
1232 #[cold]
1233 #[inline(never)]
1234 pub(crate) fn multiple_documents(hint: &'static str) -> Self {
1235 Error::MultipleDocuments {
1236 hint,
1237 location: Location::UNKNOWN,
1238 }
1239 }
1240
1241 #[cfg(any(feature = "garde", feature = "validator"))]
1242 pub(crate) fn validation_error(
1243 source: ValidationSource,
1244 issues: Vec<ValidationIssue>,
1245 locations: PathMap,
1246 ) -> Self {
1247 Error::ValidationError {
1248 source,
1249 issues,
1250 locations,
1251 }
1252 }
1253
1254 #[cfg(any(feature = "garde", feature = "validator"))]
1255 pub(crate) fn validation_errors(source: ValidationSource, errors: Vec<Error>) -> Self {
1256 Error::ValidationErrors { source, errors }
1257 }
1258
1259 #[cfg(any(feature = "garde", feature = "validator"))]
1260 pub(crate) fn is_validation_error(&self) -> bool {
1261 matches!(self, Error::ValidationError { .. })
1262 }
1263
1264 #[cold]
1269 #[inline(never)]
1270 pub(crate) fn unknown_anchor() -> Self {
1271 Error::UnknownAnchor {
1272 location: Location::UNKNOWN,
1273 }
1274 }
1275
1276 #[cold]
1281 #[inline(never)]
1282 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
1283 Error::CannotBorrowTransformedString {
1284 reason,
1285 location: Location::UNKNOWN,
1286 }
1287 }
1288
1289 #[cold]
1300 #[inline(never)]
1301 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
1302 match &mut self {
1303 Error::Message { location, .. }
1304 | Error::ExternalMessage { location, .. }
1305 | Error::Eof { location }
1306 | Error::MultipleDocuments { location, .. }
1307 | Error::Unexpected { location, .. }
1308 | Error::MergeValueNotMapOrSeqOfMaps { location }
1309 | Error::MergeKeyNotAllowed { location }
1310 | Error::InvalidBinaryBase64 { location }
1311 | Error::BinaryNotUtf8 { location }
1312 | Error::TaggedScalarCannotDeserializeIntoString { location }
1313 | Error::UnexpectedSequenceEnd { location }
1314 | Error::UnexpectedMappingEnd { location }
1315 | Error::InvalidBooleanStrict { location }
1316 | Error::InvalidCharNull { location }
1317 | Error::InvalidCharNotSingleScalar { location }
1318 | Error::NullIntoString { location }
1319 | Error::BytesNotSupportedMissingBinaryTag { location }
1320 | Error::UnexpectedValueForUnit { location }
1321 | Error::ExpectedEmptyMappingForUnitStruct { location }
1322 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1323 | Error::InternalSeedReusedForMapKey { location }
1324 | Error::ValueRequestedBeforeKey { location }
1325 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1326 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1327 | Error::UnexpectedValueForUnitEnumVariant { location }
1328 | Error::AliasReplayCounterOverflow { location }
1329 | Error::AliasReplayLimitExceeded { location, .. }
1330 | Error::AliasExpansionLimitExceeded { location, .. }
1331 | Error::AliasReplayStackDepthExceeded { location, .. }
1332 | Error::FoldedBlockScalarMustIndentContent { location }
1333 | Error::InternalDepthUnderflow { location }
1334 | Error::InternalRecursionStackEmpty { location }
1335 | Error::RecursiveReferencesRequireWeakTypes { location }
1336 | Error::InvalidScalar { location, .. }
1337 | Error::SerdeInvalidType { location, .. }
1338 | Error::SerdeInvalidValue { location, .. }
1339 | Error::SerdeUnknownVariant { location, .. }
1340 | Error::SerdeUnknownField { location, .. }
1341 | Error::SerdeMissingField { location, .. }
1342 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1343 | Error::DuplicateMappingKey { location, .. }
1344 | Error::TaggedEnumMismatch { location, .. }
1345 | Error::SerdeVariantId { location, .. }
1346 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1347 | Error::HookError { location, .. }
1348 | Error::UnresolvedProperty { location, .. }
1349 | Error::InvalidPropertyName { location, .. }
1350 | Error::PropertyRequiredButUnset { location, .. }
1351 | Error::PropertyRequiredButEmpty { location, .. }
1352 | Error::ContainerEndMismatch { location, .. }
1353 | Error::UnknownAnchor { location, .. }
1354 | Error::CyclicInclude { location, .. }
1355 | Error::UnsupportedIncludeForm { location, .. }
1356 | Error::ResolverError { location, .. }
1357 | Error::QuotingRequired { location, .. }
1358 | Error::Budget { location, .. }
1359 | Error::CannotBorrowTransformedString { location, .. }
1360 | Error::IndentationError { location, .. } => {
1361 *location = set_location;
1362 }
1363 Error::InvalidUtf8Input => {}
1364 Error::IOError { .. } => {} Error::AliasError { .. } => {
1366 }
1368 Error::WithSnippet { error, .. } => {
1369 let inner = *std::mem::replace(error, Box::new(Error::eof()));
1370 **error = inner.with_location(set_location);
1371 }
1372 #[cfg(any(feature = "garde", feature = "validator"))]
1373 Error::ValidationError { .. } => {
1374 }
1376 #[cfg(any(feature = "garde", feature = "validator"))]
1377 Error::ValidationErrors { .. } => {
1378 }
1380 }
1381 self
1382 }
1383
1384 pub fn location(&self) -> Option<Location> {
1392 match self {
1393 Error::Message { location, .. }
1394 | Error::ExternalMessage { location, .. }
1395 | Error::Eof { location }
1396 | Error::MultipleDocuments { location, .. }
1397 | Error::Unexpected { location, .. }
1398 | Error::MergeValueNotMapOrSeqOfMaps { location }
1399 | Error::MergeKeyNotAllowed { location }
1400 | Error::InvalidBinaryBase64 { location }
1401 | Error::BinaryNotUtf8 { location }
1402 | Error::TaggedScalarCannotDeserializeIntoString { location }
1403 | Error::UnexpectedSequenceEnd { location }
1404 | Error::UnexpectedMappingEnd { location }
1405 | Error::InvalidBooleanStrict { location }
1406 | Error::InvalidCharNull { location }
1407 | Error::InvalidCharNotSingleScalar { location }
1408 | Error::NullIntoString { location }
1409 | Error::BytesNotSupportedMissingBinaryTag { location }
1410 | Error::UnexpectedValueForUnit { location }
1411 | Error::ExpectedEmptyMappingForUnitStruct { location }
1412 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1413 | Error::InternalSeedReusedForMapKey { location }
1414 | Error::ValueRequestedBeforeKey { location }
1415 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1416 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1417 | Error::UnexpectedValueForUnitEnumVariant { location }
1418 | Error::AliasReplayCounterOverflow { location }
1419 | Error::AliasReplayLimitExceeded { location, .. }
1420 | Error::AliasExpansionLimitExceeded { location, .. }
1421 | Error::AliasReplayStackDepthExceeded { location, .. }
1422 | Error::FoldedBlockScalarMustIndentContent { location }
1423 | Error::InternalDepthUnderflow { location }
1424 | Error::InternalRecursionStackEmpty { location }
1425 | Error::RecursiveReferencesRequireWeakTypes { location }
1426 | Error::InvalidScalar { location, .. }
1427 | Error::SerdeInvalidType { location, .. }
1428 | Error::SerdeInvalidValue { location, .. }
1429 | Error::SerdeUnknownVariant { location, .. }
1430 | Error::SerdeUnknownField { location, .. }
1431 | Error::SerdeMissingField { location, .. }
1432 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1433 | Error::DuplicateMappingKey { location, .. }
1434 | Error::TaggedEnumMismatch { location, .. }
1435 | Error::SerdeVariantId { location, .. }
1436 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1437 | Error::HookError { location, .. }
1438 | Error::UnresolvedProperty { location, .. }
1439 | Error::InvalidPropertyName { location, .. }
1440 | Error::PropertyRequiredButUnset { location, .. }
1441 | Error::PropertyRequiredButEmpty { location, .. }
1442 | Error::ContainerEndMismatch { location, .. }
1443 | Error::UnknownAnchor { location, .. }
1444 | Error::CyclicInclude { location, .. }
1445 | Error::UnsupportedIncludeForm { location, .. }
1446 | Error::ResolverError { location, .. }
1447 | Error::QuotingRequired { location, .. }
1448 | Error::Budget { location, .. }
1449 | Error::CannotBorrowTransformedString { location, .. }
1450 | Error::IndentationError { location, .. } => {
1451 if location != &Location::UNKNOWN {
1452 Some(*location)
1453 } else {
1454 None
1455 }
1456 }
1457 Error::InvalidUtf8Input => None,
1458 Error::IOError { cause: _ } => None,
1459 Error::AliasError { locations, .. } => Locations::primary_location(*locations),
1460 Error::WithSnippet { error, .. } => error.location(),
1461 #[cfg(any(feature = "garde", feature = "validator"))]
1462 Error::ValidationError {
1463 issues, locations, ..
1464 } => issues.first().and_then(|issue| {
1465 let (locs, _) = locations.search_with_ancestor_fallback(&issue.path)?;
1466 let loc = if locs.reference_location != Location::UNKNOWN {
1467 locs.reference_location
1468 } else {
1469 locs.defined_location
1470 };
1471 if loc != Location::UNKNOWN {
1472 Some(loc)
1473 } else {
1474 None
1475 }
1476 }),
1477 #[cfg(any(feature = "garde", feature = "validator"))]
1478 Error::ValidationErrors { errors, .. } => errors.iter().find_map(|e| e.location()),
1479 }
1480 }
1481 pub fn locations(&self) -> Option<Locations> {
1491 match self {
1492 Error::Message { location, .. }
1493 | Error::ExternalMessage { location, .. }
1494 | Error::Eof { location }
1495 | Error::MultipleDocuments { location, .. }
1496 | Error::Unexpected { location, .. }
1497 | Error::MergeValueNotMapOrSeqOfMaps { location }
1498 | Error::MergeKeyNotAllowed { location }
1499 | Error::InvalidBinaryBase64 { location }
1500 | Error::BinaryNotUtf8 { location }
1501 | Error::TaggedScalarCannotDeserializeIntoString { location }
1502 | Error::UnexpectedSequenceEnd { location }
1503 | Error::UnexpectedMappingEnd { location }
1504 | Error::InvalidBooleanStrict { location }
1505 | Error::InvalidCharNull { location }
1506 | Error::InvalidCharNotSingleScalar { location }
1507 | Error::NullIntoString { location }
1508 | Error::BytesNotSupportedMissingBinaryTag { location }
1509 | Error::UnexpectedValueForUnit { location }
1510 | Error::ExpectedEmptyMappingForUnitStruct { location }
1511 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1512 | Error::InternalSeedReusedForMapKey { location }
1513 | Error::ValueRequestedBeforeKey { location }
1514 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1515 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1516 | Error::UnexpectedValueForUnitEnumVariant { location }
1517 | Error::AliasReplayCounterOverflow { location }
1518 | Error::AliasReplayLimitExceeded { location, .. }
1519 | Error::AliasExpansionLimitExceeded { location, .. }
1520 | Error::AliasReplayStackDepthExceeded { location, .. }
1521 | Error::FoldedBlockScalarMustIndentContent { location }
1522 | Error::InternalDepthUnderflow { location }
1523 | Error::InternalRecursionStackEmpty { location }
1524 | Error::RecursiveReferencesRequireWeakTypes { location }
1525 | Error::InvalidScalar { location, .. }
1526 | Error::SerdeInvalidType { location, .. }
1527 | Error::SerdeInvalidValue { location, .. }
1528 | Error::SerdeUnknownVariant { location, .. }
1529 | Error::SerdeUnknownField { location, .. }
1530 | Error::SerdeMissingField { location, .. }
1531 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1532 | Error::DuplicateMappingKey { location, .. }
1533 | Error::TaggedEnumMismatch { location, .. }
1534 | Error::SerdeVariantId { location, .. }
1535 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1536 | Error::HookError { location, .. }
1537 | Error::UnresolvedProperty { location, .. }
1538 | Error::InvalidPropertyName { location, .. }
1539 | Error::PropertyRequiredButUnset { location, .. }
1540 | Error::PropertyRequiredButEmpty { location, .. }
1541 | Error::ContainerEndMismatch { location, .. }
1542 | Error::UnknownAnchor { location, .. }
1543 | Error::CyclicInclude { location, .. }
1544 | Error::UnsupportedIncludeForm { location, .. }
1545 | Error::ResolverError { location, .. }
1546 | Error::QuotingRequired { location, .. }
1547 | Error::Budget { location, .. }
1548 | Error::CannotBorrowTransformedString { location, .. }
1549 | Error::IndentationError { location, .. } => Locations::same(location),
1550 Error::InvalidUtf8Input => None,
1551 Error::IOError { .. } => None,
1552 Error::AliasError { locations, .. } => Some(*locations),
1553 Error::WithSnippet { error, .. } => error.locations(),
1554 #[cfg(any(feature = "garde", feature = "validator"))]
1555 Error::ValidationError {
1556 issues, locations, ..
1557 } => issues
1558 .first()
1559 .and_then(|issue| locations.search_with_ancestor_fallback(&issue.path))
1560 .map(|(locs, _)| locs),
1561 #[cfg(any(feature = "garde", feature = "validator"))]
1562 Error::ValidationErrors { errors, .. } => errors.first().and_then(Error::locations),
1563 }
1564 }
1565
1566 #[cold]
1571 #[inline(never)]
1572 pub(crate) fn from_scan_error(err: ScanError) -> Self {
1573 let mark = err.marker();
1574 let location = Location::new(mark.line(), mark.col() + 1)
1575 .with_span(crate::Span::new(mark.index() as u64, 1));
1576
1577 let info = err.info();
1585 if info.to_ascii_lowercase().contains("unknown anchor") {
1586 return Error::UnknownAnchor { location };
1587 }
1588
1589 Error::ExternalMessage {
1590 source: ExternalMessageSource::Parser,
1591 msg: info.to_owned(),
1592 code: None,
1593 params: Vec::new(),
1594 location,
1595 }
1596 }
1597}
1598
1599fn fmt_error_plain_with_formatter(
1600 f: &mut fmt::Formatter<'_>,
1601 err: &Error,
1602 formatter: &dyn MessageFormatter,
1603) -> fmt::Result {
1604 let err = err.without_snippet();
1605
1606 let msg = formatter.format_message(err);
1607
1608 #[cfg(any(feature = "garde", feature = "validator"))]
1612 if matches!(err, Error::ValidationError { .. }) {
1613 return write!(f, "{msg}");
1614 }
1615
1616 if let Some(loc) = err.location() {
1617 fmt_with_location(f, formatter.localizer(), msg.as_ref(), &loc)?;
1618 } else {
1619 write!(f, "{msg}")?;
1620 }
1621
1622 #[cfg(any(feature = "garde", feature = "validator"))]
1623 if let Error::ValidationErrors { errors, .. } = err {
1624 for err in errors {
1625 writeln!(f)?;
1626 writeln!(f)?;
1627 fmt_error_plain_with_formatter(f, err, formatter)?;
1628 }
1629 }
1630
1631 Ok(())
1632}
1633
1634fn pick_cropped_region<'a>(
1635 regions: &'a [CroppedRegion],
1636 location: &Location,
1637) -> Option<&'a CroppedRegion> {
1638 let source_id = location.source_id();
1639
1640 if source_id != 0 {
1641 if let Some(region) = regions.iter().find(|r| r.covers_exact_source(location)) {
1642 return Some(region);
1643 }
1644 if let Some(region) = regions.iter().find(|r| r.location.source_id() == source_id) {
1645 return Some(region);
1646 }
1647 if let Some(region) = regions
1648 .iter()
1649 .find(|r| r.location.source_id() == 0 && r.covers(location))
1650 {
1651 return Some(region);
1652 }
1653 return None;
1654 }
1655
1656 regions
1657 .iter()
1658 .find(|r| r.covers(location))
1659 .or_else(|| regions.first())
1660}
1661
1662fn writeln_anchor_intro(
1663 f: &mut fmt::Formatter<'_>,
1664 l10n: &dyn Localizer,
1665 def_loc: Location,
1666 def_region: &CroppedRegion,
1667) -> fmt::Result {
1668 let line = l10n.value_comes_from_the_anchor(def_loc);
1669 let Some(prefix) = snippet_window_frame_prefix_offset(
1670 def_region.text.as_str(),
1671 def_region.start_line,
1672 &def_loc,
1673 ) else {
1674 return writeln!(f, "{line}");
1675 };
1676
1677 match line.strip_prefix(" |") {
1678 Some(rest) => writeln!(f, "{prefix}{rest}"),
1679 None => writeln!(f, "{line}"),
1680 }
1681}
1682
1683fn fmt_error_rendered(
1684 f: &mut fmt::Formatter<'_>,
1685 err: &Error,
1686 options: RenderOptions<'_>,
1687) -> fmt::Result {
1688 if options.snippets == SnippetMode::Off {
1689 return fmt_error_plain_with_formatter(f, err, options.formatter);
1690 }
1691
1692 match err {
1693 #[cfg(any(feature = "garde", feature = "validator"))]
1694 Error::ValidationErrors { errors, .. } => {
1695 let msg = options.formatter.format_message(err);
1696 if !msg.is_empty() {
1697 writeln!(f, "{}", msg)?;
1698 }
1699 let mut first = true;
1700 for err in errors {
1701 if !first {
1702 writeln!(f)?;
1703 writeln!(f)?;
1704 }
1705 first = false;
1706 fmt_error_rendered(f, err, options)?;
1707 }
1708 Ok(())
1709 }
1710
1711 Error::WithSnippet {
1712 regions,
1713 crop_radius,
1714 error,
1715 } => {
1716 if *crop_radius == 0 {
1717 return fmt_error_plain_with_formatter(f, error, options.formatter);
1719 }
1720
1721 if regions.is_empty() {
1722 return fmt_error_plain_with_formatter(f, error, options.formatter);
1723 }
1724
1725 #[cfg(any(feature = "garde", feature = "validator"))]
1728 if let Error::ValidationError {
1729 source,
1730 issues,
1731 locations,
1732 } = error.as_ref()
1733 {
1734 return fmt_validation_error_with_snippets_offset(
1735 f,
1736 options.formatter.localizer(),
1737 source.external_message_source(),
1738 issues,
1739 locations,
1740 regions,
1741 *crop_radius,
1742 );
1743 }
1744 #[cfg(any(feature = "garde", feature = "validator"))]
1745 if let Error::ValidationErrors { errors, .. } = error.as_ref() {
1746 let msg = options.formatter.format_message(error);
1747 if !msg.is_empty() {
1748 writeln!(f, "{}", msg)?;
1749 }
1750 let mut first = true;
1751 for err in errors {
1752 if !first {
1753 writeln!(f)?;
1754 writeln!(f)?;
1755 }
1756 first = false;
1757 fmt_error_with_snippets_offset(
1758 f,
1759 err,
1760 regions,
1761 *crop_radius,
1762 options.formatter,
1763 )?;
1764 }
1765 return Ok(());
1766 }
1767
1768 let Some(location) = error.location() else {
1771 return fmt_error_plain_with_formatter(f, error, options.formatter);
1772 };
1773 if location == Location::UNKNOWN {
1774 return fmt_error_plain_with_formatter(f, error, options.formatter);
1775 }
1776
1777 let l10n = options.formatter.localizer();
1778
1779 let region = match pick_cropped_region(regions, &location) {
1780 Some(r) => r,
1781 None => return fmt_error_plain_with_formatter(f, error, options.formatter),
1782 };
1783
1784 let dual_locations = error.locations().filter(|locs| {
1786 locs.reference_location != Location::UNKNOWN
1787 && locs.defined_location != Location::UNKNOWN
1788 && locs.reference_location != locs.defined_location
1789 });
1790
1791 let mut msg = options.formatter.format_message(error);
1792
1793 if dual_locations.is_some()
1797 && let Error::AliasError { locations, .. } = error.as_ref()
1798 {
1799 let suffix = l10n.alias_defined_at(locations.defined_location);
1800 if let Some(stripped) = msg.as_ref().strip_suffix(&suffix) {
1801 msg = Cow::Owned(stripped.to_string());
1802 }
1803 }
1804
1805 if let Some(locs) = dual_locations {
1806 let ref_loc = locs.reference_location;
1807 let def_loc = locs.defined_location;
1808
1809 let used_region = pick_cropped_region(regions, &ref_loc).unwrap_or(region);
1810 let label = l10n.value_used_here();
1811 let ctx = crate::de_snippet::Snippet::new(
1812 used_region.text.as_str(),
1813 used_region.source_name.as_str(),
1814 *crop_radius,
1815 )
1816 .with_offset(used_region.start_line);
1817 ctx.fmt_or_fallback_with_label(
1818 f,
1819 Level::ERROR,
1820 l10n,
1821 msg.as_ref(),
1822 label.as_ref(),
1823 &ref_loc,
1824 )?;
1825
1826 let def_region = pick_cropped_region(regions, &def_loc).unwrap_or(region);
1827 writeln!(f)?;
1828 writeln_anchor_intro(f, l10n, def_loc, def_region)?;
1829 fmt_snippet_window_offset_or_fallback(
1830 f,
1831 l10n,
1832 &def_loc,
1833 def_region.text.as_str(),
1834 def_region.start_line,
1835 l10n.defined_window().as_ref(),
1836 *crop_radius,
1837 )?;
1838 Ok(())
1839 } else {
1840 let ctx = crate::de_snippet::Snippet::new(
1842 region.text.as_str(),
1843 region.source_name.as_str(),
1844 *crop_radius,
1845 )
1846 .with_offset(region.start_line);
1847 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &location)?;
1848
1849 for extra_region in regions {
1850 if std::ptr::eq(extra_region, region) {
1851 continue;
1852 }
1853 writeln!(f)?;
1854 writeln!(f, "included from here:")?;
1855 let extra_ctx = crate::de_snippet::Snippet::new(
1856 extra_region.text.as_str(),
1857 extra_region.source_name.as_str(),
1858 *crop_radius,
1859 )
1860 .with_offset(extra_region.start_line);
1861 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
1862 }
1863 Ok(())
1864 }
1865 }
1866 _ => fmt_error_plain_with_formatter(f, err, options.formatter),
1867 }
1868}
1869
1870impl fmt::Display for Error {
1871 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1872 fmt_error_rendered(f, self, RenderOptions::default())
1873 }
1874}
1875
1876#[cfg(any(feature = "garde", feature = "validator"))]
1877fn fmt_validation_error_with_snippets_offset(
1878 f: &mut fmt::Formatter<'_>,
1879 l10n: &dyn Localizer,
1880 source: ExternalMessageSource,
1881 issues: &[ValidationIssue],
1882 locations: &PathMap,
1883 regions: &[CroppedRegion],
1884 crop_radius: usize,
1885) -> fmt::Result {
1886 let mut first = true;
1887 for issue in issues {
1888 if !first {
1889 writeln!(f)?;
1890 }
1891 first = false;
1892
1893 let original_leaf = issue
1894 .path
1895 .leaf_string()
1896 .unwrap_or_else(|| l10n.root_path_label().into_owned());
1897
1898 let (locs, resolved_leaf) = locations
1899 .search_with_ancestor_fallback(&issue.path)
1900 .unwrap_or((Locations::UNKNOWN, original_leaf));
1901
1902 let ref_loc = locs.reference_location;
1903 let def_loc = locs.defined_location;
1904
1905 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
1906 let entry = issue.display_entry_overridden(l10n, source);
1907 let base_msg = l10n.validation_base_message(&entry, &resolved_path);
1908
1909 let mut rendered_regions = Vec::new();
1910
1911 match (ref_loc, def_loc) {
1912 (Location::UNKNOWN, Location::UNKNOWN) => {
1913 write!(f, "{base_msg}")?;
1914 }
1915 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
1916 let label = l10n.defined();
1917 if let Some(region) = pick_cropped_region(regions, &r) {
1918 rendered_regions.push(region as *const _);
1919 let ctx = crate::de_snippet::Snippet::new(
1920 region.text.as_str(),
1921 label.as_ref(),
1922 crop_radius,
1923 )
1924 .with_offset(region.start_line);
1925 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
1926 } else {
1927 fmt_with_location(f, l10n, &base_msg, &r)?;
1928 }
1929 }
1930 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
1931 let label = l10n.defined_here();
1932 if let Some(region) = pick_cropped_region(regions, &d) {
1933 rendered_regions.push(region as *const _);
1934 let ctx = crate::de_snippet::Snippet::new(
1935 region.text.as_str(),
1936 label.as_ref(),
1937 crop_radius,
1938 )
1939 .with_offset(region.start_line);
1940 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
1941 } else {
1942 fmt_with_location(f, l10n, &base_msg, &d)?;
1943 }
1944 }
1945 (r, d) => {
1946 let label = l10n.value_used_here();
1947 let invalid_here = l10n.invalid_here(&base_msg);
1948 if let Some(region) = pick_cropped_region(regions, &r) {
1949 rendered_regions.push(region as *const _);
1950 let ctx = crate::de_snippet::Snippet::new(
1951 region.text.as_str(),
1952 region.source_name.as_str(),
1953 crop_radius,
1954 )
1955 .with_offset(region.start_line);
1956 ctx.fmt_or_fallback_with_label(
1957 f,
1958 Level::ERROR,
1959 l10n,
1960 &invalid_here,
1961 label.as_ref(),
1962 &r,
1963 )?;
1964 } else {
1965 fmt_with_location(f, l10n, &invalid_here, &r)?;
1966 }
1967 writeln!(f)?;
1968 if let Some(region) = pick_cropped_region(regions, &d) {
1969 writeln_anchor_intro(f, l10n, d, region)?;
1970 rendered_regions.push(region as *const _);
1971 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
1972 f,
1973 l10n,
1974 &d,
1975 region.text.as_str(),
1976 region.start_line,
1977 l10n.defined_window().as_ref(),
1978 crop_radius,
1979 )?;
1980 } else {
1981 writeln!(f, "{}", l10n.value_comes_from_the_anchor(d))?;
1982 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
1983 }
1984 }
1985 }
1986
1987 for extra_region in regions {
1988 if rendered_regions.contains(&(extra_region as *const _)) {
1989 continue;
1990 }
1991 writeln!(f)?;
1992 writeln!(f, "included from here:")?;
1993 let extra_ctx = crate::de_snippet::Snippet::new(
1994 extra_region.text.as_str(),
1995 extra_region.source_name.as_str(),
1996 crop_radius,
1997 )
1998 .with_offset(extra_region.start_line);
1999 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2000 }
2001 }
2002 Ok(())
2003}
2004
2005#[cfg(any(feature = "garde", feature = "validator"))]
2006fn fmt_error_with_snippets_offset(
2007 f: &mut fmt::Formatter<'_>,
2008 err: &Error,
2009 regions: &[CroppedRegion],
2010 crop_radius: usize,
2011 formatter: &dyn MessageFormatter,
2012) -> fmt::Result {
2013 if crop_radius == 0 {
2014 return fmt_error_plain_with_formatter(f, err, formatter);
2015 }
2016
2017 if let Error::WithSnippet { .. } = err {
2019 return fmt_error_rendered(f, err, RenderOptions::new(formatter));
2020 }
2021
2022 #[cfg(any(feature = "garde", feature = "validator"))]
2023 if let Error::ValidationError {
2024 source,
2025 issues,
2026 locations,
2027 } = err
2028 {
2029 return fmt_validation_error_with_snippets_offset(
2030 f,
2031 formatter.localizer(),
2032 source.external_message_source(),
2033 issues,
2034 locations,
2035 regions,
2036 crop_radius,
2037 );
2038 }
2039
2040 let msg = formatter.format_message(err);
2041 let Some(location) = err.location() else {
2042 return write!(f, "{msg}");
2043 };
2044 if location == Location::UNKNOWN {
2045 return write!(f, "{msg}");
2046 }
2047
2048 let Some(region) = pick_cropped_region(regions, &location) else {
2049 return fmt_with_location(f, formatter.localizer(), msg.as_ref(), &location);
2050 };
2051 let ctx = crate::de_snippet::Snippet::new(
2052 region.text.as_str(),
2053 region.source_name.as_str(),
2054 crop_radius,
2055 )
2056 .with_offset(region.start_line);
2057 ctx.fmt_or_fallback(
2058 f,
2059 Level::ERROR,
2060 formatter.localizer(),
2061 msg.as_ref(),
2062 &location,
2063 )
2064}
2065
2066#[cfg(feature = "validator")]
2067pub(crate) fn collect_validator_issues(errors: &ValidationErrors) -> Vec<ValidationIssue> {
2068 let mut out = Vec::new();
2069 let root = PathKey::empty();
2070 collect_validator_issues_inner(errors, &root, &mut out);
2071 out
2072}
2073
2074#[cfg(feature = "validator")]
2075fn collect_validator_issues_inner(
2076 errors: &ValidationErrors,
2077 path: &PathKey,
2078 out: &mut Vec<ValidationIssue>,
2079) {
2080 for (field, kind) in errors.errors() {
2081 let field_path = path.clone().join(field.as_ref());
2082 match kind {
2083 ValidationErrorsKind::Field(entries) => {
2084 for entry in entries {
2085 let mut params = Vec::new();
2086 for (k, v) in &entry.params {
2087 params.push((k.to_string(), v.to_string()));
2088 }
2089
2090 out.push(ValidationIssue {
2091 path: field_path.clone(),
2092 code: entry.code.to_string(),
2093 message: entry.message.as_ref().map(|m| m.to_string()),
2094 params,
2095 });
2096 }
2097 }
2098 ValidationErrorsKind::Struct(inner) => {
2099 collect_validator_issues_inner(inner, &field_path, out);
2100 }
2101 ValidationErrorsKind::List(list) => {
2102 for (idx, inner) in list {
2103 let index_path = field_path.clone().join(*idx);
2104 collect_validator_issues_inner(inner, &index_path, out);
2105 }
2106 }
2107 }
2108 }
2109}
2110
2111#[cfg(feature = "garde")]
2112pub(crate) fn collect_garde_issues(report: &garde::Report) -> Vec<ValidationIssue> {
2113 let mut out = Vec::new();
2114 for (path, entry) in report.iter() {
2115 out.push(ValidationIssue {
2116 path: path_key_from_garde(path),
2117 code: "garde".to_string(),
2118 message: Some(entry.message().to_string()),
2119 params: Vec::new(),
2120 });
2121 }
2122 out
2123}
2124impl std::error::Error for Error {}
2125
2126#[cold]
2128#[inline(never)]
2129fn maybe_attach_fallback_location(mut err: Error) -> Error {
2130 let loc = MISSING_FIELD_FALLBACK.with(|c| c.get());
2131 if let Some(loc) = loc
2132 && loc != Location::UNKNOWN
2133 {
2134 err = err.with_location(loc);
2135 }
2136 err
2137}
2138
2139impl de::Error for Error {
2140 #[cold]
2141 #[inline(never)]
2142 fn custom<T: fmt::Display>(msg: T) -> Self {
2143 Error::msg(redact_custom_message(msg.to_string()))
2147 }
2148
2149 #[cold]
2150 #[inline(never)]
2151 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2152 maybe_attach_fallback_location(Error::SerdeInvalidType {
2154 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2155 expected: exp.to_string(),
2156 location: Location::UNKNOWN,
2157 })
2158 }
2159
2160 #[cold]
2161 #[inline(never)]
2162 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2163 maybe_attach_fallback_location(Error::SerdeInvalidValue {
2164 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2165 expected: exp.to_string(),
2166 location: Location::UNKNOWN,
2167 })
2168 }
2169
2170 #[cold]
2171 #[inline(never)]
2172 fn invalid_length(len: usize, exp: &dyn de::Expected) -> Self {
2173 maybe_attach_fallback_location(Error::msg(format!("invalid length {len}, expected {exp}")))
2174 }
2175
2176 #[cold]
2177 #[inline(never)]
2178 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
2179 maybe_attach_fallback_location(Error::SerdeUnknownVariant {
2180 variant: redact_dynamic_identifier(variant, "an interpolated variant"),
2181 expected: expected.to_vec(),
2182 location: Location::UNKNOWN,
2183 })
2184 }
2185
2186 #[cold]
2187 #[inline(never)]
2188 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
2189 maybe_attach_fallback_location(Error::SerdeUnknownField {
2190 field: redact_dynamic_identifier(field, "an interpolated field"),
2191 expected: expected.to_vec(),
2192 location: Location::UNKNOWN,
2193 })
2194 }
2195
2196 #[cold]
2197 #[inline(never)]
2198 fn missing_field(field: &'static str) -> Self {
2199 maybe_attach_fallback_location(Error::SerdeMissingField {
2200 field,
2201 location: Location::UNKNOWN,
2202 })
2203 }
2204}
2205
2206#[cold]
2216#[inline(never)]
2217fn fmt_with_location(
2218 f: &mut fmt::Formatter<'_>,
2219 l10n: &dyn Localizer,
2220 msg: &str,
2221 location: &Location,
2222) -> fmt::Result {
2223 let out = l10n.attach_location(Cow::Borrowed(msg), *location);
2224 write!(f, "{out}")
2225}
2226
2227#[cold]
2238#[inline(never)]
2239pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
2240 Error::Budget {
2241 breach,
2242 location: Location::UNKNOWN,
2243 }
2244}
2245
2246#[cfg(test)]
2247mod tests {
2248 use super::*;
2249
2250 #[test]
2251 fn sanitize_snippet_source_name_replaces_control_chars() {
2252 let sanitized = sanitize_snippet_source_name("evil.yaml\nINJECTED:\u{001b}[31m");
2253 assert_eq!(sanitized, "evil.yaml INJECTED: [31m");
2254 }
2255
2256 #[test]
2257 fn with_snippet_named_sanitizes_source_name() {
2258 let err = Error::Message {
2259 msg: "oops".to_owned(),
2260 location: Location::new(1, 1),
2261 }
2262 .with_snippet_named("x: y\n", "evil.yaml\nINJECTED", 2);
2263
2264 let Error::WithSnippet { regions, .. } = err else {
2265 panic!("expected Error::WithSnippet");
2266 };
2267
2268 assert_eq!(regions.len(), 1);
2269 assert_eq!(regions[0].source_name, "evil.yaml INJECTED");
2270 }
2271
2272 #[test]
2273 fn locations_for_basic_error_duplicates_location() {
2274 let l = Location::new(3, 7);
2275 let err = Error::Message {
2276 msg: "x".to_owned(),
2277 location: l,
2278 };
2279 assert_eq!(
2280 err.locations(),
2281 Some(Locations {
2282 reference_location: l,
2283 defined_location: l,
2284 })
2285 );
2286 }
2287
2288 #[test]
2289 fn merge_key_not_allowed_location_helpers() {
2290 let l = Location::new(4, 2);
2291 let err = Error::MergeKeyNotAllowed { location: l };
2292 assert_eq!(err.location(), Some(l));
2293 assert_eq!(
2294 err.locations(),
2295 Some(Locations {
2296 reference_location: l,
2297 defined_location: l,
2298 })
2299 );
2300
2301 let updated = err.with_location(Location::new(5, 9));
2302 assert_eq!(updated.location(), Some(Location::new(5, 9)));
2303 }
2304
2305 #[test]
2306 fn locations_for_io_error_is_unknown() {
2307 let err = Error::IOError {
2308 cause: std::io::Error::other("x"),
2309 };
2310 assert_eq!(err.locations(), None);
2311 }
2312
2313 #[test]
2314 fn alias_error_returns_both_locations() {
2315 let ref_loc = Location::new(5, 10);
2316 let def_loc = Location::new(2, 3);
2317 let err = Error::AliasError {
2318 msg: "test error".to_owned(),
2319 locations: Locations {
2320 reference_location: ref_loc,
2321 defined_location: def_loc,
2322 },
2323 };
2324
2325 assert_eq!(err.location(), Some(ref_loc));
2327
2328 assert_eq!(
2330 err.locations(),
2331 Some(Locations {
2332 reference_location: ref_loc,
2333 defined_location: def_loc,
2334 })
2335 );
2336 }
2337
2338 #[test]
2339 fn alias_error_display_shows_both_locations() {
2340 let ref_loc = Location::new(5, 10);
2341 let def_loc = Location::new(2, 3);
2342 let err = Error::AliasError {
2343 msg: "invalid value".to_owned(),
2344 locations: Locations {
2345 reference_location: ref_loc,
2346 defined_location: def_loc,
2347 },
2348 };
2349
2350 let display = err.to_string();
2351 assert!(display.contains("invalid value"));
2352 assert!(display.contains("line 5"));
2353 assert!(display.contains("column 10"));
2354 assert!(display.contains("line 2"));
2355 assert!(display.contains("column 3"));
2356 }
2357
2358 #[test]
2359 fn alias_error_display_with_same_locations() {
2360 let loc = Location::new(3, 7);
2361 let err = Error::AliasError {
2362 msg: "test".to_owned(),
2363 locations: Locations {
2364 reference_location: loc,
2365 defined_location: loc,
2366 },
2367 };
2368
2369 let display = err.to_string();
2370 assert!(display.contains("line 3"));
2372 assert!(display.contains("column 7"));
2373 assert!(!display.contains("defined at"));
2375 }
2376
2377 #[test]
2378 fn with_snippet_counts_trailing_empty_line_for_end_line() {
2379 let text = "a\n";
2381 let err = Error::Message {
2382 msg: "x".to_owned(),
2383 location: Location::new(2, 1),
2384 };
2385
2386 let wrapped = err.with_snippet(text, 50);
2387 let Error::WithSnippet { regions, .. } = wrapped else {
2388 panic!("expected WithSnippet wrapper");
2389 };
2390 assert_eq!(regions.len(), 1);
2391 assert_eq!(regions[0].start_line, 1);
2392 assert_eq!(regions[0].end_line, 2);
2393 }
2394
2395 #[test]
2396 fn with_snippet_offset_counts_trailing_empty_line_for_end_line() {
2397 let text = "a\n";
2399 let err = Error::Message {
2400 msg: "x".to_owned(),
2401 location: Location::new(11, 1),
2402 };
2403
2404 let wrapped = err.with_snippet_offset_named(text, 10, "<input>", 50);
2405 let Error::WithSnippet { regions, .. } = wrapped else {
2406 panic!("expected WithSnippet wrapper");
2407 };
2408 assert_eq!(regions.len(), 1);
2409 assert_eq!(regions[0].start_line, 10);
2410 assert_eq!(regions[0].end_line, 11);
2411 }
2412
2413 #[cfg(feature = "validator")]
2414 #[test]
2415 fn locations_for_validator_error_uses_first_entry() {
2416 use validator::Validate;
2417
2418 #[derive(Debug, Validate)]
2419 struct Cfg {
2420 #[validate(length(min = 2))]
2421 second_string: String,
2422 }
2423
2424 let cfg = Cfg {
2425 second_string: "x".to_owned(),
2426 };
2427 let errors = cfg.validate().expect_err("validation error expected");
2428
2429 let referenced_loc = Location::new(3, 15);
2430 let defined_loc = Location::new(2, 18);
2431
2432 let mut locations = PathMap::new();
2433 locations.insert(
2434 PathKey::empty().join("secondString"),
2435 Locations {
2436 reference_location: referenced_loc,
2437 defined_location: defined_loc,
2438 },
2439 );
2440
2441 let err = Error::ValidationError {
2442 source: ValidationSource::Validator,
2443 issues: crate::de_error::collect_validator_issues(&errors),
2444 locations,
2445 };
2446 assert_eq!(
2447 err.locations(),
2448 Some(Locations {
2449 reference_location: referenced_loc,
2450 defined_location: defined_loc,
2451 })
2452 );
2453 }
2454
2455 #[cfg(feature = "validator")]
2456 #[test]
2457 fn validator_error_uses_ancestor_path_location_fallback() {
2458 let yaml = "parent:\n child: bad\n";
2459 let referenced_loc = Location::new(1, 1);
2460 let defined_loc = Location::new(1, 1);
2461
2462 let mut locations = PathMap::new();
2463 locations.insert(
2464 PathKey::empty().join("parent"),
2465 Locations {
2466 reference_location: referenced_loc,
2467 defined_location: defined_loc,
2468 },
2469 );
2470
2471 let err = Error::ValidationError {
2472 source: ValidationSource::Validator,
2473 issues: vec![ValidationIssue {
2474 path: PathKey::empty().join("parent").join("child").join("value"),
2475 code: "custom".to_owned(),
2476 message: Some("custom validation failed".to_owned()),
2477 params: Vec::new(),
2478 }],
2479 locations,
2480 };
2481
2482 assert_eq!(err.location(), Some(referenced_loc));
2483 assert_eq!(
2484 err.locations(),
2485 Some(Locations {
2486 reference_location: referenced_loc,
2487 defined_location: defined_loc,
2488 })
2489 );
2490
2491 let rendered = err.with_snippet(yaml, 20).render();
2492 assert!(
2493 rendered.contains("custom validation failed"),
2494 "expected validation message, got: {rendered}"
2495 );
2496 assert!(
2497 rendered.contains("for `parent.child.value`"),
2498 "expected original validation path, got: {rendered}"
2499 );
2500 assert!(
2501 rendered.contains("line 1 column 1"),
2502 "expected ancestor location, got: {rendered}"
2503 );
2504 assert!(
2505 rendered.contains("1 | parent:"),
2506 "expected snippet around ancestor path, got: {rendered}"
2507 );
2508 }
2509
2510 #[test]
2511 fn nested_snippet_preserves_custom_formatter() {
2512 struct Custom;
2513 impl MessageFormatter for Custom {
2514 fn localizer(&self) -> &dyn Localizer {
2515 &DEFAULT_ENGLISH_LOCALIZER
2516 }
2517 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
2518 match err {
2519 Error::Message { msg, .. } => Cow::Owned(format!("CUSTOM: {}", msg.as_str())),
2520 _ => Cow::Borrowed(""),
2521 }
2522 }
2523 }
2524 let loc = Location::new(1, 1);
2525 let base = Error::Message {
2526 msg: "original".to_string(),
2527 location: loc,
2528 };
2529 let text = "input";
2530 let start_line = 1;
2531 let radius = 1;
2532 let inner = base.with_snippet_offset_named(text, start_line, "<input>", radius);
2533 let outer = inner.with_snippet_offset_named(text, start_line, "<input>", radius);
2534 let rendered = outer.render_with_options(RenderOptions::new(&Custom));
2535 assert!(rendered.contains("CUSTOM: original"));
2536 }
2537
2538 #[test]
2539 fn alias_error_dual_snippet_rendering() {
2540 let yaml = r#"config:
2542 anchor: &myval 42
2543 other: stuff
2544 more: data
2545 use_it: *myval
2546"#;
2547 let ref_loc = Location::new(5, 11);
2549 let def_loc = Location::new(2, 11);
2551
2552 let err = Error::AliasError {
2553 msg: "invalid value type".to_owned(),
2554 locations: Locations {
2555 reference_location: ref_loc,
2556 defined_location: def_loc,
2557 },
2558 };
2559
2560 let wrapped = err.with_snippet(yaml, 5);
2562 let rendered = wrapped.render();
2563
2564 assert!(
2566 rendered.contains("invalid value type"),
2567 "rendered: {}",
2568 rendered
2569 );
2570
2571 assert!(
2574 !rendered.contains("(defined at line"),
2575 "did not expect alias defined-at suffix when secondary window is present: {}",
2576 rendered
2577 );
2578 assert!(
2580 rendered.contains("the value is used here") || rendered.contains("use_it"),
2581 "rendered should show reference location context: {}",
2582 rendered
2583 );
2584 assert!(
2586 rendered.contains("defined here") || rendered.contains("anchor"),
2587 "rendered should show defined location context: {}",
2588 rendered
2589 );
2590 assert!(
2592 rendered.contains("5") || rendered.contains("use_it"),
2593 "rendered should reference line 5: {}",
2594 rendered
2595 );
2596 assert!(
2597 rendered.contains("2") || rendered.contains("anchor"),
2598 "rendered should reference line 2: {}",
2599 rendered
2600 );
2601 }
2602
2603 #[test]
2604 fn alias_error_same_location_single_snippet() {
2605 let yaml = "value: &anchor 42\n";
2606 let loc = Location::new(1, 8);
2607
2608 let err = Error::AliasError {
2609 msg: "test error".to_owned(),
2610 locations: Locations {
2611 reference_location: loc,
2612 defined_location: loc,
2613 },
2614 };
2615
2616 let wrapped = err.with_snippet(yaml, 5);
2617 let rendered = wrapped.render();
2618
2619 assert!(rendered.contains("test error"), "rendered: {}", rendered);
2621 assert!(
2623 !rendered.contains("defined here"),
2624 "should not show 'defined here' when locations are same: {}",
2625 rendered
2626 );
2627 assert!(
2628 !rendered.contains("the value is used here"),
2629 "should not show 'value used here' when locations are same: {}",
2630 rendered
2631 );
2632 }
2633}