Skip to main content

tenda_reporting_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use std::collections::BTreeMap;
4use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, LitStr, Type};
5
6/// Derive macro for creating a `Diagnostic` implementation that supports:
7///
8/// - `#[span]` for the primary span
9/// - `#[label]` for extra spans (Option, SourceSpan, Vec<SourceSpan>)
10/// - `#[help]`/`#[note]` for textual messages (Option<String>, Vec<String>)
11/// - `#[message]` for overriding the default error message
12/// - `#[metadata]` for fields you want get/set methods generated for
13/// - `#[report("error_kind")]` to customize the error kind
14/// - `#[accept_hooks]` so you provide your own `HasDiagnosticHooks` impl;
15///   otherwise a default empty `HasDiagnosticHooks` is given.
16#[proc_macro_derive(
17    Diagnostic,
18    attributes(report, span, label, help, note, message, metadata, accept_hooks)
19)]
20pub fn diagnostic_derive(input: TokenStream) -> TokenStream {
21    let input = parse_macro_input!(input as DeriveInput);
22
23    let accept_hooks = input
24        .attrs
25        .iter()
26        .any(|attr| attr.path().is_ident("accept_hooks"));
27
28    let error_kind = input
29        .attrs
30        .iter()
31        .find(|attr| attr.path().is_ident("report"))
32        .and_then(|attr| attr.parse_args::<LitStr>().ok())
33        .map(|lit| lit.value())
34        .unwrap_or_else(|| "erro".into());
35
36    let enum_ident = &input.ident;
37
38    let mut get_span_arms = Vec::new();
39    let mut set_span_arms = Vec::new();
40    let mut get_label_arms = Vec::new();
41    let mut get_help_arms = Vec::new();
42    let mut get_note_arms = Vec::new();
43    let mut get_message_arms = Vec::new();
44
45    #[derive(Default)]
46    struct MetaFieldInfo {
47        field_type: Option<Type>,
48        get_arms: Vec<proc_macro2::TokenStream>,
49        get_mut_arms: Vec<proc_macro2::TokenStream>,
50        set_arms: Vec<proc_macro2::TokenStream>,
51    }
52
53    let mut metadata_map: BTreeMap<Ident, MetaFieldInfo> = BTreeMap::new();
54
55    let enum_data = match &input.data {
56        Data::Enum(data_enum) => data_enum,
57        _ => {
58            return syn::Error::new_spanned(
59                &input.ident,
60                "`Report` can only be derived for enums.",
61            )
62            .to_compile_error()
63            .into();
64        }
65    };
66
67    for variant in &enum_data.variants {
68        let variant_ident = &variant.ident;
69
70        let mut get_span_arm = quote! { Self::#variant_ident { .. } => None };
71        let mut set_span_arm = quote! { Self::#variant_ident { .. } => {} };
72        let mut get_label_arm = quote! { Self::#variant_ident { .. } => Vec::new() };
73        let mut get_help_arm = quote! { Self::#variant_ident { .. } => Vec::new() };
74        let mut get_note_arm = quote! { Self::#variant_ident { .. } => Vec::new() };
75        let mut get_message_arm = quote! { Self::#variant_ident { .. } => None };
76
77        if let Fields::Named(named_fields) = &variant.fields {
78            let mut span_fields_found = 0;
79            let mut message_fields_found = 0;
80
81            let mut span_fields = Vec::new();
82            let mut label_fields = Vec::new();
83            let mut help_fields = Vec::new();
84            let mut note_fields = Vec::new();
85            let mut message_fields = Vec::new();
86
87            for field in &named_fields.named {
88                let field_ident = field.ident.as_ref().unwrap();
89                let field_ty = &field.ty;
90
91                let mut is_span = false;
92                let mut is_label = false;
93                let mut is_help = false;
94                let mut is_note = false;
95                let mut is_message = false;
96                let mut is_metadata = false;
97
98                for attr in &field.attrs {
99                    if attr.path().is_ident("span") {
100                        is_span = true;
101                    } else if attr.path().is_ident("label") {
102                        is_label = true;
103                    } else if attr.path().is_ident("help") {
104                        is_help = true;
105                    } else if attr.path().is_ident("note") {
106                        is_note = true;
107                    } else if attr.path().is_ident("message") {
108                        is_message = true;
109                    } else if attr.path().is_ident("metadata") {
110                        is_metadata = true;
111                    }
112                }
113
114                if is_span {
115                    span_fields_found += 1;
116                    if span_fields_found > 1 {
117                        return syn::Error::new_spanned(
118                            field,
119                            format!(
120                                "Multiple `#[span]` attributes in variant `{}` are not allowed.",
121                                variant_ident
122                            ),
123                        )
124                        .to_compile_error()
125                        .into();
126                    }
127                    if let Type::Path(type_path) = &field.ty {
128                        if let Some(last_seg) = type_path.path.segments.last() {
129                            match last_seg.ident.to_string().as_str() {
130                                "Option" => {
131                                    span_fields.push((
132                                        field_ident.clone(),
133                                        quote! {
134                                            Self::#variant_ident { #field_ident, .. } => #field_ident.clone()
135                                        },
136                                        quote! {
137                                            Self::#variant_ident { #field_ident, .. } => {
138                                                *#field_ident = Some(new_span.clone());
139                                            }
140                                        }
141                                    ));
142                                }
143                                "SourceSpan" => {
144                                    span_fields.push((
145                                        field_ident.clone(),
146                                        quote! {
147                                            Self::#variant_ident { #field_ident, .. } => Some(#field_ident.clone())
148                                        },
149                                        quote! {
150                                            Self::#variant_ident { #field_ident, .. } => {
151                                                *#field_ident = new_span.clone();
152                                            }
153                                        }
154                                    ));
155                                }
156                                _ => {
157                                    return syn::Error::new_spanned(
158                                        &field.ty,
159                                        "Expected `#[span]` field to be `SourceSpan` or `Option<SourceSpan>`",
160                                    ).to_compile_error().into();
161                                }
162                            }
163                        }
164                    }
165                }
166
167                if is_label {
168                    if let Type::Path(type_path) = &field.ty {
169                        if let Some(last_seg) = type_path.path.segments.last() {
170                            match last_seg.ident.to_string().as_str() {
171                                "Option" => label_fields
172                                    .push((field_ident.clone(), "OptionSpan".to_string())),
173                                "Vec" => {
174                                    label_fields.push((field_ident.clone(), "VecSpan".to_string()))
175                                }
176                                "SourceSpan" => label_fields
177                                    .push((field_ident.clone(), "DirectSpan".to_string())),
178                                _ => {
179                                    return syn::Error::new_spanned(
180                                        &field.ty,
181                                        "Expected `#[label]` field to be `SourceSpan`, `Option<SourceSpan>`, or `Vec<SourceSpan>`",
182                                    ).to_compile_error().into();
183                                }
184                            }
185                        }
186                    }
187                }
188
189                let check_help_note = |ty: &Type| {
190                    if let Type::Path(tp) = ty {
191                        if let Some(seg) = tp.path.segments.last() {
192                            match seg.ident.to_string().as_str() {
193                                "Option" => Ok("OptionString".to_string()),
194                                "Vec" => Ok("VecString".to_string()),
195                                _ => Err(syn::Error::new_spanned(
196                                    tp,
197                                    "Expected field to be `Option<String>` or `Vec<String>`",
198                                )),
199                            }
200                        } else {
201                            Err(syn::Error::new_spanned(
202                                tp,
203                                "Expected field to be `Option<String>` or `Vec<String>`",
204                            ))
205                        }
206                    } else {
207                        Err(syn::Error::new_spanned(
208                            ty,
209                            "Expected field to be `Option<String>` or `Vec<String>`",
210                        ))
211                    }
212                };
213
214                if is_help {
215                    match check_help_note(field_ty) {
216                        Ok(kind) => help_fields.push((field_ident.clone(), kind)),
217                        Err(e) => return e.to_compile_error().into(),
218                    }
219                }
220
221                if is_note {
222                    match check_help_note(field_ty) {
223                        Ok(kind) => note_fields.push((field_ident.clone(), kind)),
224                        Err(e) => return e.to_compile_error().into(),
225                    }
226                }
227
228                if is_message {
229                    message_fields_found += 1;
230                    if message_fields_found > 1 {
231                        return syn::Error::new_spanned(
232                            field,
233                            format!(
234                                "Multiple `#[message]` attributes in variant `{}` are not allowed.",
235                                variant_ident
236                            ),
237                        )
238                        .to_compile_error()
239                        .into();
240                    }
241                    if let Type::Path(type_path) = &field.ty {
242                        if let Some(last_seg) = type_path.path.segments.last() {
243                            match last_seg.ident.to_string().as_str() {
244                                "String" => {
245                                    message_fields
246                                        .push((field_ident.clone(), "DirectString".to_string()));
247                                }
248                                "Option" => {
249                                    message_fields
250                                        .push((field_ident.clone(), "OptionString".to_string()));
251                                }
252                                _ => {
253                                    return syn::Error::new_spanned(
254                                        &field.ty,
255                                        "Expected `#[message]` field to be `String` or `Option<String>`"
256                                    ).to_compile_error().into();
257                                }
258                            }
259                        }
260                    }
261                }
262
263                if is_metadata {
264                    let entry = metadata_map.entry(field_ident.clone()).or_default();
265                    if entry.field_type.is_none() {
266                        entry.field_type = Some(field_ty.clone());
267                    }
268                    let get_arm = quote! {
269                        Self::#variant_ident { #field_ident, .. } => Some(#field_ident),
270                    };
271                    let get_mut_arm = quote! {
272                        Self::#variant_ident { ref mut #field_ident, .. } => Some(#field_ident),
273                    };
274                    let set_arm = quote! {
275                        Self::#variant_ident { #field_ident, .. } => { *#field_ident = value; },
276                    };
277                    entry.get_arms.push(get_arm);
278                    entry.get_mut_arms.push(get_mut_arm);
279                    entry.set_arms.push(set_arm);
280                }
281            }
282
283            if let Some((_, get_code, set_code)) = span_fields.last() {
284                get_span_arm = get_code.clone();
285                set_span_arm = set_code.clone();
286            }
287
288            if !label_fields.is_empty() {
289                let all_idents: Vec<_> = named_fields
290                    .named
291                    .iter()
292                    .map(|f| f.ident.as_ref().unwrap())
293                    .collect();
294
295                let gather_label_branches: Vec<_> = label_fields
296                    .iter()
297                    .map(|(ident, kind)| match kind.as_str() {
298                        "OptionSpan" => quote! {
299                            if let Some(s) = #ident.clone() {
300                                labels.push(s);
301                            }
302                        },
303                        "VecSpan" => quote! {
304                            labels.extend(#ident.clone());
305                        },
306                        "DirectSpan" => quote! {
307                            labels.push(#ident.clone());
308                        },
309                        _ => unreachable!(),
310                    })
311                    .collect();
312
313                let label_arm = quote! {
314                    Self::#variant_ident { #(ref #all_idents),* } => {
315                        let mut labels = Vec::new();
316                        #( #gather_label_branches )*
317                        labels
318                    }
319                };
320                get_label_arm = label_arm;
321            }
322
323            if !help_fields.is_empty() {
324                let all_idents: Vec<_> = named_fields
325                    .named
326                    .iter()
327                    .map(|f| f.ident.as_ref().unwrap())
328                    .collect();
329
330                let pushes: Vec<_> = help_fields
331                    .iter()
332                    .map(|(ident, kind)| {
333                        if kind == "OptionString" {
334                            quote! {
335                                if let Some(val) = #ident.clone() {
336                                    helps.push(val);
337                                }
338                            }
339                        } else {
340                            quote! {
341                                helps.extend(#ident.clone());
342                            }
343                        }
344                    })
345                    .collect();
346
347                let help_arm = quote! {
348                    Self::#variant_ident { #(ref #all_idents),* } => {
349                        let mut helps = Vec::new();
350                        #( #pushes )*
351                        helps
352                    }
353                };
354                get_help_arm = help_arm;
355            }
356
357            if !note_fields.is_empty() {
358                let all_idents: Vec<_> = named_fields
359                    .named
360                    .iter()
361                    .map(|f| f.ident.as_ref().unwrap())
362                    .collect();
363
364                let pushes: Vec<_> = note_fields
365                    .iter()
366                    .map(|(ident, kind)| {
367                        if kind == "OptionString" {
368                            quote! {
369                                if let Some(val) = #ident.clone() {
370                                    notes.push(val);
371                                }
372                            }
373                        } else {
374                            quote! {
375                                notes.extend(#ident.clone());
376                            }
377                        }
378                    })
379                    .collect();
380
381                let note_arm = quote! {
382                    Self::#variant_ident { #(ref #all_idents),* } => {
383                        let mut notes = Vec::new();
384                        #( #pushes )*
385                        notes
386                    }
387                };
388                get_note_arm = note_arm;
389            }
390
391            if let Some((ident, kind)) = message_fields.last() {
392                let message_arm = match kind.as_str() {
393                    "DirectString" => quote! {
394                        Self::#variant_ident { #ident, .. } => {
395                            Some(#ident.clone())
396                        }
397                    },
398                    "OptionString" => quote! {
399                        Self::#variant_ident { #ident, .. } => {
400                            #ident.clone()
401                        }
402                    },
403                    _ => quote! {
404                        Self::#variant_ident { .. } => None
405                    },
406                };
407                get_message_arm = message_arm;
408            }
409        }
410
411        get_span_arms.push(get_span_arm);
412        set_span_arms.push(set_span_arm);
413        get_label_arms.push(get_label_arm);
414        get_help_arms.push(get_help_arm);
415        get_note_arms.push(get_note_arm);
416        get_message_arms.push(get_message_arm);
417    }
418
419    let report_impl = quote! {
420        impl tenda_reporting::Diagnostic<tenda_common::span::SourceSpan> for #enum_ident {
421            fn get_span(&self) -> ::std::option::Option<tenda_common::span::SourceSpan> {
422                match self {
423                    #( #get_span_arms ),*
424                }
425            }
426
427            fn set_span(&mut self, new_span: &tenda_common::span::SourceSpan) {
428                match self {
429                    #( #set_span_arms ),*
430                }
431            }
432
433            fn get_labels(&self) -> ::std::vec::Vec<tenda_common::span::SourceSpan> {
434                match self {
435                    #( #get_label_arms ),*
436                }
437            }
438
439            fn get_helps(&self) -> ::std::vec::Vec<String> {
440                match self {
441                    #( #get_help_arms ),*
442                }
443            }
444
445            fn get_notes(&self) -> ::std::vec::Vec<String> {
446                match self {
447                    #( #get_note_arms ),*
448                }
449            }
450
451            fn get_message(&self) -> ::std::option::Option<String> {
452                match self {
453                    #( #get_message_arms ),*
454                }
455            }
456
457            fn build_report_config(&self) -> tenda_reporting::DiagnosticConfig<tenda_common::span::SourceSpan> {
458                let fallback_message = self.to_string();
459                let span = self.get_span();
460                let labels = self.get_labels();
461                let helps = self.get_helps();
462                let notes = self.get_notes();
463                let final_message = if let Some(custom) = self.get_message() {
464                    custom
465                } else {
466                    fallback_message
467                };
468                let stacktrace = vec![];
469
470                tenda_reporting::DiagnosticConfig::new(
471                    span,
472                    labels,
473                    helps,
474                    notes,
475                    final_message,
476                    stacktrace,
477                )
478            }
479
480            fn to_report(&self) -> tenda_reporting::Report<tenda_common::span::SourceSpan> {
481                use tenda_reporting::Fmt;
482                use tenda_reporting::{HasDiagnosticHooks, DiagnosticConfig};
483
484                let kind = tenda_reporting::ReportKind::Custom(&#error_kind, tenda_reporting::Color::Red);
485                let prefixes = tenda_reporting::Localization::new()
486                    .with_help("ajuda")
487                    .with_note("nota")
488                    .with_stacktrace("em")
489                    .with_unknown("desconhecido");
490
491                let config = tenda_reporting::Config::default()
492                    .with_index_type(tenda_reporting::IndexType::Byte)
493                    .with_prefixes(prefixes);
494
495                let mut rep_config = self.build_report_config();
496
497                for hook in <Self as HasDiagnosticHooks<tenda_common::span::SourceSpan>>::hooks() {
498                    rep_config = hook(self, rep_config);
499                }
500
501                let main_span = match &rep_config.span {
502                    Some(sp) => sp.clone(),
503                    None => {
504                        panic!("No span found for report. Please ensure at least one #[span] is present.");
505                    }
506                };
507
508                let mut main_label = tenda_reporting::Label::new(main_span.clone())
509                    .with_color(tenda_reporting::Color::Red);
510
511                if let Some(lbl) = main_span.label() {
512                    main_label = main_label.with_message(lbl.clone());
513                } else {
514                    main_label = main_label.with_message(
515                        format!("{}", "aqui".fg(tenda_reporting::Color::Red))
516                    );
517                }
518
519                let mut builder = tenda_reporting::Report::build(kind, main_span.clone())
520                    .with_config(config)
521                    .with_message(rep_config.message)
522                    .with_label(main_label);
523
524                for lbl_span in rep_config.labels {
525                    let mut label = tenda_reporting::Label::new(lbl_span.clone())
526                        .with_color(tenda_reporting::Color::Red);
527
528                    if let Some(lbl_txt) = lbl_span.label() {
529                        label = label.with_message(lbl_txt.clone());
530                    } else {
531                        label = label.with_message(format!("{}", "aqui".fg(tenda_reporting::Color::Red)));
532                    }
533                    builder = builder.with_label(label);
534                }
535
536                for h in rep_config.helps {
537                    builder = builder.with_help(h);
538                }
539
540                for n in rep_config.notes {
541                    builder = builder.with_note(n);
542                }
543
544
545                builder
546                    .with_stacktrace(rep_config.stacktrace)
547                    .finish()
548            }
549        }
550    };
551
552    let maybe_hooks_impl = if accept_hooks {
553        quote! {
554            #[allow(dead_code)]
555            const __REQUIRE_HOOKS_IMPL: () = {
556                let _ = <#enum_ident as tenda_reporting::HasDiagnosticHooks<tenda_common::span::SourceSpan>>::hooks;
557            };
558        }
559    } else {
560        quote! {
561            impl tenda_reporting::HasDiagnosticHooks<tenda_common::span::SourceSpan> for #enum_ident {
562                fn hooks() -> &'static [fn(&Self, tenda_reporting::DiagnosticConfig<tenda_common::span::SourceSpan>) -> tenda_reporting::DiagnosticConfig<tenda_common::span::SourceSpan>] {
563                    &[]
564                }
565            }
566        }
567    };
568
569    let mut metadata_impls = Vec::new();
570
571    for (field_ident, info) in metadata_map {
572        let field_name_str = field_ident.to_string();
573        let field_ty = match &info.field_type {
574            Some(ty) => ty,
575            None => continue,
576        };
577
578        let get_fn_name = Ident::new(&format!("get_{}", field_name_str), field_ident.span());
579        let get_mut_fn_name =
580            Ident::new(&format!("get_mut_{}", field_name_str), field_ident.span());
581        let set_fn_name = Ident::new(&format!("set_{}", field_name_str), field_ident.span());
582
583        let get_arms = &info.get_arms;
584        let get_mut_arms = &info.get_mut_arms;
585        let set_arms = &info.set_arms;
586
587        let get_fn = quote! {
588            pub fn #get_fn_name(&self) -> ::std::option::Option<&#field_ty> {
589                match self {
590                    #( #get_arms )*
591                    _ => None
592                }
593            }
594        };
595
596        let get_mut_fn = quote! {
597            pub fn #get_mut_fn_name(&mut self) -> ::std::option::Option<&mut #field_ty> {
598                match self {
599                    #( #get_mut_arms )*
600                    _ => None
601                }
602            }
603        };
604
605        let set_fn = quote! {
606            pub fn #set_fn_name(&mut self, value: #field_ty) {
607                match self {
608                    #( #set_arms )*
609                    _ => {}
610                }
611            }
612        };
613
614        metadata_impls.push(get_fn);
615        metadata_impls.push(get_mut_fn);
616        metadata_impls.push(set_fn);
617    }
618
619    let metadata_impl = quote! {
620        impl #enum_ident {
621            #( #metadata_impls )*
622        }
623    };
624
625    let expanded = quote! {
626        #report_impl
627        #maybe_hooks_impl
628        #metadata_impl
629    };
630
631    expanded.into()
632}