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