Skip to main content

queryweave_codegen/
errors.rs

1use proc_macro2::Span;
2use quote::ToTokens;
3
4/// Wraps a message with a helper hint with a [`syn::Error`].
5///
6/// # Errors
7///
8/// Always returns a [`syn::Error`].
9pub fn abort<T>(span: Span, msg: &str, help: &str) -> syn::Result<T> {
10    Err(syn::Error::new(span, message(msg, help)))
11}
12
13/// Wrapper over [`abort`] with call site hygiene.
14///
15/// # Errors
16///
17/// Always returns a [`syn::Error`].
18pub fn abort_call_site<T>(msg: &str, help: &str) -> syn::Result<T> {
19    abort(Span::call_site(), msg, help)
20}
21
22/// Span the given syntax tree over a [`syn::Error`].
23///
24/// # Errors
25///
26/// Always returns a [`syn::Error`].
27pub fn abort_spanned<T>(tokens: impl ToTokens, msg: &str, help: &str) -> syn::Result<T> {
28    Err(syn::Error::new_spanned(tokens, message(msg, help)))
29}
30
31/// Properly format a the error message (msg) and the suggested fix (help)
32#[must_use]
33pub fn message(msg: &str, help: &str) -> String {
34    format!("{msg}\n\n= help: {help}\n\n")
35}
36
37/// Both values, or the errors of every one that failed (in order).
38///
39/// # Errors
40///
41/// Returns the combined errors of `first` and `second`.
42pub fn both<A, B>(first: syn::Result<A>, second: syn::Result<B>) -> syn::Result<(A, B)> {
43    match (first, second) {
44        (Ok(first), Ok(second)) => Ok((first, second)),
45        (Err(mut error), Err(other)) => {
46            error.combine(other);
47            Err(error)
48        },
49        (Err(error), Ok(_)) | (Ok(_), Err(error)) => Err(error),
50    }
51}
52
53/// Errors collected while going through a derive's input (e.g. one per
54/// malformed field), so all of them are reported rather than only the first.
55#[derive(Default)]
56pub struct Errors {
57    combined: Option<syn::Error>,
58}
59
60impl Errors {
61    /// Collect an error.
62    pub fn push(&mut self, error: syn::Error) {
63        match &mut self.combined {
64            Some(combined) => combined.combine(error),
65            None => self.combined = Some(error),
66        }
67    }
68
69    /// Collect the error of `result`, if any, returning its value otherwise.
70    pub fn check<T>(&mut self, result: syn::Result<T>) -> Option<T> {
71        result.map_err(|error| self.push(error)).ok()
72    }
73
74    /// `value`, or every collected error.
75    ///
76    /// # Errors
77    ///
78    /// Returns the collected errors, combined, if there are any.
79    pub fn finish<T>(self, value: T) -> syn::Result<T> {
80        self.combined.map_or(Ok(value), Err)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::{Errors, abort, abort_call_site, abort_spanned, both, message};
87    use pretty_assertions::assert_eq;
88
89    fn failure(text: &str) -> syn::Error {
90        syn::Error::new(proc_macro2::Span::call_site(), text)
91    }
92
93    fn messages(error: syn::Error) -> Vec<String> {
94        error.into_iter().map(|error| error.to_string()).collect()
95    }
96
97    #[test]
98    fn both_keeps_both_values_or_every_error_in_order() {
99        assert_eq!(both::<_, u8>(Ok(1), Ok(2)).unwrap(), (1, 2));
100        assert_eq!(
101            messages(both::<u8, u8>(Err(failure("first")), Err(failure("second"))).unwrap_err()),
102            vec!["first", "second"]
103        );
104        assert_eq!(
105            messages(both::<u8, u8>(Ok(1), Err(failure("second"))).unwrap_err()),
106            vec!["second"]
107        );
108    }
109
110    #[test]
111    fn errors_finish_with_the_value_or_every_collected_error() {
112        assert_eq!(Errors::default().finish(7).unwrap(), 7);
113
114        let mut errors = Errors::default();
115        errors.push(failure("first"));
116        assert_eq!(errors.check::<u8>(Err(failure("second"))), None);
117        assert_eq!(errors.check::<u8>(Ok(3)), Some(3));
118
119        assert_eq!(
120            messages(errors.finish(7).unwrap_err()),
121            vec!["first", "second"]
122        );
123    }
124
125    // The exact layout is what the `trybuild` `.stderr` fixtures are matched
126    // against, so it is pinned here too: the UI tests are excluded from the
127    // profile the mutation run uses.
128    #[test]
129    fn message_lays_out_the_error_and_the_help_hint() {
130        assert_eq!(
131            message("Something went wrong", "Try this instead"),
132            "Something went wrong\n\n= help: Try this instead\n\n"
133        );
134    }
135
136    #[test]
137    fn message_keeps_both_halves_distinct() {
138        let formatted = message("the-message", "the-help");
139
140        assert!(formatted.starts_with("the-message"));
141        assert!(formatted.contains("= help: the-help"));
142    }
143
144    #[test]
145    fn abort_returns_an_error_carrying_the_formatted_message() {
146        let error = abort::<()>(proc_macro2::Span::call_site(), "msg", "help").unwrap_err();
147
148        assert_eq!(error.to_string(), message("msg", "help"));
149    }
150
151    #[test]
152    fn abort_call_site_returns_an_error_carrying_the_formatted_message() {
153        let error = abort_call_site::<()>("msg", "help").unwrap_err();
154
155        assert_eq!(error.to_string(), message("msg", "help"));
156    }
157
158    #[test]
159    fn abort_spanned_returns_an_error_carrying_the_formatted_message() {
160        let tokens = quote::quote! { struct UserModel; };
161        let error = abort_spanned::<()>(tokens, "msg", "help").unwrap_err();
162
163        assert_eq!(error.to_string(), message("msg", "help"));
164    }
165}