1use crate::Location;
3use crate::budget::BudgetBreach;
4use crate::location::Locations;
5use crate::parse_scalars::{
6 parse_int_signed, parse_yaml11_bool, parse_yaml12_float, scalar_is_nullish,
7};
8#[cfg(feature = "garde")]
9use crate::path_map::path_key_from_garde;
10#[cfg(any(feature = "garde", feature = "validator"))]
11use crate::path_map::{PathKey, PathMap, format_path_with_resolved_leaf};
12use crate::tags::SfTag;
13use saphyr_parser::{ScalarStyle, ScanError};
14use serde::de::{self};
15use std::cell::RefCell;
16use std::fmt;
17#[cfg(feature = "validator")]
18use validator::{ValidationErrors, ValidationErrorsKind};
19
20thread_local! {
21 static MISSING_FIELD_FALLBACK: RefCell<Option<Location>> = const { RefCell::new(None) };
24}
25
26pub(crate) struct MissingFieldLocationGuard {
27 prev: Option<Location>,
28}
29
30impl MissingFieldLocationGuard {
31 pub(crate) fn new(location: Location) -> Self {
32 let prev = MISSING_FIELD_FALLBACK.with(|c| c.replace(Some(location)));
33 Self { prev }
34 }
35}
36
37impl Drop for MissingFieldLocationGuard {
38 fn drop(&mut self) {
39 MISSING_FIELD_FALLBACK.with(|c| {
40 c.replace(self.prev.take());
41 });
42 }
43}
44
45#[non_exhaustive]
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TransformReason {
52 EscapeSequence,
54 LineFolding,
56 MultiLineNormalization,
58 BlockScalarProcessing,
60 SingleQuoteEscape,
62 InputNotBorrowable,
66
67 ParserReturnedOwned,
73}
74
75impl fmt::Display for TransformReason {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 TransformReason::EscapeSequence => write!(f, "escape sequence processing"),
79 TransformReason::LineFolding => write!(f, "line folding"),
80 TransformReason::MultiLineNormalization => write!(f, "multi-line whitespace normalization"),
81 TransformReason::BlockScalarProcessing => write!(f, "block scalar processing"),
82 TransformReason::SingleQuoteEscape => write!(f, "single-quote escape processing"),
83 TransformReason::InputNotBorrowable => write!(f, "input is not available for borrowing"),
84 TransformReason::ParserReturnedOwned => write!(f, "parser returned an owned string"),
85 }
86 }
87}
88
89#[non_exhaustive]
91#[derive(Debug)]
92pub enum Error {
93 Message {
95 msg: String,
96 location: Location,
97 },
98 Eof {
100 location: Location,
101 },
102 Unexpected {
104 expected: &'static str,
105 location: Location,
106 },
107 ContainerEndMismatch {
108 location: Location,
109 },
110 UnknownAnchor {
112 id: usize,
113 location: Location,
114 },
115 AliasError {
120 msg: String,
121 locations: Locations,
122 },
123 HookError {
126 msg: String,
127 location: Location,
128 },
129 Budget {
131 breach: BudgetBreach,
132 location: Location,
133 },
134 IOError {
136 cause: std::io::Error,
137 },
138 QuotingRequired {
141 value: String, location: Location,
143 },
144
145 CannotBorrowTransformedString {
151 reason: TransformReason,
153 location: Location,
154 },
155
156 WithSnippet {
158 text: String,
163 crop_radius: usize,
164 error: Box<Error>,
165 },
166
167 #[cfg(feature = "garde")]
169 ValidationError {
170 report: garde::Report,
171 locations: PathMap,
172 },
173
174 #[cfg(feature = "garde")]
176 ValidationErrors {
177 errors: Vec<Error>,
178 },
179
180 #[cfg(feature = "validator")]
182 ValidatorError {
183 errors: ValidationErrors,
184 locations: PathMap,
185 },
186
187 #[cfg(feature = "validator")]
189 ValidatorErrors {
190 errors: Vec<Error>,
191 },
192}
193
194impl Error {
195 #[cold]
196 #[inline(never)]
197 pub(crate) fn with_snippet(self, text: &str, crop_radius: usize) -> Self {
198 let inner = match self {
201 Error::WithSnippet { error, .. } => *error,
202 other => other,
203 };
204
205 let rendered = crate::de_snipped::render_error_with_snippets(&inner, text, crop_radius);
206
207 Error::WithSnippet {
208 text: rendered,
209 crop_radius,
210 error: Box::new(inner),
211 }
212 }
213
214 #[cold]
220 #[inline(never)]
221 pub(crate) fn with_snippet_offset(
222 self,
223 text: &str,
224 start_line: usize,
225 crop_radius: usize,
226 ) -> Self {
227 let inner = match self {
228 Error::WithSnippet { error, .. } => *error,
229 other => other,
230 };
231
232 let rendered =
233 crate::de_snipped::render_error_with_snippets_offset(&inner, text, start_line, crop_radius);
234
235 Error::WithSnippet {
236 text: rendered,
237 crop_radius,
238 error: Box::new(inner),
239 }
240 }
241
242 pub fn without_snippet(&self) -> &Self {
244 match self {
245 Error::WithSnippet { error, .. } => error,
246 other => other,
247 }
248 }
249
250 #[cold]
261 #[inline(never)]
262 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
263 Error::Message {
264 msg: s.into(),
265 location: Location::UNKNOWN,
266 }
267 }
268
269 #[cold]
273 #[inline(never)]
274 pub(crate) fn quoting_required(value: &str) -> Self {
275 let location = Location::UNKNOWN;
278 let value = if parse_yaml12_float::<f64>(value, location, SfTag::None, false).is_ok()
279 || parse_int_signed::<i128>(value, "i128", location, false).is_ok()
280 || parse_yaml11_bool(value).is_ok()
281 || scalar_is_nullish(value, &ScalarStyle::Plain)
282 {
283 value.to_string()
284 } else {
285 String::new()
286 };
287 Error::QuotingRequired { value, location }
288 }
289
290 #[cold]
301 #[inline(never)]
302 pub(crate) fn unexpected(what: &'static str) -> Self {
303 Error::Unexpected {
304 expected: what,
305 location: Location::UNKNOWN,
306 }
307 }
308
309 #[cold]
314 #[inline(never)]
315 pub(crate) fn eof() -> Self {
316 Error::Eof {
317 location: Location::UNKNOWN,
318 }
319 }
320
321 #[cold]
326 #[inline(never)]
327 pub(crate) fn unknown_anchor(id: usize) -> Self {
328 Error::UnknownAnchor {
329 id,
330 location: Location::UNKNOWN,
331 }
332 }
333
334 #[cold]
339 #[inline(never)]
340 pub fn cannot_borrow_transformed(reason: TransformReason) -> Self {
341 Error::CannotBorrowTransformedString {
342 reason,
343 location: Location::UNKNOWN,
344 }
345 }
346
347 #[cold]
358 #[inline(never)]
359 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
360 match &mut self {
361 Error::Message { location, .. }
362 | Error::Eof { location }
363 | Error::Unexpected { location, .. }
364 | Error::HookError { location, .. }
365 | Error::ContainerEndMismatch { location, .. }
366 | Error::UnknownAnchor { location, .. }
367 | Error::QuotingRequired { location, .. }
368 | Error::Budget { location, .. }
369 | Error::CannotBorrowTransformedString { location, .. } => {
370 *location = set_location;
371 }
372 Error::IOError { .. } => {} Error::AliasError { .. } => {
374 }
376 Error::WithSnippet { error, .. } => {
377 let inner = *std::mem::replace(error, Box::new(Error::eof()));
378 **error = inner.with_location(set_location);
379 }
380 #[cfg(feature = "garde")]
381 Error::ValidationError { .. } => {
382 }
384 #[cfg(feature = "garde")]
385 Error::ValidationErrors { .. } => {
386 }
388 #[cfg(feature = "validator")]
389 Error::ValidatorError { .. } => {
390 }
392 #[cfg(feature = "validator")]
393 Error::ValidatorErrors { .. } => {
394 }
396 }
397 self
398 }
399
400 pub fn location(&self) -> Option<Location> {
408 match self {
409 Error::Message { location, .. }
410 | Error::Eof { location }
411 | Error::Unexpected { location, .. }
412 | Error::HookError { location, .. }
413 | Error::ContainerEndMismatch { location, .. }
414 | Error::UnknownAnchor { location, .. }
415 | Error::QuotingRequired { location, .. }
416 | Error::Budget { location, .. }
417 | Error::CannotBorrowTransformedString { location, .. } => {
418 if location != &Location::UNKNOWN {
419 Some(*location)
420 } else {
421 None
422 }
423 }
424 Error::IOError { cause: _ } => None,
425 Error::AliasError { locations, .. } => Locations::primary_location(*locations),
426 Error::WithSnippet { error, .. } => error.location(),
427 #[cfg(feature = "garde")]
428 Error::ValidationError { locations, .. } => locations
429 .map
430 .values()
431 .copied()
432 .find_map(Locations::primary_location),
433 #[cfg(feature = "garde")]
434 Error::ValidationErrors { errors } => errors.iter().find_map(|e| e.location()),
435 #[cfg(feature = "validator")]
436 Error::ValidatorError { locations, .. } => locations
437 .map
438 .values()
439 .copied()
440 .find_map(Locations::primary_location),
441 #[cfg(feature = "validator")]
442 Error::ValidatorErrors { errors } => errors.iter().find_map(|e| e.location()),
443 }
444 }
445
446 pub fn locations(&self) -> Option<Locations> {
456 match self {
457 Error::Message { location, .. }
458 | Error::Eof { location }
459 | Error::Unexpected { location, .. }
460 | Error::HookError { location, .. }
461 | Error::ContainerEndMismatch { location, .. }
462 | Error::UnknownAnchor { location, .. }
463 | Error::QuotingRequired { location, .. }
464 | Error::Budget { location, .. }
465 | Error::CannotBorrowTransformedString { location, .. } => Locations::same(location),
466 Error::IOError { .. } => None,
467 Error::AliasError { locations, .. } => Some(*locations),
468 Error::WithSnippet { error, .. } => error.locations(),
469 #[cfg(feature = "garde")]
470 Error::ValidationError { report, locations } => {
471 report.iter().next().and_then(|(path, _)| {
472 let key = path_key_from_garde(path);
473 search_locations_with_ancestor_fallback(locations, &key)
474 })
475 }
476 #[cfg(feature = "garde")]
477 Error::ValidationErrors { errors } => errors.first().and_then(Error::locations),
478 #[cfg(feature = "validator")]
479 Error::ValidatorError { errors, locations } => collect_validator_entries(errors)
480 .first()
481 .and_then(|(path, _)| locations.search(path).map(|(locs, _)| locs)),
482 #[cfg(feature = "validator")]
483 Error::ValidatorErrors { errors } => errors.first().and_then(Error::locations),
484 }
485 }
486
487 #[cold]
492 #[inline(never)]
493 pub(crate) fn from_scan_error(err: ScanError) -> Self {
494 use crate::location::SpanIndex;
495 let mark = err.marker();
496 let location =
497 Location::new(mark.line(), mark.col() + 1).with_span(crate::location::Span {
498 offset: mark.index() as SpanIndex,
499 len: 1,
500 byte_info: (0, 0),
501 });
502 Error::Message {
503 msg: err.info().to_owned(),
504 location,
505 }
506 }
507}
508
509#[cfg(any(feature = "garde", feature = "validator"))]
510fn search_locations_with_ancestor_fallback(
511 locations: &PathMap,
512 path: &PathKey,
513) -> Option<Locations> {
514 if let Some((locs, _)) = locations.search(path) {
515 return Some(locs);
516 }
517
518 let mut p = path.parent();
519 while let Some(cur) = p {
520 if let Some((locs, _)) = locations.search(&cur) {
521 return Some(locs);
522 }
523 p = cur.parent();
524 }
525
526 None
527}
528
529impl fmt::Display for Error {
530 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531 match self {
532 Error::WithSnippet {
533 text,
534 crop_radius,
535 error,
536 } => {
537 if *crop_radius == 0 {
538 return write!(f, "{}", error);
540 }
541 write!(f, "{text}")
543 }
544 Error::Message { msg, location } => fmt_with_location(f, msg, location),
545 Error::HookError { msg, location } => fmt_with_location(f, msg, location),
546 Error::Eof { location } => fmt_with_location(f, "unexpected end of input", location),
547 Error::Unexpected { expected, location } => fmt_with_location(
548 f,
549 &format!("unexpected event: expected {expected}"),
550 location,
551 ),
552 Error::ContainerEndMismatch { location } => {
553 fmt_with_location(f, "list or mapping end with no start", location)
554 }
555 Error::UnknownAnchor { id, location } => fmt_with_location(
556 f,
557 &format!("alias references unknown anchor id {id}"),
558 location,
559 ),
560 Error::Budget { breach, location } => {
561 fmt_with_location(f, &format!("YAML budget breached: {breach:?}"), location)
562 }
563 Error::QuotingRequired { value, location } => fmt_with_location(
564 f,
565 &format!("The string value [{value}] must be quoted"),
566 location,
567 ),
568 Error::CannotBorrowTransformedString { reason, location } => fmt_with_location(
569 f,
570 &format!(
571 "cannot borrow string: value was transformed during parsing ({reason}). \
572 Use String or Cow<str> instead of &str"
573 ),
574 location,
575 ),
576 Error::IOError { cause } => write!(f, "IO error: {}", cause),
577 Error::AliasError { msg, locations } => {
578 fmt_alias_error_plain(f, msg, locations)
579 }
580
581 #[cfg(feature = "garde")]
582 Error::ValidationError { report, locations } => {
583 fmt_validation_error_plain(f, report, locations)
586 }
587
588 #[cfg(feature = "garde")]
589 Error::ValidationErrors { errors } => {
590 let mut first = true;
591 for err in errors {
592 if !first {
593 writeln!(f)?;
594 writeln!(f)?;
595 }
596 first = false;
597 write!(f, "{err}")?;
598 }
599 Ok(())
600 }
601
602 #[cfg(feature = "validator")]
603 Error::ValidatorError { errors, locations } => {
604 fmt_validator_error_plain(f, errors, locations)
605 }
606
607 #[cfg(feature = "validator")]
608 Error::ValidatorErrors { errors } => {
609 let mut first = true;
610 for err in errors {
611 if !first {
612 writeln!(f)?;
613 writeln!(f)?;
614 }
615 first = false;
616 write!(f, "{err}")?;
617 }
618 Ok(())
619 }
620 }
621 }
622}
623
624
625#[cfg(feature = "garde")]
626fn fmt_validation_error_plain(
627 f: &mut fmt::Formatter<'_>,
628 report: &garde::Report,
629 locations: &PathMap,
630) -> fmt::Result {
631 let mut first = true;
632 for (path, entry) in report.iter() {
633 if !first {
634 writeln!(f)?;
635 }
636 first = false;
637 let path_key = path_key_from_garde(path);
638 let original_leaf = path_key
639 .leaf_string()
640 .unwrap_or_else(|| "<root>".to_string());
641
642 let (locs, resolved_leaf) = locations
643 .search(&path_key)
644 .unwrap_or((Locations::UNKNOWN, original_leaf));
645
646 let loc = if locs.reference_location != Location::UNKNOWN {
647 locs.reference_location
648 } else {
649 locs.defined_location
650 };
651
652 let resolved_path = format_path_with_resolved_leaf(&path_key, &resolved_leaf);
653 let msg = format!("validation error at {resolved_path}: {entry}");
654 fmt_with_location(f, &msg, &loc)?;
655 }
656 Ok(())
657}
658
659#[cfg(feature = "validator")]
660fn fmt_validator_error_plain(
661 f: &mut fmt::Formatter<'_>,
662 errors: &ValidationErrors,
663 locations: &PathMap,
664) -> fmt::Result {
665 let entries = collect_validator_entries(errors);
666 let mut first = true;
667
668 for (path, entry) in entries {
669 if !first {
670 writeln!(f)?;
671 }
672 first = false;
673
674 let original_leaf = path.leaf_string().unwrap_or_else(|| "<root>".to_string());
675 let (locs, resolved_leaf) = locations
676 .search(&path)
677 .unwrap_or((Locations::UNKNOWN, original_leaf));
678
679 let loc = if locs.reference_location != Location::UNKNOWN {
680 locs.reference_location
681 } else {
682 locs.defined_location
683 };
684
685 let resolved_path = format_path_with_resolved_leaf(&path, &resolved_leaf);
686 let msg = format!("validation error at {resolved_path}: {entry}");
687 fmt_with_location(f, &msg, &loc)?;
688 }
689
690 Ok(())
691}
692
693#[cfg(feature = "validator")]
694fn collect_validator_entries(errors: &ValidationErrors) -> Vec<(PathKey, String)> {
695 let mut out = Vec::new();
696 let root = PathKey::empty();
697 collect_validator_entries_inner(errors, &root, &mut out);
698 out
699}
700
701#[cfg(feature = "validator")]
702fn collect_validator_entries_inner(
703 errors: &ValidationErrors,
704 path: &PathKey,
705 out: &mut Vec<(PathKey, String)>,
706) {
707 for (field, kind) in errors.errors() {
708 let field_path = path.clone().join(field.as_ref());
709 match kind {
710 ValidationErrorsKind::Field(entries) => {
711 for entry in entries {
712 out.push((field_path.clone(), entry.to_string()));
713 }
714 }
715 ValidationErrorsKind::Struct(inner) => {
716 collect_validator_entries_inner(inner, &field_path, out);
717 }
718 ValidationErrorsKind::List(list) => {
719 for (idx, inner) in list {
720 let index_path = field_path.clone().join(*idx);
721 collect_validator_entries_inner(inner, &index_path, out);
722 }
723 }
724 }
725 }
726}
727impl std::error::Error for Error {}
728
729fn maybe_attach_fallback_location(mut err: Error) -> Error {
730 let loc = MISSING_FIELD_FALLBACK.with(|c| *c.borrow());
731 if let Some(loc) = loc
732 && loc != Location::UNKNOWN
733 {
734 err = err.with_location(loc);
735 }
736 err
737}
738
739impl de::Error for Error {
740 fn custom<T: fmt::Display>(msg: T) -> Self {
741 Error::msg(msg.to_string())
745 }
746
747 fn invalid_type(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
748 maybe_attach_fallback_location(Error::msg(format!("invalid type: {unexp}, expected {exp}")))
750 }
751
752 fn invalid_value(unexp: de::Unexpected, exp: &dyn de::Expected) -> Self {
753 maybe_attach_fallback_location(Error::msg(format!(
754 "invalid value: {unexp}, expected {exp}"
755 )))
756 }
757
758 fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
759 maybe_attach_fallback_location(Error::msg(format!(
760 "unknown variant `{variant}`, expected one of {}",
761 expected.join(", ")
762 )))
763 }
764
765 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
766 maybe_attach_fallback_location(Error::msg(format!(
767 "unknown field `{field}`, expected one of {}",
768 expected.join(", ")
769 )))
770 }
771
772 fn missing_field(field: &'static str) -> Self {
773 maybe_attach_fallback_location(Error::msg(format!("missing field `{field}`")))
774 }
775}
776
777#[cold]
787#[inline(never)]
788fn fmt_with_location(f: &mut fmt::Formatter<'_>, msg: &str, location: &Location) -> fmt::Result {
789 if location != &Location::UNKNOWN {
790 write!(
791 f,
792 "{msg} at line {}, column {}",
793 location.line, location.column
794 )
795 } else {
796 write!(f, "{msg}")
797 }
798}
799
800#[cold]
805#[inline(never)]
806fn fmt_alias_error_plain(
807 f: &mut fmt::Formatter<'_>,
808 msg: &str,
809 locations: &Locations,
810) -> fmt::Result {
811 let ref_loc = locations.reference_location;
812 let def_loc = locations.defined_location;
813
814 match (ref_loc, def_loc) {
815 (Location::UNKNOWN, Location::UNKNOWN) => {
816 write!(f, "{msg}")
817 }
818 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
819 write!(f, "{msg} at line {}, column {}", r.line, r.column)
821 }
822 (r, d) if r == Location::UNKNOWN && d != Location::UNKNOWN => {
823 write!(f, "{msg} (defined at line {}, column {})", d.line, d.column)
825 }
826 (r, d) => {
827 write!(
829 f,
830 "{msg} at line {}, column {} (defined at line {}, column {})",
831 r.line, r.column, d.line, d.column
832 )
833 }
834 }
835}
836
837#[cold]
848#[inline(never)]
849pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
850 Error::Budget {
851 breach,
852 location: Location::UNKNOWN,
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859
860 #[test]
861 fn locations_for_basic_error_duplicates_location() {
862 let l = Location::new(3, 7);
863 let err = Error::Message {
864 msg: "x".to_owned(),
865 location: l,
866 };
867 assert_eq!(
868 err.locations(),
869 Some(Locations {
870 reference_location: l,
871 defined_location: l,
872 })
873 );
874 }
875
876 #[test]
877 fn locations_for_io_error_is_unknown() {
878 let err = Error::IOError {
879 cause: std::io::Error::other("x"),
880 };
881 assert_eq!(err.locations(), None);
882 }
883
884 #[test]
885 fn alias_error_returns_both_locations() {
886 let ref_loc = Location::new(5, 10);
887 let def_loc = Location::new(2, 3);
888 let err = Error::AliasError {
889 msg: "test error".to_owned(),
890 locations: Locations {
891 reference_location: ref_loc,
892 defined_location: def_loc,
893 },
894 };
895
896 assert_eq!(err.location(), Some(ref_loc));
898
899 assert_eq!(
901 err.locations(),
902 Some(Locations {
903 reference_location: ref_loc,
904 defined_location: def_loc,
905 })
906 );
907 }
908
909 #[test]
910 fn alias_error_display_shows_both_locations() {
911 let ref_loc = Location::new(5, 10);
912 let def_loc = Location::new(2, 3);
913 let err = Error::AliasError {
914 msg: "invalid value".to_owned(),
915 locations: Locations {
916 reference_location: ref_loc,
917 defined_location: def_loc,
918 },
919 };
920
921 let display = err.to_string();
922 assert!(display.contains("invalid value"));
923 assert!(display.contains("line 5"));
924 assert!(display.contains("column 10"));
925 assert!(display.contains("line 2"));
926 assert!(display.contains("column 3"));
927 }
928
929 #[test]
930 fn alias_error_display_with_same_locations() {
931 let loc = Location::new(3, 7);
932 let err = Error::AliasError {
933 msg: "test".to_owned(),
934 locations: Locations {
935 reference_location: loc,
936 defined_location: loc,
937 },
938 };
939
940 let display = err.to_string();
941 assert!(display.contains("line 3"));
943 assert!(display.contains("column 7"));
944 assert!(!display.contains("defined at"));
946 }
947
948 #[cfg(feature = "validator")]
949 #[test]
950 fn locations_for_validator_error_uses_first_entry() {
951 use validator::Validate;
952
953 #[derive(Debug, Validate)]
954 struct Cfg {
955 #[validate(length(min = 2))]
956 second_string: String,
957 }
958
959 let cfg = Cfg {
960 second_string: "x".to_owned(),
961 };
962 let errors = cfg.validate().expect_err("validation error expected");
963
964 let referenced_loc = Location::new(3, 15);
965 let defined_loc = Location::new(2, 18);
966
967 let mut locations = PathMap::new();
968 locations.insert(
969 PathKey::empty().join("secondString"),
970 Locations {
971 reference_location: referenced_loc,
972 defined_location: defined_loc,
973 },
974 );
975
976 let err = Error::ValidatorError { errors, locations };
977 assert_eq!(
978 err.locations(),
979 Some(Locations {
980 reference_location: referenced_loc,
981 defined_location: defined_loc,
982 })
983 );
984 }
985}