diff --git a/Cargo.toml b/Cargo.toml index f89f14b..1a963c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ license = "Apache-2.0" readme = "README.md" edition = "2018" rust-version = "1.82.0" +resolver = "2" exclude = ["images/", "tests/", "miette-derive/"] [dependencies] @@ -31,6 +32,8 @@ syntect = { version = "5.1.0", optional = true } [dev-dependencies] thiserror = "2.0.11" semver = "1.0.21" +# (kind of) hacky workaround to enable additional feature flags in tests, requires resolver = "2" +miette = { path = ".", features = ["perfect-derive"] } # Eyre devdeps futures = { version = "0.3", default-features = false } @@ -47,6 +50,7 @@ strip-ansi-escapes = "0.2.0" [features] default = ["derive"] derive = ["dep:miette-derive"] +perfect-derive = ["derive","miette-derive?/perfect-derive"] no-format-args-capture = [] fancy-base = [ "dep:owo-colors", diff --git a/README.md b/README.md index 8ecc16d..0fb3a71 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ diagnostic error code: ruget::api::bad_json - [... syntax highlighting](#-syntax-highlighting) - [... primary label](#-primary-label) - [... collection of labels](#-collection-of-labels) + - [... with generic errors](#-with-generic-errors) - [Acknowledgements](#acknowledgements) - [License](#license) @@ -782,6 +783,58 @@ let report: miette::Report = MyError { println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string())); ``` +#### ... with generic errors + +When tring to build more complex error types, it can often be useful to use generics. + +```rust +#[derive(Debug, Diagnostic, Error)] +enum MyError { + #[error(transparent)] + #[diagnostic(transparent)] + Base(T), + #[error("Some other error occured")] + #[diagnostic(help = "See the manual.")] + OtherError +} +``` +To enable this pattern, you can enable **the `perfect-derive` feature** on miette. +This will add trait bounds on generics in the `Diagnostic` implementation, depending on how +they are used inside the struct/enum. + +This should work for all other attributes as well, like `#[label]` or `#[diagnostic_source]`. + +
+ + +##### ⚠ Warning: (Small) Gotcha with the `#[related]` attribute + + + +Because of current lifetime constraints, only generic collection elements but not generic +collections are currently supported, meaning the following works: + +```rust +#[derive(Debug, Diagnostic, Error)] +#[error("Some example error")] +struct MyError { + #[related] + related_errors: Vec +} +``` +but the following does not: +```rust +#[derive(Debug, Diagnostic, Error)] +#[error("Some example error")] +struct MyError { + // See the difference here? + // Note that the collection is general, and not + // the elements inside the vec. This is **not** supported. + #[related] + related_errors: T // <- here +} +``` +
### MSRV diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index a726479..cddb729 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -10,6 +10,9 @@ repository = "https://github.com/zkat/miette" [lib] proc-macro = true +[features] +perfect-derive = ["syn/extra-traits"] + [dependencies] proc-macro2 = "1.0.83" quote = "1.0.35" diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs index 22dc795..4cf63cf 100644 --- a/miette-derive/src/code.rs +++ b/miette-derive/src/code.rs @@ -56,13 +56,25 @@ impl Code { let code = &code.as_ref()?.0; Some(match fields { syn::Fields::Named(_) => { - quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), } + quote! { + Self::#ident { .. } => { + std::option::Option::Some(std::boxed::Box::new(#code)) + }, + } } syn::Fields::Unnamed(_) => { - quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), } + quote! { + Self::#ident(..) => { + std::option::Option::Some(std::boxed::Box::new(#code)) + }, + } } syn::Fields::Unit => { - quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), } + quote! { + Self::#ident => { + std::option::Option::Some(std::boxed::Box::new(#code)) + }, + } } }) }, diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 0173d2a..df963f0 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -11,6 +11,7 @@ use crate::label::Labels; use crate::related::Related; use crate::severity::Severity; use crate::source_code::SourceCode; +use crate::trait_bounds::TypeParamBoundStore; use crate::url::Url; pub enum Diagnostic { @@ -19,11 +20,13 @@ pub enum Diagnostic { ident: syn::Ident, fields: syn::Fields, args: DiagnosticDefArgs, + bound_store: TypeParamBoundStore, }, Enum { ident: syn::Ident, generics: syn::Generics, variants: Vec, + bound_store: TypeParamBoundStore, }, } @@ -71,12 +74,15 @@ pub struct DiagnosticConcreteArgs { } impl DiagnosticConcreteArgs { - fn for_fields(fields: &syn::Fields) -> Result { - let labels = Labels::from_fields(fields)?; - let source_code = SourceCode::from_fields(fields)?; - let related = Related::from_fields(fields)?; + fn for_fields( + fields: &syn::Fields, + bounds_store: &mut TypeParamBoundStore, + ) -> Result { + let labels = Labels::from_fields(fields, bounds_store)?; + let source_code = SourceCode::from_fields(fields, bounds_store)?; + let related = Related::from_fields(fields, bounds_store)?; let help = Help::from_fields(fields)?; - let diagnostic_source = DiagnosticSource::from_fields(fields)?; + let diagnostic_source = DiagnosticSource::from_fields(fields, bounds_store)?; Ok(DiagnosticConcreteArgs { code: None, help, @@ -156,6 +162,7 @@ impl DiagnosticDefArgs { _ident: &syn::Ident, fields: &syn::Fields, attrs: &[&syn::Attribute], + bounds_store: &mut TypeParamBoundStore, allow_transparent: bool, ) -> syn::Result { let mut errors = Vec::new(); @@ -166,7 +173,7 @@ impl DiagnosticDefArgs { attrs[0].parse_args_with(Punctuated::::parse_terminated) { if matches!(args.first(), Some(DiagnosticArg::Transparent)) { - let forward = Forward::for_transparent_field(fields)?; + let forward = Forward::for_transparent_field(fields, bounds_store)?; return Ok(Self::Transparent(forward)); } } @@ -182,7 +189,7 @@ impl DiagnosticDefArgs { matches!(d, DiagnosticArg::Transparent) } - let mut concrete = DiagnosticConcreteArgs::for_fields(fields)?; + let mut concrete = DiagnosticConcreteArgs::for_fields(fields, bounds_store)?; for attr in attrs { let args = attr.parse_args_with(Punctuated::::parse_terminated); @@ -226,10 +233,13 @@ impl Diagnostic { .collect::>(); Ok(match input.data { syn::Data::Struct(data_struct) => { + let mut bounds_store = TypeParamBoundStore::new(&input.generics); + let args = DiagnosticDefArgs::parse( &input.ident, &data_struct.fields, &input_attrs, + &mut bounds_store, true, )?; @@ -238,16 +248,23 @@ impl Diagnostic { ident: input.ident, generics: input.generics, args, + bound_store: bounds_store, } } syn::Data::Enum(syn::DataEnum { variants, .. }) => { let mut vars = Vec::new(); + let mut bound_store = TypeParamBoundStore::new(&input.generics); for var in variants { let mut variant_attrs = input_attrs.clone(); variant_attrs .extend(var.attrs.iter().filter(|x| x.path().is_ident("diagnostic"))); - let args = - DiagnosticDefArgs::parse(&var.ident, &var.fields, &variant_attrs, true)?; + let args = DiagnosticDefArgs::parse( + &var.ident, + &var.fields, + &variant_attrs, + &mut bound_store, + true, + )?; vars.push(DiagnosticDef { ident: var.ident, fields: var.fields, @@ -258,6 +275,7 @@ impl Diagnostic { ident: input.ident, generics: input.generics, variants: vars, + bound_store, } } syn::Data::Union(_) => { @@ -276,8 +294,11 @@ impl Diagnostic { fields, generics, args, + bound_store, } => { - let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let where_clause = bound_store.add_to_where_clause(where_clause); + match args { DiagnosticDefArgs::Transparent(forward) => { let code_method = forward.gen_struct_method(WhichFn::Code); @@ -291,7 +312,9 @@ impl Diagnostic { forward.gen_struct_method(WhichFn::DiagnosticSource); quote! { - impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { + impl #impl_generics miette::Diagnostic + for #ident #ty_generics + #where_clause { #code_method #help_method #url_method @@ -351,7 +374,9 @@ impl Diagnostic { .and_then(|x| x.gen_struct()) .or_else(|| forward(WhichFn::DiagnosticSource)); quote! { - impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { + impl #impl_generics miette::Diagnostic + for #ident #ty_generics + #where_clause { #code_body #help_body #sev_body @@ -369,8 +394,11 @@ impl Diagnostic { ident, generics, variants, + bound_store, } => { - let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let where_clause = bound_store.add_to_where_clause(where_clause); + let code_body = Code::gen_enum(variants); let help_body = Help::gen_enum(variants); let sev_body = Severity::gen_enum(variants); diff --git a/miette-derive/src/diagnostic_source.rs b/miette-derive/src/diagnostic_source.rs index 1104eb7..4fc9c28 100644 --- a/miette-derive/src/diagnostic_source.rs +++ b/miette-derive/src/diagnostic_source.rs @@ -3,6 +3,7 @@ use quote::quote; use syn::spanned::Spanned; use crate::forward::WhichFn; +use crate::trait_bounds::TypeParamBoundStore; use crate::{ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, utils::{display_pat_members, gen_all_variants_with}, @@ -11,17 +12,25 @@ use crate::{ pub struct DiagnosticSource(syn::Member); impl DiagnosticSource { - pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result> { + pub(crate) fn from_fields( + fields: &syn::Fields, + bounds_store: &mut TypeParamBoundStore, + ) -> syn::Result> { match fields { - syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()), + syn::Fields::Named(named) => { + Self::from_fields_vec(named.named.iter().collect(), bounds_store) + } syn::Fields::Unnamed(unnamed) => { - Self::from_fields_vec(unnamed.unnamed.iter().collect()) + Self::from_fields_vec(unnamed.unnamed.iter().collect(), bounds_store) } syn::Fields::Unit => Ok(None), } } - fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result> { + fn from_fields_vec( + fields: Vec<&syn::Field>, + bounds_store: &mut TypeParamBoundStore, + ) -> syn::Result> { for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { if attr.path().is_ident("diagnostic_source") { @@ -33,6 +42,12 @@ impl DiagnosticSource { span: field.span(), }) }; + + let ty = &field.ty; + bounds_store.add_where_predicate( + syn::parse_quote!(#ty: ::miette::Diagnostic + 'static), + ); + return Ok(Some(DiagnosticSource(diagnostic_source))); } } diff --git a/miette-derive/src/forward.rs b/miette-derive/src/forward.rs index d170366..33d87c2 100644 --- a/miette-derive/src/forward.rs +++ b/miette-derive/src/forward.rs @@ -6,6 +6,8 @@ use syn::{ spanned::Spanned, }; +use crate::trait_bounds::TypeParamBoundStore; + pub enum Forward { Unnamed(usize), Named(syn::Ident), @@ -70,10 +72,14 @@ impl WhichFn { fn severity(&self) -> std::option::Option }, Self::Related => quote! { - fn related(&self) -> std::option::Option + '_>> + fn related(&self) -> std::option::Option< + std::boxed::Box + '_> + > }, Self::Labels => quote! { - fn labels(&self) -> std::option::Option + '_>> + fn labels(&self) -> std::option::Option< + std::boxed::Box + '_> + > }, Self::SourceCode => quote! { fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> @@ -90,7 +96,10 @@ impl WhichFn { } impl Forward { - pub fn for_transparent_field(fields: &syn::Fields) -> syn::Result { + pub fn for_transparent_field( + fields: &syn::Fields, + bounds_store: &mut TypeParamBoundStore, + ) -> syn::Result { let make_err = || { syn::Error::new( fields.span(), @@ -108,12 +117,22 @@ impl Forward { .ident .clone() .unwrap_or_else(|| format_ident!("unnamed")); + + let ty = &field.ty; + bounds_store + .add_where_predicate(syn::parse_quote! {#ty: ::miette::Diagnostic + 'static}); Ok(Self::Named(field_name)) } syn::Fields::Unnamed(unnamed) => { - if unnamed.unnamed.iter().len() != 1 { + let mut iter = unnamed.unnamed.iter(); + let field = iter.next().ok_or_else(make_err)?; + if iter.next().is_some() { return Err(make_err()); } + + let ty = &field.ty; + bounds_store + .add_where_predicate(syn::parse_quote! {#ty: ::miette::Diagnostic + 'static}); Ok(Self::Unnamed(0)) } _ => Err(syn::Error::new( diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs index 1c21054..398533c 100644 --- a/miette-derive/src/help.rs +++ b/miette-derive/src/help.rs @@ -94,7 +94,9 @@ impl Help { Help::Display(display) => { let (fmt, args) = display.expand_shorthand_cloned(&display_members); Some(quote! { - Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))), + Self::#ident #display_pat => { + std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) + }, }) } Help::Field(member, ty) => { @@ -123,7 +125,9 @@ impl Help { Help::Display(display) => { let (fmt, args) = display.expand_shorthand_cloned(&display_members); Some(quote! { - fn help(&self) -> std::option::Option> { + fn help(&self) -> std::option::Option< + std::boxed::Box + > { #[allow(unused_variables, deprecated)] let Self #display_pat = self; std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) @@ -133,7 +137,9 @@ impl Help { Help::Field(member, ty) => { let var = quote! { __miette_internal_var }; Some(quote! { - fn help(&self) -> std::option::Option> { + fn help(&self) -> std::option::Option< + std::boxed::Box + > { #[allow(unused_variables, deprecated)] let Self #display_pat = self; use miette::macro_helpers::ToOption; diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs index ab2ceac..3cc071b 100644 --- a/miette-derive/src/label.rs +++ b/miette-derive/src/label.rs @@ -4,14 +4,15 @@ use syn::{ parenthesized, parse::{Parse, ParseStream}, spanned::Spanned, - Token, + Lifetime, Token, }; use crate::{ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, fmt::{self, Display}, forward::WhichFn, - utils::{display_pat_members, gen_all_variants_with}, + trait_bounds::TypeParamBoundStore, + utils::{display_pat_members, extract_option, gen_all_variants_with}, }; pub struct Labels(Vec