Bound UTF-8 formatting and release diagnostic ingress before invoking callbacks. Check error-ID collisions at Rust/C++ release final link. Validation: diagnostic tests, strict Clippy, Miri and final-link cases.
247 lines
8.6 KiB
Rust
247 lines
8.6 KiB
Rust
//! Error derive and reporting-attribute expansion for `kaptan-err-reportable`.
|
|
|
|
use proc_macro::TokenStream;
|
|
use quote::{format_ident, quote};
|
|
use std::collections::BTreeMap;
|
|
use syn::{
|
|
Data, DeriveInput, Error, Fields, ItemFn, LitInt, ReturnType, Variant, parse_macro_input,
|
|
parse_quote, spanned::Spanned,
|
|
};
|
|
|
|
/// Implements `kaptan_err_reportable::ReportableError`, [`core::fmt::Display`], and
|
|
/// [`core::error::Error`] for an explicitly numbered error enum.
|
|
///
|
|
/// Every variant must declare a stable nonzero
|
|
/// `#[kaptan_error(id = ...)]`. The derive rejects duplicates within an enum.
|
|
/// Across enums, duplicate IDs emit the same reserved symbol. Collision detection
|
|
/// requires both defining objects to participate in compilation or final linking.
|
|
/// Unextracted archive members are not checked, so catalog allocation still needs
|
|
/// review. The fixture guide in `tests/final_link.md` describes Rust release-LTO
|
|
/// and native Linux checks. Embedded linker combinations need their own checks.
|
|
#[proc_macro_derive(KaptanError, attributes(kaptan_error))]
|
|
pub fn derive_kaptan_error(input: TokenStream) -> TokenStream {
|
|
let input = parse_macro_input!(input as DeriveInput);
|
|
expand_error(input)
|
|
.unwrap_or_else(Error::into_compile_error)
|
|
.into()
|
|
}
|
|
|
|
/// Reports an [`Err`] only when it leaves the annotated function.
|
|
///
|
|
/// The function must have an explicit return type compatible with
|
|
/// [`core::result::Result`]; aliases are accepted. Const, unsafe, variadic,
|
|
/// and foreign-ABI functions are rejected because an immediately invoked
|
|
/// closure or async block cannot preserve those execution contracts without
|
|
/// additional unsafe or non-const machinery.
|
|
#[proc_macro_attribute]
|
|
pub fn propagate(arguments: TokenStream, input: TokenStream) -> TokenStream {
|
|
if !arguments.is_empty() {
|
|
return Error::new(
|
|
proc_macro2::Span::call_site(),
|
|
"kaptan_err_reportable::propagate does not accept arguments",
|
|
)
|
|
.into_compile_error()
|
|
.into();
|
|
}
|
|
|
|
let function = parse_macro_input!(input as ItemFn);
|
|
expand(function)
|
|
.unwrap_or_else(Error::into_compile_error)
|
|
.into()
|
|
}
|
|
|
|
fn expand(mut function: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
|
|
validate_signature(&function)?;
|
|
|
|
let body = function.block;
|
|
function.block = if function.sig.asyncness.is_some() {
|
|
Box::new(syn::parse_quote!({
|
|
const __KAPTAN_RAISED_AT: &str = concat!(file!(), ":", line!());
|
|
let __kaptan_err_reportable_result = (async #body).await;
|
|
::kaptan_err_reportable::__private::report_result(
|
|
&__kaptan_err_reportable_result,
|
|
__KAPTAN_RAISED_AT,
|
|
);
|
|
__kaptan_err_reportable_result
|
|
}))
|
|
} else {
|
|
Box::new(syn::parse_quote!({
|
|
const __KAPTAN_RAISED_AT: &str = concat!(file!(), ":", line!());
|
|
let __kaptan_err_reportable_result = (|| #body)();
|
|
::kaptan_err_reportable::__private::report_result(
|
|
&__kaptan_err_reportable_result,
|
|
__KAPTAN_RAISED_AT,
|
|
);
|
|
__kaptan_err_reportable_result
|
|
}))
|
|
};
|
|
|
|
Ok(quote!(#function))
|
|
}
|
|
|
|
fn validate_signature(function: &ItemFn) -> syn::Result<()> {
|
|
let signature = &function.sig;
|
|
if let Some(token) = signature.constness {
|
|
return Err(Error::new(
|
|
token.span(),
|
|
"kaptan_err_reportable::propagate does not support const functions",
|
|
));
|
|
}
|
|
if let Some(token) = signature.unsafety {
|
|
return Err(Error::new(
|
|
token.span(),
|
|
"kaptan_err_reportable::propagate does not support unsafe functions",
|
|
));
|
|
}
|
|
if let Some(abi) = &signature.abi {
|
|
return Err(Error::new(
|
|
abi.span(),
|
|
"kaptan_err_reportable::propagate does not support foreign-ABI functions",
|
|
));
|
|
}
|
|
if let Some(variadic) = &signature.variadic {
|
|
return Err(Error::new(
|
|
variadic.span(),
|
|
"kaptan_err_reportable::propagate does not support variadic functions",
|
|
));
|
|
}
|
|
|
|
if matches!(signature.output, ReturnType::Default) {
|
|
return Err(Error::new(
|
|
signature.output.span(),
|
|
"kaptan_err_reportable::propagate requires an explicit Result<T, E> return type",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn expand_error(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|
let DeriveInput {
|
|
ident,
|
|
generics,
|
|
data,
|
|
..
|
|
} = input;
|
|
let Data::Enum(data) = data else {
|
|
let kind = match data {
|
|
Data::Struct(_) => "structs",
|
|
Data::Union(_) => "unions",
|
|
Data::Enum(_) => unreachable!("enum handled above"),
|
|
};
|
|
return Err(Error::new_spanned(
|
|
ident,
|
|
format!("KaptanError can only be derived for enums, not {kind}"),
|
|
));
|
|
};
|
|
|
|
let mut ids = BTreeMap::new();
|
|
let variants = data
|
|
.variants
|
|
.iter()
|
|
.map(|variant| {
|
|
let id = variant_id(variant)?;
|
|
if let Some(previous) = ids.insert(id, &variant.ident) {
|
|
return Err(Error::new_spanned(
|
|
&variant.ident,
|
|
format!("Kaptan error ID {id} is already assigned to variant {previous}",),
|
|
));
|
|
}
|
|
Ok((variant, id))
|
|
})
|
|
.collect::<syn::Result<Vec<_>>>()?;
|
|
let identities = variants.iter().map(|(variant, id)| {
|
|
let variant_ident = &variant.ident;
|
|
let static_ident = format_ident!(
|
|
"__KAPTAN_ERROR_ID_{}_{}",
|
|
ident.to_string().to_uppercase(),
|
|
variant_ident.to_string().to_uppercase(),
|
|
);
|
|
let symbol = syn::LitStr::new(&format!("__kaptan_error_id_{id}"), variant_ident.span());
|
|
quote! {
|
|
#[doc(hidden)]
|
|
#[used]
|
|
#[unsafe(export_name = #symbol)]
|
|
static #static_ident: u8 = 0;
|
|
}
|
|
});
|
|
let arms = variants.iter().map(|(variant, id)| {
|
|
let variant_ident = &variant.ident;
|
|
let pattern = match &variant.fields {
|
|
Fields::Unit => quote!(Self::#variant_ident),
|
|
Fields::Unnamed(_) => quote!(Self::#variant_ident(..)),
|
|
Fields::Named(_) => quote!(Self::#variant_ident { .. }),
|
|
};
|
|
quote! {
|
|
#pattern => ::kaptan_err_reportable::ErrorId::new(#id)
|
|
}
|
|
});
|
|
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
|
|
let mut error_generics = generics.clone();
|
|
error_generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote!(#ident #type_generics: ::core::fmt::Debug));
|
|
let (error_impl_generics, error_type_generics, error_where_clause) =
|
|
error_generics.split_for_impl();
|
|
|
|
Ok(quote! {
|
|
#(#identities)*
|
|
|
|
impl #impl_generics ::kaptan_err_reportable::ReportableError for #ident #type_generics #where_clause {
|
|
fn error_id(&self) -> ::kaptan_err_reportable::ErrorId {
|
|
match self {
|
|
#(#arms),*
|
|
}
|
|
}
|
|
}
|
|
|
|
impl #impl_generics ::core::fmt::Display for #ident #type_generics #where_clause {
|
|
fn fmt(
|
|
&self,
|
|
formatter: &mut ::core::fmt::Formatter<'_>,
|
|
) -> ::core::fmt::Result {
|
|
::core::fmt::Display::fmt(
|
|
&<Self as ::kaptan_err_reportable::ReportableError>::error_id(self).get(),
|
|
formatter,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl #error_impl_generics ::core::error::Error
|
|
for #ident #error_type_generics #error_where_clause {}
|
|
})
|
|
}
|
|
|
|
fn variant_id(variant: &Variant) -> syn::Result<u16> {
|
|
let mut id = None;
|
|
for attribute in &variant.attrs {
|
|
if !attribute.path().is_ident("kaptan_error") {
|
|
continue;
|
|
}
|
|
attribute.parse_nested_meta(|meta| {
|
|
if !meta.path.is_ident("id") {
|
|
return Err(meta.error("unsupported Kaptan error property; expected `id`"));
|
|
}
|
|
if id.is_some() {
|
|
return Err(meta.error("duplicate Kaptan error `id`"));
|
|
}
|
|
let literal: LitInt = meta.value()?.parse()?;
|
|
let value = literal.base10_parse::<u16>()?;
|
|
if value == 0 {
|
|
return Err(Error::new(
|
|
literal.span(),
|
|
"Kaptan error ID zero is reserved",
|
|
));
|
|
}
|
|
id = Some(value);
|
|
Ok(())
|
|
})?;
|
|
}
|
|
id.ok_or_else(|| {
|
|
Error::new_spanned(
|
|
&variant.ident,
|
|
"missing `#[kaptan_error(id = ...)]` on Kaptan error variant",
|
|
)
|
|
})
|
|
}
|