1use proc_macro::TokenStream;
2use quote::quote;
3use std::collections::BTreeMap;
4use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, LitStr, Type};
5
6#[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}