miette_derive/
diagnostic.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::{punctuated::Punctuated, DeriveInput, Token};
4
5use crate::code::Code;
6use crate::diagnostic_arg::DiagnosticArg;
7use crate::diagnostic_source::DiagnosticSource;
8use crate::forward::{Forward, WhichFn};
9use crate::help::Help;
10use crate::label::Labels;
11use crate::related::Related;
12use crate::severity::Severity;
13use crate::source_code::SourceCode;
14use crate::url::Url;
15
16pub 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
30pub struct DiagnosticDef {
31    pub ident: syn::Ident,
32    pub fields: syn::Fields,
33    pub args: DiagnosticDefArgs,
34}
35
36pub enum DiagnosticDefArgs {
37    Transparent(Forward),
38    Concrete(Box<DiagnosticConcreteArgs>),
39}
40
41impl DiagnosticDefArgs {
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)]
61pub 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
73impl DiagnosticConcreteArgs {
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
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
154impl DiagnosticDefArgs {
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
220impl Diagnostic {
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
272    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}