1use crate::Location;
2use crate::budget::BudgetBreach;
3use crate::de_snippet::{
4 fmt_snippet_window_offset_or_fallback, snippet_window_frame_prefix_offset,
5};
6use crate::input_source::IncludeResolveError;
7use crate::localizer::{DEFAULT_ENGLISH_LOCALIZER, ExternalMessageSource, Localizer};
8use crate::location::Locations;
9use crate::parse_scalars::{
10 parse_int_signed, parse_yaml11_bool, parse_yaml12_float, scalar_is_nullish,
11};
12#[cfg(feature = "garde")]
13use crate::path_map::path_key_from_garde;
14use crate::properties_redaction::{
15 redact_custom_message, redact_dynamic_identifier, redact_dynamic_value,
16};
17use crate::tags::SfTag;
18#[cfg(any(feature = "garde", feature = "validator"))]
19use crate::{
20 localizer::ExternalMessage,
21 path_map::{PathKey, PathMap, format_path_with_resolved_leaf},
22};
23use annotate_snippets::Level;
24use granit_parser::{ErrorKind, ScalarStyle, ScanError};
25use serde_core::de::{self};
26use std::borrow::Cow;
27use std::cell::Cell;
28use std::fmt;
29
30#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
31use crate::properties_redaction::{redact_with_ctxs, with_interp_redaction};
32
33#[cfg(feature = "validator")]
34use validator::{ValidationErrors, ValidationErrorsKind};
35
36pub trait MessageFormatter {
88 fn localizer(&self) -> &dyn Localizer {
93 &DEFAULT_ENGLISH_LOCALIZER
94 }
95
96 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str>;
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 Default for RenderOptions<'_> {
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 #[must_use]
177 pub fn new(formatter: &'a dyn MessageFormatter) -> Self {
178 Self {
179 formatter,
180 snippets: SnippetMode::Auto,
181 }
182 }
183}
184
185#[non_exhaustive]
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct CroppedRegion {
192 pub text: String,
194 pub source_name: String,
196 pub start_line: usize,
198 pub end_line: usize,
200 pub location: Location,
202}
203
204impl CroppedRegion {
205 #[must_use]
207 pub fn new(
208 text: impl Into<String>,
209 source_name: impl Into<String>,
210 start_line: usize,
211 end_line: usize,
212 location: Location,
213 ) -> Self {
214 Self {
215 text: text.into(),
216 source_name: source_name.into(),
217 start_line,
218 end_line,
219 location,
220 }
221 }
222
223 fn covers_exact_source(&self, location: &Location) -> bool {
224 if location == &Location::UNKNOWN {
225 return false;
226 }
227 let source_id = location.source_id();
228 source_id != 0 && self.location.source_id() == source_id && self.covers_line(location)
229 }
230
231 fn covers_line(&self, location: &Location) -> bool {
232 let line = location.line as usize;
233 self.start_line <= line && line <= self.end_line
234 }
235
236 fn covers(&self, location: &Location) -> bool {
237 if location == &Location::UNKNOWN {
238 return false;
239 }
240 if !self.covers_line(location) {
241 return false;
242 }
243 let region_source_id = self.location.source_id();
244 let location_source_id = location.source_id();
245 region_source_id == 0 || location_source_id == 0 || region_source_id == location_source_id
246 }
247}
248
249fn line_count_including_trailing_empty_line(text: &str) -> usize {
250 let mut lines = text.split_terminator('\n').count().max(1);
251 if text.ends_with('\n') {
252 lines = lines.saturating_add(1);
253 }
254 lines
255}
256
257fn sanitize_snippet_source_name(name: &str) -> Cow<'_, str> {
258 if !name.chars().any(char::is_control) {
259 return Cow::Borrowed(name);
260 }
261
262 let sanitized: String = name
263 .chars()
264 .map(|ch| if ch.is_control() { ' ' } else { ch })
265 .collect();
266 Cow::Owned(sanitized)
267}
268
269fn cropped_region_for_location(
270 text: &str,
271 source_name: &str,
272 location: &Location,
273 mapping: crate::de_snippet::LineMapping,
274 crop_radius: usize,
275) -> Option<CroppedRegion> {
276 if crop_radius == 0 || *location == Location::UNKNOWN {
277 return None;
278 }
279
280 let (cropped, start_line) =
281 crate::de_snippet::crop_source_window(text, location, mapping, crop_radius);
282 if cropped.is_empty() {
283 return None;
284 }
285
286 let lines = line_count_including_trailing_empty_line(cropped.as_str());
287 let end_line = start_line.saturating_add(lines.saturating_sub(1));
288 Some(CroppedRegion {
289 text: cropped,
290 source_name: source_name.to_string(),
291 start_line,
292 end_line,
293 location: *location,
294 })
295}
296
297fn push_region_for_location(
298 regions: &mut Vec<CroppedRegion>,
299 text: &str,
300 source_name: &str,
301 location: &Location,
302 mapping: crate::de_snippet::LineMapping,
303 crop_radius: usize,
304) {
305 if let Some(region) =
306 cropped_region_for_location(text, source_name, location, mapping, crop_radius)
307 {
308 regions.push(region);
309 }
310}
311
312fn push_regions_for_locations(
313 regions: &mut Vec<CroppedRegion>,
314 text: &str,
315 source_name: &str,
316 locations: Locations,
317 mapping: crate::de_snippet::LineMapping,
318 crop_radius: usize,
319) {
320 push_region_for_location(
321 regions,
322 text,
323 source_name,
324 &locations.reference_location,
325 mapping,
326 crop_radius,
327 );
328 if locations.defined_location != locations.reference_location {
329 push_region_for_location(
330 regions,
331 text,
332 source_name,
333 &locations.defined_location,
334 mapping,
335 crop_radius,
336 );
337 }
338}
339
340#[cfg(any(feature = "garde", feature = "validator"))]
341fn push_validation_issue_regions(
342 regions: &mut Vec<CroppedRegion>,
343 issues: &[ValidationIssue],
344 locations: &PathMap,
345 text: &str,
346 source_name: &str,
347 mapping: crate::de_snippet::LineMapping,
348 crop_radius: usize,
349) {
350 for issue in issues {
351 let (locs, _) = locations
352 .search_with_ancestor_fallback(&issue.path)
353 .unwrap_or((Locations::UNKNOWN, String::new()));
354 push_regions_for_locations(regions, text, source_name, locs, mapping, crop_radius);
355 }
356}
357
358fn collect_snippet_regions(
359 inner: &Error,
360 text: &str,
361 source_name: &str,
362 mapping: crate::de_snippet::LineMapping,
363 crop_radius: usize,
364) -> Vec<CroppedRegion> {
365 let mut regions = Vec::new();
366
367 #[cfg(any(feature = "garde", feature = "validator"))]
370 if let Error::ValidationError {
371 issues, locations, ..
372 } = inner
373 {
374 push_validation_issue_regions(
375 &mut regions,
376 issues,
377 locations,
378 text,
379 source_name,
380 mapping,
381 crop_radius,
382 );
383 }
384
385 if regions.is_empty() {
388 if let Some(locs) = inner.locations() {
389 push_regions_for_locations(&mut regions, text, source_name, locs, mapping, crop_radius);
390 } else if let Some(loc) = inner.location() {
391 push_region_for_location(&mut regions, text, source_name, &loc, mapping, crop_radius);
392 }
393 }
394
395 regions
396}
397
398#[cfg(any(feature = "garde", feature = "validator"))]
399#[non_exhaustive]
404#[derive(Debug, Clone)]
405pub struct ValidationIssue {
406 pub path: PathKey,
408 pub code: String,
410 pub message: Option<String>,
412 pub params: Vec<(String, String)>,
414}
415
416#[cfg(any(feature = "garde", feature = "validator"))]
417#[non_exhaustive]
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub enum ValidationSource {
420 Garde,
421 Validator,
422}
423
424#[cfg(any(feature = "garde", feature = "validator"))]
425impl ValidationSource {
426 pub(crate) fn external_message_source(self) -> ExternalMessageSource {
427 match self {
428 ValidationSource::Garde => ExternalMessageSource::Garde,
429 ValidationSource::Validator => ExternalMessageSource::Validator,
430 }
431 }
432}
433
434#[cfg(any(feature = "garde", feature = "validator"))]
435impl ValidationIssue {
436 #[must_use]
438 pub fn new(path: PathKey, code: impl Into<String>) -> Self {
439 Self {
440 path,
441 code: code.into(),
442 message: None,
443 params: Vec::new(),
444 }
445 }
446
447 #[must_use]
449 pub fn with_message(mut self, message: impl Into<String>) -> Self {
450 self.message = Some(message.into());
451 self
452 }
453
454 #[must_use]
456 pub fn with_params(mut self, params: Vec<(String, String)>) -> Self {
457 self.params = params;
458 self
459 }
460
461 pub(crate) fn display_entry(&self) -> String {
462 if let Some(msg) = &self.message {
463 return msg.clone();
464 }
465
466 if self.params.is_empty() {
467 return self.code.clone();
468 }
469
470 let mut params = String::new();
471 for (i, (k, v)) in self.params.iter().enumerate() {
472 if i > 0 {
473 params.push_str(", ");
474 }
475 params.push_str(k);
476 params.push('=');
477 params.push_str(v);
478 }
479 format!("{} ({params})", self.code)
480 }
481
482 pub(crate) fn display_entry_overridden(
483 &self,
484 l10n: &dyn Localizer,
485 source: ExternalMessageSource,
486 ) -> String {
487 let raw = self.display_entry();
488 let overridden = l10n
489 .override_external_message(ExternalMessage {
490 source,
491 original: raw.as_str(),
492 code: Some(self.code.as_str()),
493 params: &self.params,
494 })
495 .unwrap_or(Cow::Borrowed(raw.as_str()));
496 overridden.into_owned()
497 }
498}
499
500#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
501fn replace_known_effectives(
502 mut text: String,
503 ctxs: &[crate::properties_redaction::ScalarRedactionCtx],
504) -> String {
505 let mut pairs: Vec<&crate::properties_redaction::ScalarRedactionCtx> = ctxs
506 .iter()
507 .filter(|ctx| !ctx.effective.is_empty())
508 .collect();
509
510 pairs.sort_by_key(|ctx| std::cmp::Reverse(ctx.effective.len()));
511
512 for ctx in pairs {
513 if text.contains(&ctx.effective) {
514 text = text.replace(&ctx.effective, &ctx.raw);
515 }
516 }
517
518 text
519}
520
521#[cfg(all(feature = "properties", any(feature = "garde", feature = "validator")))]
522pub(crate) fn redact_issue(mut issue: ValidationIssue) -> ValidationIssue {
523 with_interp_redaction(|pairs| {
524 if pairs.is_empty() {
525 return issue;
526 }
527
528 if let Some(msg) = issue.message.take() {
529 issue.message = Some(redact_with_ctxs(msg, pairs, "invalid interpolated value"));
530 }
531
532 issue.code = replace_known_effectives(std::mem::take(&mut issue.code), pairs);
533
534 for (key, value) in &mut issue.params {
535 *key = replace_known_effectives(std::mem::take(key), pairs);
536 *value = redact_with_ctxs(std::mem::take(value), pairs, "<redacted>");
537 }
538
539 issue
540 })
541}
542
543#[cfg(all(
544 not(feature = "properties"),
545 any(feature = "garde", feature = "validator")
546))]
547pub(crate) fn redact_issue(issue: ValidationIssue) -> ValidationIssue {
548 issue
549}
550
551thread_local! {
559 static MISSING_FIELD_FALLBACK: Cell<Option<Location>> = const { Cell::new(None) };
560}
561
562pub(crate) struct MissingFieldLocationGuard {
565 prev: Option<Location>,
566}
567
568impl MissingFieldLocationGuard {
569 pub(crate) fn new(location: Location) -> Self {
570 let prev = MISSING_FIELD_FALLBACK.with(|c| c.replace(Some(location)));
571 Self { prev }
572 }
573
574 #[allow(clippy::unused_self)] pub(crate) fn replace_location(&mut self, location: Location) {
577 MISSING_FIELD_FALLBACK.with(|c| c.set(Some(location)));
578 }
579}
580
581impl Drop for MissingFieldLocationGuard {
582 fn drop(&mut self) {
583 MISSING_FIELD_FALLBACK.with(|c| c.set(self.prev));
584 }
585}
586
587#[non_exhaustive]
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593pub enum TransformReason {
594 EscapeSequence,
596 LineFolding,
598 MultiLineNormalization,
600 BlockScalarProcessing,
602 SingleQuoteEscape,
604 InputNotBorrowable,
608
609 ParserReturnedOwned,
615
616 VariableInterpolation,
618}
619
620impl fmt::Display for TransformReason {
621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
622 match self {
623 TransformReason::EscapeSequence => write!(f, "escape sequence processing"),
624 TransformReason::LineFolding => write!(f, "line folding"),
625 TransformReason::MultiLineNormalization => {
626 write!(f, "multi-line whitespace normalization")
627 }
628 TransformReason::BlockScalarProcessing => write!(f, "block scalar processing"),
629 TransformReason::SingleQuoteEscape => write!(f, "single-quote escape processing"),
630 TransformReason::InputNotBorrowable => {
631 write!(f, "input is not available for borrowing")
632 }
633 TransformReason::ParserReturnedOwned => write!(f, "parser returned an owned string"),
634 TransformReason::VariableInterpolation => write!(f, "variable interpolation"),
635 }
636 }
637}
638
639#[non_exhaustive]
641pub enum Error {
642 Message {
644 msg: String,
645 location: Location,
646 },
647
648 InvalidOptions {
650 msg: String,
651 location: Location,
652 },
653
654 ExternalMessage {
659 source: Box<ExternalMessageSource>,
663 msg: String,
664 code: Option<String>,
666 params: Vec<(String, String)>,
668 location: Location,
669 },
670 Eof {
672 location: Location,
673 },
674 MultipleDocuments {
679 hint: &'static str,
681 location: Location,
682 },
683 Unexpected {
685 expected: &'static str,
686 location: Location,
687 },
688
689 MergeValueNotMapOrSeqOfMaps {
691 location: Location,
692 },
693
694 MergeKeyNotAllowed {
696 location: Location,
697 },
698
699 InvalidBinaryBase64 {
701 location: Location,
702 },
703
704 BinaryNotUtf8 {
706 location: Location,
707 },
708
709 TaggedScalarCannotDeserializeIntoString {
711 location: Location,
712 },
713
714 UnexpectedSequenceEnd {
716 location: Location,
717 },
718
719 UnexpectedMappingEnd {
721 location: Location,
722 },
723
724 InvalidBooleanStrict {
726 location: Location,
727 },
728
729 InvalidCharNull {
731 location: Location,
732 },
733
734 InvalidCharNotSingleScalar {
736 location: Location,
737 },
738
739 NullIntoString {
741 location: Location,
742 },
743
744 BytesNotSupportedMissingBinaryTag {
746 location: Location,
747 },
748
749 UnexpectedValueForUnit {
751 location: Location,
752 },
753
754 ExpectedEmptyMappingForUnitStruct {
756 location: Location,
757 },
758
759 UnexpectedContainerEndWhileSkippingNode {
761 location: Location,
762 },
763
764 InternalSeedReusedForMapKey {
766 location: Location,
767 },
768
769 ValueRequestedBeforeKey {
771 location: Location,
772 },
773
774 ExpectedStringKeyForExternallyTaggedEnum {
776 location: Location,
777 },
778
779 ExternallyTaggedEnumExpectedScalarOrMapping {
781 location: Location,
782 },
783
784 UnexpectedValueForUnitEnumVariant {
786 location: Location,
787 },
788
789 InvalidUtf8Input,
791
792 AliasReplayCounterOverflow {
794 location: Location,
795 },
796
797 AliasReplayLimitExceeded {
799 total_replayed_events: usize,
800 max_total_replayed_events: usize,
801 location: Location,
802 },
803
804 AliasExpansionLimitExceeded {
806 anchor_id: usize,
807 expansions: usize,
808 max_expansions_per_anchor: usize,
809 location: Location,
810 },
811
812 AliasReplayStackDepthExceeded {
814 depth: usize,
815 max_depth: usize,
816 location: Location,
817 },
818
819 FoldedBlockScalarMustIndentContent {
821 location: Location,
822 },
823
824 InternalDepthUnderflow {
826 location: Location,
827 },
828
829 InternalRecursionStackEmpty {
831 location: Location,
832 },
833
834 RecursiveReferencesRequireWeakTypes {
836 location: Location,
837 },
838
839 InvalidScalar {
841 ty: &'static str,
842 location: Location,
843 },
844
845 NonFiniteFloat {
851 value: String,
853 location: Location,
854 },
855
856 SerdeInvalidType {
858 unexpected: String,
859 expected: String,
860 location: Location,
861 },
862
863 SerdeInvalidValue {
865 unexpected: String,
866 expected: String,
867 location: Location,
868 },
869
870 SerdeUnknownVariant {
872 variant: String,
873 expected: Vec<&'static str>,
874 location: Location,
875 },
876
877 SerdeUnknownField {
879 field: String,
880 expected: Vec<&'static str>,
881 location: Location,
882 },
883
884 SerdeMissingField {
886 field: &'static str,
887 location: Location,
888 },
889
890 UnexpectedContainerEndWhileReadingKeyNode {
894 location: Location,
895 },
896
897 DuplicateMappingKey {
901 key: Option<String>,
902 location: Location,
903 },
904
905 TaggedEnumMismatch {
907 tagged: String,
908 target: &'static str,
909 location: Location,
910 },
911
912 SerdeVariantId {
914 msg: String,
915 location: Location,
916 },
917
918 ExpectedMappingEndAfterEnumVariantValue {
920 location: Location,
921 },
922 ContainerEndMismatch {
923 location: Location,
924 },
925 UnknownAnchor {
927 location: Location,
928 },
929 CyclicInclude {
931 id: String,
932 stack: Vec<String>,
933 location: Location,
934 },
935 UnsupportedIncludeForm {
937 location: Location,
938 },
939 ResolverError {
941 target: String,
942 error: IncludeResolveError,
943 stack: Vec<String>,
944 location: Location,
945 },
946 AliasError {
951 msg: String,
952 locations: Locations,
953 },
954 HookError {
957 msg: String,
958 location: Location,
959 },
960 UnresolvedProperty {
962 name: String,
964 location: Location,
965 },
966 InvalidPropertyName {
968 name: String,
970 location: Location,
971 },
972 PropertyRequiredButUnset {
975 name: String,
976 message: String,
977 location: Location,
978 },
979 PropertyRequiredButEmpty {
982 name: String,
983 message: String,
984 location: Location,
985 },
986 Budget {
988 breach: BudgetBreach,
989 location: Location,
990 },
991 IOError {
993 cause: std::io::Error,
994 },
995 QuotingRequired {
998 value: String, location: Location,
1000 },
1001
1002 CannotBorrowTransformedString {
1008 reason: TransformReason,
1010 location: Location,
1011 },
1012
1013 IndentationError {
1015 required: crate::indentation::RequireIndent,
1017 actual: usize,
1019 location: Location,
1020 },
1021
1022 WithSnippet {
1024 regions: Vec<CroppedRegion>,
1029 crop_radius: usize,
1030 error: Box<Error>,
1031 },
1032
1033 #[cfg(any(feature = "garde", feature = "validator"))]
1035 ValidationError {
1036 source: ValidationSource,
1037 issues: Vec<ValidationIssue>,
1038 locations: PathMap,
1039 },
1040
1041 #[cfg(any(feature = "garde", feature = "validator"))]
1043 ValidationErrors {
1044 source: ValidationSource,
1045 errors: Vec<Error>,
1046 },
1047}
1048
1049impl Error {
1050 #[cold]
1051 #[inline(never)]
1052 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
1053 self.with_snippet_named(text, "<input>", crop_radius)
1054 }
1055
1056 #[cold]
1057 #[inline(never)]
1058 pub(crate) fn with_snippet_named(
1059 self,
1060 text: &str,
1061 source_name: &str,
1062 crop_radius: usize,
1063 ) -> Self {
1064 let source_name = sanitize_snippet_source_name(source_name);
1065
1066 let inner = match self {
1069 Error::WithSnippet { error, .. } => *error,
1070 other => other,
1071 };
1072
1073 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1075
1076 let regions = collect_snippet_regions(
1077 &inner,
1078 text,
1079 source_name.as_ref(),
1080 crate::de_snippet::LineMapping::Identity,
1081 crop_radius,
1082 );
1083
1084 Error::WithSnippet {
1085 regions,
1086 crop_radius,
1087 error: Box::new(inner),
1088 }
1089 }
1090
1091 #[cfg(feature = "include")]
1092 #[cold]
1093 #[inline(never)]
1094 pub(crate) fn with_additional_snippet_named(
1095 mut self,
1096 text: &str,
1097 source_name: &str,
1098 location: &Location,
1099 crop_radius: usize,
1100 ) -> Self {
1101 let source_name = sanitize_snippet_source_name(source_name);
1102
1103 if crop_radius == 0 || *location == Location::UNKNOWN {
1104 return self;
1105 }
1106
1107 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1108 let mapping = crate::de_snippet::LineMapping::Identity;
1109
1110 let Some(region) =
1111 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1112 else {
1113 return self;
1114 };
1115
1116 if let Error::WithSnippet {
1117 ref mut regions, ..
1118 } = self
1119 {
1120 regions.push(region);
1121 }
1122 self
1123 }
1124
1125 #[cfg(feature = "include")]
1126 #[cold]
1127 #[inline(never)]
1128 pub(crate) fn with_additional_snippet_offset_named(
1129 mut self,
1130 text: &str,
1131 start_line: usize,
1132 source_name: &str,
1133 location: &Location,
1134 crop_radius: usize,
1135 ) -> Self {
1136 let source_name = sanitize_snippet_source_name(source_name);
1137
1138 if crop_radius == 0 || *location == Location::UNKNOWN {
1139 return self;
1140 }
1141
1142 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1143 let mapping = crate::de_snippet::LineMapping::Offset { start_line };
1144
1145 let Some(region) =
1146 cropped_region_for_location(text, source_name.as_ref(), location, mapping, crop_radius)
1147 else {
1148 return self;
1149 };
1150
1151 if let Error::WithSnippet {
1152 ref mut regions, ..
1153 } = self
1154 {
1155 regions.push(region);
1156 }
1157 self
1158 }
1159
1160 #[cold]
1161 #[inline(never)]
1162 pub(crate) fn with_snippet_offset_named(
1163 self,
1164 text: &str,
1165 start_line: usize,
1166 source_name: &str,
1167 crop_radius: usize,
1168 ) -> Self {
1169 let source_name = sanitize_snippet_source_name(source_name);
1170
1171 let inner = match self {
1172 Error::WithSnippet { error, .. } => *error,
1173 other => other,
1174 };
1175
1176 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
1178
1179 let regions = collect_snippet_regions(
1180 &inner,
1181 text,
1182 source_name.as_ref(),
1183 crate::de_snippet::LineMapping::Offset { start_line },
1184 crop_radius,
1185 );
1186
1187 Error::WithSnippet {
1188 regions,
1189 crop_radius,
1190 error: Box::new(inner),
1191 }
1192 }
1193
1194 #[must_use]
1196 pub fn without_snippet(&self) -> &Self {
1197 match self {
1198 Error::WithSnippet { error, .. } => error,
1199 other => other,
1200 }
1201 }
1202
1203 #[must_use]
1209 pub fn render(&self) -> String {
1210 self.render_with_options(RenderOptions::default())
1211 }
1212
1213 #[must_use]
1215 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
1216 self.render_with_options(RenderOptions {
1217 formatter,
1218 snippets: SnippetMode::Auto,
1219 })
1220 }
1221
1222 #[must_use]
1224 pub fn render_with_options(&self, options: RenderOptions<'_>) -> String {
1225 struct RenderDisplay<'a> {
1226 err: &'a Error,
1227 options: RenderOptions<'a>,
1228 }
1229
1230 impl fmt::Display for RenderDisplay<'_> {
1231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1232 fmt_error_rendered(f, self.err, self.options)
1233 }
1234 }
1235
1236 RenderDisplay { err: self, options }.to_string()
1237 }
1238
1239 #[cold]
1250 #[inline(never)]
1251 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
1252 Error::Message {
1253 msg: s.into(),
1254 location: Location::UNKNOWN,
1255 }
1256 }
1257
1258 #[cold]
1260 #[inline(never)]
1261 pub(crate) fn invalid_options<S: Into<String>>(s: S) -> Self {
1262 Error::InvalidOptions {
1263 msg: s.into(),
1264 location: Location::UNKNOWN,
1265 }
1266 }
1267
1268 #[cold]
1272 #[inline(never)]
1273 pub(crate) fn quoting_required(value: &str, interpolated: bool) -> Self {
1274 let location = Location::UNKNOWN;
1277 let value = if !interpolated
1278 && (parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
1279 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
1280 || parse_yaml11_bool(value).is_ok()
1281 || scalar_is_nullish(value, &ScalarStyle::Plain))
1282 {
1283 value.to_string()
1284 } else {
1285 String::new()
1286 };
1287 Error::QuotingRequired { value, location }
1288 }
1289
1290 #[cold]
1301 #[inline(never)]
1302 pub(crate) fn unexpected(what: &'static str) -> Self {
1303 Error::Unexpected {
1304 expected: what,
1305 location: Location::UNKNOWN,
1306 }
1307 }
1308
1309 #[cold]
1314 #[inline(never)]
1315 pub(crate) fn eof() -> Self {
1316 Error::Eof {
1317 location: Location::UNKNOWN,
1318 }
1319 }
1320
1321 #[cold]
1322 #[inline(never)]
1323 pub(crate) fn multiple_documents(hint: &'static str) -> Self {
1324 Error::MultipleDocuments {
1325 hint,
1326 location: Location::UNKNOWN,
1327 }
1328 }
1329
1330 #[cfg(any(feature = "garde", feature = "validator"))]
1331 pub(crate) fn validation_error(
1332 source: ValidationSource,
1333 issues: Vec<ValidationIssue>,
1334 locations: PathMap,
1335 ) -> Self {
1336 Error::ValidationError {
1337 source,
1338 issues,
1339 locations,
1340 }
1341 }
1342
1343 #[cfg(any(feature = "garde", feature = "validator"))]
1344 pub(crate) fn validation_errors(source: ValidationSource, errors: Vec<Error>) -> Self {
1345 Error::ValidationErrors { source, errors }
1346 }
1347
1348 #[cfg(any(feature = "garde", feature = "validator"))]
1349 pub(crate) fn is_validation_error(&self) -> bool {
1350 matches!(self, Error::ValidationError { .. })
1351 }
1352
1353 #[cold]
1358 #[inline(never)]
1359 pub(crate) fn unknown_anchor() -> Self {
1360 Error::UnknownAnchor {
1361 location: Location::UNKNOWN,
1362 }
1363 }
1364
1365 #[cold]
1370 #[inline(never)]
1371 #[must_use]
1372 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
1373 Error::CannotBorrowTransformedString {
1374 reason,
1375 location: Location::UNKNOWN,
1376 }
1377 }
1378
1379 #[cold]
1390 #[inline(never)]
1391 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
1392 match &mut self {
1393 Error::Message { location, .. }
1394 | Error::InvalidOptions { location, .. }
1395 | Error::ExternalMessage { location, .. }
1396 | Error::Eof { location }
1397 | Error::MultipleDocuments { location, .. }
1398 | Error::Unexpected { location, .. }
1399 | Error::MergeValueNotMapOrSeqOfMaps { location }
1400 | Error::MergeKeyNotAllowed { location }
1401 | Error::InvalidBinaryBase64 { location }
1402 | Error::BinaryNotUtf8 { location }
1403 | Error::TaggedScalarCannotDeserializeIntoString { location }
1404 | Error::UnexpectedSequenceEnd { location }
1405 | Error::UnexpectedMappingEnd { location }
1406 | Error::InvalidBooleanStrict { location }
1407 | Error::InvalidCharNull { location }
1408 | Error::InvalidCharNotSingleScalar { location }
1409 | Error::NullIntoString { location }
1410 | Error::BytesNotSupportedMissingBinaryTag { location }
1411 | Error::UnexpectedValueForUnit { location }
1412 | Error::ExpectedEmptyMappingForUnitStruct { location }
1413 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1414 | Error::InternalSeedReusedForMapKey { location }
1415 | Error::ValueRequestedBeforeKey { location }
1416 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1417 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1418 | Error::UnexpectedValueForUnitEnumVariant { location }
1419 | Error::AliasReplayCounterOverflow { location }
1420 | Error::AliasReplayLimitExceeded { location, .. }
1421 | Error::AliasExpansionLimitExceeded { location, .. }
1422 | Error::AliasReplayStackDepthExceeded { location, .. }
1423 | Error::FoldedBlockScalarMustIndentContent { location }
1424 | Error::InternalDepthUnderflow { location }
1425 | Error::InternalRecursionStackEmpty { location }
1426 | Error::RecursiveReferencesRequireWeakTypes { location }
1427 | Error::InvalidScalar { location, .. }
1428 | Error::NonFiniteFloat { location, .. }
1429 | Error::SerdeInvalidType { location, .. }
1430 | Error::SerdeInvalidValue { location, .. }
1431 | Error::SerdeUnknownVariant { location, .. }
1432 | Error::SerdeUnknownField { location, .. }
1433 | Error::SerdeMissingField { location, .. }
1434 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1435 | Error::DuplicateMappingKey { location, .. }
1436 | Error::TaggedEnumMismatch { location, .. }
1437 | Error::SerdeVariantId { location, .. }
1438 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1439 | Error::HookError { location, .. }
1440 | Error::UnresolvedProperty { location, .. }
1441 | Error::InvalidPropertyName { location, .. }
1442 | Error::PropertyRequiredButUnset { location, .. }
1443 | Error::PropertyRequiredButEmpty { location, .. }
1444 | Error::ContainerEndMismatch { location, .. }
1445 | Error::UnknownAnchor { location, .. }
1446 | Error::CyclicInclude { location, .. }
1447 | Error::UnsupportedIncludeForm { location, .. }
1448 | Error::ResolverError { location, .. }
1449 | Error::QuotingRequired { location, .. }
1450 | Error::Budget { location, .. }
1451 | Error::CannotBorrowTransformedString { location, .. }
1452 | Error::IndentationError { location, .. } => {
1453 *location = set_location;
1454 }
1455 Error::InvalidUtf8Input => {}
1456 Error::IOError { .. } => {} Error::AliasError { .. } => {
1458 }
1460 Error::WithSnippet { error, .. } => {
1461 let inner = *std::mem::replace(error, Box::new(Error::eof()));
1462 **error = inner.with_location(set_location);
1463 }
1464 #[cfg(any(feature = "garde", feature = "validator"))]
1465 Error::ValidationError { .. } => {
1466 }
1468 #[cfg(any(feature = "garde", feature = "validator"))]
1469 Error::ValidationErrors { .. } => {
1470 }
1472 }
1473 self
1474 }
1475
1476 #[must_use]
1484 pub fn location(&self) -> Option<Location> {
1485 #[cfg(any(feature = "garde", feature = "validator"))]
1486 if let Error::ValidationErrors { errors, .. } = self {
1487 return errors.iter().find_map(Error::location);
1490 }
1491
1492 self.locations().and_then(Locations::primary_location)
1493 }
1494 #[must_use]
1504 pub fn locations(&self) -> Option<Locations> {
1505 match self {
1506 Error::Message { location, .. }
1507 | Error::InvalidOptions { location, .. }
1508 | Error::ExternalMessage { location, .. }
1509 | Error::Eof { location }
1510 | Error::MultipleDocuments { location, .. }
1511 | Error::Unexpected { location, .. }
1512 | Error::MergeValueNotMapOrSeqOfMaps { location }
1513 | Error::MergeKeyNotAllowed { location }
1514 | Error::InvalidBinaryBase64 { location }
1515 | Error::BinaryNotUtf8 { location }
1516 | Error::TaggedScalarCannotDeserializeIntoString { location }
1517 | Error::UnexpectedSequenceEnd { location }
1518 | Error::UnexpectedMappingEnd { location }
1519 | Error::InvalidBooleanStrict { location }
1520 | Error::InvalidCharNull { location }
1521 | Error::InvalidCharNotSingleScalar { location }
1522 | Error::NullIntoString { location }
1523 | Error::BytesNotSupportedMissingBinaryTag { location }
1524 | Error::UnexpectedValueForUnit { location }
1525 | Error::ExpectedEmptyMappingForUnitStruct { location }
1526 | Error::UnexpectedContainerEndWhileSkippingNode { location }
1527 | Error::InternalSeedReusedForMapKey { location }
1528 | Error::ValueRequestedBeforeKey { location }
1529 | Error::ExpectedStringKeyForExternallyTaggedEnum { location }
1530 | Error::ExternallyTaggedEnumExpectedScalarOrMapping { location }
1531 | Error::UnexpectedValueForUnitEnumVariant { location }
1532 | Error::AliasReplayCounterOverflow { location }
1533 | Error::AliasReplayLimitExceeded { location, .. }
1534 | Error::AliasExpansionLimitExceeded { location, .. }
1535 | Error::AliasReplayStackDepthExceeded { location, .. }
1536 | Error::FoldedBlockScalarMustIndentContent { location }
1537 | Error::InternalDepthUnderflow { location }
1538 | Error::InternalRecursionStackEmpty { location }
1539 | Error::RecursiveReferencesRequireWeakTypes { location }
1540 | Error::InvalidScalar { location, .. }
1541 | Error::NonFiniteFloat { location, .. }
1542 | Error::SerdeInvalidType { location, .. }
1543 | Error::SerdeInvalidValue { location, .. }
1544 | Error::SerdeUnknownVariant { location, .. }
1545 | Error::SerdeUnknownField { location, .. }
1546 | Error::SerdeMissingField { location, .. }
1547 | Error::UnexpectedContainerEndWhileReadingKeyNode { location }
1548 | Error::DuplicateMappingKey { location, .. }
1549 | Error::TaggedEnumMismatch { location, .. }
1550 | Error::SerdeVariantId { location, .. }
1551 | Error::ExpectedMappingEndAfterEnumVariantValue { location }
1552 | Error::HookError { location, .. }
1553 | Error::UnresolvedProperty { location, .. }
1554 | Error::InvalidPropertyName { location, .. }
1555 | Error::PropertyRequiredButUnset { location, .. }
1556 | Error::PropertyRequiredButEmpty { location, .. }
1557 | Error::ContainerEndMismatch { location, .. }
1558 | Error::UnknownAnchor { location, .. }
1559 | Error::CyclicInclude { location, .. }
1560 | Error::UnsupportedIncludeForm { location, .. }
1561 | Error::ResolverError { location, .. }
1562 | Error::QuotingRequired { location, .. }
1563 | Error::Budget { location, .. }
1564 | Error::CannotBorrowTransformedString { location, .. }
1565 | Error::IndentationError { location, .. } => Locations::same(location),
1566 Error::InvalidUtf8Input => None,
1567 Error::IOError { .. } => None,
1568 Error::AliasError { locations, .. } => Some(*locations),
1569 Error::WithSnippet { error, .. } => error.locations(),
1570 #[cfg(any(feature = "garde", feature = "validator"))]
1571 Error::ValidationError {
1572 issues, locations, ..
1573 } => issues
1574 .first()
1575 .and_then(|issue| locations.search_with_ancestor_fallback(&issue.path))
1576 .map(|(locs, _)| locs),
1577 #[cfg(any(feature = "garde", feature = "validator"))]
1578 Error::ValidationErrors { errors, .. } => errors.first().and_then(Error::locations),
1579 }
1580 }
1581
1582 #[cold]
1587 #[inline(never)]
1588 pub(crate) fn from_scan_error(err: ScanError) -> Self {
1589 let err = match err.try_into_input_io_error() {
1590 Ok(error) => {
1591 let cause = match error.try_into_io_error() {
1592 Ok(error) => error,
1593 Err(error) => {
1594 let kind = error
1595 .io_error()
1596 .map_or(std::io::ErrorKind::Other, std::io::Error::kind);
1597 std::io::Error::new(kind, error)
1598 }
1599 };
1600 return Error::IOError { cause };
1601 }
1602 Err(err) => err,
1603 };
1604
1605 let mark = err.marker();
1606 let location = Location::new(mark.line(), mark.col() + 1)
1607 .with_span(crate::Span::new(mark.index() as u64, 1));
1608
1609 match err.kind() {
1610 ErrorKind::InputDecoding { message } => {
1611 return Error::IOError {
1612 cause: std::io::Error::new(std::io::ErrorKind::InvalidData, message.clone()),
1613 };
1614 }
1615 ErrorKind::InputByteLimitExceeded { limit } => {
1616 return Error::IOError {
1617 cause: std::io::Error::new(
1618 std::io::ErrorKind::FileTooLarge,
1619 format!("input size limit of {limit} bytes exceeded"),
1620 ),
1621 };
1622 }
1623 ErrorKind::MultipleDocumentsUnsupported => {
1624 return Error::MultipleDocuments {
1625 hint: "only one document is supported in this context",
1626 location,
1627 };
1628 }
1629 ErrorKind::UnknownAnchor => return Error::UnknownAnchor { location },
1630 _ => {}
1631 }
1632
1633 let message = err.info();
1634 Error::ExternalMessage {
1635 source: Box::new(ExternalMessageSource::Parser(err)),
1636 msg: message,
1637 code: None,
1638 params: Vec::new(),
1639 location,
1640 }
1641 }
1642}
1643
1644fn fmt_error_plain_with_formatter(
1645 f: &mut fmt::Formatter<'_>,
1646 err: &Error,
1647 formatter: &dyn MessageFormatter,
1648) -> fmt::Result {
1649 let err = err.without_snippet();
1650
1651 let msg = formatter.format_message(err);
1652
1653 #[cfg(any(feature = "garde", feature = "validator"))]
1657 if matches!(err, Error::ValidationError { .. }) {
1658 return write!(f, "{msg}");
1659 }
1660
1661 if let Some(loc) = err.location() {
1662 fmt_with_location(f, formatter.localizer(), msg.as_ref(), &loc)?;
1663 } else {
1664 write!(f, "{msg}")?;
1665 }
1666
1667 #[cfg(any(feature = "garde", feature = "validator"))]
1668 if let Error::ValidationErrors { errors, .. } = err {
1669 for err in errors {
1670 writeln!(f)?;
1671 writeln!(f)?;
1672 fmt_error_plain_with_formatter(f, err, formatter)?;
1673 }
1674 }
1675
1676 Ok(())
1677}
1678
1679fn pick_cropped_region<'a>(
1680 regions: &'a [CroppedRegion],
1681 location: &Location,
1682) -> Option<&'a CroppedRegion> {
1683 let source_id = location.source_id();
1684
1685 if source_id != 0 {
1686 if let Some(region) = regions.iter().find(|r| r.covers_exact_source(location)) {
1687 return Some(region);
1688 }
1689 if let Some(region) = regions.iter().find(|r| r.location.source_id() == source_id) {
1690 return Some(region);
1691 }
1692 if let Some(region) = regions
1693 .iter()
1694 .find(|r| r.location.source_id() == 0 && r.covers(location))
1695 {
1696 return Some(region);
1697 }
1698 return None;
1699 }
1700
1701 regions
1702 .iter()
1703 .find(|r| r.covers(location))
1704 .or_else(|| regions.first())
1705}
1706
1707fn writeln_anchor_intro(
1708 f: &mut fmt::Formatter<'_>,
1709 l10n: &dyn Localizer,
1710 def_loc: Location,
1711 def_region: &CroppedRegion,
1712) -> fmt::Result {
1713 let line = l10n.value_comes_from_the_anchor(def_loc);
1714 let Some(prefix) = snippet_window_frame_prefix_offset(
1715 def_region.text.as_str(),
1716 def_region.start_line,
1717 &def_loc,
1718 ) else {
1719 return writeln!(f, "{line}");
1720 };
1721
1722 match line.strip_prefix(" |") {
1723 Some(rest) => writeln!(f, "{prefix}{rest}"),
1724 None => writeln!(f, "{line}"),
1725 }
1726}
1727
1728fn fmt_error_rendered(
1729 f: &mut fmt::Formatter<'_>,
1730 err: &Error,
1731 options: RenderOptions<'_>,
1732) -> fmt::Result {
1733 if options.snippets == SnippetMode::Off {
1734 return fmt_error_plain_with_formatter(f, err, options.formatter);
1735 }
1736
1737 match err {
1738 #[cfg(any(feature = "garde", feature = "validator"))]
1739 Error::ValidationErrors { errors, .. } => {
1740 let msg = options.formatter.format_message(err);
1741 if !msg.is_empty() {
1742 writeln!(f, "{msg}")?;
1743 }
1744 let mut first = true;
1745 for err in errors {
1746 if !first {
1747 writeln!(f)?;
1748 writeln!(f)?;
1749 }
1750 first = false;
1751 fmt_error_rendered(f, err, options)?;
1752 }
1753 Ok(())
1754 }
1755
1756 Error::WithSnippet {
1757 regions,
1758 crop_radius,
1759 error,
1760 } => {
1761 if *crop_radius == 0 {
1762 return fmt_error_plain_with_formatter(f, error, options.formatter);
1764 }
1765
1766 if regions.is_empty() {
1767 return fmt_error_plain_with_formatter(f, error, options.formatter);
1768 }
1769
1770 #[cfg(any(feature = "garde", feature = "validator"))]
1773 if let Error::ValidationError {
1774 source,
1775 issues,
1776 locations,
1777 } = error.as_ref()
1778 {
1779 return fmt_validation_error_with_snippets_offset(
1780 f,
1781 options.formatter.localizer(),
1782 &source.external_message_source(),
1783 issues,
1784 locations,
1785 regions,
1786 *crop_radius,
1787 );
1788 }
1789 #[cfg(any(feature = "garde", feature = "validator"))]
1790 if let Error::ValidationErrors { errors, .. } = error.as_ref() {
1791 let msg = options.formatter.format_message(error);
1792 if !msg.is_empty() {
1793 writeln!(f, "{msg}")?;
1794 }
1795 let mut first = true;
1796 for err in errors {
1797 if !first {
1798 writeln!(f)?;
1799 writeln!(f)?;
1800 }
1801 first = false;
1802 fmt_error_with_snippets_offset(
1803 f,
1804 err,
1805 regions,
1806 *crop_radius,
1807 options.formatter,
1808 )?;
1809 }
1810 return Ok(());
1811 }
1812
1813 let Some(location) = error.location() else {
1816 return fmt_error_plain_with_formatter(f, error, options.formatter);
1817 };
1818 if location == Location::UNKNOWN {
1819 return fmt_error_plain_with_formatter(f, error, options.formatter);
1820 }
1821
1822 let l10n = options.formatter.localizer();
1823
1824 let Some(region) = pick_cropped_region(regions, &location) else {
1825 return fmt_error_plain_with_formatter(f, error, options.formatter);
1826 };
1827
1828 let dual_locations = error.locations().filter(|locs| {
1830 locs.reference_location != Location::UNKNOWN
1831 && locs.defined_location != Location::UNKNOWN
1832 && locs.reference_location != locs.defined_location
1833 });
1834
1835 let mut msg = options.formatter.format_message(error);
1836
1837 if dual_locations.is_some()
1841 && let Error::AliasError { locations, .. } = error.as_ref()
1842 {
1843 let suffix = l10n.alias_defined_at(locations.defined_location);
1844 if let Some(stripped) = msg.as_ref().strip_suffix(&suffix) {
1845 msg = Cow::Owned(stripped.to_string());
1846 }
1847 }
1848
1849 if let Some(locs) = dual_locations {
1850 let ref_loc = locs.reference_location;
1851 let def_loc = locs.defined_location;
1852
1853 let used_region = pick_cropped_region(regions, &ref_loc).unwrap_or(region);
1854 let label = l10n.value_used_here();
1855 let ctx = crate::de_snippet::Snippet::new(
1856 used_region.text.as_str(),
1857 used_region.source_name.as_str(),
1858 *crop_radius,
1859 )
1860 .with_offset(used_region.start_line);
1861 ctx.fmt_or_fallback_with_label(
1862 f,
1863 Level::ERROR,
1864 l10n,
1865 msg.as_ref(),
1866 label.as_ref(),
1867 &ref_loc,
1868 )?;
1869
1870 let def_region = pick_cropped_region(regions, &def_loc).unwrap_or(region);
1871 writeln!(f)?;
1872 writeln_anchor_intro(f, l10n, def_loc, def_region)?;
1873 fmt_snippet_window_offset_or_fallback(
1874 f,
1875 l10n,
1876 &def_loc,
1877 def_region.text.as_str(),
1878 def_region.start_line,
1879 l10n.defined_window().as_ref(),
1880 *crop_radius,
1881 )?;
1882 Ok(())
1883 } else {
1884 let ctx = crate::de_snippet::Snippet::new(
1886 region.text.as_str(),
1887 region.source_name.as_str(),
1888 *crop_radius,
1889 )
1890 .with_offset(region.start_line);
1891 ctx.fmt_or_fallback(f, Level::ERROR, l10n, msg.as_ref(), &location)?;
1892
1893 for extra_region in regions {
1894 if std::ptr::eq(extra_region, region) {
1895 continue;
1896 }
1897 writeln!(f)?;
1898 writeln!(f, "included from here:")?;
1899 let extra_ctx = crate::de_snippet::Snippet::new(
1900 extra_region.text.as_str(),
1901 extra_region.source_name.as_str(),
1902 *crop_radius,
1903 )
1904 .with_offset(extra_region.start_line);
1905 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
1906 }
1907 Ok(())
1908 }
1909 }
1910 _ => fmt_error_plain_with_formatter(f, err, options.formatter),
1911 }
1912}
1913
1914impl fmt::Display for Error {
1915 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1916 fmt_error_rendered(f, self, RenderOptions::default())
1917 }
1918}
1919
1920impl fmt::Debug for Error {
1921 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1922 fmt::Display::fmt(self, f)
1923 }
1924}
1925
1926#[cfg(any(feature = "garde", feature = "validator"))]
1927fn fmt_validation_error_with_snippets_offset(
1928 f: &mut fmt::Formatter<'_>,
1929 l10n: &dyn Localizer,
1930 source: &ExternalMessageSource,
1931 issues: &[ValidationIssue],
1932 locations: &PathMap,
1933 regions: &[CroppedRegion],
1934 crop_radius: usize,
1935) -> fmt::Result {
1936 let mut first = true;
1937 for issue in issues {
1938 if !first {
1939 writeln!(f)?;
1940 }
1941 first = false;
1942
1943 let original_leaf = issue
1944 .path
1945 .leaf_string()
1946 .unwrap_or_else(|| l10n.root_path_label().into_owned());
1947
1948 let (locs, resolved_leaf) = locations
1949 .search_with_ancestor_fallback(&issue.path)
1950 .unwrap_or((Locations::UNKNOWN, original_leaf));
1951
1952 let ref_loc = locs.reference_location;
1953 let def_loc = locs.defined_location;
1954
1955 let resolved_path = format_path_with_resolved_leaf(&issue.path, &resolved_leaf);
1956 let entry = issue.display_entry_overridden(l10n, (*source).clone());
1957 let base_msg = l10n.validation_base_message(&entry, &resolved_path);
1958
1959 let mut rendered_regions = Vec::new();
1960
1961 match (ref_loc, def_loc) {
1962 (Location::UNKNOWN, Location::UNKNOWN) => {
1963 write!(f, "{base_msg}")?;
1964 }
1965 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
1966 let label = l10n.defined();
1967 if let Some(region) = pick_cropped_region(regions, &r) {
1968 rendered_regions.push(std::ptr::from_ref(region));
1969 let ctx = crate::de_snippet::Snippet::new(
1970 region.text.as_str(),
1971 label.as_ref(),
1972 crop_radius,
1973 )
1974 .with_offset(region.start_line);
1975 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &r)?;
1976 } else {
1977 fmt_with_location(f, l10n, &base_msg, &r)?;
1978 }
1979 }
1980 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
1981 let label = l10n.defined_here();
1982 if let Some(region) = pick_cropped_region(regions, &d) {
1983 rendered_regions.push(std::ptr::from_ref(region));
1984 let ctx = crate::de_snippet::Snippet::new(
1985 region.text.as_str(),
1986 label.as_ref(),
1987 crop_radius,
1988 )
1989 .with_offset(region.start_line);
1990 ctx.fmt_or_fallback(f, Level::ERROR, l10n, &base_msg, &d)?;
1991 } else {
1992 fmt_with_location(f, l10n, &base_msg, &d)?;
1993 }
1994 }
1995 (r, d) => {
1996 let label = l10n.value_used_here();
1997 let invalid_here = l10n.invalid_here(&base_msg);
1998 if let Some(region) = pick_cropped_region(regions, &r) {
1999 rendered_regions.push(std::ptr::from_ref(region));
2000 let ctx = crate::de_snippet::Snippet::new(
2001 region.text.as_str(),
2002 region.source_name.as_str(),
2003 crop_radius,
2004 )
2005 .with_offset(region.start_line);
2006 ctx.fmt_or_fallback_with_label(
2007 f,
2008 Level::ERROR,
2009 l10n,
2010 &invalid_here,
2011 label.as_ref(),
2012 &r,
2013 )?;
2014 } else {
2015 fmt_with_location(f, l10n, &invalid_here, &r)?;
2016 }
2017 writeln!(f)?;
2018 if let Some(region) = pick_cropped_region(regions, &d) {
2019 writeln_anchor_intro(f, l10n, d, region)?;
2020 rendered_regions.push(std::ptr::from_ref(region));
2021 crate::de_snippet::fmt_snippet_window_offset_or_fallback(
2022 f,
2023 l10n,
2024 &d,
2025 region.text.as_str(),
2026 region.start_line,
2027 l10n.defined_window().as_ref(),
2028 crop_radius,
2029 )?;
2030 } else {
2031 writeln!(f, "{}", l10n.value_comes_from_the_anchor(d))?;
2032 fmt_with_location(f, l10n, l10n.defined_window().as_ref(), &d)?;
2033 }
2034 }
2035 }
2036
2037 for extra_region in regions {
2038 if rendered_regions.contains(&std::ptr::from_ref(extra_region)) {
2039 continue;
2040 }
2041 writeln!(f)?;
2042 writeln!(f, "included from here:")?;
2043 let extra_ctx = crate::de_snippet::Snippet::new(
2044 extra_region.text.as_str(),
2045 extra_region.source_name.as_str(),
2046 crop_radius,
2047 )
2048 .with_offset(extra_region.start_line);
2049 extra_ctx.fmt_or_fallback(f, Level::NOTE, l10n, "", &extra_region.location)?;
2050 }
2051 }
2052 Ok(())
2053}
2054
2055#[cfg(any(feature = "garde", feature = "validator"))]
2056fn fmt_error_with_snippets_offset(
2057 f: &mut fmt::Formatter<'_>,
2058 err: &Error,
2059 regions: &[CroppedRegion],
2060 crop_radius: usize,
2061 formatter: &dyn MessageFormatter,
2062) -> fmt::Result {
2063 if crop_radius == 0 {
2064 return fmt_error_plain_with_formatter(f, err, formatter);
2065 }
2066
2067 if let Error::WithSnippet { .. } = err {
2069 return fmt_error_rendered(f, err, RenderOptions::new(formatter));
2070 }
2071
2072 #[cfg(any(feature = "garde", feature = "validator"))]
2073 if let Error::ValidationError {
2074 source,
2075 issues,
2076 locations,
2077 } = err
2078 {
2079 return fmt_validation_error_with_snippets_offset(
2080 f,
2081 formatter.localizer(),
2082 &source.external_message_source(),
2083 issues,
2084 locations,
2085 regions,
2086 crop_radius,
2087 );
2088 }
2089
2090 let msg = formatter.format_message(err);
2091 let Some(location) = err.location() else {
2092 return write!(f, "{msg}");
2093 };
2094 if location == Location::UNKNOWN {
2095 return write!(f, "{msg}");
2096 }
2097
2098 let Some(region) = pick_cropped_region(regions, &location) else {
2099 return fmt_with_location(f, formatter.localizer(), msg.as_ref(), &location);
2100 };
2101 let ctx = crate::de_snippet::Snippet::new(
2102 region.text.as_str(),
2103 region.source_name.as_str(),
2104 crop_radius,
2105 )
2106 .with_offset(region.start_line);
2107 ctx.fmt_or_fallback(
2108 f,
2109 Level::ERROR,
2110 formatter.localizer(),
2111 msg.as_ref(),
2112 &location,
2113 )
2114}
2115
2116#[cfg(feature = "validator")]
2117pub(crate) fn collect_validator_issues(errors: &ValidationErrors) -> Vec<ValidationIssue> {
2118 let mut out = Vec::new();
2119 let root = PathKey::empty();
2120 collect_validator_issues_inner(errors, &root, &mut out);
2121 out
2122}
2123
2124#[cfg(feature = "validator")]
2125fn collect_validator_issues_inner(
2126 errors: &ValidationErrors,
2127 path: &PathKey,
2128 out: &mut Vec<ValidationIssue>,
2129) {
2130 for (field, kind) in errors.errors() {
2131 let field_path = path.clone().join(field.as_ref());
2132 match kind {
2133 ValidationErrorsKind::Field(entries) => {
2134 for entry in entries {
2135 let mut params = Vec::new();
2136 for (k, v) in &entry.params {
2137 params.push((k.to_string(), v.to_string()));
2138 }
2139
2140 out.push(ValidationIssue {
2141 path: field_path.clone(),
2142 code: entry.code.to_string(),
2143 message: entry.message.as_ref().map(std::string::ToString::to_string),
2144 params,
2145 });
2146 }
2147 }
2148 ValidationErrorsKind::Struct(inner) => {
2149 collect_validator_issues_inner(inner, &field_path, out);
2150 }
2151 ValidationErrorsKind::List(list) => {
2152 for (idx, inner) in list {
2153 let index_path = field_path.clone().join(*idx);
2154 collect_validator_issues_inner(inner, &index_path, out);
2155 }
2156 }
2157 }
2158 }
2159}
2160
2161#[cfg(feature = "garde")]
2162pub(crate) fn collect_garde_issues(report: &garde::Report) -> Vec<ValidationIssue> {
2163 let mut out = Vec::new();
2164 for (path, entry) in report.iter() {
2165 out.push(ValidationIssue {
2166 path: path_key_from_garde(path),
2167 code: "garde".to_string(),
2168 message: Some(entry.message().to_string()),
2169 params: Vec::new(),
2170 });
2171 }
2172 out
2173}
2174impl std::error::Error for Error {}
2175
2176#[cold]
2178#[inline(never)]
2179fn maybe_attach_fallback_location(mut err: Error) -> Error {
2180 let loc = MISSING_FIELD_FALLBACK.with(std::cell::Cell::get);
2181 if let Some(loc) = loc
2182 && loc != Location::UNKNOWN
2183 {
2184 err = err.with_location(loc);
2185 }
2186 err
2187}
2188
2189impl de::Error for Error {
2190 #[cold]
2191 #[inline(never)]
2192 fn custom<T: fmt::Display>(msg: T) -> Self {
2193 Error::msg(redact_custom_message(msg.to_string()))
2197 }
2198
2199 #[cold]
2200 #[inline(never)]
2201 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2202 maybe_attach_fallback_location(Error::SerdeInvalidType {
2204 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2205 expected: exp.to_string(),
2206 location: Location::UNKNOWN,
2207 })
2208 }
2209
2210 #[cold]
2211 #[inline(never)]
2212 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
2213 maybe_attach_fallback_location(Error::SerdeInvalidValue {
2214 unexpected: redact_dynamic_value(unexp.to_string(), "an interpolated value"),
2215 expected: exp.to_string(),
2216 location: Location::UNKNOWN,
2217 })
2218 }
2219
2220 #[cold]
2221 #[inline(never)]
2222 fn invalid_length(len: usize, exp: &dyn de::Expected) -> Self {
2223 maybe_attach_fallback_location(Error::msg(format!("invalid length {len}, expected {exp}")))
2224 }
2225
2226 #[cold]
2227 #[inline(never)]
2228 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
2229 maybe_attach_fallback_location(Error::SerdeUnknownVariant {
2230 variant: redact_dynamic_identifier(variant, "an interpolated variant"),
2231 expected: expected.to_vec(),
2232 location: Location::UNKNOWN,
2233 })
2234 }
2235
2236 #[cold]
2237 #[inline(never)]
2238 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
2239 maybe_attach_fallback_location(Error::SerdeUnknownField {
2240 field: redact_dynamic_identifier(field, "an interpolated field"),
2241 expected: expected.to_vec(),
2242 location: Location::UNKNOWN,
2243 })
2244 }
2245
2246 #[cold]
2247 #[inline(never)]
2248 fn missing_field(field: &'static str) -> Self {
2249 maybe_attach_fallback_location(Error::SerdeMissingField {
2250 field,
2251 location: Location::UNKNOWN,
2252 })
2253 }
2254}
2255
2256#[cold]
2266#[inline(never)]
2267fn fmt_with_location(
2268 f: &mut fmt::Formatter<'_>,
2269 l10n: &dyn Localizer,
2270 msg: &str,
2271 location: &Location,
2272) -> fmt::Result {
2273 let out = l10n.attach_location(Cow::Borrowed(msg), *location);
2274 write!(f, "{out}")
2275}
2276
2277#[cold]
2288#[inline(never)]
2289pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
2290 Error::Budget {
2291 breach,
2292 location: Location::UNKNOWN,
2293 }
2294}
2295
2296#[cfg(test)]
2297mod tests {
2298 use super::*;
2299
2300 #[test]
2301 fn message_only_input_io_scan_error_uses_portable_fallback() {
2302 let input = core::iter::once(Err::<char, _>(ErrorKind::InputIo {
2303 error: granit_parser::InputIoError::from_message("portable reader failure"),
2304 }));
2305 let scan_error = granit_parser::Parser::new_from_fallible_iter(input)
2306 .find_map(Result::err)
2307 .expect("the source error should be reported");
2308 let error = Error::from_scan_error(scan_error);
2309
2310 match error {
2311 Error::IOError { cause } => {
2312 assert_eq!(cause.kind(), std::io::ErrorKind::Other);
2313 assert_eq!(cause.to_string(), "portable reader failure");
2314 }
2315 other => panic!("expected reader I/O error, got {other:?}"),
2316 }
2317 }
2318
2319 #[rstest::rstest]
2320 #[case::unknown_anchor("while parsing node, found unknown anchor")]
2321 #[case::multiple_documents("multiple documents not supported here")]
2322 fn custom_scan_error_messages_are_not_reclassified_as_builtin_kinds(#[case] message: &str) {
2323 let scan_error = ScanError::new(granit_parser::Marker::new(0, 1, 0), message);
2324 let mapped = Error::from_scan_error(scan_error);
2325
2326 assert!(matches!(
2327 mapped,
2328 Error::ExternalMessage {
2329 ref source,
2330 ref msg,
2331 ..
2332 } if msg == message
2333 && matches!(source.as_ref(), ExternalMessageSource::Parser(error)
2334 if matches!(error.kind(), ErrorKind::Custom(_)))
2335 ));
2336 }
2337
2338 #[test]
2339 fn sanitize_snippet_source_name_replaces_control_chars() {
2340 let sanitized = sanitize_snippet_source_name("evil.yaml\nINJECTED:\u{001b}[31m");
2341 assert_eq!(sanitized, "evil.yaml INJECTED: [31m");
2342 }
2343
2344 #[test]
2345 fn with_snippet_named_sanitizes_source_name() {
2346 let err = Error::Message {
2347 msg: "oops".to_owned(),
2348 location: Location::new(1, 1),
2349 }
2350 .with_snippet_named("x: y\n", "evil.yaml\nINJECTED", 2);
2351
2352 let Error::WithSnippet { regions, .. } = err else {
2353 panic!("expected Error::WithSnippet");
2354 };
2355
2356 assert_eq!(regions.len(), 1);
2357 assert_eq!(regions[0].source_name, "evil.yaml INJECTED");
2358 }
2359
2360 #[test]
2361 fn locations_for_basic_error_duplicates_location() {
2362 let l = Location::new(3, 7);
2363 let err = Error::Message {
2364 msg: "x".to_owned(),
2365 location: l,
2366 };
2367 assert_eq!(
2368 err.locations(),
2369 Some(Locations {
2370 reference_location: l,
2371 defined_location: l,
2372 })
2373 );
2374 }
2375
2376 #[test]
2377 fn merge_key_not_allowed_location_helpers() {
2378 let l = Location::new(4, 2);
2379 let err = Error::MergeKeyNotAllowed { location: l };
2380 assert_eq!(err.location(), Some(l));
2381 assert_eq!(
2382 err.locations(),
2383 Some(Locations {
2384 reference_location: l,
2385 defined_location: l,
2386 })
2387 );
2388
2389 let updated = err.with_location(Location::new(5, 9));
2390 assert_eq!(updated.location(), Some(Location::new(5, 9)));
2391 }
2392
2393 #[test]
2394 fn serde_invalid_type_location_helpers() {
2395 let l = Location::new(4, 2);
2396 let err = Error::SerdeInvalidType {
2397 unexpected: "string".to_owned(),
2398 expected: "an integer".to_owned(),
2399 location: l,
2400 };
2401 assert_eq!(err.location(), Some(l));
2402 assert_eq!(
2403 err.locations(),
2404 Some(Locations {
2405 reference_location: l,
2406 defined_location: l,
2407 })
2408 );
2409
2410 let updated = err.with_location(Location::new(5, 9));
2411 assert_eq!(updated.location(), Some(Location::new(5, 9)));
2412 }
2413
2414 #[test]
2415 fn locations_for_io_error_is_unknown() {
2416 let err = Error::IOError {
2417 cause: std::io::Error::other("x"),
2418 };
2419 assert_eq!(err.locations(), None);
2420 }
2421
2422 #[test]
2423 fn alias_error_returns_both_locations() {
2424 let ref_loc = Location::new(5, 10);
2425 let def_loc = Location::new(2, 3);
2426 let err = Error::AliasError {
2427 msg: "test error".to_owned(),
2428 locations: Locations {
2429 reference_location: ref_loc,
2430 defined_location: def_loc,
2431 },
2432 };
2433
2434 assert_eq!(err.location(), Some(ref_loc));
2436
2437 assert_eq!(
2439 err.locations(),
2440 Some(Locations {
2441 reference_location: ref_loc,
2442 defined_location: def_loc,
2443 })
2444 );
2445 }
2446
2447 #[test]
2448 fn alias_error_display_shows_both_locations() {
2449 let ref_loc = Location::new(5, 10);
2450 let def_loc = Location::new(2, 3);
2451 let err = Error::AliasError {
2452 msg: "invalid value".to_owned(),
2453 locations: Locations {
2454 reference_location: ref_loc,
2455 defined_location: def_loc,
2456 },
2457 };
2458
2459 let display = err.to_string();
2460 assert!(display.contains("invalid value"));
2461 assert!(display.contains("line 5"));
2462 assert!(display.contains("column 10"));
2463 assert!(display.contains("line 2"));
2464 assert!(display.contains("column 3"));
2465 }
2466
2467 #[test]
2468 fn alias_error_display_with_same_locations() {
2469 let loc = Location::new(3, 7);
2470 let err = Error::AliasError {
2471 msg: "test".to_owned(),
2472 locations: Locations {
2473 reference_location: loc,
2474 defined_location: loc,
2475 },
2476 };
2477
2478 let display = err.to_string();
2479 assert!(display.contains("line 3"));
2481 assert!(display.contains("column 7"));
2482 assert!(!display.contains("defined at"));
2484 }
2485
2486 #[test]
2487 fn with_snippet_counts_trailing_empty_line_for_end_line() {
2488 let text = "a\n";
2490 let err = Error::Message {
2491 msg: "x".to_owned(),
2492 location: Location::new(2, 1),
2493 };
2494
2495 let wrapped = err.with_snippet(text, 50);
2496 let Error::WithSnippet { regions, .. } = wrapped else {
2497 panic!("expected WithSnippet wrapper");
2498 };
2499 assert_eq!(regions.len(), 1);
2500 assert_eq!(regions[0].start_line, 1);
2501 assert_eq!(regions[0].end_line, 2);
2502 }
2503
2504 #[test]
2505 fn with_snippet_offset_counts_trailing_empty_line_for_end_line() {
2506 let text = "a\n";
2508 let err = Error::Message {
2509 msg: "x".to_owned(),
2510 location: Location::new(11, 1),
2511 };
2512
2513 let wrapped = err.with_snippet_offset_named(text, 10, "<input>", 50);
2514 let Error::WithSnippet { regions, .. } = wrapped else {
2515 panic!("expected WithSnippet wrapper");
2516 };
2517 assert_eq!(regions.len(), 1);
2518 assert_eq!(regions[0].start_line, 10);
2519 assert_eq!(regions[0].end_line, 11);
2520 }
2521
2522 #[cfg(feature = "validator")]
2523 #[test]
2524 fn locations_for_validator_error_uses_first_entry() {
2525 use validator::Validate;
2526
2527 #[derive(Debug, Validate)]
2528 struct Cfg {
2529 #[validate(length(min = 2))]
2530 second_string: String,
2531 }
2532
2533 let cfg = Cfg {
2534 second_string: "x".to_owned(),
2535 };
2536 let errors = cfg.validate().expect_err("validation error expected");
2537
2538 let referenced_loc = Location::new(3, 15);
2539 let defined_loc = Location::new(2, 18);
2540
2541 let mut locations = PathMap::new();
2542 locations.insert(
2543 PathKey::empty().join("secondString"),
2544 Locations {
2545 reference_location: referenced_loc,
2546 defined_location: defined_loc,
2547 },
2548 );
2549
2550 let err = Error::ValidationError {
2551 source: ValidationSource::Validator,
2552 issues: crate::de_error::collect_validator_issues(&errors),
2553 locations,
2554 };
2555 assert_eq!(
2556 err.locations(),
2557 Some(Locations {
2558 reference_location: referenced_loc,
2559 defined_location: defined_loc,
2560 })
2561 );
2562 }
2563
2564 #[cfg(feature = "validator")]
2565 #[test]
2566 fn validator_error_uses_ancestor_path_location_fallback() {
2567 let yaml = "parent:\n child: bad\n";
2568 let referenced_loc = Location::new(1, 1);
2569 let defined_loc = Location::new(1, 1);
2570
2571 let mut locations = PathMap::new();
2572 locations.insert(
2573 PathKey::empty().join("parent"),
2574 Locations {
2575 reference_location: referenced_loc,
2576 defined_location: defined_loc,
2577 },
2578 );
2579
2580 let err = Error::ValidationError {
2581 source: ValidationSource::Validator,
2582 issues: vec![ValidationIssue {
2583 path: PathKey::empty().join("parent").join("child").join("value"),
2584 code: "custom".to_owned(),
2585 message: Some("custom validation failed".to_owned()),
2586 params: Vec::new(),
2587 }],
2588 locations,
2589 };
2590
2591 assert_eq!(err.location(), Some(referenced_loc));
2592 assert_eq!(
2593 err.locations(),
2594 Some(Locations {
2595 reference_location: referenced_loc,
2596 defined_location: defined_loc,
2597 })
2598 );
2599
2600 let rendered = err.with_snippet(yaml, 20).render();
2601 assert!(
2602 rendered.contains("custom validation failed"),
2603 "expected validation message, got: {rendered}"
2604 );
2605 assert!(
2606 rendered.contains("for `parent.child.value`"),
2607 "expected original validation path, got: {rendered}"
2608 );
2609 assert!(
2610 rendered.contains("line 1 column 1"),
2611 "expected ancestor location, got: {rendered}"
2612 );
2613 assert!(
2614 rendered.contains("1 | parent:"),
2615 "expected snippet around ancestor path, got: {rendered}"
2616 );
2617 }
2618
2619 #[test]
2620 fn nested_snippet_preserves_custom_formatter() {
2621 struct Custom;
2622 impl MessageFormatter for Custom {
2623 fn localizer(&self) -> &dyn Localizer {
2624 &DEFAULT_ENGLISH_LOCALIZER
2625 }
2626 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
2627 match err {
2628 Error::Message { msg, .. } => Cow::Owned(format!("CUSTOM: {}", msg.as_str())),
2629 _ => Cow::Borrowed(""),
2630 }
2631 }
2632 }
2633 let loc = Location::new(1, 1);
2634 let base = Error::Message {
2635 msg: "original".to_string(),
2636 location: loc,
2637 };
2638 let text = "input";
2639 let start_line = 1;
2640 let radius = 1;
2641 let inner = base.with_snippet_offset_named(text, start_line, "<input>", radius);
2642 let outer = inner.with_snippet_offset_named(text, start_line, "<input>", radius);
2643 let rendered = outer.render_with_options(RenderOptions::new(&Custom));
2644 assert!(rendered.contains("CUSTOM: original"));
2645 }
2646
2647 #[test]
2648 fn alias_error_dual_snippet_rendering() {
2649 let yaml = r#"config:
2651 anchor: &myval 42
2652 other: stuff
2653 more: data
2654 use_it: *myval
2655"#;
2656 let ref_loc = Location::new(5, 11);
2658 let def_loc = Location::new(2, 11);
2660
2661 let err = Error::AliasError {
2662 msg: "invalid value type".to_owned(),
2663 locations: Locations {
2664 reference_location: ref_loc,
2665 defined_location: def_loc,
2666 },
2667 };
2668
2669 let wrapped = err.with_snippet(yaml, 5);
2671 let rendered = wrapped.render();
2672
2673 assert!(
2675 rendered.contains("invalid value type"),
2676 "rendered: {}",
2677 rendered
2678 );
2679
2680 assert!(
2683 !rendered.contains("(defined at line"),
2684 "did not expect alias defined-at suffix when secondary window is present: {}",
2685 rendered
2686 );
2687 assert!(
2689 rendered.contains("the value is used here") || rendered.contains("use_it"),
2690 "rendered should show reference location context: {}",
2691 rendered
2692 );
2693 assert!(
2695 rendered.contains("defined here") || rendered.contains("anchor"),
2696 "rendered should show defined location context: {}",
2697 rendered
2698 );
2699 assert!(
2701 rendered.contains('5') || rendered.contains("use_it"),
2702 "rendered should reference line 5: {}",
2703 rendered
2704 );
2705 assert!(
2706 rendered.contains('2') || rendered.contains("anchor"),
2707 "rendered should reference line 2: {}",
2708 rendered
2709 );
2710 }
2711
2712 #[test]
2713 fn alias_error_same_location_single_snippet() {
2714 let yaml = "value: &anchor 42\n";
2715 let loc = Location::new(1, 8);
2716
2717 let err = Error::AliasError {
2718 msg: "test error".to_owned(),
2719 locations: Locations {
2720 reference_location: loc,
2721 defined_location: loc,
2722 },
2723 };
2724
2725 let wrapped = err.with_snippet(yaml, 5);
2726 let rendered = wrapped.render();
2727
2728 assert!(rendered.contains("test error"), "rendered: {}", rendered);
2730 assert!(
2732 !rendered.contains("defined here"),
2733 "should not show 'defined here' when locations are same: {}",
2734 rendered
2735 );
2736 assert!(
2737 !rendered.contains("the value is used here"),
2738 "should not show 'value used here' when locations are same: {}",
2739 rendered
2740 );
2741 }
2742}