uv_warnings/
lib.rs

1use std::error::Error;
2use std::iter;
3use std::sync::atomic::AtomicBool;
4use std::sync::{LazyLock, Mutex};
5
6// macro hygiene: The user might not have direct dependencies on those crates
7#[doc(hidden)]
8pub use anstream;
9#[doc(hidden)]
10pub use owo_colors;
11use owo_colors::{DynColor, OwoColorize};
12use rustc_hash::FxHashSet;
13
14/// Whether user-facing warnings are enabled.
15pub static ENABLED: AtomicBool = AtomicBool::new(false);
16
17/// Enable user-facing warnings.
18pub fn enable() {
19    ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
20}
21
22/// Disable user-facing warnings.
23pub fn disable() {
24    ENABLED.store(false, std::sync::atomic::Ordering::Relaxed);
25}
26
27/// Warn a user, if warnings are enabled.
28#[macro_export]
29macro_rules! warn_user {
30    ($($arg:tt)*) => {{
31        use $crate::anstream::eprintln;
32        use $crate::owo_colors::OwoColorize;
33
34        if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
35            let message = format!("{}", format_args!($($arg)*));
36            let formatted = message.bold();
37            eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold());
38        }
39    }};
40}
41
42pub static WARNINGS: LazyLock<Mutex<FxHashSet<String>>> = LazyLock::new(Mutex::default);
43
44/// Warn a user once, if warnings are enabled, with uniqueness determined by the content of the
45/// message.
46#[macro_export]
47macro_rules! warn_user_once {
48    ($($arg:tt)*) => {{
49        use $crate::anstream::eprintln;
50        use $crate::owo_colors::OwoColorize;
51
52        if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
53            if let Ok(mut states) = $crate::WARNINGS.lock() {
54                let message = format!("{}", format_args!($($arg)*));
55                if states.insert(message.clone()) {
56                    eprintln!("{}{} {}", "warning".yellow().bold(), ":".bold(), message.bold());
57                }
58            }
59        }
60    }};
61}
62
63/// Format an error or warning chain.
64///
65/// # Example
66///
67/// ```text
68/// error: Failed to install app
69///   Caused By: Failed to install dependency
70///   Caused By: Error writing failed `/home/ferris/deps/foo`: Permission denied
71/// ```
72///
73/// ```text
74/// warning: Failed to create registry entry for Python 3.12
75///   Caused By: Security policy forbids chaining registry entries
76/// ```
77///
78/// ```text
79/// error: Failed to download Python 3.12
80///  Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
81///             Server says: This endpoint only support POST requests.
82///
83///             For downloads, please refer to https://example.com/download/python3.13.tar.zst
84///  Caused by: Caused By: HTTP Error 400
85/// ```
86pub fn write_error_chain(
87    err: &dyn Error,
88    mut stream: impl std::fmt::Write,
89    level: impl AsRef<str>,
90    color: impl DynColor + Copy,
91) -> std::fmt::Result {
92    writeln!(
93        &mut stream,
94        "{}{} {}",
95        level.as_ref().color(color).bold(),
96        ":".bold(),
97        err.to_string().trim()
98    )?;
99    for source in iter::successors(err.source(), |&err| err.source()) {
100        let msg = source.to_string();
101        let mut lines = msg.lines();
102        if let Some(first) = lines.next() {
103            let padding = "  ";
104            let cause = "Caused by";
105            let child_padding = " ".repeat(padding.len() + cause.len() + 2);
106            writeln!(
107                &mut stream,
108                "{}{}: {}",
109                padding,
110                cause.color(color).bold(),
111                first.trim()
112            )?;
113            for line in lines {
114                let line = line.trim_end();
115                if line.is_empty() {
116                    // Avoid showing indents on empty lines
117                    writeln!(&mut stream)?;
118                } else {
119                    writeln!(&mut stream, "{}{}", child_padding, line.trim_end())?;
120                }
121            }
122        }
123    }
124    Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129    use crate::write_error_chain;
130    use anyhow::anyhow;
131    use indoc::indoc;
132    use insta::assert_snapshot;
133    use owo_colors::AnsiColors;
134
135    #[test]
136    fn format_multiline_message() {
137        let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
138        Server says: This endpoint only support POST requests.
139
140        For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
141        let err = anyhow!("Caused By: HTTP Error 400")
142            .context(err_middle)
143            .context("Failed to download Python 3.12");
144
145        let mut rendered = String::new();
146        write_error_chain(err.as_ref(), &mut rendered, "error", AnsiColors::Red).unwrap();
147        let rendered = anstream::adapter::strip_str(&rendered);
148
149        assert_snapshot!(rendered, @r"
150        error: Failed to download Python 3.12
151          Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
152                     Server says: This endpoint only support POST requests.
153
154                     For downloads, please refer to https://example.com/download/python3.13.tar.zst
155          Caused by: Caused By: HTTP Error 400
156        ");
157    }
158}