use proc_macro2::TokenStream; use quote::quote; use syn::{punctuated::Punctuated, DeriveInput, Token}; use crate::code::Code; use crate::diagnostic_arg::DiagnosticArg; use crate::diagnostic_source::DiagnosticSource; use crate::forward::{Forward, WhichFn}; use crate::help::Help; use crate::label::Labels; use crate::related::Related; use crate::severity::Severity; use crate::source_code::SourceCode; use crate::url::Url; pub enum Diagnostic { Struct { generics: syn::Generics, ident: syn::Ident, fields: syn::Fields, args: DiagnosticDefArgs, }, Enum { ident: syn::Ident, generics: syn::Generics, variants: Vec, }, } pub struct DiagnosticDef { pub ident: syn::Ident, pub fields: syn::Fields, pub args: DiagnosticDefArgs, } pub enum DiagnosticDefArgs { Transparent(Forward), Concrete(Box), } impl DiagnosticDefArgs { pub(crate) fn forward_or_override_enum( &self, variant: &syn::Ident, which_fn: WhichFn, mut f: impl FnMut(&DiagnosticConcreteArgs) -> Option, ) -> Option { match self { Self::Transparent(forward) => Some(forward.gen_enum_match_arm(variant, which_fn)), Self::Concrete(concrete) => f(concrete).or_else(|| { concrete .forward .as_ref() .map(|forward| forward.gen_enum_match_arm(variant, which_fn)) }), } } } #[derive(Default)] pub struct DiagnosticConcreteArgs { pub code: Option, pub severity: Option, pub help: Option, pub labels: Option, pub source_code: Option, pub url: Option, pub forward: Option, pub related: Option, pub diagnostic_source: Option, } 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)?; let help = Help::from_fields(fields)?; let diagnostic_source = DiagnosticSource::from_fields(fields)?; Ok(DiagnosticConcreteArgs { code: None, help, related, severity: None, labels, url: None, forward: None, source_code, diagnostic_source, }) } fn add_args( &mut self, attr: &syn::Attribute, args: impl Iterator, errors: &mut Vec, ) { for arg in args { match arg { DiagnosticArg::Transparent => { errors.push(syn::Error::new_spanned(attr, "transparent not allowed")); } DiagnosticArg::Forward(to_field) => { if self.forward.is_some() { errors.push(syn::Error::new_spanned( attr, "forward has already been specified", )); } self.forward = Some(to_field); } DiagnosticArg::Code(new_code) => { if self.code.is_some() { errors.push(syn::Error::new_spanned( attr, "code has already been specified", )); } self.code = Some(new_code); } DiagnosticArg::Severity(sev) => { if self.severity.is_some() { errors.push(syn::Error::new_spanned( attr, "severity has already been specified", )); } self.severity = Some(sev); } DiagnosticArg::Help(hl) => { if self.help.is_some() { errors.push(syn::Error::new_spanned( attr, "help has already been specified", )); } self.help = Some(hl); } DiagnosticArg::Url(u) => { if self.url.is_some() { errors.push(syn::Error::new_spanned( attr, "url has already been specified", )); } self.url = Some(u); } } } } } impl DiagnosticDefArgs { fn parse( _ident: &syn::Ident, fields: &syn::Fields, attrs: &[&syn::Attribute], allow_transparent: bool, ) -> syn::Result { let mut errors = Vec::new(); // Handle the only condition where Transparent is allowed if allow_transparent && attrs.len() == 1 { if let Ok(args) = attrs[0].parse_args_with(Punctuated::::parse_terminated) { if matches!(args.first(), Some(DiagnosticArg::Transparent)) { let forward = Forward::for_transparent_field(fields)?; return Ok(Self::Transparent(forward)); } } } // Create errors for any appearances of Transparent let error_message = if allow_transparent { "diagnostic(transparent) not allowed in combination with other args" } else { "diagnostic(transparent) not allowed here" }; fn is_transparent(d: &DiagnosticArg) -> bool { matches!(d, DiagnosticArg::Transparent) } let mut concrete = DiagnosticConcreteArgs::for_fields(fields)?; for attr in attrs { let args = attr.parse_args_with(Punctuated::::parse_terminated); let args = match args { Ok(args) => args, Err(error) => { errors.push(error); continue; } }; if args.iter().any(is_transparent) { errors.push(syn::Error::new_spanned(attr, error_message)); } let args = args .into_iter() .filter(|x| !matches!(x, DiagnosticArg::Transparent)); concrete.add_args(attr, args, &mut errors); } let combined_error = errors.into_iter().reduce(|mut lhs, rhs| { lhs.combine(rhs); lhs }); if let Some(error) = combined_error { Err(error) } else { Ok(DiagnosticDefArgs::Concrete(Box::new(concrete))) } } } impl Diagnostic { pub fn from_derive_input(input: DeriveInput) -> Result { let input_attrs = input .attrs .iter() .filter(|x| x.path().is_ident("diagnostic")) .collect::>(); Ok(match input.data { syn::Data::Struct(data_struct) => { let args = DiagnosticDefArgs::parse( &input.ident, &data_struct.fields, &input_attrs, true, )?; Diagnostic::Struct { fields: data_struct.fields, ident: input.ident, generics: input.generics, args, } } syn::Data::Enum(syn::DataEnum { variants, .. }) => { let mut vars = Vec::new(); 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)?; vars.push(DiagnosticDef { ident: var.ident, fields: var.fields, args, }); } Diagnostic::Enum { ident: input.ident, generics: input.generics, variants: vars, } } syn::Data::Union(_) => { return Err(syn::Error::new( input.ident.span(), "Can't derive Diagnostic for Unions", )) } }) } pub fn gen(&self) -> TokenStream { match self { Self::Struct { ident, fields, generics, args, } => { let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); match args { DiagnosticDefArgs::Transparent(forward) => { let code_method = forward.gen_struct_method(WhichFn::Code); let help_method = forward.gen_struct_method(WhichFn::Help); let url_method = forward.gen_struct_method(WhichFn::Url); let labels_method = forward.gen_struct_method(WhichFn::Labels); let source_code_method = forward.gen_struct_method(WhichFn::SourceCode); let severity_method = forward.gen_struct_method(WhichFn::Severity); let related_method = forward.gen_struct_method(WhichFn::Related); let diagnostic_source_method = forward.gen_struct_method(WhichFn::DiagnosticSource); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { #code_method #help_method #url_method #labels_method #severity_method #source_code_method #related_method #diagnostic_source_method } } } DiagnosticDefArgs::Concrete(concrete) => { let forward = |which| { concrete .forward .as_ref() .map(|fwd| fwd.gen_struct_method(which)) }; let code_body = concrete .code .as_ref() .and_then(|x| x.gen_struct()) .or_else(|| forward(WhichFn::Code)); let help_body = concrete .help .as_ref() .and_then(|x| x.gen_struct(fields)) .or_else(|| forward(WhichFn::Help)); let sev_body = concrete .severity .as_ref() .and_then(|x| x.gen_struct()) .or_else(|| forward(WhichFn::Severity)); let rel_body = concrete .related .as_ref() .and_then(|x| x.gen_struct()) .or_else(|| forward(WhichFn::Related)); let url_body = concrete .url .as_ref() .and_then(|x| x.gen_struct(ident, fields)) .or_else(|| forward(WhichFn::Url)); let labels_body = concrete .labels .as_ref() .and_then(|x| x.gen_struct(fields)) .or_else(|| forward(WhichFn::Labels)); let src_body = concrete .source_code .as_ref() .and_then(|x| x.gen_struct(fields)) .or_else(|| forward(WhichFn::SourceCode)); let diagnostic_source = concrete .diagnostic_source .as_ref() .and_then(|x| x.gen_struct()) .or_else(|| forward(WhichFn::DiagnosticSource)); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { #code_body #help_body #sev_body #rel_body #url_body #labels_body #src_body #diagnostic_source } } } } } Self::Enum { ident, generics, variants, } => { let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl(); let code_body = Code::gen_enum(variants); let help_body = Help::gen_enum(variants); let sev_body = Severity::gen_enum(variants); let labels_body = Labels::gen_enum(variants); let src_body = SourceCode::gen_enum(variants); let rel_body = Related::gen_enum(variants); let url_body = Url::gen_enum(ident, variants); let diagnostic_source_body = DiagnosticSource::gen_enum(variants); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { #code_body #help_body #sev_body #labels_body #src_body #rel_body #url_body #diagnostic_source_body } } } } } }