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() => eyre::bail!("solar reported errors:\n\n{e}"),
49        _ if dcx.has_errors().is_err() => {
50            // Non-buffer emitter: diagnostics already went to stderr; include the count.
51            let n = dcx.err_count();
52            let plural = if n == 1 { "" } else { "s" };
53            eyre::bail!(
54                "solar reported {n} error{plural}; see the diagnostic{plural} printed above"
55            )
56        }
57        _ => Ok(()),
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use solar::interface::diagnostics::{DiagCtxt, SilentEmitter};
65
66    #[test]
67    fn dedups_contained() {
68        #[derive(thiserror::Error, Debug)]
69        #[error("my error: {0}")]
70        struct A(#[from] B);
71
72        #[derive(thiserror::Error, Debug)]
73        #[error("{0}")]
74        struct B(String);
75
76        let ee = eyre::Report::from(A(B("hello".into())));
77        assert_eq!(ee.chain().count(), 2, "{ee:?}");
78        let full = all_sources(&ee).join("; ");
79        assert_eq!(full, "my error: hello; hello");
80        let chained = display_chain(&ee);
81        assert_eq!(chained, "my error: hello");
82    }
83
84    /// Regression test for the "non-buffer emitter" branch of [`convert_solar_errors`].
85    ///
86    /// Simulates an unhandled solar edge case: the linter installs a non-buffer (stderr-style)
87    /// emitter, errors are emitted to it, and only the count is recoverable afterwards. The
88    /// returned eyre error must reference the count and direct the user to the diagnostics that
89    /// were already printed above.
90    #[test]
91    fn solar_non_buffer_emitter_singular() {
92        let dcx = DiagCtxt::new(Box::new(SilentEmitter::new_silent()));
93        dcx.err("boom").emit();
94
95        let err = convert_solar_errors(&dcx).unwrap_err();
96        assert_eq!(err.to_string(), "solar reported 1 error; see the diagnostic printed above");
97    }
98
99    #[test]
100    fn solar_non_buffer_emitter_plural() {
101        let dcx = DiagCtxt::new(Box::new(SilentEmitter::new_silent()));
102        dcx.err("boom 1").emit();
103        dcx.err("boom 2").emit();
104
105        let err = convert_solar_errors(&dcx).unwrap_err();
106        assert_eq!(err.to_string(), "solar reported 2 errors; see the diagnostics printed above");
107    }
108
109    #[test]
110    fn solar_no_errors_is_ok() {
111        let dcx = DiagCtxt::new(Box::new(SilentEmitter::new_silent()));
112        assert!(convert_solar_errors(&dcx).is_ok());
113    }
114}