1 use proc_macro2::TokenStream;
2 use quote::quote;
3 use syn::{punctuated::Punctuated, DeriveInput, Token};
4 
5 use crate::code::Code;
6 use crate::diagnostic_arg::DiagnosticArg;
7 use crate::diagnostic_source::DiagnosticSource;
8 use crate::forward::{Forward, WhichFn};
9 use crate::help::Help;
10 use crate::label::Labels;
11 use crate::related::Related;
12 use crate::severity::Severity;
13 use crate::source_code::SourceCode;
14 use crate::url::Url;
15 
16 pub enum Diagnostic {
17     Struct {
18         generics: syn::Generics,
19         ident: syn::Ident,
20         fields: syn::Fields,
21         args: DiagnosticDefArgs,
22     },
23     Enum {
24         ident: syn::Ident,
25         generics: syn::Generics,
26         variants: Vec<DiagnosticDef>,
27     },
28 }
29 
30 pub struct DiagnosticDef {
31     pub ident: syn::Ident,
32     pub fields: syn::Fields,
33     pub args: DiagnosticDefArgs,
34 }
35 
36 pub enum DiagnosticDefArgs {
37     Transparent(Forward),
38     Concrete(Box<DiagnosticConcreteArgs>),
39 }
40 
41 impl DiagnosticDefArgs {
forward_or_override_enum( &self, variant: &syn::Ident, which_fn: WhichFn, mut f: impl FnMut(&DiagnosticConcreteArgs) -> Option<TokenStream>, ) -> Option<TokenStream>42     pub(crate) fn forward_or_override_enum(
43         &self,
44         variant: &syn::Ident,
45         which_fn: WhichFn,
46         mut f: impl FnMut(&DiagnosticConcreteArgs) -> Option<TokenStream>,
47     ) -> Option<TokenStream> {
48         match self {
49             Self::Transparent(forward) => Some(forward.gen_enum_match_arm(variant, which_fn)),
50             Self::Concrete(concrete) => f(concrete).or_else(|| {
51                 concrete
52                     .forward
53                     .as_ref()
54                     .map(|forward| forward.gen_enum_match_arm(variant, which_fn))
55             }),
56         }
57     }
58 }
59 
60 #[derive(Default)]
61 pub struct DiagnosticConcreteArgs {
62     pub code: Option<Code>,
63     pub severity: Option<Severity>,
64     pub help: Option<Help>,
65     pub labels: Option<Labels>,
66     pub source_code: Option<SourceCode>,
67     pub url: Option<Url>,
68     pub forward: Option<Forward>,
69     pub related: Option<Related>,
70     pub diagnostic_source: Option<DiagnosticSource>,
71 }
72 
73 impl DiagnosticConcreteArgs {
for_fields(fields: &syn::Fields) -> Result<Self, syn::Error>74     fn for_fields(fields: &syn::Fields) -> Result<Self, syn::Error> {
75         let labels = Labels::from_fields(fields)?;
76         let source_code = SourceCode::from_fields(fields)?;
77         let related = Related::from_fields(fields)?;
78         let help = Help::from_fields(fields)?;
79         let diagnostic_source = DiagnosticSource::from_fields(fields)?;
80         Ok(DiagnosticConcreteArgs {
81             code: None,
82             help,
83             related,
84             severity: None,
85             labels,
86             url: None,
87             forward: None,
88             source_code,
89             diagnostic_source,
90         })
91     }
92 
add_args( &mut self, attr: &syn::Attribute, args: impl Iterator<Item = DiagnosticArg>, errors: &mut Vec<syn::Error>, )93     fn add_args(
94         &mut self,
95         attr: &syn::Attribute,
96         args: impl Iterator<Item = DiagnosticArg>,
97         errors: &mut Vec<syn::Error>,
98     ) {
99         for arg in args {
100             match arg {
101                 DiagnosticArg::Transparent => {
102                     errors.push(syn::Error::new_spanned(attr, "transparent not allowed"));
103                 }
104                 DiagnosticArg::Forward(to_field) => {
105                     if self.forward.is_some() {
106                         errors.push(syn::Error::new_spanned(
107                             attr,
108                             "forward has already been specified",
109                         ));
110                     }
111                     self.forward = Some(to_field);
112                 }
113                 DiagnosticArg::Code(new_code) => {
114                     if self.code.is_some() {
115                         errors.push(syn::Error::new_spanned(
116                             attr,
117                             "code has already been specified",
118                         ));
119                     }
120                     self.code = Some(new_code);
121                 }
122                 DiagnosticArg::Severity(sev) => {
123                     if self.severity.is_some() {
124                         errors.push(syn::Error::new_spanned(
125                             attr,
126                             "severity has already been specified",
127                         ));
128                     }
129                     self.severity = Some(sev);
130                 }
131                 DiagnosticArg::Help(hl) => {
132                     if self.help.is_some() {
133                         errors.push(syn::Error::new_spanned(
134                             attr,
135                             "help has already been specified",
136                         ));
137                     }
138                     self.help = Some(hl);
139                 }
140                 DiagnosticArg::Url(u) => {
141                     if self.url.is_some() {
142                         errors.push(syn::Error::new_spanned(
143                             attr,
144                             "url has already been specified",
145                         ));
146                     }
147                     self.url = Some(u);
148                 }
149             }
150         }
151     }
152 }
153 
154 impl DiagnosticDefArgs {
parse( _ident: &syn::Ident, fields: &syn::Fields, attrs: &[&syn::Attribute], allow_transparent: bool, ) -> syn::Result<Self>155     fn parse(
156         _ident: &syn::Ident,
157         fields: &syn::Fields,
158         attrs: &[&syn::Attribute],
159         allow_transparent: bool,
160     ) -> syn::Result<Self> {
161         let mut errors = Vec::new();
162 
163         // Handle the only condition where Transparent is allowed
164         if allow_transparent && attrs.len() == 1 {
165             if let Ok(args) =
166                 attrs[0].parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)
167             {
168                 if matches!(args.first(), Some(DiagnosticArg::Transparent)) {
169                     let forward = Forward::for_transparent_field(fields)?;
170                     return Ok(Self::Transparent(forward));
171                 }
172             }
173         }
174 
175         // Create errors for any appearances of Transparent
176         let error_message = if allow_transparent {
177             "diagnostic(transparent) not allowed in combination with other args"
178         } else {
179             "diagnostic(transparent) not allowed here"
180         };
181         fn is_transparent(d: &DiagnosticArg) -> bool {
182             matches!(d, DiagnosticArg::Transparent)
183         }
184 
185         let mut concrete = DiagnosticConcreteArgs::for_fields(fields)?;
186         for attr in attrs {
187             let args =
188                 attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated);
189             let args = match args {
190                 Ok(args) => args,
191                 Err(error) => {
192                     errors.push(error);
193                     continue;
194                 }
195             };
196 
197             if args.iter().any(is_transparent) {
198                 errors.push(syn::Error::new_spanned(attr, error_message));
199             }
200 
201             let args = args
202                 .into_iter()
203                 .filter(|x| !matches!(x, DiagnosticArg::Transparent));
204 
205             concrete.add_args(attr, args, &mut errors);
206         }
207 
208         let combined_error = errors.into_iter().reduce(|mut lhs, rhs| {
209             lhs.combine(rhs);
210             lhs
211         });
212         if let Some(error) = combined_error {
213             Err(error)
214         } else {
215             Ok(DiagnosticDefArgs::Concrete(Box::new(concrete)))
216         }
217     }
218 }
219 
220 impl Diagnostic {
from_derive_input(input: DeriveInput) -> Result<Self, syn::Error>221     pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> {
222         let input_attrs = input
223             .attrs
224             .iter()
225             .filter(|x| x.path().is_ident("diagnostic"))
226             .collect::<Vec<&syn::Attribute>>();
227         Ok(match input.data {
228             syn::Data::Struct(data_struct) => {
229                 let args = DiagnosticDefArgs::parse(
230                     &input.ident,
231                     &data_struct.fields,
232                     &input_attrs,
233                     true,
234                 )?;
235 
236                 Diagnostic::Struct {
237                     fields: data_struct.fields,
238                     ident: input.ident,
239                     generics: input.generics,
240                     args,
241                 }
242             }
243             syn::Data::Enum(syn::DataEnum { variants, .. }) => {
244                 let mut vars = Vec::new();
245                 for var in variants {
246                     let mut variant_attrs = input_attrs.clone();
247                     variant_attrs
248                         .extend(var.attrs.iter().filter(|x| x.path().is_ident("diagnostic")));
249                     let args =
250                         DiagnosticDefArgs::parse(&var.ident, &var.fields, &variant_attrs, true)?;
251                     vars.push(DiagnosticDef {
252                         ident: var.ident,
253                         fields: var.fields,
254                         args,
255                     });
256                 }
257                 Diagnostic::Enum {
258                     ident: input.ident,
259                     generics: input.generics,
260                     variants: vars,
261                 }
262             }
263             syn::Data::Union(_) => {
264                 return Err(syn::Error::new(
265                     input.ident.span(),
266                     "Can't derive Diagnostic for Unions",
267                 ))
268             }
269         })
270     }
271 
gen(&self) -> TokenStream272     pub fn gen(&self) -> TokenStream {
273         match self {
274             Self::Struct {
275                 ident,
276                 fields,
277                 generics,
278                 args,
279             } => {
280                 let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
281                 match args {
282                     DiagnosticDefArgs::Transparent(forward) => {
283                         let code_method = forward.gen_struct_method(WhichFn::Code);
284                         let help_method = forward.gen_struct_method(WhichFn::Help);
285                         let url_method = forward.gen_struct_method(WhichFn::Url);
286                         let labels_method = forward.gen_struct_method(WhichFn::Labels);
287                         let source_code_method = forward.gen_struct_method(WhichFn::SourceCode);
288                         let severity_method = forward.gen_struct_method(WhichFn::Severity);
289                         let related_method = forward.gen_struct_method(WhichFn::Related);
290                         let diagnostic_source_method =
291                             forward.gen_struct_method(WhichFn::DiagnosticSource);
292 
293                         quote! {
294                             impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
295                                 #code_method
296                                 #help_method
297                                 #url_method
298                                 #labels_method
299                                 #severity_method
300                                 #source_code_method
301                                 #related_method
302                                 #diagnostic_source_method
303                             }
304                         }
305                     }
306                     DiagnosticDefArgs::Concrete(concrete) => {
307                         let forward = |which| {
308                             concrete
309                                 .forward
310                                 .as_ref()
311                                 .map(|fwd| fwd.gen_struct_method(which))
312                         };
313                         let code_body = concrete
314                             .code
315                             .as_ref()
316                             .and_then(|x| x.gen_struct())
317                             .or_else(|| forward(WhichFn::Code));
318                         let help_body = concrete
319                             .help
320                             .as_ref()
321                             .and_then(|x| x.gen_struct(fields))
322                             .or_else(|| forward(WhichFn::Help));
323                         let sev_body = concrete
324                             .severity
325                             .as_ref()
326                             .and_then(|x| x.gen_struct())
327                             .or_else(|| forward(WhichFn::Severity));
328                         let rel_body = concrete
329                             .related
330                             .as_ref()
331                             .and_then(|x| x.gen_struct())
332                             .or_else(|| forward(WhichFn::Related));
333                         let url_body = concrete
334                             .url
335                             .as_ref()
336                             .and_then(|x| x.gen_struct(ident, fields))
337                             .or_else(|| forward(WhichFn::Url));
338                         let labels_body = concrete
339                             .labels
340                             .as_ref()
341                             .and_then(|x| x.gen_struct(fields))
342                             .or_else(|| forward(WhichFn::Labels));
343                         let src_body = concrete
344                             .source_code
345                             .as_ref()
346                             .and_then(|x| x.gen_struct(fields))
347                             .or_else(|| forward(WhichFn::SourceCode));
348                         let diagnostic_source = concrete
349                             .diagnostic_source
350                             .as_ref()
351                             .and_then(|x| x.gen_struct())
352                             .or_else(|| forward(WhichFn::DiagnosticSource));
353                         quote! {
354                             impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
355                                 #code_body
356                                 #help_body
357                                 #sev_body
358                                 #rel_body
359                                 #url_body
360                                 #labels_body
361                                 #src_body
362                                 #diagnostic_source
363                             }
364                         }
365                     }
366                 }
367             }
368             Self::Enum {
369                 ident,
370                 generics,
371                 variants,
372             } => {
373                 let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
374                 let code_body = Code::gen_enum(variants);
375                 let help_body = Help::gen_enum(variants);
376                 let sev_body = Severity::gen_enum(variants);
377                 let labels_body = Labels::gen_enum(variants);
378                 let src_body = SourceCode::gen_enum(variants);
379                 let rel_body = Related::gen_enum(variants);
380                 let url_body = Url::gen_enum(ident, variants);
381                 let diagnostic_source_body = DiagnosticSource::gen_enum(variants);
382                 quote! {
383                     impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
384                         #code_body
385                         #help_body
386                         #sev_body
387                         #labels_body
388                         #src_body
389                         #rel_body
390                         #url_body
391                         #diagnostic_source_body
392                     }
393                 }
394             }
395         }
396     }
397 }
398