1mod line_wrap;
2
3use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6use std::iter;
7
8use owo_colors::{AnsiColors, DynColor, OwoColorize};
9
10use line_wrap::{get_wrap_width, wrap_text};
11
12pub trait Hinted {
18 fn hints(&self) -> Hints<'_> {
20 Hints::none()
21 }
22}
23
24#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum HintOrdering {
27 First,
29 #[default]
31 Any,
32 Last,
34}
35
36pub struct Hint<'a> {
38 message: Cow<'a, str>,
39 ordering: HintOrdering,
40}
41
42impl<'a> Hint<'a> {
43 pub fn new(message: impl Into<Cow<'a, str>>) -> Self {
45 Self {
46 message: message.into(),
47 ordering: HintOrdering::default(),
48 }
49 }
50
51 #[must_use]
53 pub fn with_ordering(mut self, ordering: HintOrdering) -> Self {
54 self.ordering = ordering;
55 self
56 }
57
58 fn into_owned(self) -> Hint<'static> {
60 Hint {
61 message: Cow::Owned(self.message.into_owned()),
62 ordering: self.ordering,
63 }
64 }
65}
66
67impl<'a> From<&'a str> for Hint<'a> {
68 fn from(message: &'a str) -> Self {
69 Self::new(message)
70 }
71}
72
73impl From<String> for Hint<'_> {
74 fn from(message: String) -> Self {
75 Self::new(message)
76 }
77}
78
79pub struct Hints<'a>(Vec<Hint<'a>>);
84
85impl<'a> Hints<'a> {
86 pub fn none() -> Self {
88 Self(Vec::new())
89 }
90
91 pub fn push(&mut self, hint: impl Into<Hint<'a>>) {
93 self.0.push(hint.into());
94 }
95
96 #[must_use]
98 pub fn with_ordering(mut self, ordering: HintOrdering) -> Self {
99 for hint in &mut self.0 {
100 hint.ordering = ordering;
101 }
102 self
103 }
104
105 pub fn into_owned(self) -> Hints<'static> {
107 Hints(self.0.into_iter().map(Hint::into_owned).collect())
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.0.is_empty()
113 }
114
115 pub fn iter(&self) -> HintsIter<'_, 'a> {
117 HintsIter {
118 hints: &self.0,
119 current: self.0.iter(),
120 ordering: HintOrdering::First,
121 remaining: [HintOrdering::Any, HintOrdering::Last].into_iter(),
122 }
123 }
124
125 pub fn extend(&mut self, other: Hints<'_>) {
129 for hint in other.0 {
130 if let Some(existing) = self
131 .0
132 .iter_mut()
133 .find(|existing| existing.message == hint.message)
134 {
135 existing.ordering = existing.ordering.min(hint.ordering);
136 } else {
137 self.0.push(hint.into_owned());
138 }
139 }
140 }
141}
142
143pub struct HintsIter<'h, 'a> {
145 hints: &'h [Hint<'a>],
146 current: std::slice::Iter<'h, Hint<'a>>,
147 ordering: HintOrdering,
148 remaining: std::array::IntoIter<HintOrdering, 2>,
149}
150
151impl<'h> Iterator for HintsIter<'h, '_> {
152 type Item = &'h str;
153
154 fn next(&mut self) -> Option<Self::Item> {
155 loop {
156 if let Some(hint) = self.current.find(|hint| hint.ordering == self.ordering) {
157 return Some(hint.message.as_ref());
158 }
159 self.ordering = self.remaining.next()?;
160 self.current = self.hints.iter();
161 }
162 }
163}
164
165impl<'h, 'a> IntoIterator for &'h Hints<'a> {
166 type Item = &'h str;
167 type IntoIter = HintsIter<'h, 'a>;
168
169 fn into_iter(self) -> Self::IntoIter {
170 self.iter()
171 }
172}
173
174impl<'a> IntoIterator for Hints<'a> {
175 type Item = Cow<'a, str>;
176 type IntoIter = std::iter::Map<std::vec::IntoIter<Hint<'a>>, fn(Hint<'a>) -> Cow<'a, str>>;
177
178 fn into_iter(mut self) -> Self::IntoIter {
179 self.0.sort_by_key(|hint| hint.ordering);
180 self.0.into_iter().map(|hint| hint.message)
181 }
182}
183
184pub struct ErrorWithHints<'a, E> {
189 error: E,
190 hints: Hints<'a>,
191}
192
193impl<'a, E> ErrorWithHints<'a, E> {
194 pub fn new(error: E, hints: Hints<'a>) -> Self {
196 Self { error, hints }
197 }
198}
199
200impl<E: fmt::Display> fmt::Display for ErrorWithHints<'_, E> {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{}", self.error)?;
203 if !self.hints.is_empty() {
204 writeln!(f)?;
205 write!(f, "{}", self.hints)?;
206 }
207 Ok(())
208 }
209}
210
211impl<'a> From<&'a str> for Hints<'a> {
212 fn from(hint: &'a str) -> Self {
213 Self::from(Hint::from(hint))
214 }
215}
216
217impl From<String> for Hints<'_> {
218 fn from(hint: String) -> Self {
219 Self::from(Hint::from(hint))
220 }
221}
222
223impl<'a> From<Hint<'a>> for Hints<'a> {
224 fn from(hint: Hint<'a>) -> Self {
225 Self(vec![hint])
226 }
227}
228
229impl<'a, T: Into<Hint<'a>>> FromIterator<T> for Hints<'a> {
230 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
231 Self(iter.into_iter().map(Into::into).collect())
232 }
233}
234
235impl fmt::Display for Hints<'_> {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 for hint in self {
238 write!(f, "\n{HintPrefix} {hint}")?;
239 }
240 Ok(())
241 }
242}
243
244pub struct HintPrefix;
246
247impl fmt::Display for HintPrefix {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 write!(f, "{}{}", "hint".bold().cyan(), ":".bold())
250 }
251}
252
253#[must_use]
255pub struct ErrorOptions<'a, C = AnsiColors, W = Stderr> {
256 level: Cow<'a, str>,
257 color: C,
258 width_override: Option<usize>,
259 stream: W,
260}
261
262#[derive(Debug, Clone, Copy, Default)]
264pub struct Stderr;
265
266impl fmt::Write for Stderr {
267 fn write_str(&mut self, output: &str) -> fmt::Result {
268 anstream::eprint!("{output}");
269 Ok(())
270 }
271}
272
273impl Default for ErrorOptions<'_, AnsiColors, Stderr> {
274 fn default() -> Self {
275 Self {
276 level: Cow::Borrowed("error"),
277 color: AnsiColors::Red,
278 width_override: None,
279 stream: Stderr,
280 }
281 }
282}
283
284impl<'a, C, W> ErrorOptions<'a, C, W> {
285 pub fn with_level(mut self, level: impl Into<Cow<'a, str>>) -> Self {
287 self.level = level.into();
288 self
289 }
290
291 pub fn with_color<D>(self, color: D) -> ErrorOptions<'a, D, W> {
293 ErrorOptions {
294 level: self.level,
295 color,
296 width_override: self.width_override,
297 stream: self.stream,
298 }
299 }
300
301 #[cfg(test)]
305 fn with_width_override(mut self, width_override: usize) -> Self {
306 self.width_override = Some(width_override);
307 self
308 }
309
310 pub fn with_stream<D>(self, stream: D) -> ErrorOptions<'a, C, D> {
312 ErrorOptions {
313 level: self.level,
314 color: self.color,
315 width_override: self.width_override,
316 stream,
317 }
318 }
319}
320
321pub fn write_error_chain(err: &dyn Error, hints: &Hints<'_>) -> fmt::Result {
324 write_error_chain_with_options(err, hints, ErrorOptions::default())
325}
326
327pub fn debug_error_chain(err: &dyn Error) -> impl fmt::Display + '_ {
329 DebugErrorChain(err)
330}
331
332struct DebugErrorChain<'a>(&'a dyn Error);
333
334impl fmt::Display for DebugErrorChain<'_> {
335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336 for (index, error) in iter::successors(Some(self.0), |&error| error.source()).enumerate() {
337 if index > 0 {
338 formatter.write_str("\n")?;
339 }
340 write!(formatter, "{index}: {error:?}")?;
341 }
342 Ok(())
343 }
344}
345
346pub fn write_error_chain_with_options<C: DynColor + Copy, W: fmt::Write>(
350 err: &dyn Error,
351 hints: &Hints<'_>,
352 options: ErrorOptions<'_, C, W>,
353) -> fmt::Result {
354 let ErrorOptions {
355 level,
356 color,
357 width_override,
358 mut stream,
359 } = options;
360 let width = get_wrap_width(width_override);
361
362 let main_msg = err.to_string();
363 let main_padding = " ".repeat(level.len() + 2);
364 let wrapped_main = wrap_text(&main_msg, width, &main_padding, &main_padding, "");
365 writeln!(
366 &mut stream,
367 "{}{} {}",
368 level.as_ref().color(color).bold(),
369 ":".bold(),
370 wrapped_main.trim()
371 )?;
372
373 for source in iter::successors(err.source(), |&err| err.source()) {
374 let msg = source.to_string();
375 let wrapped = wrap_text(&msg, width.map(|width| width.saturating_sub(9)), "", "", "");
378
379 let mut lines = wrapped.lines();
380 if let Some(first) = lines.next() {
381 writeln!(
382 &mut stream,
383 " {}{} {}",
384 "cause".color(color).bold(),
385 ":".bold(),
386 first.trim()
387 )?;
388 for line in lines {
389 if line.trim().is_empty() {
390 writeln!(&mut stream)?;
391 } else {
392 writeln!(&mut stream, " {line}")?;
393 }
394 }
395 }
396 }
397
398 for hint in hints {
399 writeln!(&mut stream, "\n{HintPrefix} {hint}")?;
400 }
401
402 Ok(())
403}
404
405#[cfg(test)]
406mod tests {
407 use anyhow::anyhow;
408 use indoc::indoc;
409 use insta::{assert_debug_snapshot, assert_snapshot};
410 use owo_colors::AnsiColors;
411
412 use super::{
413 ErrorOptions, ErrorWithHints, Hint, HintOrdering, Hints, debug_error_chain,
414 write_error_chain_with_options,
415 };
416
417 #[test]
418 fn extend_deduplicates_matching_hints() {
419 let mut hints = Hints::from("same");
420 hints.extend(Hints::from("same"));
421 hints.extend(Hints::from("other"));
422
423 let hints = hints.iter().collect::<Vec<_>>();
424 assert_debug_snapshot!(hints, @r#"
425 [
426 "same",
427 "other",
428 ]
429 "#);
430 }
431
432 #[test]
433 fn hint_ordering_retains_insertion_order() {
434 let mut hints = Hints::from("any 1");
435 hints.push(Hint::new("last 1").with_ordering(HintOrdering::Last));
436 hints.push(Hint::new("first 1").with_ordering(HintOrdering::First));
437 hints.push("any 2".to_string());
438 hints.extend(Hints::from("last 2").with_ordering(HintOrdering::Last));
439 hints.extend(Hints::from("first 2").with_ordering(HintOrdering::First));
440
441 assert_snapshot!(anstream::adapter::strip_str(&hints.to_string()), @"
442 hint: first 1
443 hint: first 2
444 hint: any 1
445 hint: any 2
446 hint: last 1
447 hint: last 2
448 ");
449 assert_debug_snapshot!((&hints).into_iter().collect::<Vec<_>>(), @r#"
450 [
451 "first 1",
452 "first 2",
453 "any 1",
454 "any 2",
455 "last 1",
456 "last 2",
457 ]
458 "#);
459 assert_debug_snapshot!(hints.into_iter().collect::<Vec<_>>(), @r#"
460 [
461 "first 1",
462 "first 2",
463 "any 1",
464 "any 2",
465 "last 1",
466 "last 2",
467 ]
468 "#);
469 }
470
471 #[test]
472 fn changing_ordering_retains_insertion_order() {
473 let hints = [
474 Hint::new("last").with_ordering(HintOrdering::Last),
475 Hint::new("first").with_ordering(HintOrdering::First),
476 Hint::new("any"),
477 ]
478 .into_iter()
479 .collect::<Hints<'_>>()
480 .with_ordering(HintOrdering::Any);
481
482 assert_debug_snapshot!(hints.iter().collect::<Vec<_>>(), @r#"
483 [
484 "last",
485 "first",
486 "any",
487 ]
488 "#);
489 }
490
491 #[test]
492 fn duplicate_hints_retain_the_earliest_ordering() {
493 let message = String::from("shared");
494 let mut hints = Hints::from(message.as_str())
495 .with_ordering(HintOrdering::Last)
496 .into_owned();
497 drop(message);
498
499 hints.extend(Hints::from("first").with_ordering(HintOrdering::First));
500 hints.extend(Hints::from("shared").with_ordering(HintOrdering::First));
501 hints.extend(Hints::from("shared").with_ordering(HintOrdering::Last));
502 hints.extend(Hints::from("any"));
503
504 assert_snapshot!(anstream::adapter::strip_str(&hints.to_string()), @"
505 hint: shared
506 hint: first
507 hint: any
508 ");
509 }
510
511 #[test]
512 fn error_with_hints_separates_hints_from_error() {
513 let output = ErrorWithHints::new("error", Hints::from("fix it")).to_string();
514 assert_snapshot!(anstream::adapter::strip_str(&output), @"
515 error
516
517 hint: fix it
518 ");
519 assert_snapshot!(ErrorWithHints::new("error", Hints::none()), @"error");
520 }
521
522 #[test]
523 fn test_error_wrapping_with_columns() {
524 #[derive(Debug, thiserror::Error)]
525 #[error(
526 "Because fiasobfhuasbf was not found in the package registry and you require fiasobfhuasbf, we can conclude that your requirements are unsatisfiable."
527 )]
528 struct Inner;
529
530 #[derive(Debug, thiserror::Error)]
531 #[error("No solution found when resolving dependencies")]
532 struct Outer {
533 #[source]
534 source: Inner,
535 }
536
537 let error = Outer { source: Inner };
538 let mut output = String::new();
539 write_error_chain_with_options(
540 &error,
541 &Hints::none(),
542 ErrorOptions::default()
543 .with_width_override(80)
544 .with_stream(&mut output),
545 )
546 .unwrap();
547 let output = anstream::adapter::strip_str(&output);
548
549 assert_snapshot!(output, @"
550 error: No solution found when resolving dependencies
551 cause: Because fiasobfhuasbf was not found in the package registry and you
552 require fiasobfhuasbf, we can conclude that your requirements are
553 unsatisfiable.
554 ");
555 }
556
557 #[test]
558 fn test_error_chain_with_cause() {
559 #[derive(Debug, thiserror::Error)]
560 #[error("Permission denied")]
561 struct Inner;
562
563 #[derive(Debug, thiserror::Error)]
564 #[error("Failed to write file")]
565 struct Outer {
566 #[source]
567 source: Inner,
568 }
569
570 let error = Outer { source: Inner };
571 let mut output = String::new();
572 write_error_chain_with_options(
573 &error,
574 &Hints::none(),
575 ErrorOptions::default().with_stream(&mut output),
576 )
577 .unwrap();
578 assert_snapshot!(format!("{output:?}"), @r#""\u{1b}[1m\u{1b}[31merror\u{1b}[39m\u{1b}[0m\u{1b}[1m:\u{1b}[0m Failed to write file\n \u{1b}[1m\u{1b}[31mcause\u{1b}[39m\u{1b}[0m\u{1b}[1m:\u{1b}[0m Permission denied\n""#);
579 let output = anstream::adapter::strip_str(&output);
580
581 assert_snapshot!(output, @"
582 error: Failed to write file
583 cause: Permission denied
584 ");
585 }
586
587 #[test]
588 fn formats_debug_error_chain() {
589 #[derive(Debug, thiserror::Error)]
590 #[error("inner error")]
591 struct InnerError {
592 code: u8,
593 }
594
595 #[derive(Debug, thiserror::Error)]
596 #[error("outer error")]
597 struct OuterError {
598 #[source]
599 source: InnerError,
600 }
601
602 let error = OuterError {
603 source: InnerError { code: 42 },
604 };
605
606 assert_eq!(
607 debug_error_chain(&error).to_string(),
608 "0: OuterError { source: InnerError { code: 42 } }\n1: InnerError { code: 42 }"
609 );
610 }
611
612 #[test]
613 fn format_with_custom_level() {
614 let error = anyhow!("Failed to create registry entry");
615 let mut output = String::new();
616 write_error_chain_with_options(
617 error.as_ref(),
618 &Hints::none(),
619 ErrorOptions::default()
620 .with_level("warning")
621 .with_color(AnsiColors::Yellow)
622 .with_stream(&mut output),
623 )
624 .unwrap();
625 let output = anstream::adapter::strip_str(&output);
626
627 assert_snapshot!(output, @"warning: Failed to create registry entry
628");
629 }
630
631 #[test]
632 fn test_no_hyphenation() {
633 #[derive(Debug, thiserror::Error)]
634 #[error(
635 "Failed to download package from https://files.pythonhosted.org/packages/verylongpackagename"
636 )]
637 struct LongWord;
638
639 let error = LongWord;
640 let mut output = String::new();
641 write_error_chain_with_options(
642 &error,
643 &Hints::none(),
644 ErrorOptions::default()
645 .with_width_override(50)
646 .with_stream(&mut output),
647 )
648 .unwrap();
649 let output = anstream::adapter::strip_str(&output);
650 assert_snapshot!(output, @r"
651 error: Failed to download package from
652 https://files.pythonhosted.org/packages/verylongpackagename
653 ");
654 }
655
656 #[test]
657 fn test_long_words_not_broken() {
658 #[derive(Debug, thiserror::Error)]
659 #[error(
660 "The package supercalifragilisticexpialidocious-extraordinarily-long-name was not found"
661 )]
662 struct VeryLongWord;
663
664 let error = VeryLongWord;
665 let mut output = String::new();
666 write_error_chain_with_options(
667 &error,
668 &Hints::none(),
669 ErrorOptions::default()
670 .with_width_override(40)
671 .with_stream(&mut output),
672 )
673 .unwrap();
674 let output = anstream::adapter::strip_str(&output);
675 assert_snapshot!(output, @r"
676 error: The package
677 supercalifragilisticexpialidocious-extraordinarily-long-name
678 was not found
679 ");
680 }
681
682 #[test]
683 fn test_multiple_error_sources() {
684 #[derive(Debug, thiserror::Error)]
685 #[error("Network connection timeout after multiple retry attempts")]
686 struct DeepError;
687
688 #[derive(Debug, thiserror::Error)]
689 #[error("Failed to fetch package metadata from registry")]
690 struct MiddleError {
691 #[source]
692 source: DeepError,
693 }
694
695 #[derive(Debug, thiserror::Error)]
696 #[error("Unable to resolve package dependencies")]
697 struct TopError {
698 #[source]
699 source: MiddleError,
700 }
701
702 let error = TopError {
703 source: MiddleError { source: DeepError },
704 };
705 let mut output = String::new();
706 write_error_chain_with_options(
707 &error,
708 &Hints::none(),
709 ErrorOptions::default()
710 .with_width_override(40)
711 .with_stream(&mut output),
712 )
713 .unwrap();
714 let output = anstream::adapter::strip_str(&output);
715 assert_snapshot!(output, @"
716 error: Unable to resolve package
717 dependencies
718 cause: Failed to fetch package
719 metadata from registry
720 cause: Network connection timeout
721 after multiple retry attempts
722 ");
723 }
724
725 #[test]
726 fn format_cause_with_narrow_width() {
727 let error = anyhow!("one two").context("root");
728 let mut output = String::new();
729 write_error_chain_with_options(
730 error.as_ref(),
731 &Hints::none(),
732 ErrorOptions::default()
733 .with_width_override(4)
734 .with_stream(&mut output),
735 )
736 .unwrap();
737 let output = anstream::adapter::strip_str(&output);
738
739 assert_snapshot!(output, @"
740 error: root
741 cause: one
742 two
743 ");
744 }
745
746 #[test]
747 fn test_multiline_main_message_wraps_each_line() {
748 #[derive(Debug, thiserror::Error)]
749 #[error(
750 "There is no command `foobar` for `uv`. Did you mean one of:\n auth\n run\n init"
751 )]
752 struct Suggestions;
753
754 let error = Suggestions;
755 let mut output = String::new();
756 write_error_chain_with_options(
757 &error,
758 &Hints::none(),
759 ErrorOptions::default()
760 .with_width_override(50)
761 .with_stream(&mut output),
762 )
763 .unwrap();
764 let output = anstream::adapter::strip_str(&output);
765
766 assert_snapshot!(output, @r"
767 error: There is no command `foobar` for `uv`. Did
768 you mean one of:
769 auth
770 run
771 init
772 ");
773 }
774
775 #[test]
776 fn test_wrap_only_on_ascii_space() {
777 #[derive(Debug, thiserror::Error)]
778 #[error("Path /usr/local/lib/python3.12/site-packages not found in filesystem hierarchy")]
779 struct SpecialChars;
780
781 let error = SpecialChars;
782 let mut output = String::new();
783 write_error_chain_with_options(
784 &error,
785 &Hints::none(),
786 ErrorOptions::default()
787 .with_width_override(50)
788 .with_stream(&mut output),
789 )
790 .unwrap();
791 let output = anstream::adapter::strip_str(&output);
792 assert_snapshot!(output, @r"
793 error: Path /usr/local/lib/python3.12/site-packages
794 not found in filesystem hierarchy
795 ");
796 }
797
798 #[test]
799 fn format_with_hints() {
800 let err = anyhow!("Permission denied").context("Failed to fetch package");
801
802 let hints = [
803 "Try running with `--verbose` for more information.".to_string(),
804 "Try running without --offline.".to_string(),
805 ]
806 .into_iter()
807 .collect();
808
809 let mut rendered = String::new();
810 write_error_chain_with_options(
811 err.as_ref(),
812 &hints,
813 ErrorOptions::default().with_stream(&mut rendered),
814 )
815 .unwrap();
816 let rendered = anstream::adapter::strip_str(&rendered);
817
818 assert_snapshot!(rendered, @"
819 error: Failed to fetch package
820 cause: Permission denied
821
822 hint: Try running with `--verbose` for more information.
823
824 hint: Try running without --offline.
825 ");
826 }
827
828 #[test]
829 fn format_multiline_message() {
830 let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
831 Server says: This endpoint only support POST requests.
832
833 For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
834 let err = anyhow!("Caused By: HTTP Error 400")
835 .context(err_middle)
836 .context("Failed to download Python 3.12");
837
838 let mut rendered = String::new();
839 write_error_chain_with_options(
840 err.as_ref(),
841 &Hints::none(),
842 ErrorOptions::default().with_stream(&mut rendered),
843 )
844 .unwrap();
845 let rendered = anstream::adapter::strip_str(&rendered);
846
847 assert_snapshot!(rendered, @"
848 error: Failed to download Python 3.12
849 cause: Failed to fetch https://example.com/upload/python3.13.tar.zst
850 Server says: This endpoint only support POST requests.
851
852 For downloads, please refer to https://example.com/download/python3.13.tar.zst
853 cause: Caused By: HTTP Error 400
854 ");
855 }
856}