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