Skip to main content

foundry_common/errors/
mod.rs

1//! Commonly used errors
2
3mod fs;
4pub use fs::FsPathError;
5
6mod private {
7    use eyre::Chain;
8    use std::error::Error;
9
10    pub trait ErrorChain {
11        fn chain(&self) -> Chain<'_>;
12    }
13
14    impl ErrorChain for dyn Error + 'static {
15        fn chain(&self) -> Chain<'_> {
16            Chain::new(self)
17        }
18    }
19
20    impl ErrorChain for eyre::Report {
21        fn chain(&self) -> Chain<'_> {
22            self.chain()
23        }
24    }
25}
26
27/// Displays a chain of errors in a single line.
28pub fn display_chain<E: private::ErrorChain + ?Sized>(error: &E) -> String {
29    dedup_chain(error).join("; ")
30}
31
32/// Deduplicates a chain of errors.
33pub fn dedup_chain<E: private::ErrorChain + ?Sized>(error: &E) -> Vec<String> {
34    let mut causes = all_sources(error);
35    // Deduplicate the common pattern `msg1: msg2; msg2` -> `msg1: msg2`.
36    causes.dedup_by(|b, a| a.contains(b.as_str()));
37    causes
38}
39
40fn all_sources<E: private::ErrorChain + ?Sized>(err: &E) -> Vec<String> {
41    err.chain().map(|cause| cause.to_string().trim().to_string()).collect()
42}
43
44/// Converts solar errors to an eyre error.
45pub fn convert_solar_errors(dcx: &solar::interface::diagnostics::DiagCtxt) -> eyre::Result<()> {
46    match dcx.emitted_errors() {
47        Some(Ok(())) => Ok(()),
48        Some(Err(e)) if !e.is_empty() => {
49            eyre::bail!("solar reported errors:\n\n{e}");
50        }
51        _ if dcx.has_errors().is_err() => {
52            // Non-buffer emitter: diagnostics already went to stderr; include the count.
53            let n = dcx.err_count();
54            let plural = if n == 1 { "" } else { "s" };
55            eyre::bail!(
56                "solar reported {n} error{plural}; see the diagnostic{plural} printed above"
57            );
58        }
59        _ => Ok(()),
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use solar::interface::diagnostics::{DiagCtxt, SilentEmitter};
67
68    #[test]
69    fn dedups_contained() {
70        #[derive(thiserror::Error, Debug)]
71        #[error("my error: {0}")]
72        struct A(#[from] B);
73
74        #[derive(thiserror::Error, Debug)]
75        #[error("{0}")]
76        struct B(String);
77
78        let ee = eyre::Report::from(A(B("hello".into())));
79        assert_eq!(ee.chain().count(), 2, "{ee:?}");
80        let full = all_sources(&ee).join("; ");
81        assert_eq!(full, "my error: hello; hello");
82        let chained = display_chain(&ee);
83        assert_eq!(chained, "my error: hello");
84    }
85
86    /// Regression test for the "non-buffer emitter" branch of [`convert_solar_errors`].
87    ///
88    /// Simulates an unhandled solar edge case: the linter installs a non-buffer (stderr-style)
89    /// emitter, errors are emitted to it, and only the count is recoverable afterwards. The
90    /// returned eyre error must reference the count and direct the user to the diagnostics that
91    /// were already printed above.
92    #[test]
93    fn solar_non_buffer_emitter_singular() {
94        let dcx = DiagCtxt::new(Box::new(SilentEmitter::new_silent()));
95        dcx.err("boom").emit();
96
97        let err = convert_solar_errors(&dcx).unwrap_err();
98        assert_eq!(err.to_string(), "solar reported 1 error; see the diagnostic printed above");
99    }
100
101    #[test]
102    fn solar_non_buffer_emitter_plural() {
103        let dcx = DiagCtxt::new(Box::new(SilentEmitter::new_silent()));
104        dcx.err("boom 1").emit();
105        dcx.err("boom 2").emit();
106
107        let err = convert_solar_errors(&dcx).unwrap_err();
108        assert_eq!(err.to_string(), "solar reported 2 errors; see the diagnostics printed above");
109    }
110
111    #[test]
112    fn solar_no_errors_is_ok() {
113        let dcx = DiagCtxt::new(Box::new(SilentEmitter::new_silent()));
114        assert!(convert_solar_errors(&dcx).is_ok());
115    }
116}