// 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> = const { RefCell::new(None) }; } // Allows `syn::Error` to be emitted into the current diagnostic context with just `?`. impl From 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(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, 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::() { s.as_str() } else { "Box" }; 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 { 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 { 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 }) }, ) } }