diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 9a275622..57aa7a47 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -13,4 +13,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.78" quote = "1.0.35" -syn = "2.0.48" +syn = { version = "2.0.48", features = ["extra-traits"] } diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 0173d2a8..3206e833 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::TraitBoundStore; use crate::url::Url; pub enum Diagnostic { @@ -19,11 +20,13 @@ pub enum Diagnostic { ident: syn::Ident, fields: syn::Fields, args: DiagnosticDefArgs, + bound_store: TraitBoundStore, }, Enum { ident: syn::Ident, generics: syn::Generics, variants: Vec, + bound_store: TraitBoundStore, }, } @@ -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 TraitBoundStore, + ) -> 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 TraitBoundStore, 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 = TraitBoundStore::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 = TraitBoundStore::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.merge_with(where_clause); + match args { DiagnosticDefArgs::Transparent(forward) => { let code_method = forward.gen_struct_method(WhichFn::Code); @@ -369,8 +390,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.merge_with(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 1104eb74..8e66e29c 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::TraitBoundStore; 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 TraitBoundStore, + ) -> 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 TraitBoundStore, + ) -> syn::Result> { for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { if attr.path().is_ident("diagnostic_source") { @@ -33,6 +42,9 @@ impl DiagnosticSource { span: field.span(), }) }; + + bounds_store.register_source_usage(&field.ty); + return Ok(Some(DiagnosticSource(diagnostic_source))); } } diff --git a/miette-derive/src/forward.rs b/miette-derive/src/forward.rs index 171019af..729d2a44 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::TraitBoundStore; + pub enum Forward { Unnamed(usize), Named(syn::Ident), @@ -90,7 +92,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 TraitBoundStore, + ) -> syn::Result { let make_err = || { syn::Error::new( fields.span(), @@ -108,12 +113,18 @@ impl Forward { .ident .clone() .unwrap_or_else(|| format_ident!("unnamed")); + + bounds_store.register_transparent_usage(&field.ty); 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()); } + + bounds_store.register_transparent_usage(&field.ty); Ok(Self::Unnamed(0)) } _ => Err(syn::Error::new( diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs index ab2ceac1..74e601b4 100644 --- a/miette-derive/src/label.rs +++ b/miette-derive/src/label.rs @@ -11,6 +11,7 @@ use crate::{ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, fmt::{self, Display}, forward::WhichFn, + trait_bounds::TraitBoundStore, utils::{display_pat_members, gen_all_variants_with}, }; @@ -101,22 +102,31 @@ impl Parse for LabelAttr { } else { (LabelType::Default, None) }; + Ok(LabelAttr { label, lbl_ty }) } } impl Labels { - pub fn from_fields(fields: &syn::Fields) -> syn::Result> { + pub fn from_fields( + fields: &syn::Fields, + bounds_store: &mut TraitBoundStore, + ) -> 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 TraitBoundStore, + ) -> syn::Result> { let mut labels = Vec::new(); for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { @@ -144,6 +154,16 @@ impl Labels { )); } + match lbl_ty { + LabelType::Default | LabelType::Primary => { + bounds_store.register_label_usage(&field.ty); + } + + LabelType::Collection => { + bounds_store.register_label_collection_usage(&field.ty); + } + } + labels.push(Label { label, span, @@ -187,9 +207,10 @@ impl Labels { Some(quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) + .as_ref() .map(|#var| #ctor( #display, - #var.clone(), + (*#var).to_owned(), )) }) }); @@ -209,12 +230,13 @@ impl Labels { } else { quote! { std::option::Option::None } }; + Some(quote! { .chain({ let display = #display; - self.#span.iter().map(move |span| { + ::std::iter::IntoIterator::into_iter(&self.#span).map(move |span| { use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan}; - let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone()); + let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.to_owned()); if display.is_some() && labeled_span.label().is_none() { labeled_span.set_label(display.clone()) } diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index 0f7e64e5..81c61120 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -14,6 +14,7 @@ mod label; mod related; mod severity; mod source_code; +mod trait_bounds; mod url; mod utils; diff --git a/miette-derive/src/related.rs b/miette-derive/src/related.rs index 9b7f9e14..de189cf9 100644 --- a/miette-derive/src/related.rs +++ b/miette-derive/src/related.rs @@ -5,23 +5,32 @@ use syn::spanned::Spanned; use crate::{ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, forward::WhichFn, + trait_bounds::TraitBoundStore, utils::{display_pat_members, gen_all_variants_with}, }; pub struct Related(syn::Member); impl Related { - pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result> { + pub(crate) fn from_fields( + fields: &syn::Fields, + bounds_store: &mut TraitBoundStore, + ) -> 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 TraitBoundStore, + ) -> syn::Result> { for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { if attr.path().is_ident("related") { @@ -33,6 +42,7 @@ impl Related { span: field.span(), }) }; + bounds_store.register_related_usage(&field.ty); return Ok(Some(Related(related))); } } diff --git a/miette-derive/src/source_code.rs b/miette-derive/src/source_code.rs index e1b85ab4..493d3a5e 100644 --- a/miette-derive/src/source_code.rs +++ b/miette-derive/src/source_code.rs @@ -1,10 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::spanned::Spanned; +use syn::{spanned::Spanned, AngleBracketedGenericArguments, GenericArgument, PathArguments}; use crate::{ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, forward::WhichFn, + trait_bounds::TraitBoundStore, utils::{display_pat_members, gen_all_variants_with}, }; @@ -14,32 +15,35 @@ pub struct SourceCode { } impl SourceCode { - pub fn from_fields(fields: &syn::Fields) -> syn::Result> { + pub fn from_fields( + fields: &syn::Fields, + bounds_store: &mut TraitBoundStore, + ) -> 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 TraitBoundStore, + ) -> syn::Result> { for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { if attr.path().is_ident("source_code") { - let is_option = if let syn::Type::Path(syn::TypePath { - path: syn::Path { segments, .. }, - .. - }) = &field.ty - { - segments - .last() - .map(|seg| seg.ident == "Option") - .unwrap_or(false) + let is_option = TraitBoundStore::extract_option(&field.ty); + + if let Some(option_ty) = is_option { + bounds_store.register_source_code_usage(option_ty); } else { - false - }; + bounds_store.register_source_code_usage(&field.ty); + } let source_code = if let Some(ident) = field.ident.clone() { syn::Member::Named(ident) @@ -51,7 +55,7 @@ impl SourceCode { }; return Ok(Some(SourceCode { source_code, - is_option, + is_option: is_option.is_some(), })); } } diff --git a/miette-derive/src/trait_bounds.rs b/miette-derive/src/trait_bounds.rs new file mode 100644 index 00000000..a1912ca5 --- /dev/null +++ b/miette-derive/src/trait_bounds.rs @@ -0,0 +1,461 @@ +use std::{ + collections::{HashMap, VecDeque}, + iter::{once, FromIterator}, +}; + +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, token::Plus, AngleBracketedGenericArguments, AssocType, BoundLifetimes, + GenericArgument, GenericParam, Generics, ParenthesizedGenericArguments, Path, PathArguments, + PathSegment, PredicateType, ReturnType, Token, TraitBound, Type, TypeArray, TypeGroup, + TypeParamBound, TypeParen, TypePath, TypePtr, TypeReference, TypeSlice, TypeTuple, WhereClause, + WherePredicate, +}; + +#[derive(Default)] +pub struct RequiredTraitBound { + r#static: bool, + std_error: bool, + miette_diagnostic: bool, + source_code: bool, + into_source_span: bool, + std_into_iter: bool, + std_deref: bool, + std_to_owned: bool, +} + +impl RequiredTraitBound { + fn to_bounds(&self) -> Punctuated { + let mut bounds = Punctuated::new(); + if self.std_error && !self.miette_diagnostic { + bounds.push(TypeParamBound::Trait(syn::parse_quote!( + ::std::error::Error + ))); + } + + if self.miette_diagnostic { + bounds.push(TypeParamBound::Trait(syn::parse_quote!( + ::miette::Diagnostic + ))) + } + + if self.source_code { + bounds.push(TypeParamBound::Trait(syn::parse_quote!( + ::miette::SourceCode + ))) + } + + if self.into_source_span { + bounds.push(TypeParamBound::Trait(syn::parse_quote!( + ::std::convert::Into<::miette::SourceSpan> + ))) + } + + if self.std_into_iter { + bounds.push(TypeParamBound::Trait(syn::parse_quote!( + ::std::iter::IntoIterator + ))) + } + + if self.std_deref { + bounds.push(TypeParamBound::Trait(syn::parse_quote!(::std::ops::Deref))) + } + + if self.std_to_owned { + bounds.push(TypeParamBound::Trait(syn::parse_quote!( + ::std::borrow::ToOwned + ))) + } + + if self.r#static { + bounds.push(TypeParamBound::Lifetime(syn::parse_quote!('static))) + } + + bounds + } + + fn register_transparent_usage(&mut self) { + self.r#static = true; + self.miette_diagnostic = true; + } + + fn register_source_code_usage(&mut self) { + self.source_code = true; + } + + fn register_label_usage(&mut self) { + self.into_source_span = true; + } + + fn register_collection_usage(&mut self) { + self.std_into_iter = true; + } + fn register_related_item_usage(&mut self) { + self.miette_diagnostic = true; + self.r#static = true; + } + + fn register_source_usage(&mut self) { + self.miette_diagnostic = true; + self.r#static = true; + } + + fn register_deref_usage(&mut self) { + self.std_deref = true; + } + + fn register_to_owned_usage(&mut self) { + self.std_to_owned = true; + } +} + +pub struct TraitBoundStore(HashMap<(Option, Type), RequiredTraitBound>); + +impl TraitBoundStore { + pub fn new(generics: &Generics) -> Self { + let hash_map = generics + .params + .iter() + .filter_map(|param| { + if let GenericParam::Type(ty) = param { + Some(ty) + } else { + None + } + }) + .map(|param| { + Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments: Punctuated::from_iter(once(PathSegment { + ident: param.ident.clone(), + arguments: PathArguments::None, + })), + }, + }) + }) + .map(|v| ((None, v), RequiredTraitBound::default())) + .collect::>(); + + Self(hash_map) + } + + pub fn extract_option(r#type: &Type) -> Option<&Type> { + if let syn::Type::Path(syn::TypePath { + path: syn::Path { segments, .. }, + .. + }) = r#type + { + segments + .last() + .filter(|seg| seg.ident == "Option") + .and_then(|seg| match &seg.arguments { + PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args, .. + }) => { + let mut iter = args.iter(); + + let ty = iter.next(); + iter.next().xor(ty) + } + _ => None, + }) + .and_then(|arg| match arg { + GenericArgument::Type(ty) => Some(ty), + _ => None, + }) + } else { + None + } + } + + fn check_generic_usage<'ty>(&self, mut r#type: &'ty Type) -> Option<&'ty Type> { + // in theory we could skip all this logic and just allow trivial bounds but that would add redundant trait bounds + // to the derived impl - would be another choice to make. I choose to filter as much as possible so that we don't + // introduce unneccessary bounds. + + // this reduces the type down as much as possible to remove unneeded groups. + let original_type = loop { + match r#type { + Type::Paren(TypeParen { elem, .. }) => r#type = &**elem, + Type::Group(TypeGroup { elem, .. }) => r#type = &**elem, + x => break x, + } + }; + + let mut depends_on_generic = false; + + // max depth to check, after which we'll just add the (maybe redundant) bound anyways. + // this is a tradeoff between filtering speed and compiler speed so I'll keep it + // reasonably low for now, since I assume the compiler is better optimized for more complex + // checks. + let max_depth = 8; + + let mut to_check_queue: VecDeque<(&Type, usize)> = VecDeque::new(); + to_check_queue.push_back((original_type, 0)); + + while !depends_on_generic { + // this needs to be like this cuz if-let-chains aren't supported yet + let Some((elem, current_depth)) = to_check_queue.pop_front() else { + break; + }; + + // if we exceed the max depth we just assume it depends on the generic and let the compiler check it + if current_depth > max_depth { + depends_on_generic = true; + break; + } + + // the map contains types that we know depend on generics so we can just short circuit + // + // this is also the "bottom" check since we add the generics themselves to the map when + // constructing self + if self.0.contains_key(&(None, elem.clone())) { + depends_on_generic = true; + break; + } + + // basically go through the type and add all referenced types inside it to the check queue + match elem { + Type::Group(_) => unreachable!("This is unwrapped above"), + Type::Paren(_) => unreachable!("This is unwrapped above"), + // function pointer's can never implement the required trait bounds anyways so we just accept the errors + Type::BareFn(_) => return None, + // impl trait types aren't allowed from struct/enum definitions anyways so we can just ignore them + Type::ImplTrait(_) => return None, + // infered types aren't allowed either + Type::Infer(_) => return None, + // macros are opaque to us and i don't really know how to properly implement this. + // we could in theory I think introduce a type alias and use that instead but honestly + // type macros are such a niche usecase especially in combination with a generic, + // I would say we should just recommend to implement + // the trait manually, as such we just accept the error if any occurs (this still allows using macros when they + // return concrete types which don't depend on any generic or when the generic doesn't affect the + // required trait implementation) + Type::Macro(_) => return None, + // trait objects which depend on a generic inside them seem like very much a hassle to implement so i'll ignore + // them for now, if the need arises we could support that in a future pr maybe? + // + // this again doesn't restrict the usage of trait objects which implement the required traits regardless of the generics. + Type::TraitObject(_) => return None, + // Well never is never and never never. + Type::Never(_) => return None, + Type::Array(TypeArray { elem, .. }) + | Type::Ptr(TypePtr { elem, .. }) + | Type::Reference(TypeReference { elem, .. }) + | Type::Slice(TypeSlice { elem, .. }) => { + to_check_queue.push_back((&**elem, current_depth + 1)); + } + Type::Path(TypePath { qself, path }) => { + if let Some(qself) = qself { + to_check_queue.push_back((&qself.ty, current_depth + 1)); + } + + for segment in &path.segments { + match &segment.arguments { + PathArguments::None => {} + PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args, + .. + }) => { + for argument in args { + match argument { + GenericArgument::Type(ty) + | GenericArgument::AssocType(AssocType { ty, .. }) => { + to_check_queue.push_back((ty, current_depth + 1)); + } + _ => {} + } + } + } + PathArguments::Parenthesized(ParenthesizedGenericArguments { + inputs, + output, + .. + }) => { + for inp in inputs { + to_check_queue.push_back((inp, current_depth + 1)); + } + + if let ReturnType::Type(_, ty) = output { + to_check_queue.push_back((ty, current_depth + 1)); + } + } + } + } + } + Type::Tuple(TypeTuple { elems, .. }) => { + for elem in elems { + to_check_queue.push_back((elem, current_depth + 1)); + } + } + // we can't really handle verbatim so we just assume it depends on the generics + Type::Verbatim(_) => depends_on_generic = true, + _ => depends_on_generic = true, + } + } + + depends_on_generic.then_some(original_type) + } + + pub fn merge_with(&self, where_clause: Option<&WhereClause>) -> Option { + let mut where_clause = where_clause.cloned().unwrap_or_else(|| WhereClause { + where_token: Token![where](Span::mixed_site()), + predicates: Punctuated::new(), + }); + + where_clause + .predicates + .extend(self.0.iter().filter_map(|(ty, bounds)| { + let bounds = bounds.to_bounds(); + (!bounds.is_empty()).then(|| { + WherePredicate::Type(PredicateType { + lifetimes: ty.0.clone(), + bounded_ty: ty.1.clone(), + colon_token: Token![:](Span::mixed_site()), + bounds, + }) + }) + })); + + where_clause + .predicates + .push(WherePredicate::Type(PredicateType { + lifetimes: None, + bounded_ty: Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments: Punctuated::from_iter(once(PathSegment { + ident: syn::Ident::new("Self", Span::mixed_site()), + arguments: PathArguments::None, + })), + }, + }), + colon_token: syn::Token![:](Span::mixed_site()), + bounds: Punctuated::from_iter(once(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: syn::TraitBoundModifier::None, + lifetimes: None, + path: syn::parse_quote!(::std::error::Error), + }))), + })); + + Some(where_clause) + } + + pub fn register_transparent_usage(&mut self, r#type: &Type) { + let Some(r#type) = self.check_generic_usage(r#type) else { + return; + }; + + let type_opts = self.0.entry((None, r#type.clone())).or_default(); + type_opts.register_transparent_usage() + } + + pub fn register_source_code_usage(&mut self, r#type: &Type) { + let Some(r#type) = self.check_generic_usage(r#type) else { + return; + }; + + let type_opts = self.0.entry((None, r#type.clone())).or_default(); + type_opts.register_source_code_usage() + } + + pub fn register_label_usage(&mut self, r#type: &Type) { + let r#type = Self::extract_option(r#type).unwrap_or(r#type); + + let Some(ty) = self.check_generic_usage(r#type) else { + return; + }; + + let type_opts = self.0.entry((None, ty.clone())).or_default(); + + type_opts.register_to_owned_usage(); + + let type_opts_to_owned = self + .0 + .entry(( + None, + syn::parse_quote!(<#ty as ::std::borrow::ToOwned>::Owned), + )) + .or_default(); + type_opts_to_owned.register_label_usage(); + } + + pub fn register_label_collection_usage(&mut self, r#type: &Type) { + let Some(ty) = self.check_generic_usage(r#type) else { + return; + }; + + let ty: syn::Type = syn::parse_quote!(&'__miette_internal_lt #ty); + + let type_opts = self + .0 + .entry(( + Some(syn::parse_quote!(for<'__miette_internal_lt>)), + ty.clone(), + )) + .or_default(); + type_opts.register_collection_usage(); + + let type_opts_item = self + .0 + .entry(( + Some(syn::parse_quote!(for<'__miette_internal_lt>)), + syn::parse_quote!(<#ty as ::std::iter::IntoIterator>::Item), + )) + .or_default(); + type_opts_item.register_deref_usage(); + + let type_opts_deref_item = self + .0 + .entry(( + Some(syn::parse_quote!(for<'__miette_internal_lt>)), + syn::parse_quote!(<<#ty as ::std::iter::IntoIterator>::Item as ::std::ops::Deref>::Target), + )) + .or_default(); + type_opts_deref_item.register_to_owned_usage(); + + let type_opts_deref_to_owned_item = self + .0 + .entry(( + Some(syn::parse_quote!(for<'__miette_internal_lt>)), + syn::parse_quote!(<<<#ty as ::std::iter::IntoIterator>::Item as ::std::ops::Deref>::Target as ::std::borrow::ToOwned>::Owned), + )) + .or_default(); + type_opts_deref_to_owned_item.register_label_usage(); + } + + pub fn register_related_usage(&mut self, r#type: &Type) { + let Some(ty) = self.check_generic_usage(r#type) else { + return; + }; + + // this is somewhat hacky and only supports concrete types for the #[related] type + // ittself but supports generics for the arguments, i.e. Vec where T is generic. + // + // I think that this is a current limitation of the design of the Diagnostic trait, + // since we'd need bounds on the method and we can't do that (to refer to the lifetime) + // + // Someone smarter than me might be able to figure out a better solution (?) + let type_opts_item = self + .0 + .entry(( + None, + syn::parse_quote!(<#ty as ::std::iter::IntoIterator>::Item), + )) + .or_default(); + type_opts_item.register_related_item_usage(); + } + + pub fn register_source_usage(&mut self, r#type: &Type) { + let Some(ty) = self.check_generic_usage(r#type) else { + return; + }; + + let type_opts = self.0.entry((None, ty.clone())).or_default(); + type_opts.register_source_usage(); + } +} diff --git a/tests/derive.rs b/tests/derive.rs index aa631dc5..c141e448 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -217,6 +217,8 @@ fn fmt_help() { #[diagnostic(code(foo::x), help("{} x {len} x {:?}", 1, "2"))] Y { len: usize }, + // for some reason rust analyzer has a false positive with the self = self in the format! + // here but it compiles and tests just fine. (02/02/2025) #[diagnostic(code(foo::x), help("{} x {self:?} x {:?}", 1, "2"))] Z, } diff --git a/tests/test_derive_attr.rs b/tests/test_derive_attr.rs index f1b0f3d9..19487a0f 100644 --- a/tests/test_derive_attr.rs +++ b/tests/test_derive_attr.rs @@ -145,3 +145,133 @@ fn attr_not_required() { let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize); assert_eq!(err_span, expectation); } + +fn assert_impl_diagnostic() {} + +#[test] +fn transparent_generic() { + #[derive(Debug, Diagnostic, Error)] + enum Combined { + #[error(transparent)] + #[diagnostic(transparent)] + Other(T), + #[error("foo")] + Custom, + } + + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_label() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[label] + label: T, + } + + assert_impl_diagnostic::>(); + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_source_code() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[source_code] + label: T, + } + + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_optional_source_code() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[source_code] + label: Option, + } + + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_label_primary() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[label(primary)] + label: T, + } + + assert_impl_diagnostic::>(); + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_label_collection() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[label(collection)] + label: Vec, + } + + assert_impl_diagnostic::>(); + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_label_generic_collection() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[label(collection)] + label: T, + } + + assert_impl_diagnostic::>>(); + assert_impl_diagnostic::>>(); +} + +#[test] +fn generic_related() { + #[derive(Debug, Diagnostic, Error)] + #[error("foo")] + struct Combined { + #[related] + label: Vec, + } + + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_diagnostic_source() { + #[derive(Debug, Diagnostic, Error)] + enum Combined { + #[error(transparent)] + Other(#[diagnostic_source] T), + #[error("foo")] + Custom, + } + + assert_impl_diagnostic::>(); +} + +#[test] +fn generic_not_influencing_default() { + #[derive(Debug, Diagnostic, Error)] + enum Combined { + #[error("bar")] + Other(T), + #[error("foo")] + Custom, + } + + assert_impl_diagnostic::>(); +}