blob: e88e520e326b664fb19f1fcebad2688289f2f350 [file]
// SPDX-License-Identifier: Apache-2.0 OR MIT
use std::cell::RefCell;
use std::fmt::Display;
use std::marker::PhantomData;
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, Error};
pub(crate) struct DiagCtxt(PhantomData<*mut ()>);
pub(crate) struct ErrorGuaranteed(());
struct DiagCtxtData {
diag: TokenStream,
}
thread_local! {
static DIAGNOSTICS: RefCell<Option<DiagCtxtData>> = const { RefCell::new(None) };
}
// Allows `syn::Error` to be emitted into the current diagnostic context with just `?`.
impl From<syn::Error> for ErrorGuaranteed {
fn from(error: syn::Error) -> Self {
DIAGNOSTICS.with_borrow_mut(|data| {
data.as_mut()
.unwrap()
.diag
.extend(error.into_compile_error());
});
Self(())
}
}
impl DiagCtxt {
pub(crate) fn error(&self, span: impl Spanned, msg: impl Display) -> ErrorGuaranteed {
Error::new(span.span(), msg).into()
}
pub(crate) fn warn(&self, span: impl Spanned, msg: impl Display) {
// Have the message start on a new line for visual clarity.
let msg = format!("\n{}", msg);
DIAGNOSTICS.with_borrow_mut(|data| {
data.as_mut()
.unwrap()
.diag
.extend(quote_spanned!(span.span() =>
// Approximate using deprecated warning while `proc_macro_diagnostic` is
// unstable.
const _: () = {
#[deprecated = #msg]
const fn warn() {}
warn();
};
))
});
}
/// Execute the provided function with the current diagnostic context.
pub(crate) fn current<R>(f: impl FnOnce(&DiagCtxt) -> R) -> R {
DIAGNOSTICS.with_borrow(|data| {
assert!(data.is_some(), "No active `DiagCtxt`");
});
f(&DiagCtxt(PhantomData))
}
fn with(
f: impl FnOnce(&mut DiagCtxt) -> Result<TokenStream, ErrorGuaranteed>,
merge_diag: impl FnOnce(TokenStream, TokenStream) -> TokenStream,
convert_diag: impl FnOnce(TokenStream) -> TokenStream,
) -> TokenStream {
DIAGNOSTICS.with_borrow_mut(|data| {
assert!(data.is_none(), "`DiagCtxt` cannot be nested");
*data = Some(DiagCtxtData {
diag: TokenStream::new(),
});
});
let mut dcx = DiagCtxt(PhantomData);
let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&mut dcx))) {
Ok(result) => result,
Err(payload) => {
// Robustness against panicking in macros.
//
// Ensure that any error messages are still emitted when this happens.
let message = if let Some(&s) = payload.downcast_ref::<&'static str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"Box<dyn Any>"
};
Err(dcx.error(
Span::mixed_site(),
format!("proc macro panicked: {message}"),
))
}
};
let data = DIAGNOSTICS.with_borrow_mut(|data| data.take().unwrap());
match result {
Ok(stream) => {
if data.diag.is_empty() {
stream
} else {
merge_diag(stream, data.diag)
}
}
Err(ErrorGuaranteed(())) => convert_diag(data.diag),
}
}
pub(crate) fn for_item(
f: impl FnOnce(&mut DiagCtxt) -> Result<TokenStream, ErrorGuaranteed>,
) -> TokenStream {
Self::with(
f,
|mut out, diag| {
out.extend(diag);
out
},
std::convert::identity,
)
}
pub(crate) fn for_expr(
f: impl FnOnce(&mut DiagCtxt) -> Result<TokenStream, ErrorGuaranteed>,
) -> TokenStream {
Self::with(
f,
|out, diag| {
// Diagnostics that we generate are always items.
// So for expressions create a block to place diagnostics in item position.
quote!({
#diag
#out
})
},
|diag| {
quote!({
#diag
})
},
)
}
}