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}