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;
28#[cfg(any(feature = "garde", feature = "validator"))]
29use std::collections::HashSet;
30use std::fmt;
31
32#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
33use crate::properties_redaction::{redact_with_ctxs, with_interp_redaction};
34
35#[cfg(feature = "validator")]
36use validator::{ValidationErrors, ValidationErrorsKind};
37
38pub trait MessageFormatter {
90 fn localizer(&self) -> &dyn Localizer {
95 &DEFAULT_ENGLISH_LOCALIZER
96 }
97
98 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str>;
115
116 fn format_standalone_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
127 self.format_message(err)
128 }
129
130 #[cfg(any(feature = "garde", feature = "validator"))]
142 fn format_validation_issue_lines(&self, _err: &Error) -> Option<Vec<String>> {
143 None
144 }
145}
146
147pub(crate) fn sanitize_message_text(text: Cow<'_, str>) -> Cow<'_, str> {
152 let Some(first_unsafe) = text
153 .char_indices()
154 .find_map(|(offset, ch)| message_char_needs_escape(ch).then_some(offset))
155 else {
156 return text;
157 };
158
159 let mut sanitized = String::with_capacity(text.len());
160 sanitized.push_str(&text[..first_unsafe]);
161 for ch in text[first_unsafe..].chars() {
162 if message_char_needs_escape(ch) {
163 sanitized.extend(ch.escape_debug());
164 } else {
165 sanitized.push(ch);
166 }
167 }
168 Cow::Owned(sanitized)
169}
170
171#[inline]
172fn message_char_needs_escape(ch: char) -> bool {
173 ch.is_control() || matches!(ch, '\u{2028}' | '\u{2029}')
174}
175
176#[inline]
177fn original_error(mut err: &Error) -> &Error {
178 while let Error::AliasError { error, .. } | Error::WithSnippet { error, .. } = err {
179 err = error;
180 }
181 err
182}
183
184#[inline]
185pub(crate) fn render_message_text<'a>(
186 formatter: &dyn MessageFormatter,
187 err: &'a Error,
188) -> Cow<'a, str> {
189 sanitize_message_text(formatter.format_message(original_error(err)))
190}
191
192pub(crate) fn distinct_alias_error_location(mut err: &Error) -> Option<Location> {
195 while let Error::WithSnippet { error, .. } = err {
196 err = error;
197 }
198 let Error::AliasError { locations, .. } = err else {
199 return None;
200 };
201 original_error(err).location().filter(|location| {
202 *location != Location::UNKNOWN
203 && *location != locations.reference_location
204 && *location != locations.defined_location
205 })
206}
207
208#[derive(Debug, Default, Clone, Copy)]
220pub struct UserMessageFormatter;
221
222#[non_exhaustive]
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum SnippetMode {
226 Auto,
228 Off,
230}
231
232#[non_exhaustive]
250#[derive(Clone, Copy)]
251pub struct RenderOptions<'a> {
252 pub formatter: &'a dyn MessageFormatter,
254 pub snippets: SnippetMode,
256 pub line_offset: u64,
273 pub source_name: Option<&'a str>,
283}
284
285impl Default for RenderOptions<'_> {
286 #[inline]
287 fn default() -> Self {
288 static DEFAULT_FMT: crate::message_formatters::DefaultMessageFormatter =
290 crate::message_formatters::DefaultMessageFormatter;
291
292 Self::new(&DEFAULT_FMT)
293 }
294}
295
296impl<'a> RenderOptions<'a> {
297 #[inline]
305 #[must_use]
306 pub fn new(formatter: &'a dyn MessageFormatter) -> Self {
307 Self {
308 formatter,
309 snippets: SnippetMode::Auto,
310 line_offset: 0,
311 source_name: None,
312 }
313 }
314}
315
316fn display_source_name<'a>(
319 location: Location,
320 recorded_name: &'a str,
321 root_name: Option<&'a str>,
322) -> &'a str {
323 if location.source_id() <= 1 {
324 root_name.unwrap_or(recorded_name)
325 } else {
326 recorded_name
327 }
328}
329
330pub(crate) fn display_location(mut location: Location, line_offset: u64) -> Location {
332 if location.line != 0 && location.source_id() <= 1 {
333 location.line = location
334 .line()
335 .saturating_add(line_offset)
336 .min(u32::MAX as u64) as u32;
337 }
338 location
339}
340
341pub(crate) fn render_line_offset(err: &Error, requested: u64) -> u64 {
344 if requested == 0 {
345 return 0;
346 }
347 fn root_line(location: Location) -> u64 {
348 if location.source_id() <= 1 {
349 location.line()
350 } else {
351 0
352 }
353 }
354 fn max_line(err: &Error) -> u64 {
355 let own = err.location().map_or(0, root_line);
356 match err {
357 Error::WithSnippet { error, regions, .. } => regions
358 .iter()
359 .filter(|region| region.location.source_id() <= 1)
360 .map(|region| region.end_line as u64)
361 .fold(max_line(error).max(own), u64::max),
362 Error::AliasError {
363 error, locations, ..
364 } => max_line(error)
365 .max(root_line(locations.reference_location))
366 .max(root_line(locations.defined_location))
367 .max(own),
368 #[cfg(any(feature = "garde", feature = "validator"))]
369 Error::ValidationError { locations, .. } => locations
370 .map
371 .values()
372 .flat_map(|locs| {
373 [
374 root_line(locs.reference_location),
375 root_line(locs.defined_location),
376 ]
377 })
378 .fold(own, u64::max),
379 #[cfg(any(feature = "garde", feature = "validator"))]
380 Error::ValidationErrors { errors, .. } => {
381 errors.iter().map(max_line).fold(own, u64::max)
382 }
383 _ => own,
384 }
385 }
386 requested.min((u32::MAX as u64).saturating_sub(max_line(err).saturating_add(2)))
387}
388
389#[non_exhaustive]
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct CroppedRegion {
396 pub text: String,
398 pub source_name: String,
400 pub start_line: usize,
402 pub end_line: usize,
404 pub location: Location,
406}
407
408impl CroppedRegion {
409 #[must_use]
411 pub fn new(
412 text: impl Into<String>,
413 source_name: impl Into<String>,
414 start_line: usize,
415 end_line: usize,
416 location: Location,
417 ) -> Self {
418 Self {
419 text: text.into(),
420 source_name: source_name.into(),
421 start_line,
422 end_line,
423 location,
424 }
425 }
426
427 fn snippet<'a>(
428 &'a self,
429 crop_radius: usize,
430 options: RenderOptions<'a>,
431 ) -> crate::de_snippet::Snippet<'a> {
432 crate::de_snippet::Snippet::new(
433 self.text.as_str(),
434 display_source_name(
435 self.location,
436 self.source_name.as_str(),
437 options.source_name,
438 ),
439 crop_radius,
440 )
441 .with_offset(self.start_line)
442 .with_line_offset(options.line_offset)
443 }
444
445 fn covers_exact_source(&self, location: &Location) -> bool {
446 if location == &Location::UNKNOWN {
447 return false;
448 }
449 let source_id = location.source_id();
450 source_id != 0 && self.location.source_id() == source_id && self.covers_line(location)
451 }
452
453 fn covers_line(&self, location: &Location) -> bool {
454 let line = location.line as usize;
455 self.start_line <= line && line <= self.end_line
456 }
457
458 fn covers(&self, location: &Location) -> bool {
459 if location == &Location::UNKNOWN {
460 return false;
461 }
462 if !self.covers_line(location) {
463 return false;
464 }
465 let region_source_id = self.location.source_id();
466 let location_source_id = location.source_id();
467 region_source_id == 0 || location_source_id == 0 || region_source_id == location_source_id
468 }
469}
470
471fn line_count_including_trailing_empty_line(text: &str) -> usize {
472 let mut lines = text.split_terminator('\n').count().max(1);
473 if text.ends_with('\n') {
474 lines = lines.saturating_add(1);
475 }
476 lines
477}
478
479fn sanitize_snippet_source_name(name: &str) -> Cow<'_, str> {
480 if !name.chars().any(char::is_control) {
481 return Cow::Borrowed(name);
482 }
483
484 let sanitized: String = name
485 .chars()
486 .map(|ch| if ch.is_control() { ' ' } else { ch })
487 .collect();
488 Cow::Owned(sanitized)
489}
490
491fn cropped_region_for_location(
492 text: &str,
493 source_name: &str,
494 location: &Location,
495 mapping: crate::de_snippet::LineMapping,
496 crop_radius: usize,
497) -> Option<CroppedRegion> {
498 if crop_radius == 0 || *location == Location::UNKNOWN {
499 return None;
500 }
501
502 let (cropped, start_line) =
503 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
504 if cropped.is_empty() {
505 return None;
506 }
507
508 let lines = line_count_including_trailing_empty_line(cropped.as_str());
509 let end_line = start_line.saturating_add(lines.saturating_sub(1));
510 Some(CroppedRegion {
511 text: cropped,
512 source_name: source_name.to_string(),
513 start_line,
514 end_line,
515 location: *location,
516 })
517}
518
519fn push_region_for_location(
520 regions: &mut Vec<CroppedRegion>,
521 text: &str,
522 source_name: &str,
523 location: &Location,
524 mapping: crate::de_snippet::LineMapping,
525 crop_radius: usize,
526) {
527 if let Some(region) =
528 cropped_region_for_location(text, source_name, location, mapping, crop_radius)
529 {
530 regions.push(region);
531 }
532}
533
534fn push_regions_for_locations(
535 regions: &mut Vec<CroppedRegion>,
536 text: &str,
537 source_name: &str,
538 locations: Locations,
539 mapping: crate::de_snippet::LineMapping,
540 crop_radius: usize,
541) {
542 push_region_for_location(
543 regions,
544 text,
545 source_name,
546 &locations.reference_location,
547 mapping,
548 crop_radius,
549 );
550 if locations.defined_location != locations.reference_location {
551 push_region_for_location(
552 regions,
553 text,
554 source_name,
555 &locations.defined_location,
556 mapping,
557 crop_radius,
558 );
559 }
560}
561
562#[cfg(any(feature = "garde", feature = "validator"))]
563fn push_validation_issue_regions(
564 regions: &mut Vec<CroppedRegion>,
565 issues: &[ValidationIssue],
566 locations: &PathMap,
567 text: &str,
568 source_name: &str,
569 mapping: crate::de_snippet::LineMapping,
570 crop_radius: usize,
571) {
572 for issue in issues {
573 let (locs, _) = locations
574 .search_with_ancestor_fallback(&issue.path)
575 .unwrap_or((Locations::UNKNOWN, String::new()));
576 push_regions_for_locations(regions, text, source_name, locs, mapping, crop_radius);
577 }
578}
579
580fn collect_snippet_regions(
581 inner: &Error,
582 text: &str,
583 source_name: &str,
584 mapping: crate::de_snippet::LineMapping,
585 crop_radius: usize,
586) -> Vec<CroppedRegion> {
587 let mut regions = Vec::new();
588
589 #[cfg(any(feature = "garde", feature = "validator"))]
592 if let Error::ValidationError {
593 issues, locations, ..
594 } = inner
595 {
596 push_validation_issue_regions(
597 &mut regions,
598 issues,
599 locations,
600 text,
601 source_name,
602 mapping,
603 crop_radius,
604 );
605 }
606
607 if regions.is_empty() {
610 if let Some(locs) = inner.locations() {
611 push_regions_for_locations(&mut regions, text, source_name, locs, mapping, crop_radius);
612 } else if let Some(loc) = inner.location() {
613 push_region_for_location(&mut regions, text, source_name, &loc, mapping, crop_radius);
614 }
615 }
616
617 if let Some(location) = distinct_alias_error_location(inner) {
618 push_region_for_location(
619 &mut regions,
620 text,
621 source_name,
622 &location,
623 mapping,
624 crop_radius,
625 );
626 }
627
628 regions
629}
630
631#[cfg(any(feature = "garde", feature = "validator"))]
632#[non_exhaustive]
637#[derive(Debug, Clone)]
638pub struct ValidationIssue {
639 pub path: PathKey,
641 pub code: String,
643 pub message: Option<String>,
645 pub params: Vec<(String, String)>,
647}
648
649#[cfg(any(feature = "garde", feature = "validator"))]
650#[non_exhaustive]
651#[derive(Debug, Clone, Copy, PartialEq, Eq)]
652pub enum ValidationSource {
653 Garde,
654 Validator,
655}
656
657#[cfg(any(feature = "garde", feature = "validator"))]
658impl ValidationSource {
659 pub(crate) fn external_message_source(self) -> ExternalMessageSource {
660 match self {
661 ValidationSource::Garde => ExternalMessageSource::Garde,
662 ValidationSource::Validator => ExternalMessageSource::Validator,
663 }
664 }
665}
666
667#[cfg(any(feature = "garde", feature = "validator"))]
668impl ValidationIssue {
669 #[must_use]
671 pub fn new(path: PathKey, code: impl Into<String>) -> Self {
672 Self {
673 path,
674 code: code.into(),
675 message: None,
676 params: Vec::new(),
677 }
678 }
679
680 #[must_use]
682 pub fn with_message(mut self, message: impl Into<String>) -> Self {
683 self.message = Some(message.into());
684 self
685 }
686
687 #[must_use]
689 pub fn with_params(mut self, params: Vec<(String, String)>) -> Self {
690 self.params = params;
691 self
692 }
693
694 pub(crate) fn display_entry(&self) -> String {
695 if let Some(msg) = &self.message {
696 return msg.clone();
697 }
698
699 if self.params.is_empty() {
700 return self.code.clone();
701 }
702
703 let mut params = String::new();
704 for (i, (k, v)) in self.params.iter().enumerate() {
705 if i > 0 {
706 params.push_str(", ");
707 }
708 params.push_str(k);
709 params.push('=');
710 params.push_str(v);
711 }
712 format!("{} ({params})", self.code)
713 }
714
715 pub(crate) fn display_entry_overridden(
716 &self,
717 l10n: &dyn Localizer,
718 source: ExternalMessageSource,
719 ) -> String {
720 let raw = self.display_entry();
721 let overridden = l10n
722 .override_external_message(ExternalMessage {
723 source,
724 original: raw.as_str(),
725 code: Some(self.code.as_str()),
726 params: &self.params,
727 })
728 .unwrap_or(Cow::Borrowed(raw.as_str()));
729 overridden.into_owned()
730 }
731}
732
733#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
734fn replace_known_effectives(
735 mut text: String,
736 ctxs: &[crate::properties_redaction::ScalarRedactionCtx],
737) -> String {
738 let mut pairs: Vec<&crate::properties_redaction::ScalarRedactionCtx> = ctxs
739 .iter()
740 .filter(|ctx| !ctx.effective.is_empty())
741 .collect();
742
743 pairs.sort_by_key(|ctx| std::cmp::Reverse(ctx.effective.len()));
744
745 for ctx in pairs {
746 if text.contains(&ctx.effective) {
747 text = text.replace(&ctx.effective, &ctx.raw);
748 }
749 }
750
751 text
752}
753
754#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
755pub(crate) fn redact_issue(mut issue: ValidationIssue) -> ValidationIssue {
756 with_interp_redaction(|pairs| {
757 if pairs.is_empty() {
758 return issue;
759 }
760
761 if let Some(msg) = issue.message.take() {
762 issue.message = Some(redact_with_ctxs(msg, pairs, "invalid interpolated value"));
763 }
764
765 issue.code = replace_known_effectives(std::mem::take(&mut issue.code), pairs);
766
767 for (key, value) in &mut issue.params {
768 *key = replace_known_effectives(std::mem::take(key), pairs);
769 *value = redact_with_ctxs(std::mem::take(value), pairs, "<redacted>");
770 }
771
772 issue
773 })
774}
775
776#[cfg(all(
777 not(feature = "properties"),
778 any(feature = "garde", feature = "validator")
779))]
780pub(crate) fn redact_issue(issue: ValidationIssue) -> ValidationIssue {
781 issue
782}
783
784thread_local! {
792 static MISSING_FIELD_FALLBACK: Cell<Option<Location>> = const { Cell::new(None) };
793}
794
795pub(crate) struct MissingFieldLocationGuard {
798 prev: Option<Location>,
799}
800
801impl MissingFieldLocationGuard {
802 pub(crate) fn new(location: Location) -> Self {
803 let prev = MISSING_FIELD_FALLBACK.with(|c| c.replace(Some(location)));
804 Self { prev }
805 }
806
807 #[allow(clippy::unused_self)] pub(crate) fn replace_location(&mut self, location: Location) {
810 MISSING_FIELD_FALLBACK.with(|c| c.set(Some(location)));
811 }
812}
813
814impl Drop for MissingFieldLocationGuard {
815 fn drop(&mut self) {
816 MISSING_FIELD_FALLBACK.with(|c| c.set(self.prev));
817 }
818}
819
820#[non_exhaustive]
825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub enum TransformReason {
827 EscapeSequence,
829 LineFolding,
831 MultiLineNormalization,
833 BlockScalarProcessing,
835 SingleQuoteEscape,
837 InputNotBorrowable,
841
842 ParserReturnedOwned,
848
849 VariableInterpolation,
851}
852
853impl fmt::Display for TransformReason {
854 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
855 match self {
856 TransformReason::EscapeSequence => write!(f, "escape sequence processing"),
857 TransformReason::LineFolding => write!(f, "line folding"),
858 TransformReason::MultiLineNormalization => {
859 write!(f, "multi-line whitespace normalization")
860 }
861 TransformReason::BlockScalarProcessing => write!(f, "block scalar processing"),
862 TransformReason::SingleQuoteEscape => write!(f, "single-quote escape processing"),
863 TransformReason::InputNotBorrowable => {
864 write!(f, "input is not available for borrowing")
865 }
866 TransformReason::ParserReturnedOwned => write!(f, "parser returned an owned string"),
867 TransformReason::VariableInterpolation => write!(f, "variable interpolation"),
868 }
869 }
870}
871
872#[non_exhaustive]
874pub enum Error {
875 Message {
877 msg: String,
878 location: Location,
879 },
880
881 InvalidOptions {
883 msg: String,
884 location: Location,
885 },
886
887 ExternalMessage {
892 source: Box<ExternalMessageSource>,
896 msg: String,
897 code: Option<String>,
899 params: Vec<(String, String)>,
901 location: Location,
902 },
903 Eof {
905 location: Location,
906 },
907 MultipleDocuments {
912 hint: &'static str,
914 location: Location,
915 },
916 Unexpected {
918 expected: &'static str,
919 location: Location,
920 },
921
922 MergeValueNotMapOrSeqOfMaps {
924 location: Location,
925 },
926
927 MergeKeyNotAllowed {
929 location: Location,
930 },
931
932 InvalidBinaryBase64 {
934 location: Location,
935 },
936
937 BinaryNotUtf8 {
939 location: Location,
940 },
941
942 TaggedScalarCannotDeserializeIntoString {
944 location: Location,
945 },
946
947 UnexpectedSequenceEnd {
949 location: Location,
950 },
951
952 UnexpectedMappingEnd {
954 location: Location,
955 },
956
957 InvalidBooleanStrict {
959 location: Location,
960 },
961
962 InvalidCharNull {
964 location: Location,
965 },
966
967 InvalidCharNotSingleScalar {
969 location: Location,
970 },
971
972 NullIntoString {
974 location: Location,
975 },
976
977 BytesNotSupportedMissingBinaryTag {
979 location: Location,
980 },
981
982 UnexpectedValueForUnit {
984 location: Location,
985 },
986
987 ExpectedEmptyMappingForUnitStruct {
989 location: Location,
990 },
991
992 UnexpectedContainerEndWhileSkippingNode {
994 location: Location,
995 },
996
997 InternalSeedReusedForMapKey {
999 location: Location,
1000 },
1001
1002 ValueRequestedBeforeKey {
1004 location: Location,
1005 },
1006
1007 ExpectedStringKeyForExternallyTaggedEnum {
1009 location: Location,
1010 },
1011
1012 ExternallyTaggedEnumExpectedScalarOrMapping {
1014 location: Location,
1015 },
1016
1017 UnexpectedValueForUnitEnumVariant {
1019 location: Location,
1020 },
1021
1022 InvalidUtf8Input,
1024
1025 AliasReplayCounterOverflow {
1027 location: Location,
1028 },
1029
1030 AliasReplayLimitExceeded {
1032 total_replayed_events: usize,
1033 max_total_replayed_events: usize,
1034 location: Location,
1035 },
1036
1037 AliasExpansionLimitExceeded {
1039 anchor_id: usize,
1040 expansions: usize,
1041 max_expansions_per_anchor: usize,
1042 location: Location,
1043 },
1044
1045 AliasReplayStackDepthExceeded {
1047 depth: usize,
1048 max_depth: usize,
1049 location: Location,
1050 },
1051
1052 FoldedBlockScalarMustIndentContent {
1054 location: Location,
1055 },
1056
1057 InternalDepthUnderflow {
1059 location: Location,
1060 },
1061
1062 InternalRecursionStackEmpty {
1064 location: Location,
1065 },
1066
1067 RecursiveReferencesRequireWeakTypes {
1069 location: Location,
1070 },
1071
1072 InvalidScalar {
1074 ty: &'static str,
1075 location: Location,
1076 },
1077
1078 NonFiniteFloat {
1084 value: String,
1086 location: Location,
1087 },
1088
1089 SerdeInvalidType {
1091 unexpected: String,
1092 expected: String,
1093 location: Location,
1094 },
1095
1096 SerdeInvalidValue {
1098 unexpected: String,
1099 expected: String,
1100 location: Location,
1101 },
1102
1103 SerdeInvalidLength {
1105 len: usize,
1107 expected: String,
1109 location: Location,
1110 },
1111
1112 SerdeUnknownVariant {
1114 variant: String,
1115 expected: Vec<&'static str>,
1116 location: Location,
1117 },
1118
1119 SerdeUnknownField {
1121 field: String,
1122 expected: Vec<&'static str>,
1123 location: Location,
1124 },
1125
1126 SerdeMissingField {
1128 field: &'static str,
1129 location: Location,
1130 },
1131
1132 UnexpectedContainerEndWhileReadingKeyNode {
1136 location: Location,
1137 },
1138
1139 DuplicateMappingKey {
1143 key: Option<String>,
1144 location: Location,
1145 },
1146
1147 TaggedEnumMismatch {
1149 tagged: String,
1150 target: &'static str,
1151 location: Location,
1152 },
1153
1154 SerdeVariantId {
1156 msg: String,
1157 location: Location,
1158 },
1159
1160 ExpectedMappingEndAfterEnumVariantValue {
1162 location: Location,
1163 },
1164 ContainerEndMismatch {
1165 location: Location,
1166 },
1167 UnknownAnchor {
1169 location: Location,
1170 },
1171 CyclicInclude {
1173 id: String,
1174 stack: Vec<String>,
1175 location: Location,
1176 },
1177 UnsupportedIncludeForm {
1179 location: Location,
1180 },
1181 ResolverError {
1183 target: String,
1184 error: IncludeResolveError,
1185 stack: Vec<String>,
1186 location: Location,
1187 },
1188 AliasError {
1193 #[deprecated(since = "2.0.0", note = "use error to access the original error")]
1197 msg: String,
1198 error: Box<Error>,
1200 locations: Locations,
1201 },
1202 HookError {
1205 msg: String,
1206 location: Location,
1207 },
1208 UnresolvedProperty {
1210 name: String,
1212 location: Location,
1213 },
1214 InvalidPropertyName {
1216 name: String,
1218 location: Location,
1219 },
1220 PropertyRequiredButUnset {
1224 name: String,
1225 message: String,
1226 location: Location,
1227 },
1228 PropertyRequiredButEmpty {
1232 name: String,
1233 message: String,
1234 location: Location,
1235 },
1236 Budget {
1238 breach: BudgetBreach,
1239 location: Location,
1240 },
1241 IOError {
1246 cause: std::io::Error,
1247 },
1248 QuotingRequired {
1251 value: String, location: Location,
1253 },
1254
1255 CannotBorrowTransformedString {
1261 reason: TransformReason,
1263 location: Location,
1264 },
1265
1266 IndentationError {
1268 required: crate::indentation::RequireIndent,
1270 actual: usize,
1272 location: Location,
1273 },
1274
1275 WithSnippet {
1277 regions: Vec<CroppedRegion>,
1282 crop_radius: usize,
1283 error: Box<Error>,
1284 },
1285
1286 #[cfg(any(feature = "garde", feature = "validator"))]
1288 ValidationError {
1289 source: ValidationSource,
1290 issues: Vec<ValidationIssue>,
1291 locations: PathMap,
1292 },
1293
1294 #[cfg(any(feature = "garde", feature = "validator"))]
1296 ValidationErrors {
1297 source: ValidationSource,
1298 errors: Vec<Error>,
1299 },
1300
1301 UnsupportedTag {
1303 tag: String,
1304 location: Location,
1305 },
1306
1307 TaggedStructFieldName {
1310 tag: String,
1311 location: Location,
1312 },
1313}
1314
1315impl Error {
1316 #[cfg(feature = "include")]
1319 #[cold]
1320 #[inline(never)]
1321 pub(crate) fn with_no_snippet_regions(self, crop_radius: usize) -> Self {
1322 let inner = match self {
1323 Error::WithSnippet { error, .. } => *error,
1324 other => other,
1325 };
1326 Error::WithSnippet {
1327 regions: Vec::new(),
1328 crop_radius,
1329 error: Box::new(inner),
1330 }
1331 }
1332
1333 #[cold]
1334 #[inline(never)]
1335 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
1336 self.with_snippet_named(text, "<input>", crop_radius)
1337 }
1338
1339 #[cold]
1340 #[inline(never)]
1341 pub(crate) fn with_snippet_named(
1342 self,
1343 text: &str,
1344 source_name: &str,
1345 crop_radius: usize,
1346 ) -> Self {
1347 let source_name = sanitize_snippet_source_name(source_name);
1348
1349 let inner = match self {
1352 Error::WithSnippet { error, .. } => *error,
1353 other => other,
1354 };
1355
1356 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1358
1359 let regions = collect_snippet_regions(
1360 &inner,
1361 text,
1362 source_name.as_ref(),
1363 crate::de_snippet::LineMapping::Identity,
1364 crop_radius,
1365 );
1366
1367 Error::WithSnippet {
1368 regions,
1369 crop_radius,
1370 error: Box::new(inner),
1371 }
1372 }
1373
1374 #[cfg(feature = "include")]
1375 #[cold]
1376 #[inline(never)]
1377 pub(crate) fn with_additional_snippet_named(
1378 mut self,
1379 text: &str,
1380 source_name: &str,
1381 location: &Location,
1382 crop_radius: usize,
1383 ) -> Self {
1384 let source_name = sanitize_snippet_source_name(source_name);
1385
1386 if crop_radius == 0 || *location == Location::UNKNOWN {
1387 return self;
1388 }
1389
1390 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1391 let mapping = crate::de_snippet::LineMapping::Identity;
1392
1393 let Some(region) =
1394 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1395 else {
1396 return self;
1397 };
1398
1399 if let Error::WithSnippet {
1400 ref mut regions, ..
1401 } = self
1402 {
1403 regions.push(region);
1404 }
1405 self
1406 }
1407
1408 #[cfg(feature = "include")]
1409 #[cold]
1410 #[inline(never)]
1411 pub(crate) fn with_additional_snippet_offset_named(
1412 mut self,
1413 text: &str,
1414 start_line: usize,
1415 source_name: &str,
1416 location: &Location,
1417 crop_radius: usize,
1418 ) -> Self {
1419 let source_name = sanitize_snippet_source_name(source_name);
1420
1421 if crop_radius == 0 || *location == Location::UNKNOWN {
1422 return self;
1423 }
1424
1425 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1426 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1427
1428 let Some(region) =
1429 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1430 else {
1431 return self;
1432 };
1433
1434 if let Error::WithSnippet {
1435 ref mut regions, ..
1436 } = self
1437 {
1438 regions.push(region);
1439 }
1440 self
1441 }
1442
1443 #[cold]
1444 #[inline(never)]
1445 pub(crate) fn with_snippet_offset_named(
1446 self,
1447 text: &str,
1448 start_line: usize,
1449 source_name: &str,
1450 crop_radius: usize,
1451 ) -> Self {
1452 let source_name = sanitize_snippet_source_name(source_name);
1453
1454 let inner = match self {
1455 Error::WithSnippet { error, .. } => *error,
1456 other => other,
1457 };
1458
1459 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1461
1462 let regions = collect_snippet_regions(
1463 &inner,
1464 text,
1465 source_name.as_ref(),
1466 crate::de_snippet::LineMapping::Offset { start_line },
1467 crop_radius,
1468 );
1469
1470 Error::WithSnippet {
1471 regions,
1472 crop_radius,
1473 error: Box::new(inner),
1474 }
1475 }
1476
1477 #[must_use]
1479 pub fn without_snippet(&self) -> &Self {
1480 match self {
1481 Error::WithSnippet { error, .. } => error,
1482 other => other,
1483 }
1484 }
1485
1486 pub(crate) fn is_property_interpolation_budget_breach(&self) -> bool {
1489 matches!(
1490 original_error(self),
1491 Error::Budget {
1492 breach: BudgetBreach::PropertyExpansionDepth { .. }
1493 | BudgetBreach::PropertyInterpolationWork { .. },
1494 ..
1495 }
1496 )
1497 }
1498
1499 #[must_use]
1505 pub fn render(&self) -> String {
1506 self.render_with_options(RenderOptions::default())
1507 }
1508
1509 #[must_use]
1511 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
1512 self.render_with_options(RenderOptions {
1513 formatter,
1514 ..RenderOptions::default()
1515 })
1516 }
1517
1518 #[must_use]
1522 pub fn render_with_options(&self, options: RenderOptions<'_>) -> String {
1523 RenderDisplay { err: self, options }.to_string()
1524 }
1525
1526 pub fn render_to<W: fmt::Write + ?Sized>(
1546 &self,
1547 writer: &mut W,
1548 options: RenderOptions<'_>,
1549 ) -> fmt::Result {
1550 writer.write_fmt(format_args!("{}", RenderDisplay { err: self, options }))
1551 }
1552
1553 #[cold]
1564 #[inline(never)]
1565 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
1566 Error::Message {
1567 msg: s.into(),
1568 location: Location::UNKNOWN,
1569 }
1570 }
1571
1572 #[cold]
1574 #[inline(never)]
1575 pub(crate) fn invalid_options<S: Into<String>>(s: S) -> Self {
1576 Error::InvalidOptions {
1577 msg: s.into(),
1578 location: Location::UNKNOWN,
1579 }
1580 }
1581
1582 #[cold]
1586 #[inline(never)]
1587 pub(crate) fn quoting_required(value: &str, interpolated: bool) -> Self {
1588 let location = Location::UNKNOWN;
1591 let value = if !interpolated
1592 && (parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
1593 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
1594 || parse_yaml11_bool(value).is_some()
1595 || scalar_is_nullish(value, &ScalarStyle::Plain))
1596 {
1597 value.to_string()
1598 } else {
1599 String::new()
1600 };
1601 Error::QuotingRequired { value, location }
1602 }
1603
1604 #[cold]
1615 #[inline(never)]
1616 pub(crate) fn unexpected(what: &'static str) -> Self {
1617 Error::Unexpected {
1618 expected: what,
1619 location: Location::UNKNOWN,
1620 }
1621 }
1622
1623 #[cold]
1628 #[inline(never)]
1629 pub(crate) fn eof() -> Self {
1630 Error::Eof {
1631 location: Location::UNKNOWN,
1632 }
1633 }
1634
1635 #[cold]
1636 #[inline(never)]
1637 pub(crate) fn multiple_documents(hint: &'static str) -> Self {
1638 Error::MultipleDocuments {
1639 hint,
1640 location: Location::UNKNOWN,
1641 }
1642 }
1643
1644 #[cfg(any(feature = "garde", feature = "validator"))]
1645 pub(crate) fn validation_error(
1646 source: ValidationSource,
1647 issues: Vec<ValidationIssue>,
1648 locations: PathMap,
1649 ) -> Self {
1650 Error::ValidationError {
1651 source,
1652 issues,
1653 locations,
1654 }
1655 }
1656
1657 #[cfg(any(feature = "garde", feature = "validator"))]
1658 pub(crate) fn validation_errors(source: ValidationSource, errors: Vec<Error>) -> Self {
1659 Error::ValidationErrors { source, errors }
1660 }
1661
1662 #[cfg(any(feature = "garde", feature = "validator"))]
1663 pub(crate) fn is_validation_error(&self) -> bool {
1664 matches!(self, Error::ValidationError { .. })
1665 }
1666
1667 #[cold]
1672 #[inline(never)]
1673 pub(crate) fn unknown_anchor() -> Self {
1674 Error::UnknownAnchor {
1675 location: Location::UNKNOWN,
1676 }
1677 }
1678
1679 #[cold]
1684 #[inline(never)]
1685 #[must_use]
1686 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
1687 Error::CannotBorrowTransformedString {
1688 reason,
1689 location: Location::UNKNOWN,
1690 }
1691 }
1692
1693 #[cold]
1704 #[inline(never)]
1705 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
1706 match &mut self {
1707 Error::Message { location, .. }
1708 | Error::InvalidOptions { location, .. }
1709 | Error::ExternalMessage { location, .. }
1710 | Error::Eof { location }
1711 | Error::MultipleDocuments { location, .. }
1712 | Error::Unexpected { location, .. }
1713 | Error::MergeValueNotMapOrSeqOfMaps { location }
1714 | Error::MergeKeyNotAllowed { location }
1715 | Error::UnsupportedTag { location, .. }
1716 | Error::TaggedStructFieldName { location, .. }
1717 | Error::InvalidBinaryBase64 { location }
1718 | Error::BinaryNotUtf8 { location }
1719 | Error::TaggedScalarCannotDeserializeIntoString { location }
1720 | Error::UnexpectedSequenceEnd { location }
1721 | Error::UnexpectedMappingEnd { location }
1722 | Error::InvalidBooleanStrict { location }
1723 | Error::InvalidCharNull { location }
1724 | Error::InvalidCharNotSingleScalar { location }
1725 | Error::NullIntoString { location }
1726 | Error::BytesNotSupportedMissingBinaryTag { location }
1727 | Error::UnexpectedValueForUnit { location }
1728 | Error::ExpectedEmptyMappingForUnitStruct { location }
1729 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1730 | Error::InternalSeedReusedForMapKey { location }
1731 | Error::ValueRequestedBeforeKey { location }
1732 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1733 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1734 | Error::UnexpectedValueForUnitEnumVariant { location }
1735 | Error::AliasReplayCounterOverflow { location }
1736 | Error::AliasReplayLimitExceeded { location, .. }
1737 | Error::AliasExpansionLimitExceeded { location, .. }
1738 | Error::AliasReplayStackDepthExceeded { location, .. }
1739 | Error::FoldedBlockScalarMustIndentContent { location }
1740 | Error::InternalDepthUnderflow { location }
1741 | Error::InternalRecursionStackEmpty { location }
1742 | Error::RecursiveReferencesRequireWeakTypes { location }
1743 | Error::InvalidScalar { location, .. }
1744 | Error::NonFiniteFloat { location, .. }
1745 | Error::SerdeInvalidType { location, .. }
1746 | Error::SerdeInvalidValue { location, .. }
1747 | Error::SerdeInvalidLength { location, .. }
1748 | Error::SerdeUnknownVariant { location, .. }
1749 | Error::SerdeUnknownField { location, .. }
1750 | Error::SerdeMissingField { location, .. }
1751 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1752 | Error::DuplicateMappingKey { location, .. }
1753 | Error::TaggedEnumMismatch { location, .. }
1754 | Error::SerdeVariantId { location, .. }
1755 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1756 | Error::HookError { location, .. }
1757 | Error::UnresolvedProperty { location, .. }
1758 | Error::InvalidPropertyName { location, .. }
1759 | Error::PropertyRequiredButUnset { location, .. }
1760 | Error::PropertyRequiredButEmpty { location, .. }
1761 | Error::ContainerEndMismatch { location, .. }
1762 | Error::UnknownAnchor { location, .. }
1763 | Error::CyclicInclude { location, .. }
1764 | Error::UnsupportedIncludeForm { location, .. }
1765 | Error::ResolverError { location, .. }
1766 | Error::QuotingRequired { location, .. }
1767 | Error::Budget { location, .. }
1768 | Error::CannotBorrowTransformedString { location, .. }
1769 | Error::IndentationError { location, .. } => {
1770 *location = set_location;
1771 }
1772 Error::InvalidUtf8Input => {}
1773 Error::IOError { .. } => {} Error::AliasError { .. } => {
1775 }
1777 Error::WithSnippet { error, .. } => {
1778 let inner = *std::mem::replace(error, Box::new(Error::eof()));
1779 **error = inner.with_location(set_location);
1780 }
1781 #[cfg(any(feature = "garde", feature = "validator"))]
1782 Error::ValidationError { .. } => {
1783 }
1785 #[cfg(any(feature = "garde", feature = "validator"))]
1786 Error::ValidationErrors { .. } => {
1787 }
1789 }
1790 self
1791 }
1792
1793 #[must_use]
1801 pub fn location(&self) -> Option<Location> {
1802 #[cfg(any(feature = "garde", feature = "validator"))]
1803 if let Error::ValidationErrors { errors, .. } = self {
1804 return errors.iter().find_map(Error::location);
1807 }
1808
1809 self.locations().and_then(Locations::primary_location)
1810 }
1811 #[must_use]
1821 pub fn locations(&self) -> Option<Locations> {
1822 match self {
1823 Error::Message { location, .. }
1824 | Error::InvalidOptions { location, .. }
1825 | Error::ExternalMessage { location, .. }
1826 | Error::Eof { location }
1827 | Error::MultipleDocuments { location, .. }
1828 | Error::Unexpected { location, .. }
1829 | Error::MergeValueNotMapOrSeqOfMaps { location }
1830 | Error::MergeKeyNotAllowed { location }
1831 | Error::UnsupportedTag { location, .. }
1832 | Error::TaggedStructFieldName { location, .. }
1833 | Error::InvalidBinaryBase64 { location }
1834 | Error::BinaryNotUtf8 { location }
1835 | Error::TaggedScalarCannotDeserializeIntoString { location }
1836 | Error::UnexpectedSequenceEnd { location }
1837 | Error::UnexpectedMappingEnd { location }
1838 | Error::InvalidBooleanStrict { location }
1839 | Error::InvalidCharNull { location }
1840 | Error::InvalidCharNotSingleScalar { location }
1841 | Error::NullIntoString { location }
1842 | Error::BytesNotSupportedMissingBinaryTag { location }
1843 | Error::UnexpectedValueForUnit { location }
1844 | Error::ExpectedEmptyMappingForUnitStruct { location }
1845 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1846 | Error::InternalSeedReusedForMapKey { location }
1847 | Error::ValueRequestedBeforeKey { location }
1848 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1849 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1850 | Error::UnexpectedValueForUnitEnumVariant { location }
1851 | Error::AliasReplayCounterOverflow { location }
1852 | Error::AliasReplayLimitExceeded { location, .. }
1853 | Error::AliasExpansionLimitExceeded { location, .. }
1854 | Error::AliasReplayStackDepthExceeded { location, .. }
1855 | Error::FoldedBlockScalarMustIndentContent { location }
1856 | Error::InternalDepthUnderflow { location }
1857 | Error::InternalRecursionStackEmpty { location }
1858 | Error::RecursiveReferencesRequireWeakTypes { location }
1859 | Error::InvalidScalar { location, .. }
1860 | Error::NonFiniteFloat { location, .. }
1861 | Error::SerdeInvalidType { location, .. }
1862 | Error::SerdeInvalidValue { location, .. }
1863 | Error::SerdeInvalidLength { location, .. }
1864 | Error::SerdeUnknownVariant { location, .. }
1865 | Error::SerdeUnknownField { location, .. }
1866 | Error::SerdeMissingField { location, .. }
1867 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1868 | Error::DuplicateMappingKey { location, .. }
1869 | Error::TaggedEnumMismatch { location, .. }
1870 | Error::SerdeVariantId { location, .. }
1871 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1872 | Error::HookError { location, .. }
1873 | Error::UnresolvedProperty { location, .. }
1874 | Error::InvalidPropertyName { location, .. }
1875 | Error::PropertyRequiredButUnset { location, .. }
1876 | Error::PropertyRequiredButEmpty { location, .. }
1877 | Error::ContainerEndMismatch { location, .. }
1878 | Error::UnknownAnchor { location, .. }
1879 | Error::CyclicInclude { location, .. }
1880 | Error::UnsupportedIncludeForm { location, .. }
1881 | Error::ResolverError { location, .. }
1882 | Error::QuotingRequired { location, .. }
1883 | Error::Budget { location, .. }
1884 | Error::CannotBorrowTransformedString { location, .. }
1885 | Error::IndentationError { location, .. } => Locations::same(location),
1886 Error::InvalidUtf8Input => None,
1887 Error::IOError { .. } => None,
1888 Error::AliasError { locations, .. } => Some(*locations),
1889 Error::WithSnippet { error, .. } => error.locations(),
1890 #[cfg(any(feature = "garde", feature = "validator"))]
1891 Error::ValidationError {
1892 issues, locations, ..
1893 } => issues
1894 .first()
1895 .and_then(|issue| locations.search_with_ancestor_fallback(&issue.path))
1896 .map(|(locs, _)| locs),
1897 #[cfg(any(feature = "garde", feature = "validator"))]
1898 Error::ValidationErrors { errors, .. } => errors.first().and_then(Error::locations),
1899 }
1900 }
1901
1902 #[cold]
1907 #[inline(never)]
1908 pub(crate) fn from_scan_error(err: ScanError) -> Self {
1909 let err = match err.try_into_input_io_error() {
1910 Ok(error) => {
1911 let cause = match error.try_into_io_error() {
1912 Ok(error) => error,
1913 Err(error) => {
1914 let kind = error
1915 .io_error()
1916 .map_or(std::io::ErrorKind::Other, std::io::Error::kind);
1917 std::io::Error::new(kind, error)
1918 }
1919 };
1920 return Error::IOError { cause };
1921 }
1922 Err(err) => err,
1923 };
1924
1925 let mark = err.marker();
1926 let location = Location::new(mark.line(), mark.col() + 1)
1927 .with_span(crate::Span::new(mark.index() as u64, 1));
1928
1929 match err.kind() {
1930 ErrorKind::InputDecoding { message } => {
1931 return Error::IOError {
1932 cause: std::io::Error::new(std::io::ErrorKind::InvalidData, message.clone()),
1933 };
1934 }
1935 ErrorKind::InputByteLimitExceeded { limit } => {
1936 return Error::IOError {
1937 cause: std::io::Error::new(
1938 std::io::ErrorKind::FileTooLarge,
1939 format!("input size limit of {limit} bytes exceeded"),
1940 ),
1941 };
1942 }
1943 ErrorKind::MultipleDocumentsUnsupported => {
1944 return Error::MultipleDocuments {
1945 hint: "only one document is supported in this context",
1946 location,
1947 };
1948 }
1949 ErrorKind::UnknownAnchor => return Error::UnknownAnchor { location },
1950 _ => {}
1951 }
1952
1953 let message = err.info();
1954 Error::ExternalMessage {
1955 source: Box::new(ExternalMessageSource::Parser(err)),
1956 msg: message,
1957 code: None,
1958 params: Vec::new(),
1959 location,
1960 }
1961 }
1962}
1963
1964fn fmt_error_plain_with_formatter(
1965 f: &mut fmt::Formatter<'_>,
1966 err: &Error,
1967 formatter: &dyn MessageFormatter,
1968 line_offset: u64,
1969) -> fmt::Result {
1970 let mut err = err;
1971 while let Error::WithSnippet { error, .. } = err {
1972 err = error;
1973 }
1974
1975 #[cfg(any(feature = "garde", feature = "validator"))]
1979 let display_error = if line_offset != 0
1980 && let Error::ValidationError {
1981 source,
1982 issues,
1983 locations,
1984 } = err
1985 {
1986 let mut locations = locations.clone();
1987 for locs in locations.map.values_mut() {
1988 locs.reference_location = display_location(locs.reference_location, line_offset);
1989 locs.defined_location = display_location(locs.defined_location, line_offset);
1990 }
1991 Some(Error::ValidationError {
1992 source: *source,
1993 issues: issues.clone(),
1994 locations,
1995 })
1996 } else {
1997 None
1998 };
1999 #[cfg(any(feature = "garde", feature = "validator"))]
2000 let err = display_error.as_ref().unwrap_or(err);
2001
2002 #[cfg(any(feature = "garde", feature = "validator"))]
2003 if matches!(err, Error::ValidationError { .. })
2004 && let Some(lines) = formatter.format_validation_issue_lines(err)
2005 {
2006 let lines: Vec<_> = lines
2007 .into_iter()
2008 .map(|line| sanitize_message_text(Cow::Owned(line)).into_owned())
2009 .collect();
2010 let joined = formatter.localizer().join_validation_issues(&lines);
2011 for (index, line) in joined.split('\n').enumerate() {
2014 if index > 0 {
2015 writeln!(f)?;
2016 }
2017 write!(f, "{}", sanitize_message_text(Cow::Borrowed(line)))?;
2018 }
2019 return Ok(());
2020 }
2021
2022 let msg = render_message_text(formatter, err);
2023
2024 let msg = if let Some(location) = distinct_alias_error_location(err) {
2025 sanitize_message_text(
2026 formatter
2027 .localizer()
2028 .attach_location(msg, display_location(location, line_offset)),
2029 )
2030 } else {
2031 msg
2032 };
2033
2034 let msg = if let Error::AliasError { locations, .. } = err
2036 && locations.reference_location != Location::UNKNOWN
2037 && locations.defined_location != Location::UNKNOWN
2038 && locations.defined_location != locations.reference_location
2039 {
2040 let suffix = sanitize_message_text(Cow::Owned(
2041 formatter
2042 .localizer()
2043 .alias_defined_at(display_location(locations.defined_location, line_offset)),
2044 ));
2045 Cow::Owned(format!("{msg}{suffix}"))
2046 } else {
2047 msg
2048 };
2049
2050 #[cfg(any(feature = "garde", feature = "validator"))]
2054 if matches!(err, Error::ValidationError { .. }) {
2055 return write!(f, "{msg}");
2056 }
2057
2058 if let Error::AliasError { locations, .. } = err
2059 && locations.reference_location != Location::UNKNOWN
2060 {
2061 let suffix = sanitize_message_text(Cow::Owned(
2062 formatter
2063 .localizer()
2064 .alias_used_at(display_location(locations.reference_location, line_offset)),
2065 ));
2066 write!(f, "{msg}{suffix}")?;
2067 } else if let Some(loc) = err.location() {
2068 fmt_with_location(
2069 f,
2070 formatter.localizer(),
2071 msg.as_ref(),
2072 &display_location(loc, line_offset),
2073 )?;
2074 } else {
2075 write!(f, "{msg}")?;
2076 }
2077
2078 #[cfg(any(feature = "garde", feature = "validator"))]
2079 if let Error::ValidationErrors { errors, .. } = err {
2080 for err in errors {
2081 writeln!(f)?;
2082 writeln!(f)?;
2083 fmt_error_plain_with_formatter(f, err, formatter, line_offset)?;
2084 }
2085 }
2086
2087 Ok(())
2088}
2089
2090fn pick_cropped_region<'a>(
2091 regions: &'a [CroppedRegion],
2092 location: &Location,
2093) -> Option<&'a CroppedRegion> {
2094 if let Some(region) = regions.iter().find(|region| region.location == *location) {
2096 return Some(region);
2097 }
2098 let source_id = location.source_id();
2099
2100 if source_id != 0 {
2101 if let Some(region) = regions.iter().find(|r| r.covers_exact_source(location)) {
2102 return Some(region);
2103 }
2104 if let Some(region) = regions.iter().find(|r| r.location.source_id() == source_id) {
2105 return Some(region);
2106 }
2107 if let Some(region) = regions
2108 .iter()
2109 .find(|r| r.location.source_id() == 0 && r.covers(location))
2110 {
2111 return Some(region);
2112 }
2113 return None;
2114 }
2115
2116 regions
2117 .iter()
2118 .find(|r| r.covers(location))
2119 .or_else(|| regions.first())
2120}
2121
2122fn writeln_anchor_intro(
2123 f: &mut fmt::Formatter<'_>,
2124 l10n: &dyn Localizer,
2125 def_loc: Location,
2126 def_region: &CroppedRegion,
2127 line_offset: u64,
2128) -> fmt::Result {
2129 let line = sanitize_message_text(Cow::Owned(
2130 l10n.value_comes_from_the_anchor(display_location(def_loc, line_offset)),
2131 ));
2132 let Some(prefix) = snippet_window_frame_prefix_offset(
2133 def_region.text.as_str(),
2134 def_region.start_line,
2135 &def_loc,
2136 line_offset,
2137 ) else {
2138 return writeln!(f, "{line}");
2139 };
2140
2141 match line.strip_prefix(" |") {
2142 Some(rest) => writeln!(f, "{prefix}{rest}"),
2143 None => writeln!(f, "{line}"),
2144 }
2145}
2146
2147struct RenderDisplay<'a> {
2148 err: &'a Error,
2149 options: RenderOptions<'a>,
2150}
2151
2152impl fmt::Display for RenderDisplay<'_> {
2153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2154 let mut options = self.options;
2155 options.line_offset = render_line_offset(self.err, options.line_offset);
2156 fmt_error_rendered(f, self.err, options)
2157 }
2158}
2159
2160fn fmt_error_rendered(
2161 f: &mut fmt::Formatter<'_>,
2162 err: &Error,
2163 options: RenderOptions<'_>,
2164) -> fmt::Result {
2165 if options.snippets == SnippetMode::Off {
2166 return fmt_error_plain_with_formatter(f, err, options.formatter, options.line_offset);
2167 }
2168
2169 match err {
2170 #[cfg(any(feature = "garde", feature = "validator"))]
2171 Error::ValidationErrors { errors, .. } => {
2172 let msg = render_message_text(options.formatter, err);
2173 if !msg.is_empty() {
2174 writeln!(f, "{msg}")?;
2175 }
2176 let mut first = true;
2177 for err in errors {
2178 if !first {
2179 writeln!(f)?;
2180 writeln!(f)?;
2181 }
2182 first = false;
2183 fmt_error_rendered(f, err, options)?;
2184 }
2185 Ok(())
2186 }
2187
2188 Error::WithSnippet {
2189 regions,
2190 crop_radius,
2191 error,
2192 } => {
2193 if *crop_radius == 0 {
2194 return fmt_error_plain_with_formatter(
2196 f,
2197 error,
2198 options.formatter,
2199 options.line_offset,
2200 );
2201 }
2202
2203 if regions.is_empty() {
2204 return fmt_error_plain_with_formatter(
2205 f,
2206 error,
2207 options.formatter,
2208 options.line_offset,
2209 );
2210 }
2211
2212 #[cfg(any(feature = "garde", feature = "validator"))]
2215 if let Error::ValidationError {
2216 source,
2217 issues,
2218 locations,
2219 } = error.as_ref()
2220 {
2221 return fmt_validation_error_with_snippets_offset(
2222 f,
2223 options,
2224 &source.external_message_source(),
2225 issues,
2226 locations,
2227 regions,
2228 *crop_radius,
2229 );
2230 }
2231 #[cfg(any(feature = "garde", feature = "validator"))]
2232 if let Error::ValidationErrors { errors, .. } = error.as_ref() {
2233 let msg = render_message_text(options.formatter, error);
2234 if !msg.is_empty() {
2235 writeln!(f, "{msg}")?;
2236 }
2237 let mut first = true;
2238 for err in errors {
2239 if !first {
2240 writeln!(f)?;
2241 writeln!(f)?;
2242 }
2243 first = false;
2244 fmt_error_with_snippets_offset(f, err, regions, *crop_radius, options)?;
2245 }
2246 return Ok(());
2247 }
2248
2249 let Some(location) = error.location() else {
2252 return fmt_error_plain_with_formatter(
2253 f,
2254 error,
2255 options.formatter,
2256 options.line_offset,
2257 );
2258 };
2259 if location == Location::UNKNOWN {
2260 return fmt_error_plain_with_formatter(
2261 f,
2262 error,
2263 options.formatter,
2264 options.line_offset,
2265 );
2266 }
2267
2268 let l10n = options.formatter.localizer();
2269
2270 let Some(region) = pick_cropped_region(regions, &location) else {
2271 fmt_error_plain_with_formatter(f, error, options.formatter, options.line_offset)?;
2274 if location.source_id() != 0 {
2275 for extra_region in regions {
2276 writeln!(f)?;
2277 writeln!(f, "{}:", sanitize_message_text(l10n.included_from_here()))?;
2278 extra_region
2279 .snippet(*crop_radius, options)
2280 .fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2281 }
2282 }
2283 return Ok(());
2284 };
2285
2286 let dual_locations = error.locations().filter(|locs| {
2288 locs.reference_location != Location::UNKNOWN
2289 && locs.defined_location != Location::UNKNOWN
2290 && locs.reference_location != locs.defined_location
2291 });
2292
2293 let msg = render_message_text(options.formatter, error);
2294 let failing_location = distinct_alias_error_location(error);
2295
2296 if let Some(locs) = dual_locations {
2297 let ref_loc = locs.reference_location;
2298 let def_loc = locs.defined_location;
2299
2300 let used_region = pick_cropped_region(regions, &ref_loc).unwrap_or(region);
2301 let label = l10n.value_used_here();
2302 let ctx = used_region.snippet(*crop_radius, options);
2303 ctx.fmt_or_fallback_with_label(
2304 f,
2305 Level::ERROR,
2306 l10n,
2307 msg.as_ref(),
2308 label.as_ref(),
2309 &ref_loc,
2310 )?;
2311
2312 let def_region = pick_cropped_region(regions, &def_loc).unwrap_or(region);
2313 writeln!(f)?;
2314 writeln_anchor_intro(f, l10n, def_loc, def_region, options.line_offset)?;
2315 fmt_snippet_window_offset_or_fallback(
2316 f,
2317 &def_loc,
2318 def_region.text.as_str(),
2319 def_region.start_line,
2320 l10n.defined_window().as_ref(),
2321 *crop_radius,
2322 options.line_offset,
2323 )?;
2324 } else {
2325 let ctx = region.snippet(*crop_radius, options);
2327 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &location)?;
2328
2329 for extra_region in regions {
2330 if std::ptr::eq(extra_region, region)
2331 || failing_location == Some(extra_region.location)
2332 {
2333 continue;
2334 }
2335 writeln!(f)?;
2336 writeln!(f, "{}:", sanitize_message_text(l10n.included_from_here()))?;
2337 let extra_ctx = extra_region.snippet(*crop_radius, options);
2338 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2339 }
2340 }
2341
2342 if let Some(location) = failing_location {
2343 writeln!(f)?;
2344 let label = l10n.error_here();
2345 if let Some(region) = pick_cropped_region(regions, &location) {
2346 let ctx = region.snippet(*crop_radius, options);
2347 ctx.fmt_or_fallback(f, Level::NOTE, l10n, label.as_ref(), &location)?;
2348 } else {
2349 fmt_with_location(
2350 f,
2351 l10n,
2352 label.as_ref(),
2353 &display_location(location, options.line_offset),
2354 )?;
2355 }
2356 }
2357 Ok(())
2358 }
2359 _ => fmt_error_plain_with_formatter(f, err, options.formatter, options.line_offset),
2360 }
2361}
2362
2363impl fmt::Display for Error {
2364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2365 fmt_error_rendered(f, self, RenderOptions::default())
2366 }
2367}
2368
2369impl fmt::Debug for Error {
2370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2371 fmt::Display::fmt(self, f)
2372 }
2373}
2374
2375#[cfg(any(feature = "garde", feature = "validator"))]
2376fn fmt_validation_error_with_snippets_offset(
2377 f: &mut fmt::Formatter<'_>,
2378 options: RenderOptions<'_>,
2379 source: &ExternalMessageSource,
2380 issues: &[ValidationIssue],
2381 locations: &PathMap,
2382 regions: &[CroppedRegion],
2383 crop_radius: usize,
2384) -> fmt::Result {
2385 let l10n = options.formatter.localizer();
2386 let line_offset = options.line_offset;
2387 let issue_locations: HashSet<Location> = issues
2390 .iter()
2391 .filter_map(|issue| locations.search_with_ancestor_fallback(&issue.path))
2392 .flat_map(|(locs, _)| [locs.reference_location, locs.defined_location])
2393 .filter(|loc| *loc != Location::UNKNOWN)
2394 .collect();
2395 let include_regions: Vec<&CroppedRegion> = regions
2397 .iter()
2398 .filter(|region| !issue_locations.contains(®ion.location))
2399 .collect();
2400
2401 let mut first = true;
2402 for issue in issues {
2403 if !first {
2404 writeln!(f)?;
2405 }
2406 first = false;
2407
2408 let original_leaf = issue
2409 .path
2410 .leaf_string()
2411 .unwrap_or_else(|| l10n.root_path_label().into_owned());
2412
2413 let (locs, resolved_leaf) = locations
2414 .search_with_ancestor_fallback(&issue.path)
2415 .unwrap_or((Locations::UNKNOWN, original_leaf));
2416
2417 let ref_loc = locs.reference_location;
2418 let def_loc = locs.defined_location;
2419
2420 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
2421 let entry = issue.display_entry_overridden(l10n, (*source).clone());
2422 let base_msg = sanitize_message_text(Cow::Owned(
2423 l10n.validation_base_message(&entry, &resolved_path),
2424 ));
2425
2426 let mut rendered_regions = Vec::new();
2427
2428 match (ref_loc, def_loc) {
2429 (Location::UNKNOWN, Location::UNKNOWN) => {
2430 write!(f, "{base_msg}")?;
2431 }
2432 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
2433 if let Some(region) = pick_cropped_region(regions, &r) {
2434 rendered_regions.push(std::ptr::from_ref(region));
2435 let ctx = region.snippet(crop_radius, options);
2436 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
2437 } else {
2438 fmt_with_location(f, l10n, &base_msg, &display_location(r, line_offset))?;
2439 }
2440 }
2441 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
2442 if let Some(region) = pick_cropped_region(regions, &d) {
2443 rendered_regions.push(std::ptr::from_ref(region));
2444 let ctx = region.snippet(crop_radius, options);
2445 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
2446 } else {
2447 fmt_with_location(f, l10n, &base_msg, &display_location(d, line_offset))?;
2448 }
2449 }
2450 (r, d) => {
2451 let label = l10n.value_used_here();
2452 let invalid_here = l10n.invalid_here(&base_msg);
2453 if let Some(region) = pick_cropped_region(regions, &r) {
2454 rendered_regions.push(std::ptr::from_ref(region));
2455 let ctx = region.snippet(crop_radius, options);
2456 ctx.fmt_or_fallback_with_label(
2457 f,
2458 Level::ERROR,
2459 l10n,
2460 &invalid_here,
2461 label.as_ref(),
2462 &r,
2463 )?;
2464 } else {
2465 fmt_with_location(f, l10n, &invalid_here, &display_location(r, line_offset))?;
2466 }
2467 writeln!(f)?;
2468 if let Some(region) = pick_cropped_region(regions, &d) {
2469 writeln_anchor_intro(f, l10n, d, region, line_offset)?;
2470 rendered_regions.push(std::ptr::from_ref(region));
2471 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2472 f,
2473 &d,
2474 region.text.as_str(),
2475 region.start_line,
2476 l10n.defined_window().as_ref(),
2477 crop_radius,
2478 line_offset,
2479 )?;
2480 } else {
2481 let anchor_intro = sanitize_message_text(Cow::Owned(
2482 l10n.value_comes_from_the_anchor(display_location(d, line_offset)),
2483 ));
2484 writeln!(f, "{anchor_intro}")?;
2485 fmt_with_location(
2486 f,
2487 l10n,
2488 l10n.defined_window().as_ref(),
2489 &display_location(d, line_offset),
2490 )?;
2491 }
2492 }
2493 }
2494
2495 for &extra_region in &include_regions {
2496 if rendered_regions.contains(&std::ptr::from_ref(extra_region)) {
2497 continue;
2498 }
2499 writeln!(f)?;
2500 writeln!(f, "{}:", sanitize_message_text(l10n.included_from_here()))?;
2501 let extra_ctx = extra_region.snippet(crop_radius, options);
2502 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2503 }
2504 }
2505 Ok(())
2506}
2507
2508#[cfg(any(feature = "garde", feature = "validator"))]
2509fn fmt_error_with_snippets_offset(
2510 f: &mut fmt::Formatter<'_>,
2511 err: &Error,
2512 regions: &[CroppedRegion],
2513 crop_radius: usize,
2514 options: RenderOptions<'_>,
2515) -> fmt::Result {
2516 let formatter = options.formatter;
2517 let line_offset = options.line_offset;
2518 if crop_radius == 0 {
2519 return fmt_error_plain_with_formatter(f, err, formatter, line_offset);
2520 }
2521
2522 if let Error::WithSnippet { .. } = err {
2524 return fmt_error_rendered(f, err, options);
2525 }
2526
2527 #[cfg(any(feature = "garde", feature = "validator"))]
2528 if let Error::ValidationError {
2529 source,
2530 issues,
2531 locations,
2532 } = err
2533 {
2534 return fmt_validation_error_with_snippets_offset(
2535 f,
2536 options,
2537 &source.external_message_source(),
2538 issues,
2539 locations,
2540 regions,
2541 crop_radius,
2542 );
2543 }
2544
2545 let msg = render_message_text(formatter, err);
2546 let Some(location) = err.location() else {
2547 return write!(f, "{msg}");
2548 };
2549 if location == Location::UNKNOWN {
2550 return write!(f, "{msg}");
2551 }
2552
2553 let Some(region) = pick_cropped_region(regions, &location) else {
2554 return fmt_with_location(
2555 f,
2556 formatter.localizer(),
2557 msg.as_ref(),
2558 &display_location(location, line_offset),
2559 );
2560 };
2561 let ctx = region.snippet(crop_radius, options);
2562 ctx.fmt_or_fallback(
2563 f,
2564 Level::ERROR,
2565 formatter.localizer(),
2566 msg.as_ref(),
2567 &location,
2568 )
2569}
2570
2571#[cfg(feature = "validator")]
2572pub(crate) fn collect_validator_issues(errors: &ValidationErrors) -> Vec<ValidationIssue> {
2573 let mut out = Vec::new();
2574 let root = PathKey::empty();
2575 collect_validator_issues_inner(errors, &root, &mut out);
2576 out
2577}
2578
2579#[cfg(feature = "validator")]
2580fn collect_validator_issues_inner(
2581 errors: &ValidationErrors,
2582 path: &PathKey,
2583 out: &mut Vec<ValidationIssue>,
2584) {
2585 for (field, kind) in errors.errors() {
2586 let field_path = path.clone().join(field.as_ref());
2587 match kind {
2588 ValidationErrorsKind::Field(entries) => {
2589 for entry in entries {
2590 let mut params = Vec::new();
2591 for (k, v) in &entry.params {
2592 params.push((k.to_string(), v.to_string()));
2593 }
2594
2595 out.push(ValidationIssue {
2596 path: field_path.clone(),
2597 code: entry.code.to_string(),
2598 message: entry.message.as_ref().map(std::string::ToString::to_string),
2599 params,
2600 });
2601 }
2602 }
2603 ValidationErrorsKind::Struct(inner) => {
2604 collect_validator_issues_inner(inner, &field_path, out);
2605 }
2606 ValidationErrorsKind::List(list) => {
2607 for (idx, inner) in list {
2608 let index_path = field_path.clone().join(*idx);
2609 collect_validator_issues_inner(inner, &index_path, out);
2610 }
2611 }
2612 }
2613 }
2614}
2615
2616#[cfg(feature = "garde")]
2617pub(crate) fn collect_garde_issues(report: &garde::Report) -> Vec<ValidationIssue> {
2618 let mut out = Vec::new();
2619 for (path, entry) in report.iter() {
2620 out.push(ValidationIssue {
2621 path: path_key_from_garde(path),
2622 code: "garde".to_string(),
2623 message: Some(entry.message().to_string()),
2624 params: Vec::new(),
2625 });
2626 }
2627 out
2628}
2629impl std::error::Error for Error {
2630 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2636 match self {
2637 Error::WithSnippet { error, .. } => {
2638 let mut inner = error.as_ref();
2639 while let Error::WithSnippet { error, .. } = inner {
2640 inner = error;
2641 }
2642 if matches!(inner, Error::AliasError { .. }) {
2643 Some(error.as_ref())
2644 } else {
2645 inner.source()
2646 }
2647 }
2648 Error::AliasError { error, .. } => Some(error.as_ref()),
2649 Error::IOError { cause } => Some(cause),
2650 _ => None,
2651 }
2652 }
2653}
2654
2655#[cold]
2657#[inline(never)]
2658fn maybe_attach_fallback_location(mut err: Error) -> Error {
2659 let loc = MISSING_FIELD_FALLBACK.with(std::cell::Cell::get);
2660 if let Some(loc) = loc
2661 && loc != Location::UNKNOWN
2662 {
2663 err = err.with_location(loc);
2664 }
2665 err
2666}
2667
2668fn unexpected_text(unexp: &de::Unexpected<'_>) -> String {
2672 use std::fmt::Write as _;
2673
2674 let mut text = String::with_capacity(64);
2675 let _ = write!(text, "{unexp}");
2676 text
2677}
2678
2679impl de::Error for Error {
2680 #[cold]
2681 #[inline(never)]
2682 fn custom<T: fmt::Display>(msg: T) -> Self {
2683 Error::msg(redact_custom_message(msg.to_string()))
2687 }
2688
2689 #[cold]
2690 #[inline(never)]
2691 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2692 maybe_attach_fallback_location(Error::SerdeInvalidType {
2694 unexpected: redact_dynamic_value(unexpected_text(&unexp), "an interpolated value"),
2695 expected: exp.to_string(),
2696 location: Location::UNKNOWN,
2697 })
2698 }
2699
2700 #[cold]
2701 #[inline(never)]
2702 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2703 maybe_attach_fallback_location(Error::SerdeInvalidValue {
2704 unexpected: redact_dynamic_value(unexpected_text(&unexp), "an interpolated value"),
2705 expected: exp.to_string(),
2706 location: Location::UNKNOWN,
2707 })
2708 }
2709
2710 #[cold]
2711 #[inline(never)]
2712 fn invalid_length(len: usize, exp: &dyn de::Expected) -> Self {
2713 maybe_attach_fallback_location(Error::SerdeInvalidLength {
2714 len,
2715 expected: exp.to_string(),
2716 location: Location::UNKNOWN,
2717 })
2718 }
2719
2720 #[cold]
2721 #[inline(never)]
2722 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
2723 maybe_attach_fallback_location(Error::SerdeUnknownVariant {
2724 variant: redact_dynamic_identifier(variant, "an interpolated variant"),
2725 expected: expected.to_vec(),
2726 location: Location::UNKNOWN,
2727 })
2728 }
2729
2730 #[cold]
2731 #[inline(never)]
2732 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
2733 maybe_attach_fallback_location(Error::SerdeUnknownField {
2734 field: redact_dynamic_identifier(field, "an interpolated field"),
2735 expected: expected.to_vec(),
2736 location: Location::UNKNOWN,
2737 })
2738 }
2739
2740 #[cold]
2741 #[inline(never)]
2742 fn missing_field(field: &'static str) -> Self {
2743 maybe_attach_fallback_location(Error::SerdeMissingField {
2744 field,
2745 location: Location::UNKNOWN,
2746 })
2747 }
2748}
2749
2750#[cold]
2760#[inline(never)]
2761fn fmt_with_location(
2762 f: &mut fmt::Formatter<'_>,
2763 l10n: &dyn Localizer,
2764 msg: &str,
2765 location: &Location,
2766) -> fmt::Result {
2767 let out = sanitize_message_text(l10n.attach_location(Cow::Borrowed(msg), *location));
2768 write!(f, "{out}")
2769}
2770
2771#[cold]
2782#[inline(never)]
2783pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
2784 Error::Budget {
2785 breach,
2786 location: Location::UNKNOWN,
2787 }
2788}
2789
2790#[cfg(test)]
2791mod tests {
2792 use super::*;
2793
2794 #[test]
2795 fn message_sanitizer_is_selective_and_idempotent() {
2796 let clean = Cow::Borrowed("quotes: '\"', slash: \\, Unicode: ☃");
2797 let sanitized = sanitize_message_text(clean.clone());
2798 assert!(matches!(sanitized, Cow::Borrowed(_)));
2799 assert_eq!(sanitized, clean);
2800
2801 let unsafe_text = Cow::Borrowed(
2802 "nul:\0 tab:\t line:\n esc:\u{1b} del:\u{7f} c1:\u{9b} ls:\u{2028} ps:\u{2029}",
2803 );
2804 let sanitized = sanitize_message_text(unsafe_text);
2805 assert_eq!(
2806 sanitized,
2807 r"nul:\0 tab:\t line:\n esc:\u{1b} del:\u{7f} c1:\u{9b} ls:\u{2028} ps:\u{2029}"
2808 );
2809 assert_eq!(
2810 sanitize_message_text(Cow::Owned(sanitized.clone().into_owned())),
2811 sanitized
2812 );
2813 }
2814
2815 #[test]
2816 fn formatter_newlines_are_semantic_text_not_renderer_layout() {
2817 let error = Error::CyclicInclude {
2818 id: "child.yaml".to_owned(),
2819 stack: vec!["root.yaml".to_owned(), "parent.yaml".to_owned()],
2820 location: Location::UNKNOWN,
2821 };
2822 let formatter = crate::message_formatters::DefaultMessageFormatter;
2823
2824 assert!(formatter.format_message(&error).contains('\n'));
2825 let rendered = error.render_with_formatter(&formatter);
2826 assert_eq!(
2827 rendered,
2828 r"cyclic include detected: child.yaml\nwhile processing include from root.yaml -> parent.yaml"
2829 );
2830 assert!(!rendered.contains('\n'));
2831 }
2832
2833 #[cfg(any(feature = "garde", feature = "validator"))]
2834 #[rstest::rstest]
2835 #[case::reference_only(Location::new(10, 8), Location::UNKNOWN)]
2836 #[case::definition_only(Location::UNKNOWN, Location::new(10, 8))]
2837 #[case::same_location(Location::new(10, 8), Location::new(10, 8))]
2838 fn validation_snippet_headers_use_region_source_names(
2839 #[case] reference_location: Location,
2840 #[case] defined_location: Location,
2841 ) {
2842 struct DefinitionLabels;
2843 impl Localizer for DefinitionLabels {
2844 fn defined(&self) -> Cow<'static, str> {
2845 Cow::Borrowed("LOCALIZED DEFINITION")
2846 }
2847
2848 fn defined_here(&self) -> Cow<'static, str> {
2849 Cow::Borrowed("LOCALIZED DEFINITION HERE")
2850 }
2851 }
2852
2853 let path = PathKey::new().join_key("value");
2854 let mut locations = PathMap::new();
2855 locations.insert(
2856 path.clone(),
2857 Locations {
2858 reference_location,
2859 defined_location,
2860 },
2861 );
2862 let default = crate::DefaultMessageFormatter;
2863 let localized = default.with_localizer(&DefinitionLabels);
2864 for (source_name, displayed_name) in [
2865 ("config.yaml", "config.yaml"),
2866 ("config.yaml\n\u{1b}[31m", r"config.yaml\n\u{1b}[31m"),
2867 ] {
2868 let error = Error::WithSnippet {
2869 error: Box::new(Error::ValidationError {
2870 source: ValidationSource::Validator,
2871 issues: vec![
2872 ValidationIssue::new(path.clone(), "bad").with_message("invalid value"),
2873 ],
2874 locations: locations.clone(),
2875 }),
2876 regions: vec![CroppedRegion::new(
2877 "value: bad\n",
2878 source_name,
2879 10,
2880 11,
2881 Location::new(10, 8),
2882 )],
2883 crop_radius: 64,
2884 };
2885 for formatter in [&default as &dyn MessageFormatter, &localized] {
2886 let rendered = error.render_with_formatter(formatter);
2887 assert!(
2888 rendered.contains(&format!(" --> {displayed_name}:10:8")),
2889 "expected the source name and absolute coordinates: {rendered}"
2890 );
2891 assert!(
2892 rendered.contains("^ validation error: invalid value for `value`"),
2893 "expected the validation annotation to be preserved: {rendered}"
2894 );
2895 assert!(!rendered.contains("LOCALIZED DEFINITION"), "{rendered}");
2896 assert!(!rendered.contains("included from here"), "{rendered}");
2897 assert!(!rendered.contains('\u{1b}'), "{rendered:?}");
2898 }
2899 }
2900 }
2901
2902 #[cfg(any(feature = "garde", feature = "validator"))]
2903 #[test]
2904 fn validation_issue_separators_preserve_only_layout_newlines() {
2905 let error = Error::ValidationError {
2906 source: ValidationSource::Validator,
2907 issues: vec![
2908 ValidationIssue::new(PathKey::new().join_key("first\nkey"), "bad")
2909 .with_message("invalid\nvalue\r\t\u{1b}\u{9b}\u{2028}\u{2029}"),
2910 ValidationIssue::new(PathKey::new().join_key("second"), "bad")
2911 .with_message("literal \\n stays literal"),
2912 ],
2913 locations: PathMap::new(),
2914 };
2915 let expected = concat!(
2916 "validation error at first\\nkey: invalid\\nvalue\\r\\t\\u{1b}\\u{9b}\\u{2028}\\u{2029}\n",
2917 "validation error at second: literal \\n stays literal",
2918 );
2919 let default = crate::DefaultMessageFormatter;
2920 let localized_default = default.with_localizer(&DEFAULT_ENGLISH_LOCALIZER);
2921 let localized_user = UserMessageFormatter.with_localizer(&DEFAULT_ENGLISH_LOCALIZER);
2922 for formatter in [
2923 &default as &dyn MessageFormatter,
2924 &UserMessageFormatter,
2925 &localized_default,
2926 &localized_user,
2927 ] {
2928 assert_eq!(error.render_with_formatter(formatter), expected);
2929 }
2930 }
2931
2932 #[cfg(any(feature = "garde", feature = "validator"))]
2933 #[test]
2934 fn validation_layout_honors_localizer_hooks_and_escapes_their_text() {
2935 struct Localized;
2936 impl Localizer for Localized {
2937 fn override_external_message<'a>(
2938 &self,
2939 _msg: ExternalMessage<'a>,
2940 ) -> Option<Cow<'a, str>> {
2941 Some(Cow::Borrowed("localized\nmessage\u{1b}"))
2942 }
2943
2944 fn validation_issue_line(
2945 &self,
2946 path: &str,
2947 entry: &str,
2948 _loc: Option<Location>,
2949 ) -> String {
2950 format!("{path}: {entry}\t")
2951 }
2952
2953 fn join_validation_issues(&self, lines: &[String]) -> String {
2954 format!("{}\u{1b}", lines.join("\n---\n"))
2955 }
2956 }
2957
2958 let error = Error::ValidationError {
2959 source: ValidationSource::Validator,
2960 issues: ["first", "second"]
2961 .into_iter()
2962 .map(|key| ValidationIssue::new(PathKey::new().join_key(key), "bad"))
2963 .collect(),
2964 locations: PathMap::new(),
2965 };
2966 let formatter = crate::DefaultMessageFormatter.with_localizer(&Localized);
2967 assert_eq!(
2968 error.render_with_formatter(&formatter),
2969 concat!(
2970 "first: localized\\nmessage\\u{1b}\\t\n---\n",
2971 "second: localized\\nmessage\\u{1b}\\t\\u{1b}",
2972 )
2973 );
2974 }
2975
2976 #[cfg(any(feature = "garde", feature = "validator"))]
2977 #[test]
2978 fn validation_plain_rendering_keeps_custom_whole_message_overrides() {
2979 struct Custom(std::cell::Cell<usize>);
2980 impl MessageFormatter for Custom {
2981 fn format_message<'a>(&self, error: &'a Error) -> Cow<'a, str> {
2982 self.0.set(self.0.get() + 1);
2983 let Error::ValidationError { issues, .. } = error else {
2984 panic!("expected the whole validation error");
2985 };
2986 Cow::Owned(format!("{} problems\ncustom\u{1b}", issues.len()))
2987 }
2988 }
2989
2990 let error = Error::ValidationError {
2991 source: ValidationSource::Validator,
2992 issues: ["first", "second"]
2993 .into_iter()
2994 .map(|key| ValidationIssue::new(PathKey::new().join_key(key), "bad"))
2995 .collect(),
2996 locations: PathMap::new(),
2997 };
2998 let formatter = Custom(std::cell::Cell::new(0));
2999 assert_eq!(
3000 error.render_with_formatter(&formatter),
3001 r"2 problems\ncustom\u{1b}"
3002 );
3003 assert_eq!(formatter.0.get(), 1);
3004 }
3005
3006 #[cfg(any(feature = "garde", feature = "validator"))]
3007 #[test]
3008 fn validation_messages_cross_the_same_sanitization_boundary() {
3009 let path = PathKey::empty().join("field\n\u{1b}");
3010 let mut locations = PathMap::new();
3011 locations.insert(
3012 path.clone(),
3013 Locations {
3014 reference_location: Location::new(1, 1),
3015 defined_location: Location::new(1, 1),
3016 },
3017 );
3018 let error = Error::ValidationError {
3019 source: ValidationSource::Validator,
3020 issues: vec![
3021 ValidationIssue::new(path, "bad\u{9b}")
3022 .with_message("invalid\nvalue\u{1b}]0;owned\u{7}"),
3023 ],
3024 locations,
3025 };
3026
3027 let plain = error.render();
3028 assert!(
3029 plain.contains(r"invalid\nvalue\u{1b}]0;owned\u{7}"),
3030 "{plain:?}"
3031 );
3032 assert!(plain.contains(r"field\n\u{1b}"), "{plain:?}");
3033 assert!(!plain.contains("invalid\nvalue\u{1b}"), "{plain:?}");
3034
3035 let snippet = error.with_snippet("field: bad\n", 20).render();
3036 assert!(
3037 snippet.contains(r"invalid\nvalue\u{1b}]0;owned\u{7}"),
3038 "{snippet:?}"
3039 );
3040 assert!(snippet.contains(r"field\n\u{1b}"), "{snippet:?}");
3041 assert!(
3042 snippet.contains('\n'),
3043 "renderer-owned snippet layout should retain newlines: {snippet:?}"
3044 );
3045 assert!(!snippet.contains("invalid\nvalue\u{1b}"), "{snippet:?}");
3046 }
3047
3048 #[test]
3049 fn message_only_input_io_scan_error_uses_portable_fallback() {
3050 let input = core::iter::once(Err::<char, _>(ErrorKind::InputIo {
3051 error: granit_parser::InputIoError::from_message("portable reader failure"),
3052 }));
3053 let scan_error = granit_parser::Parser::new_from_fallible_iter(input)
3054 .find_map(Result::err)
3055 .expect("the source error should be reported");
3056 let error = Error::from_scan_error(scan_error);
3057
3058 match error {
3059 Error::IOError { cause } => {
3060 assert_eq!(cause.kind(), std::io::ErrorKind::Other);
3061 assert_eq!(cause.to_string(), "portable reader failure");
3062 }
3063 other => panic!("expected reader I/O error, got {other:?}"),
3064 }
3065 }
3066
3067 #[rstest::rstest]
3068 #[case::unknown_anchor("while parsing node, found unknown anchor")]
3069 #[case::multiple_documents("multiple documents not supported here")]
3070 fn custom_scan_error_messages_are_not_reclassified_as_builtin_kinds(#[case] message: &str) {
3071 let scan_error = ScanError::new(granit_parser::Marker::new(0, 1, 0), message);
3072 let mapped = Error::from_scan_error(scan_error);
3073
3074 assert!(matches!(
3075 mapped,
3076 Error::ExternalMessage {
3077 ref source,
3078 ref msg,
3079 ..
3080 } if msg == message
3081 && matches!(source.as_ref(), ExternalMessageSource::Parser(error)
3082 if matches!(error.kind(), ErrorKind::Custom(_)))
3083 ));
3084 }
3085
3086 #[test]
3087 fn sanitize_snippet_source_name_replaces_control_chars() {
3088 let sanitized = sanitize_snippet_source_name("evil.yaml\nINJECTED:\u{001b}[31m");
3089 assert_eq!(sanitized, "evil.yaml INJECTED: [31m");
3090 }
3091
3092 #[test]
3093 fn with_snippet_named_sanitizes_source_name() {
3094 let err = Error::Message {
3095 msg: "oops".to_owned(),
3096 location: Location::new(1, 1),
3097 }
3098 .with_snippet_named("x: y\n", "evil.yaml\nINJECTED", 2);
3099
3100 let Error::WithSnippet { regions, .. } = err else {
3101 panic!("expected Error::WithSnippet");
3102 };
3103
3104 assert_eq!(regions.len(), 1);
3105 assert_eq!(regions[0].source_name, "evil.yaml INJECTED");
3106 }
3107
3108 #[test]
3109 fn locations_for_basic_error_duplicates_location() {
3110 let l = Location::new(3, 7);
3111 let err = Error::Message {
3112 msg: "x".to_owned(),
3113 location: l,
3114 };
3115 assert_eq!(
3116 err.locations(),
3117 Some(Locations {
3118 reference_location: l,
3119 defined_location: l,
3120 })
3121 );
3122 }
3123
3124 #[test]
3125 fn merge_key_not_allowed_location_helpers() {
3126 let l = Location::new(4, 2);
3127 let err = Error::MergeKeyNotAllowed { location: l };
3128 assert_eq!(err.location(), Some(l));
3129 assert_eq!(
3130 err.locations(),
3131 Some(Locations {
3132 reference_location: l,
3133 defined_location: l,
3134 })
3135 );
3136
3137 let updated = err.with_location(Location::new(5, 9));
3138 assert_eq!(updated.location(), Some(Location::new(5, 9)));
3139 }
3140
3141 #[test]
3142 fn serde_invalid_type_location_helpers() {
3143 let l = Location::new(4, 2);
3144 let err = Error::SerdeInvalidType {
3145 unexpected: "string".to_owned(),
3146 expected: "an integer".to_owned(),
3147 location: l,
3148 };
3149 assert_eq!(err.location(), Some(l));
3150 assert_eq!(
3151 err.locations(),
3152 Some(Locations {
3153 reference_location: l,
3154 defined_location: l,
3155 })
3156 );
3157
3158 let updated = err.with_location(Location::new(5, 9));
3159 assert_eq!(updated.location(), Some(Location::new(5, 9)));
3160 }
3161
3162 #[test]
3163 fn locations_for_io_error_is_unknown() {
3164 let err = Error::IOError {
3165 cause: std::io::Error::other("x"),
3166 };
3167 assert_eq!(err.locations(), None);
3168 }
3169
3170 #[test]
3171 #[allow(deprecated)] fn alias_error_returns_both_locations() {
3173 let ref_loc = Location::new(5, 10);
3174 let def_loc = Location::new(2, 3);
3175 let err = Error::AliasError {
3176 msg: "test error".to_owned(),
3177 error: Box::new(Error::msg("test error")),
3178 locations: Locations {
3179 reference_location: ref_loc,
3180 defined_location: def_loc,
3181 },
3182 };
3183
3184 assert_eq!(err.location(), Some(ref_loc));
3186
3187 assert_eq!(
3189 err.locations(),
3190 Some(Locations {
3191 reference_location: ref_loc,
3192 defined_location: def_loc,
3193 })
3194 );
3195 }
3196
3197 #[test]
3198 #[allow(deprecated)] fn alias_error_keeps_its_locations_and_returns_the_inner_error_as_source() {
3200 let locations = Locations {
3201 reference_location: Location::new(5, 10),
3202 defined_location: Location::new(2, 3),
3203 };
3204 let err = Error::AliasError {
3205 msg: "inner".to_owned(),
3206 error: Box::new(Error::msg("inner")),
3207 locations,
3208 }
3209 .with_location(Location::new(9, 9));
3210
3211 assert_eq!(err.locations(), Some(locations));
3213
3214 let source = std::error::Error::source(&err).expect("AliasError has a source");
3215 assert_eq!(source.to_string(), "inner");
3216 assert!(std::error::Error::source(&Error::msg("plain")).is_none());
3217 }
3218
3219 #[test]
3220 #[allow(deprecated)] fn alias_error_display_shows_both_locations() {
3222 let ref_loc = Location::new(5, 10);
3223 let def_loc = Location::new(2, 3);
3224 let err = Error::AliasError {
3225 msg: "invalid value".to_owned(),
3226 error: Box::new(Error::msg("invalid value")),
3227 locations: Locations {
3228 reference_location: ref_loc,
3229 defined_location: def_loc,
3230 },
3231 };
3232
3233 let display = err.to_string();
3234 assert!(display.contains("invalid value"));
3235 assert!(display.contains("line 5"));
3236 assert!(display.contains("column 10"));
3237 assert!(display.contains("line 2"));
3238 assert!(display.contains("column 3"));
3239 }
3240
3241 #[test]
3242 #[allow(deprecated)] fn alias_error_display_with_same_locations() {
3244 let loc = Location::new(3, 7);
3245 let err = Error::AliasError {
3246 msg: "test".to_owned(),
3247 error: Box::new(Error::msg("test")),
3248 locations: Locations {
3249 reference_location: loc,
3250 defined_location: loc,
3251 },
3252 };
3253
3254 let display = err.to_string();
3255 assert_eq!(display, "test (used at line 3, column 7)");
3257 }
3258
3259 #[test]
3260 #[allow(deprecated)] fn alias_error_with_unknown_definition_reports_use_once() {
3262 let err = Error::AliasError {
3263 msg: "test error".to_owned(),
3264 error: Box::new(Error::msg("test error")),
3265 locations: Locations {
3266 reference_location: Location::new(3, 7),
3267 defined_location: Location::UNKNOWN,
3268 },
3269 };
3270
3271 assert_eq!(err.to_string(), "test error (used at line 3, column 7)");
3272 }
3273
3274 #[test]
3275 #[allow(deprecated)] fn alias_error_with_unknown_reference_reports_definition_once() {
3277 let err = Error::AliasError {
3278 msg: "test error".to_owned(),
3279 error: Box::new(Error::msg("test error")),
3280 locations: Locations {
3281 reference_location: Location::UNKNOWN,
3282 defined_location: Location::new(2, 3),
3283 },
3284 };
3285
3286 assert_eq!(err.to_string(), "test error at line 2, column 3");
3287 }
3288
3289 #[test]
3290 #[allow(deprecated)] fn alias_error_deduplicates_a_failure_at_either_alias_location() {
3292 let reference_location = Location::new(3, 7);
3293 let defined_location = Location::new(1, 10);
3294 for location in [reference_location, defined_location, Location::UNKNOWN] {
3295 let err = Error::AliasError {
3296 msg: "invalid u16".to_owned(),
3297 error: Box::new(Error::InvalidScalar {
3298 ty: "u16",
3299 location,
3300 }),
3301 locations: Locations {
3302 reference_location,
3303 defined_location,
3304 },
3305 };
3306 assert_eq!(
3307 err.render(),
3308 "invalid u16 (defined at line 1, column 10) (used at line 3, column 7)"
3309 );
3310 let rendered = err
3311 .with_snippet("base: &b\n port: eighty\ncopy: *b\n", 64)
3312 .render();
3313 assert!(!rendered.contains("the error occurred here"), "{rendered}");
3314 }
3315 }
3316
3317 #[test]
3318 #[allow(deprecated)] fn alias_error_preserves_a_known_failure_when_alias_locations_are_unknown() {
3320 let failure = Location::new(2, 9);
3321 for defined_location in [Location::new(2, 3), Location::UNKNOWN] {
3322 let err = Error::AliasError {
3323 msg: "invalid u16".to_owned(),
3324 error: Box::new(Error::InvalidScalar {
3325 ty: "u16",
3326 location: failure,
3327 }),
3328 locations: Locations {
3329 reference_location: Location::UNKNOWN,
3330 defined_location,
3331 },
3332 };
3333 let expected = if defined_location == Location::UNKNOWN {
3334 "invalid u16 at line 2, column 9"
3335 } else {
3336 "invalid u16 at line 2, column 9 at line 2, column 3"
3337 };
3338 assert_eq!(err.render(), expected);
3339 let wrapped = err.with_snippet("base: &b\n port: eighty\ncopy: *b\n", 64);
3340 let rendered = wrapped.render();
3341 if defined_location == Location::UNKNOWN {
3342 assert_eq!(rendered, expected);
3343 } else {
3344 assert!(rendered.contains("<input>:2:9"), "{rendered}");
3345 assert!(rendered.contains("the error occurred here"), "{rendered}");
3346 assert!(!rendered.contains("included from here"), "{rendered}");
3347 }
3348 }
3349 }
3350
3351 #[test]
3352 fn with_snippet_counts_trailing_empty_line_for_end_line() {
3353 let text = "a\n";
3355 let err = Error::Message {
3356 msg: "x".to_owned(),
3357 location: Location::new(2, 1),
3358 };
3359
3360 let wrapped = err.with_snippet(text, 50);
3361 let Error::WithSnippet { regions, .. } = wrapped else {
3362 panic!("expected WithSnippet wrapper");
3363 };
3364 assert_eq!(regions.len(), 1);
3365 assert_eq!(regions[0].start_line, 1);
3366 assert_eq!(regions[0].end_line, 2);
3367 }
3368
3369 #[test]
3370 fn with_snippet_offset_counts_trailing_empty_line_for_end_line() {
3371 let text = "a\n";
3373 let err = Error::Message {
3374 msg: "x".to_owned(),
3375 location: Location::new(11, 1),
3376 };
3377
3378 let wrapped = err.with_snippet_offset_named(text, 10, "<input>", 50);
3379 let Error::WithSnippet { regions, .. } = wrapped else {
3380 panic!("expected WithSnippet wrapper");
3381 };
3382 assert_eq!(regions.len(), 1);
3383 assert_eq!(regions[0].start_line, 10);
3384 assert_eq!(regions[0].end_line, 11);
3385 }
3386
3387 #[cfg(feature = "validator")]
3388 #[test]
3389 fn locations_for_validator_error_uses_first_entry() {
3390 use validator::Validate;
3391
3392 #[derive(Debug, Validate)]
3393 struct Cfg {
3394 #[validate(length(min = 2))]
3395 second_string: String,
3396 }
3397
3398 let cfg = Cfg {
3399 second_string: "x".to_owned(),
3400 };
3401 let errors = cfg.validate().expect_err("validation error expected");
3402
3403 let referenced_loc = Location::new(3, 15);
3404 let defined_loc = Location::new(2, 18);
3405
3406 let mut locations = PathMap::new();
3407 locations.insert(
3408 PathKey::empty().join("secondString"),
3409 Locations {
3410 reference_location: referenced_loc,
3411 defined_location: defined_loc,
3412 },
3413 );
3414
3415 let err = Error::ValidationError {
3416 source: ValidationSource::Validator,
3417 issues: crate::de_error::collect_validator_issues(&errors),
3418 locations,
3419 };
3420 assert_eq!(
3421 err.locations(),
3422 Some(Locations {
3423 reference_location: referenced_loc,
3424 defined_location: defined_loc,
3425 })
3426 );
3427 }
3428
3429 #[cfg(feature = "validator")]
3430 #[test]
3431 fn validator_error_uses_ancestor_path_location_fallback() {
3432 let yaml = "parent:\n child: bad\n";
3433 let referenced_loc = Location::new(1, 1);
3434 let defined_loc = Location::new(1, 1);
3435
3436 let mut locations = PathMap::new();
3437 locations.insert(
3438 PathKey::empty().join("parent"),
3439 Locations {
3440 reference_location: referenced_loc,
3441 defined_location: defined_loc,
3442 },
3443 );
3444
3445 let err = Error::ValidationError {
3446 source: ValidationSource::Validator,
3447 issues: vec![ValidationIssue {
3448 path: PathKey::empty().join("parent").join("child").join("value"),
3449 code: "custom".to_owned(),
3450 message: Some("custom validation failed".to_owned()),
3451 params: Vec::new(),
3452 }],
3453 locations,
3454 };
3455
3456 assert_eq!(err.location(), Some(referenced_loc));
3457 assert_eq!(
3458 err.locations(),
3459 Some(Locations {
3460 reference_location: referenced_loc,
3461 defined_location: defined_loc,
3462 })
3463 );
3464
3465 let rendered = err.with_snippet(yaml, 20).render();
3466 assert!(
3467 rendered.contains("custom validation failed"),
3468 "expected validation message, got: {rendered}"
3469 );
3470 assert!(
3471 rendered.contains("for `parent.child.value`"),
3472 "expected original validation path, got: {rendered}"
3473 );
3474 assert!(
3475 rendered.contains("line 1 column 1"),
3476 "expected ancestor location, got: {rendered}"
3477 );
3478 assert!(
3479 rendered.contains("1 | parent:"),
3480 "expected snippet around ancestor path, got: {rendered}"
3481 );
3482 }
3483
3484 #[test]
3485 fn nested_snippet_preserves_custom_formatter() {
3486 struct Custom;
3487 impl MessageFormatter for Custom {
3488 fn localizer(&self) -> &dyn Localizer {
3489 &DEFAULT_ENGLISH_LOCALIZER
3490 }
3491 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
3492 match err {
3493 Error::Message { msg, .. } => Cow::Owned(format!("CUSTOM: {}", msg.as_str())),
3494 _ => Cow::Borrowed(""),
3495 }
3496 }
3497 }
3498 let loc = Location::new(1, 1);
3499 let base = Error::Message {
3500 msg: "original".to_string(),
3501 location: loc,
3502 };
3503 let text = "input";
3504 let start_line = 1;
3505 let radius = 1;
3506 let inner = base.with_snippet_offset_named(text, start_line, "<input>", radius);
3507 let outer = inner.with_snippet_offset_named(text, start_line, "<input>", radius);
3508 let rendered = outer.render_with_options(RenderOptions::new(&Custom));
3509 assert!(rendered.contains("CUSTOM: original"));
3510 }
3511
3512 #[test]
3513 #[allow(deprecated)] fn alias_error_dual_snippet_rendering() {
3515 let yaml = r#"config:
3517 anchor: &myval 42
3518 other: stuff
3519 more: data
3520 use_it: *myval
3521"#;
3522 let ref_loc = Location::new(5, 11);
3524 let def_loc = Location::new(2, 11);
3526
3527 let err = Error::AliasError {
3528 msg: "invalid value type".to_owned(),
3529 error: Box::new(Error::msg("invalid value type")),
3530 locations: Locations {
3531 reference_location: ref_loc,
3532 defined_location: def_loc,
3533 },
3534 };
3535
3536 let wrapped = err.with_snippet(yaml, 5);
3538 let rendered = wrapped.render();
3539
3540 assert!(
3542 rendered.contains("invalid value type"),
3543 "rendered: {}",
3544 rendered
3545 );
3546
3547 assert!(
3550 !rendered.contains("(defined at line"),
3551 "did not expect alias defined-at suffix when secondary window is present: {}",
3552 rendered
3553 );
3554 assert!(
3556 rendered.contains("the value is used here") || rendered.contains("use_it"),
3557 "rendered should show reference location context: {}",
3558 rendered
3559 );
3560 assert!(
3562 rendered.contains("defined here") || rendered.contains("anchor"),
3563 "rendered should show defined location context: {}",
3564 rendered
3565 );
3566 assert!(
3568 rendered.contains('5') || rendered.contains("use_it"),
3569 "rendered should reference line 5: {}",
3570 rendered
3571 );
3572 assert!(
3573 rendered.contains('2') || rendered.contains("anchor"),
3574 "rendered should reference line 2: {}",
3575 rendered
3576 );
3577 }
3578
3579 #[test]
3580 #[allow(deprecated)] fn alias_error_same_location_single_snippet() {
3582 let yaml = "value: &anchor 42\n";
3583 let loc = Location::new(1, 8);
3584
3585 let err = Error::AliasError {
3586 msg: "test error".to_owned(),
3587 error: Box::new(Error::msg("test error")),
3588 locations: Locations {
3589 reference_location: loc,
3590 defined_location: loc,
3591 },
3592 };
3593
3594 let wrapped = err.with_snippet(yaml, 5);
3595 let rendered = wrapped.render();
3596
3597 assert!(rendered.contains("test error"), "rendered: {}", rendered);
3599 assert!(
3601 !rendered.contains("defined here"),
3602 "should not show 'defined here' when locations are same: {}",
3603 rendered
3604 );
3605 assert!(
3606 !rendered.contains("the value is used here"),
3607 "should not show 'value used here' when locations are same: {}",
3608 rendered
3609 );
3610 }
3611}