bon_macros/error/
mod.rs

1mod panic_context;
2
3use crate::util::prelude::*;
4use proc_macro2::{Group, TokenTree};
5use std::panic::AssertUnwindSafe;
6use syn::parse::Parse;
7
8/// Handle the error or panic returned from the macro logic.
9///
10/// The error may be either a syntax error or a logic error. In either case, we
11/// want to return a [`TokenStream`] that still provides good IDE experience.
12/// See [`Fallback`] for details.
13///
14/// This function also catches panics. Importantly, we don't use panics for error
15/// handling! A panic is always a bug! However, we still handle it to provide
16/// better IDE experience even if there are some bugs in the macro implementation.
17///
18/// One known bug that may cause panics when using Rust Analyzer is the following one:
19/// <https://github.com/rust-lang/rust-analyzer/issues/18244>
20pub(crate) fn handle_errors(
21    item: TokenStream,
22    imp: impl FnOnce() -> Result<TokenStream>,
23) -> Result<TokenStream, TokenStream> {
24    let panic_listener = panic_context::PanicListener::register();
25
26    std::panic::catch_unwind(AssertUnwindSafe(imp))
27        .unwrap_or_else(|err| {
28            let msg = panic_context::message_from_panic_payload(err.as_ref())
29                .unwrap_or_else(|| "<unknown error message>".to_owned());
30
31            let msg = if msg.contains("unsupported proc macro punctuation character") {
32                format!(
33                    "known bug in rust-analyzer: {msg};\n\
34                    Github issue: https://github.com/rust-lang/rust-analyzer/issues/18244"
35                )
36            } else {
37                let context = panic_listener
38                    .get_last_panic()
39                    .map(|ctx| format!("\n\n{ctx}"))
40                    .unwrap_or_default();
41
42                format!(
43                    "proc-macro panicked (may be a bug in the crate `bon`): {msg};\n\
44                    please report this issue at our Github repository: \
45                    https://github.com/elastio/bon{context}"
46                )
47            };
48
49            Err(err!(&Span::call_site(), "{msg}"))
50        })
51        .map_err(|err| {
52            let compile_error = err.write_errors();
53            let item = strip_invalid_tt(item);
54
55            syn::parse2::<Fallback>(item)
56                .map(|fallback| quote!(#compile_error #fallback))
57                .unwrap_or_else(|_| compile_error)
58        })
59}
60
61/// This is used in error handling for better IDE experience. For example, while
62/// the developer is writing the function code they'll have a bunch of syntax
63/// errors in the process. While that happens the proc macro should output at
64/// least some representation of the input code that the developer wrote with
65/// a separate compile error entry. This keeps the syntax highlighting and IDE
66/// type analysis, completions and other hints features working even if macro
67/// fails to parse some syntax or finds some other logic errors.
68///
69/// This utility does very low-level parsing to strip doc comments from the
70/// input. This is to prevent the IDE from showing errors that "doc comments
71/// aren't allowed on function arguments". It also removes `#[builder(...)]`
72/// attributes that need to be processed by this macro to avoid the IDE from
73/// reporting those as well.
74struct Fallback {
75    output: TokenStream,
76}
77
78impl Parse for Fallback {
79    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
80        let mut output = TokenStream::new();
81
82        loop {
83            let found_attr = input.step(|cursor| {
84                let mut cursor = *cursor;
85                while let Some((tt, next)) = cursor.token_tree() {
86                    match &tt {
87                        TokenTree::Group(group) => {
88                            let fallback: Self = syn::parse2(group.stream())?;
89                            let new_group = Group::new(group.delimiter(), fallback.output);
90                            output.extend([TokenTree::Group(new_group)]);
91                        }
92                        TokenTree::Punct(punct) if punct.as_char() == '#' => {
93                            return Ok((true, cursor));
94                        }
95                        TokenTree::Punct(_) | TokenTree::Ident(_) | TokenTree::Literal(_) => {
96                            output.extend([tt]);
97                        }
98                    }
99
100                    cursor = next;
101                }
102
103                Ok((false, cursor))
104            })?;
105
106            if !found_attr {
107                return Ok(Self { output });
108            }
109
110            input
111                .call(syn::Attribute::parse_outer)?
112                .into_iter()
113                .filter(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder"))
114                .for_each(|attr| attr.to_tokens(&mut output));
115        }
116    }
117}
118
119impl ToTokens for Fallback {
120    fn to_tokens(&self, tokens: &mut TokenStream) {
121        self.output.to_tokens(tokens);
122    }
123}
124
125/// Workaround for the RA bug where it generates an invalid Punct token tree with
126/// the character `{`.
127///
128/// ## Issues
129///
130/// - [Bug in RA](https://github.com/rust-lang/rust-analyzer/issues/18244)
131/// - [Bug in proc-macro2](https://github.com/dtolnay/proc-macro2/issues/470) (already fixed)
132fn strip_invalid_tt(tokens: TokenStream) -> TokenStream {
133    fn recurse(tt: TokenTree) -> TokenTree {
134        match &tt {
135            TokenTree::Group(group) => {
136                let mut group = Group::new(group.delimiter(), strip_invalid_tt(group.stream()));
137                group.set_span(group.span());
138
139                TokenTree::Group(group)
140            }
141            _ => tt,
142        }
143    }
144
145    let mut tokens = tokens.into_iter();
146
147    std::iter::from_fn(|| {
148        // In newer versions of `proc-macro2` this code panics here (earlier)
149        loop {
150            // If this panics it means the next token tree is invalid.
151            // We can't do anything about it, and we just ignore it.
152            // Luckily, `proc-macro2` consumes the invalid token tree
153            // so this doesn't cause an infinite loop.
154            match std::panic::catch_unwind(AssertUnwindSafe(|| tokens.next())) {
155                Ok(tt) => return tt.map(recurse),
156                Err(_) => continue,
157            }
158        }
159    })
160    .collect()
161}