refactor(errors): adopt reportable error terminology
This commit is contained in:
1 parent
21e9497915
commit
9fec2fa095
92 files changed
+270
-269
No files matched your search
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "kaptan-err-reportable-macros"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = "1"
|
||||
@@ -0,0 +1,242 @@
|
||||
//! 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 = ...)]`. Duplicate IDs fail compilation within an enum
|
||||
/// or crate and fail final linking when they originate in different crates.
|
||||
#[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",
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#[test]
|
||||
fn propagate_rejects_unsupported_signatures() {
|
||||
let tests = trybuild::TestCases::new();
|
||||
tests.pass("tests/ui/enum.rs");
|
||||
tests.compile_fail("tests/ui/arguments.rs");
|
||||
tests.compile_fail("tests/ui/const_fn.rs");
|
||||
tests.compile_fail("tests/ui/duplicate_id.rs");
|
||||
tests.compile_fail("tests/ui/duplicate_id_across_enums.rs");
|
||||
tests.compile_fail("tests/ui/missing_id.rs");
|
||||
tests.compile_fail("tests/ui/missing_return.rs");
|
||||
tests.compile_fail("tests/ui/non_result.rs");
|
||||
tests.compile_fail("tests/ui/no_error_report.rs");
|
||||
tests.compile_fail("tests/ui/struct.rs");
|
||||
tests.compile_fail("tests/ui/union.rs");
|
||||
tests.compile_fail("tests/ui/unsafe_fn.rs");
|
||||
tests.compile_fail("tests/ui/zero_id.rs");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use kaptan_err_reportable_macros::propagate;
|
||||
|
||||
#[propagate(report)]
|
||||
fn invalid() -> Result<(), ()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,7 @@
|
||||
error: kaptan_err_reportable::propagate does not accept arguments
|
||||
--> tests/ui/arguments.rs:3:1
|
||||
|
|
||||
3 | #[propagate(report)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,8 @@
|
||||
use kaptan_err_reportable_macros::propagate;
|
||||
|
||||
#[propagate]
|
||||
const fn invalid() -> Result<(), ()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: kaptan_err_reportable::propagate does not support const functions
|
||||
--> tests/ui/const_fn.rs:4:1
|
||||
|
|
||||
4 | const fn invalid() -> Result<(), ()> {
|
||||
| ^^^^^
|
||||
@@ -0,0 +1,13 @@
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(Debug, KaptanError)]
|
||||
enum DuplicateId {
|
||||
#[kaptan_error(id = 42)]
|
||||
First,
|
||||
#[kaptan_error(id = 42)]
|
||||
Second,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: Kaptan error ID 42 is already assigned to variant First
|
||||
--> tests/ui/duplicate_id.rs:10:5
|
||||
|
|
||||
10 | Second,
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1,37 @@
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ErrorId(u16);
|
||||
|
||||
impl ErrorId {
|
||||
pub const fn new(id: u16) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub const fn get(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReportableError {
|
||||
fn error_id(&self) -> ErrorId;
|
||||
}
|
||||
|
||||
#[derive(Debug, KaptanError)]
|
||||
enum FirstError {
|
||||
#[kaptan_error(id = 77)]
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, KaptanError)]
|
||||
enum SecondError {
|
||||
#[kaptan_error(id = 77)]
|
||||
Failed,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = FirstError::Failed.error_id();
|
||||
let _ = SecondError::Failed.error_id();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
error: symbol `__kaptan_error_id_77` is already defined
|
||||
--> tests/ui/duplicate_id_across_enums.rs:28:17
|
||||
|
|
||||
28 | #[derive(Debug, KaptanError)]
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the derive macro `KaptanError` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,43 @@
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ErrorId(u16);
|
||||
|
||||
impl ErrorId {
|
||||
pub const fn new(id: u16) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub const fn get(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReportableError {
|
||||
fn error_id(&self) -> ErrorId;
|
||||
}
|
||||
|
||||
#[derive(Debug, KaptanError)]
|
||||
enum Generic<'a, T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
#[kaptan_error(id = 65201)]
|
||||
Unit,
|
||||
#[kaptan_error(id = 65202)]
|
||||
Tuple(T, u8),
|
||||
#[kaptan_error(id = 65203)]
|
||||
Named { label: &'a str },
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn assert_error<T: core::error::Error>() {}
|
||||
assert_error::<Generic<'_, u8>>();
|
||||
assert_eq!(Generic::<u8>::Unit.error_id().get(), 65201);
|
||||
assert_eq!(Generic::Tuple(1_u8, 2).error_id().get(), 65202);
|
||||
let named: Generic<'_, u8> = Generic::Named { label: "test" };
|
||||
assert_eq!(named.error_id().get(), 65203);
|
||||
assert_eq!(named.to_string(), "65203");
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(Debug, KaptanError)]
|
||||
enum MissingId {
|
||||
Variant,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: missing `#[kaptan_error(id = ...)]` on Kaptan error variant
|
||||
--> tests/ui/missing_id.rs:7:5
|
||||
|
|
||||
7 | Variant,
|
||||
| ^^^^^^^
|
||||
@@ -0,0 +1,6 @@
|
||||
use kaptan_err_reportable_macros::propagate;
|
||||
|
||||
#[propagate]
|
||||
fn invalid() {}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,7 @@
|
||||
error: kaptan_err_reportable::propagate requires an explicit Result<T, E> return type
|
||||
--> tests/ui/missing_return.rs:3:1
|
||||
|
|
||||
3 | #[propagate]
|
||||
| ^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,18 @@
|
||||
use kaptan_err_reportable_macros::propagate;
|
||||
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
pub trait ReportableError {}
|
||||
|
||||
pub mod __private {
|
||||
pub fn report_result<T, E: crate::ReportableError>(_: &Result<T, E>, _: &'static str) {}
|
||||
}
|
||||
|
||||
struct PlainError;
|
||||
|
||||
#[propagate]
|
||||
fn invalid() -> Result<(), PlainError> {
|
||||
Err(PlainError)
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,17 @@
|
||||
error[E0277]: the trait bound `PlainError: ReportableError` is not satisfied
|
||||
--> tests/ui/no_error_report.rs:13:1
|
||||
|
|
||||
13 | #[propagate]
|
||||
| ^^^^^^^^^^^^ the trait `ReportableError` is not implemented for `PlainError`
|
||||
|
|
||||
help: this trait has no implementations, consider adding one
|
||||
--> tests/ui/no_error_report.rs:5:1
|
||||
|
|
||||
5 | pub trait ReportableError {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
note: required by a bound in `report_result`
|
||||
--> tests/ui/no_error_report.rs:8:32
|
||||
|
|
||||
8 | pub fn report_result<T, E: crate::ReportableError>(_: &Result<T, E>, _: &'static str) {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `report_result`
|
||||
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,14 @@
|
||||
use kaptan_err_reportable_macros::propagate;
|
||||
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
pub mod __private {
|
||||
pub fn report_result<T, E>(_: &Result<T, E>, _: &'static str) {}
|
||||
}
|
||||
|
||||
#[propagate]
|
||||
fn invalid() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,17 @@
|
||||
error[E0308]: mismatched types
|
||||
--> tests/ui/non_result.rs:9:1
|
||||
|
|
||||
9 | #[propagate]
|
||||
| ^^^^^^^^^^^^
|
||||
| |
|
||||
| expected `&Result<_, _>`, found `&bool`
|
||||
| arguments to this function are incorrect
|
||||
|
|
||||
= note: expected reference `&Result<_, _>`
|
||||
found reference `&bool`
|
||||
note: function defined here
|
||||
--> tests/ui/non_result.rs:6:12
|
||||
|
|
||||
6 | pub fn report_result<T, E>(_: &Result<T, E>, _: &'static str) {}
|
||||
| ^^^^^^^^^^^^^ ----------------
|
||||
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,8 @@
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(KaptanError)]
|
||||
struct NotAnError {
|
||||
code: u8,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: KaptanError can only be derived for enums, not structs
|
||||
--> tests/ui/struct.rs:4:8
|
||||
|
|
||||
4 | struct NotAnError {
|
||||
| ^^^^^^^^^^
|
||||
@@ -0,0 +1,8 @@
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(KaptanError)]
|
||||
union NotAnError {
|
||||
code: u8,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: KaptanError can only be derived for enums, not unions
|
||||
--> tests/ui/union.rs:4:7
|
||||
|
|
||||
4 | union NotAnError {
|
||||
| ^^^^^^^^^^
|
||||
@@ -0,0 +1,8 @@
|
||||
use kaptan_err_reportable_macros::propagate;
|
||||
|
||||
#[propagate]
|
||||
unsafe fn invalid() -> Result<(), ()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: kaptan_err_reportable::propagate does not support unsafe functions
|
||||
--> tests/ui/unsafe_fn.rs:4:1
|
||||
|
|
||||
4 | unsafe fn invalid() -> Result<(), ()> {
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1,11 @@
|
||||
extern crate self as kaptan_err_reportable;
|
||||
|
||||
use kaptan_err_reportable_macros::KaptanError;
|
||||
|
||||
#[derive(Debug, KaptanError)]
|
||||
enum ZeroId {
|
||||
#[kaptan_error(id = 0)]
|
||||
Variant,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: Kaptan error ID zero is reserved
|
||||
--> tests/ui/zero_id.rs:7:25
|
||||
|
|
||||
7 | #[kaptan_error(id = 0)]
|
||||
| ^
|
||||
Reference in new issue
Block a user