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 Hint {
18 fn hints(&self) -> Hints<'_> {
20 Hints::none()
21 }
22}
23
24pub struct Hints<'a>(Vec<Cow<'a, str>>);
28
29impl Hints<'_> {
30 pub fn none() -> Self {
32 Self(Vec::new())
33 }
34
35 pub fn push(&mut self, hint: String) {
37 self.0.push(Cow::Owned(hint));
38 }
39
40 pub fn into_owned(self) -> Hints<'static> {
42 Hints(
43 self.0
44 .into_iter()
45 .map(|cow| Cow::Owned(cow.into_owned()))
46 .collect(),
47 )
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.0.is_empty()
53 }
54
55 pub fn extend(&mut self, other: Hints<'_>) {
57 for hint in other.0 {
58 let hint = Cow::Owned(hint.into_owned());
59 if !self.0.iter().any(|existing| existing == &hint) {
60 self.0.push(hint);
61 }
62 }
63 }
64}
65
66pub struct ErrorWithHints<'a, E> {
71 error: E,
72 hints: Hints<'a>,
73}
74
75impl<'a, E> ErrorWithHints<'a, E> {
76 pub fn new(error: E, hints: Hints<'a>) -> Self {
78 Self { error, hints }
79 }
80}
81
82impl<E: fmt::Display> fmt::Display for ErrorWithHints<'_, E> {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write!(f, "{}", self.error)?;
85 if !self.hints.is_empty() {
86 writeln!(f)?;
87 write!(f, "{}", self.hints)?;
88 }
89 Ok(())
90 }
91}
92
93impl<'a> From<&'a str> for Hints<'a> {
94 fn from(hint: &'a str) -> Self {
95 Self(vec![Cow::Borrowed(hint)])
96 }
97}
98
99impl From<String> for Hints<'_> {
100 fn from(hint: String) -> Self {
101 Self(vec![Cow::Owned(hint)])
102 }
103}
104
105impl FromIterator<String> for Hints<'_> {
106 fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
107 Self(iter.into_iter().map(Cow::Owned).collect())
108 }
109}
110
111impl fmt::Display for Hints<'_> {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 for hint in &self.0 {
114 write!(f, "\n{HintPrefix} {hint}")?;
115 }
116 Ok(())
117 }
118}
119
120impl<'a> IntoIterator for Hints<'a> {
121 type Item = Cow<'a, str>;
122 type IntoIter = std::vec::IntoIter<Cow<'a, str>>;
123
124 fn into_iter(self) -> Self::IntoIter {
125 self.0.into_iter()
126 }
127}
128
129pub struct HintPrefix;
131
132impl fmt::Display for HintPrefix {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}{}", "hint".bold().cyan(), ":".bold())
135 }
136}
137
138#[must_use]
140pub struct ErrorOptions<'a, C = AnsiColors, W = Stderr> {
141 level: Cow<'a, str>,
142 color: C,
143 width_override: Option<usize>,
144 stream: W,
145}
146
147#[derive(Debug, Clone, Copy, Default)]
149pub struct Stderr;
150
151impl fmt::Write for Stderr {
152 fn write_str(&mut self, output: &str) -> fmt::Result {
153 anstream::eprint!("{output}");
154 Ok(())
155 }
156}
157
158impl Default for ErrorOptions<'_, AnsiColors, Stderr> {
159 fn default() -> Self {
160 Self {
161 level: Cow::Borrowed("error"),
162 color: AnsiColors::Red,
163 width_override: None,
164 stream: Stderr,
165 }
166 }
167}
168
169impl<'a, C, W> ErrorOptions<'a, C, W> {
170 pub fn with_level(mut self, level: impl Into<Cow<'a, str>>) -> Self {
172 self.level = level.into();
173 self
174 }
175
176 pub fn with_color<D>(self, color: D) -> ErrorOptions<'a, D, W> {
178 ErrorOptions {
179 level: self.level,
180 color,
181 width_override: self.width_override,
182 stream: self.stream,
183 }
184 }
185
186 #[cfg(test)]
190 fn with_width_override(mut self, width_override: usize) -> Self {
191 self.width_override = Some(width_override);
192 self
193 }
194
195 pub fn with_stream<D>(self, stream: D) -> ErrorOptions<'a, C, D> {
197 ErrorOptions {
198 level: self.level,
199 color: self.color,
200 width_override: self.width_override,
201 stream,
202 }
203 }
204}
205
206pub fn write_error_chain(err: &dyn Error, hints: Hints<'_>) -> fmt::Result {
209 write_error_chain_with_options(err, hints, ErrorOptions::default())
210}
211
212pub fn debug_error_chain(err: &dyn Error) -> impl fmt::Display + '_ {
214 DebugErrorChain(err)
215}
216
217struct DebugErrorChain<'a>(&'a dyn Error);
218
219impl fmt::Display for DebugErrorChain<'_> {
220 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221 for (index, error) in iter::successors(Some(self.0), |&error| error.source()).enumerate() {
222 if index > 0 {
223 formatter.write_str("\n")?;
224 }
225 write!(formatter, "{index}: {error:?}")?;
226 }
227 Ok(())
228 }
229}
230
231pub fn write_error_chain_with_options<C: DynColor + Copy, W: fmt::Write>(
235 err: &dyn Error,
236 hints: Hints<'_>,
237 options: ErrorOptions<'_, C, W>,
238) -> fmt::Result {
239 let ErrorOptions {
240 level,
241 color,
242 width_override,
243 mut stream,
244 } = options;
245 let width = get_wrap_width(width_override);
246
247 let main_msg = err.to_string();
248 let main_padding = " ".repeat(level.len() + 2);
249 let wrapped_main = wrap_text(&main_msg, width, &main_padding, &main_padding, "");
250 writeln!(
251 &mut stream,
252 "{}{} {}",
253 level.as_ref().color(color).bold(),
254 ":".bold(),
255 wrapped_main.trim()
256 )?;
257
258 for source in iter::successors(err.source(), |&err| err.source()) {
259 let msg = source.to_string();
260 let padding = " ";
261 let cause = "Caused by";
262 let child_padding = " ".repeat(padding.len() + cause.len() + 2);
263 let authored_line_padding = " ";
264
265 let wrapped = wrap_text(&msg, width, "", &child_padding, authored_line_padding);
266
267 let mut lines = wrapped.lines();
268 if let Some(first) = lines.next() {
269 writeln!(
270 &mut stream,
271 "{}{}: {}",
272 padding,
273 cause.color(color).bold(),
274 first.trim()
275 )?;
276 for line in lines {
277 if line.trim().is_empty() {
278 writeln!(&mut stream)?;
279 } else {
280 writeln!(&mut stream, "{line}")?;
281 }
282 }
283 }
284 }
285
286 for hint in hints {
287 writeln!(&mut stream, "\n{HintPrefix} {hint}")?;
288 }
289
290 Ok(())
291}
292
293#[cfg(test)]
294mod tests {
295 use anyhow::anyhow;
296 use indoc::indoc;
297 use insta::assert_snapshot;
298 use owo_colors::AnsiColors;
299
300 use super::{
301 ErrorOptions, ErrorWithHints, HintPrefix, Hints, debug_error_chain,
302 write_error_chain_with_options,
303 };
304
305 #[test]
306 fn extend_deduplicates_matching_hints() {
307 let mut hints = Hints::from("same");
308 hints.extend(Hints::from("same"));
309 hints.extend(Hints::from("other"));
310
311 let hints = hints
312 .into_iter()
313 .map(std::borrow::Cow::into_owned)
314 .collect::<Vec<_>>();
315 assert_eq!(hints, vec!["same".to_string(), "other".to_string()]);
316 }
317
318 #[test]
319 fn error_with_hints_separates_hints_from_error() {
320 assert_eq!(
321 ErrorWithHints::new("error", Hints::from("fix it")).to_string(),
322 format!("error\n\n{HintPrefix} fix it")
323 );
324 assert_eq!(
325 ErrorWithHints::new("error", Hints::none()).to_string(),
326 "error"
327 );
328 }
329
330 #[test]
331 fn test_error_wrapping_with_columns() {
332 #[derive(Debug, thiserror::Error)]
333 #[error(
334 "Because fiasobfhuasbf was not found in the package registry and you require fiasobfhuasbf, we can conclude that your requirements are unsatisfiable."
335 )]
336 struct Inner;
337
338 #[derive(Debug, thiserror::Error)]
339 #[error("No solution found when resolving dependencies")]
340 struct Outer {
341 #[source]
342 source: Inner,
343 }
344
345 let error = Outer { source: Inner };
346 let mut output = String::new();
347 write_error_chain_with_options(
348 &error,
349 Hints::none(),
350 ErrorOptions::default()
351 .with_width_override(80)
352 .with_stream(&mut output),
353 )
354 .unwrap();
355 let output = anstream::adapter::strip_str(&output);
356
357 assert_snapshot!(output, @r"
358 error: No solution found when resolving dependencies
359 Caused by: Because fiasobfhuasbf was not found in the package registry and you require
360 fiasobfhuasbf, we can conclude that your requirements are
361 unsatisfiable.
362 ");
363 }
364
365 #[test]
366 fn test_error_chain_with_cause() {
367 #[derive(Debug, thiserror::Error)]
368 #[error("Permission denied")]
369 struct Inner;
370
371 #[derive(Debug, thiserror::Error)]
372 #[error("Failed to write file")]
373 struct Outer {
374 #[source]
375 source: Inner,
376 }
377
378 let error = Outer { source: Inner };
379 let mut output = String::new();
380 write_error_chain_with_options(
381 &error,
382 Hints::none(),
383 ErrorOptions::default().with_stream(&mut output),
384 )
385 .unwrap();
386 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}[31mCaused by\u{1b}[39m\u{1b}[0m: Permission denied\n""#);
387 let output = anstream::adapter::strip_str(&output);
388
389 assert_snapshot!(output, @r"
390 error: Failed to write file
391 Caused by: Permission denied
392 ");
393 }
394
395 #[test]
396 fn formats_debug_error_chain() {
397 #[derive(Debug, thiserror::Error)]
398 #[error("inner error")]
399 struct InnerError {
400 code: u8,
401 }
402
403 #[derive(Debug, thiserror::Error)]
404 #[error("outer error")]
405 struct OuterError {
406 #[source]
407 source: InnerError,
408 }
409
410 let error = OuterError {
411 source: InnerError { code: 42 },
412 };
413
414 assert_eq!(
415 debug_error_chain(&error).to_string(),
416 "0: OuterError { source: InnerError { code: 42 } }\n1: InnerError { code: 42 }"
417 );
418 }
419
420 #[test]
421 fn format_with_custom_level() {
422 let error = anyhow!("Failed to create registry entry");
423 let mut output = String::new();
424 write_error_chain_with_options(
425 error.as_ref(),
426 Hints::none(),
427 ErrorOptions::default()
428 .with_level("warning")
429 .with_color(AnsiColors::Yellow)
430 .with_stream(&mut output),
431 )
432 .unwrap();
433 let output = anstream::adapter::strip_str(&output);
434
435 assert_snapshot!(output, @"warning: Failed to create registry entry
436");
437 }
438
439 #[test]
440 fn test_no_hyphenation() {
441 #[derive(Debug, thiserror::Error)]
442 #[error(
443 "Failed to download package from https://files.pythonhosted.org/packages/verylongpackagename"
444 )]
445 struct LongWord;
446
447 let error = LongWord;
448 let mut output = String::new();
449 write_error_chain_with_options(
450 &error,
451 Hints::none(),
452 ErrorOptions::default()
453 .with_width_override(50)
454 .with_stream(&mut output),
455 )
456 .unwrap();
457 let output = anstream::adapter::strip_str(&output);
458 assert_snapshot!(output, @r"
459 error: Failed to download package from
460 https://files.pythonhosted.org/packages/verylongpackagename
461 ");
462 }
463
464 #[test]
465 fn test_long_words_not_broken() {
466 #[derive(Debug, thiserror::Error)]
467 #[error(
468 "The package supercalifragilisticexpialidocious-extraordinarily-long-name was not found"
469 )]
470 struct VeryLongWord;
471
472 let error = VeryLongWord;
473 let mut output = String::new();
474 write_error_chain_with_options(
475 &error,
476 Hints::none(),
477 ErrorOptions::default()
478 .with_width_override(40)
479 .with_stream(&mut output),
480 )
481 .unwrap();
482 let output = anstream::adapter::strip_str(&output);
483 assert_snapshot!(output, @r"
484 error: The package
485 supercalifragilisticexpialidocious-extraordinarily-long-name
486 was not found
487 ");
488 }
489
490 #[test]
491 fn test_multiple_error_sources() {
492 #[derive(Debug, thiserror::Error)]
493 #[error("Network connection timeout after multiple retry attempts")]
494 struct DeepError;
495
496 #[derive(Debug, thiserror::Error)]
497 #[error("Failed to fetch package metadata from registry")]
498 struct MiddleError {
499 #[source]
500 source: DeepError,
501 }
502
503 #[derive(Debug, thiserror::Error)]
504 #[error("Unable to resolve package dependencies")]
505 struct TopError {
506 #[source]
507 source: MiddleError,
508 }
509
510 let error = TopError {
511 source: MiddleError { source: DeepError },
512 };
513 let mut output = String::new();
514 write_error_chain_with_options(
515 &error,
516 Hints::none(),
517 ErrorOptions::default()
518 .with_width_override(60)
519 .with_stream(&mut output),
520 )
521 .unwrap();
522 let output = anstream::adapter::strip_str(&output);
523 assert_snapshot!(output, @r"
524 error: Unable to resolve package dependencies
525 Caused by: Failed to fetch package metadata from registry
526 Caused by: Network connection timeout after multiple retry attempts
527 ");
528 }
529
530 #[test]
531 fn test_multiline_main_message_wraps_each_line() {
532 #[derive(Debug, thiserror::Error)]
533 #[error(
534 "There is no command `foobar` for `uv`. Did you mean one of:\n auth\n run\n init"
535 )]
536 struct Suggestions;
537
538 let error = Suggestions;
539 let mut output = String::new();
540 write_error_chain_with_options(
541 &error,
542 Hints::none(),
543 ErrorOptions::default()
544 .with_width_override(50)
545 .with_stream(&mut output),
546 )
547 .unwrap();
548 let output = anstream::adapter::strip_str(&output);
549
550 assert_snapshot!(output, @r"
551 error: There is no command `foobar` for `uv`. Did
552 you mean one of:
553 auth
554 run
555 init
556 ");
557 }
558
559 #[test]
560 fn test_wrap_only_on_ascii_space() {
561 #[derive(Debug, thiserror::Error)]
562 #[error("Path /usr/local/lib/python3.12/site-packages not found in filesystem hierarchy")]
563 struct SpecialChars;
564
565 let error = SpecialChars;
566 let mut output = String::new();
567 write_error_chain_with_options(
568 &error,
569 Hints::none(),
570 ErrorOptions::default()
571 .with_width_override(50)
572 .with_stream(&mut output),
573 )
574 .unwrap();
575 let output = anstream::adapter::strip_str(&output);
576 assert_snapshot!(output, @r"
577 error: Path /usr/local/lib/python3.12/site-packages
578 not found in filesystem hierarchy
579 ");
580 }
581
582 #[test]
583 fn format_with_hints() {
584 let err = anyhow!("Permission denied").context("Failed to fetch package");
585
586 let hints = [
587 "Try running with `--verbose` for more information.".to_string(),
588 "Try running without --offline.".to_string(),
589 ]
590 .into_iter()
591 .collect();
592
593 let mut rendered = String::new();
594 write_error_chain_with_options(
595 err.as_ref(),
596 hints,
597 ErrorOptions::default().with_stream(&mut rendered),
598 )
599 .unwrap();
600 let rendered = anstream::adapter::strip_str(&rendered);
601
602 assert_snapshot!(rendered, @r"
603 error: Failed to fetch package
604 Caused by: Permission denied
605
606 hint: Try running with `--verbose` for more information.
607
608 hint: Try running without --offline.
609 ");
610 }
611
612 #[test]
613 fn format_multiline_message() {
614 let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
615 Server says: This endpoint only support POST requests.
616
617 For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
618 let err = anyhow!("Caused By: HTTP Error 400")
619 .context(err_middle)
620 .context("Failed to download Python 3.12");
621
622 let mut rendered = String::new();
623 write_error_chain_with_options(
624 err.as_ref(),
625 Hints::none(),
626 ErrorOptions::default().with_stream(&mut rendered),
627 )
628 .unwrap();
629 let rendered = anstream::adapter::strip_str(&rendered);
630
631 assert_snapshot!(rendered, @r"
632 error: Failed to download Python 3.12
633 Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
634 Server says: This endpoint only support POST requests.
635
636 For downloads, please refer to https://example.com/download/python3.13.tar.zst
637 Caused by: Caused By: HTTP Error 400
638 ");
639 }
640}