1 // SPDX-License-Identifier: Apache-2.0 OR MIT 2 3 use std::cell::RefCell; 4 use std::fmt::Display; 5 use std::marker::PhantomData; 6 7 use proc_macro2::{Span, TokenStream}; 8 use quote::{quote, quote_spanned}; 9 use syn::{spanned::Spanned, Error}; 10 11 pub(crate) struct DiagCtxt(PhantomData<*mut ()>); 12 pub(crate) struct ErrorGuaranteed(()); 13 14 struct DiagCtxtData { 15 diag: TokenStream, 16 } 17 18 thread_local! { 19 static DIAGNOSTICS: RefCell<Option<DiagCtxtData>> = const { RefCell::new(None) }; 20 } 21 22 // Allows `syn::Error` to be emitted into the current diagnostic context with just `?`. 23 impl From<syn::Error> for ErrorGuaranteed { 24 fn from(error: syn::Error) -> Self { 25 DIAGNOSTICS.with_borrow_mut(|data| { 26 data.as_mut() 27 .unwrap() 28 .diag 29 .extend(error.into_compile_error()); 30 }); 31 Self(()) 32 } 33 } 34 35 impl DiagCtxt { 36 pub(crate) fn error(&self, span: impl Spanned, msg: impl Display) -> ErrorGuaranteed { 37 Error::new(span.span(), msg).into() 38 } 39 40 pub(crate) fn warn(&self, span: impl Spanned, msg: impl Display) { 41 // Have the message start on a new line for visual clarity. 42 let msg = format!("\n{}", msg); 43 DIAGNOSTICS.with_borrow_mut(|data| { 44 data.as_mut() 45 .unwrap() 46 .diag 47 .extend(quote_spanned!(span.span() => 48 // Approximate using deprecated warning while `proc_macro_diagnostic` is 49 // unstable. 50 const _: () = { 51 #[deprecated = #msg] 52 const fn warn() {} 53 warn(); 54 }; 55 )) 56 }); 57 } 58 59 /// Execute the provided function with the current diagnostic context. 60 pub(crate) fn current<R>(f: impl FnOnce(&DiagCtxt) -> R) -> R { 61 DIAGNOSTICS.with_borrow(|data| { 62 assert!(data.is_some(), "No active `DiagCtxt`"); 63 }); 64 65 f(&DiagCtxt(PhantomData)) 66 } 67 68 fn with( 69 f: impl FnOnce(&mut DiagCtxt) -> Result<TokenStream, ErrorGuaranteed>, 70 merge_diag: impl FnOnce(TokenStream, TokenStream) -> TokenStream, 71 convert_diag: impl FnOnce(TokenStream) -> TokenStream, 72 ) -> TokenStream { 73 DIAGNOSTICS.with_borrow_mut(|data| { 74 assert!(data.is_none(), "`DiagCtxt` cannot be nested"); 75 *data = Some(DiagCtxtData { 76 diag: TokenStream::new(), 77 }); 78 }); 79 80 let mut dcx = DiagCtxt(PhantomData); 81 let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&mut dcx))) { 82 Ok(result) => result, 83 Err(payload) => { 84 // Robustness against panicking in macros. 85 // 86 // Ensure that any error messages are still emitted when this happens. 87 let message = if let Some(&s) = payload.downcast_ref::<&'static str>() { 88 s 89 } else if let Some(s) = payload.downcast_ref::<String>() { 90 s.as_str() 91 } else { 92 "Box<dyn Any>" 93 }; 94 95 Err(dcx.error( 96 Span::mixed_site(), 97 format!("proc macro panicked: {message}"), 98 )) 99 } 100 }; 101 102 let data = DIAGNOSTICS.with_borrow_mut(|data| data.take().unwrap()); 103 104 match result { 105 Ok(stream) => { 106 if data.diag.is_empty() { 107 stream 108 } else { 109 merge_diag(stream, data.diag) 110 } 111 } 112 Err(ErrorGuaranteed(())) => convert_diag(data.diag), 113 } 114 } 115 116 pub(crate) fn for_item( 117 f: impl FnOnce(&mut DiagCtxt) -> Result<TokenStream, ErrorGuaranteed>, 118 ) -> TokenStream { 119 Self::with( 120 f, 121 |mut out, diag| { 122 out.extend(diag); 123 out 124 }, 125 std::convert::identity, 126 ) 127 } 128 129 pub(crate) fn for_expr( 130 f: impl FnOnce(&mut DiagCtxt) -> Result<TokenStream, ErrorGuaranteed>, 131 ) -> TokenStream { 132 Self::with( 133 f, 134 |out, diag| { 135 // Diagnostics that we generate are always items. 136 // So for expressions create a block to place diagnostics in item position. 137 quote!({ 138 #diag 139 #out 140 }) 141 }, 142 |diag| { 143 quote!({ 144 #diag 145 }) 146 }, 147 ) 148 } 149 } 150