Skip to main content

err_marks_the_spot_macro/
lib.rs

1//!
2#![allow(non_snake_case)]
3
4use proc_macro::{Delimiter, TokenStream, TokenTree, token_stream::IntoIter};
5use proc_macro2::{
6    Ident as Ident2, Punct as Punct2, Spacing as Spacing2, Span as Span2,
7    TokenStream as TokenStream2, TokenTree as TokenTree2,
8};
9use quote::quote;
10use regex::{Match, Regex};
11use std::collections::HashMap;
12use std::iter::Peekable;
13use std::ops::Range;
14use std::sync::LazyLock;
15use syn::{
16    AttrStyle, Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr,
17    ExprLit, Field, FieldMutability, Fields, FieldsNamed, FieldsUnnamed, Lit,
18    LitInt, LitStr, MacroDelimiter, Meta, MetaList, Path, PathArguments,
19    PathSegment, Type, TypePath, Variant, Visibility, parse_macro_input,
20    token::{Brace, Bracket, Colon, Paren, Pound, Pub},
21};
22
23// FIXME: Duplicated `#[cfg(feature = "example-build-flag")]` on generated ctx
24//        fields in both structs and enums
25
26#[proc_macro_attribute]
27pub fn err_marks_the_spot(attr: TokenStream, item: TokenStream) -> TokenStream {
28    let item2 = item.clone();
29    let type_item @ DeriveInput {
30        attrs: item_attrs,
31        vis: item_vis,
32        ident: item_ident,
33        generics: item_generics,
34        data: item_data,
35    } = &parse_macro_input!(item2 as DeriveInput);
36
37    let type_attr_args = TypeAttrArgs::parse(attr);
38    let field_attrs = type_attr_args.field_attr_vec();
39    let ctor_attrs = type_attr_args.ctor_attr_vec();
40    let impl_ctors_for_type = generate_ctor_impl_block(
41        &ctor_attrs,
42        &item_ident,
43        &item_data,
44        &field_attrs,
45    );
46
47    let augmented_type_item = DeriveInput {
48        attrs: item_attrs.clone(),
49        vis: item_vis.clone(),
50        ident: item_ident.clone(),
51        generics: item_generics.clone(),
52        data: match &item_data {
53            Data::Union(_) => panic!("Unions are not supported"),
54            Data::Enum(e) => Data::Enum(augment_enum(
55                type_attr_args.build_feature.as_ref(),
56                &e,
57                &field_attrs,
58            )),
59            Data::Struct(s) => Data::Struct(augment_struct(
60                type_attr_args.build_feature.as_ref(),
61                &s,
62                &field_attrs,
63            )),
64        },
65    };
66
67    let impl_Display_for_type: TokenStream2 = gen_impl_Display_for_type(
68        type_attr_args.build_feature.as_ref(),
69        &type_item,
70    );
71
72    TokenStream::from(quote! {
73        #augmented_type_item
74        #impl_ctors_for_type
75        #impl_Display_for_type
76    })
77}
78
79#[rustfmt::skip]
80fn augment_enum(
81    build_feature: Option<&BuildFeatureAttr>,
82    e: &DataEnum,
83    field_attrs: &[Attribute],
84) -> DataEnum {
85    let output_variants = e.variants.iter()
86        .map(|Variant { attrs, ident, fields, discriminant }| {
87            if discriminant.is_some() {
88                panic!("Enum variant discriminants are not supported");
89            }
90            let field_name = Some(Ident2::new("ctx", Span2::call_site()));
91            let field_vis = Visibility::Inherited;
92            let output_fields = match fields {
93                Fields::Named(n) => Fields::Named(FieldsNamed {
94                    brace_token: n.brace_token,
95                    named: std::iter::empty()
96                        .chain(n.named.iter().cloned())
97                        .chain(vec![
98                            ctx_field(
99                                build_feature,
100                                field_attrs,
101                                field_vis,
102                                field_name
103                            )
104                        ])
105                        .collect(),
106                }),
107                Fields::Unit => Fields::Named(FieldsNamed {
108                    brace_token: Brace(Span2::call_site()),
109                    named: std::iter::empty()
110                        .chain(vec![
111                            ctx_field(
112                                build_feature,
113                                field_attrs,
114                                field_vis,
115                                field_name
116                            )
117                        ])
118                        .collect(),
119                }),
120                Fields::Unnamed(u) => Fields::Unnamed(FieldsUnnamed {
121                    paren_token: u.paren_token,
122                    unnamed: std::iter::empty()
123                        .chain(u.unnamed.iter().cloned()) // user-defined fields
124                        .chain(vec![
125                            ctx_field(
126                                build_feature,
127                                field_attrs,
128                                field_vis,
129                                None
130                            )
131                        ])
132                        .collect(),
133                })
134            };
135            Variant {
136                attrs: attrs.clone(),
137                ident: ident.clone(),
138                fields: output_fields,
139                discriminant: discriminant.clone(),
140            }
141        })
142        .collect();
143    DataEnum {
144        enum_token: e.enum_token,
145        brace_token: e.brace_token,
146        variants: output_variants,
147    }
148}
149
150#[rustfmt::skip]
151fn augment_struct(
152    build_feature: Option<&BuildFeatureAttr>,
153    s: &DataStruct,
154    field_attrs: &[Attribute],
155) -> DataStruct {
156    let field_vis = Visibility::Public(Pub(Span2::call_site()));
157    let field_name = Some(Ident2::new("ctx", Span2::call_site()));
158    match &s.fields {
159        Fields::Named(n) => DataStruct {
160            struct_token: s.struct_token,
161            fields: Fields::Named(FieldsNamed {
162                brace_token: Brace(Span2::call_site()),
163                named: std::iter::empty()
164                    .chain(n.named.iter().cloned()) // user-defined fields
165                    .chain([
166                        ctx_field(
167                            build_feature,
168                            field_attrs,
169                            field_vis,
170                            field_name
171                        ),
172                    ])
173                    .collect(),
174            }),
175            semi_token: s.semi_token,
176        },
177        Fields::Unit => DataStruct {
178            struct_token: s.struct_token,
179            fields: Fields::Named(FieldsNamed {
180                brace_token: Brace(Span2::call_site()),
181                named: std::iter::empty()
182                    .chain([
183                        ctx_field(
184                            build_feature,
185                            field_attrs,
186                            field_vis,
187                            field_name
188                        ),
189                    ])
190                    .collect(),
191            }),
192            semi_token: s.semi_token,
193        },
194        Fields::Unnamed(u) => DataStruct {
195            struct_token: s.struct_token,
196            fields: Fields::Unnamed(FieldsUnnamed {
197                paren_token: Paren(Span2::call_site()),
198                unnamed: std::iter::empty()
199                    .chain(u.unnamed.iter().cloned()) // user-defined fields
200                    .chain([
201                        ctx_field(
202                            build_feature,
203                            field_attrs,
204                            field_vis,
205                            None
206                        ),
207                    ])
208                    .collect(),
209            }),
210            semi_token: s.semi_token,
211        },
212    }
213}
214
215// pub ctx: error_context_core::ErrorCtx
216fn ctx_field(
217    build_feature: Option<&BuildFeatureAttr>,
218    field_attrs: &[Attribute],
219    vis: Visibility,
220    field_name: Option<Ident2>,
221) -> Field {
222    Field {
223        attrs: field_attrs.to_vec().into_iter()
224            .chain(if let Some(feature) = build_feature {
225                vec![ feature.to_attr() ]
226            } else {
227                vec![/* Don't add #[cfg(feature = <FEATURE>)] attribure */]
228            })
229            .collect(),
230        vis,
231        mutability: FieldMutability::None,
232        ident: field_name,
233        colon_token: Some(Colon(Span2::call_site())),
234        ty: Type::Path(TypePath {
235            qself: None,
236            path: Path {
237                leading_colon: None,
238                segments: vec![
239                    PathSegment {
240                        ident: Ident2::new("err_marks_the_spot", Span2::call_site()),
241                        arguments: PathArguments::None,
242                    },
243                    PathSegment {
244                        ident: Ident2::new("ErrorCtx", Span2::call_site()),
245                        arguments: PathArguments::None,
246                    },
247                ].into_iter().collect()
248            },
249        }),
250    }
251}
252
253fn generate_ctor_impl_block(
254    ctor_attrs: &[Attribute],
255    type_name: &Ident2,
256    item_data: &Data,
257    field_attrs: &[Attribute],
258) -> TokenStream2 {
259    match &item_data {
260        Data::Union(_) => panic!("Unions are not supported"),
261        Data::Enum(e) => {
262            let enum_ctors = generate_enum_ctors(&e, field_attrs, ctor_attrs);
263            quote! {
264                impl #type_name {
265                    #( #enum_ctors )*
266                }
267            }
268        },
269        Data::Struct(s) => {
270            let struct_ctor = generate_struct_ctor(&s, field_attrs, ctor_attrs);
271            quote! {
272                impl #type_name {
273                    #struct_ctor
274                }
275            }
276        },
277    }
278}
279
280fn generate_struct_ctor(
281    s: &DataStruct,
282    field_attrs: &[Attribute],
283    ctor_attrs: &[Attribute],
284) -> TokenStream2 {
285    match &s.fields {
286        Fields::Named(n) => {
287            let params = n.named.iter()
288                .map(|Field { ident, ty, .. }| quote! { #ident : impl Into<#ty> });
289            let field_initializers = n.named.iter()
290                .map(|Field { ident, ty: _, .. }| quote! { #ident: #ident.into() })
291                .chain([
292                    quote! {
293                        #(#field_attrs)*
294                        ctx: err_marks_the_spot::ErrorCtx::new(),
295                    },
296                ]);
297            quote! {
298                #(#ctor_attrs)*
299                #[track_caller]
300                pub fn new( #(#params),* ) -> Self {
301                    Self {
302                        #(#field_initializers),*
303                    }
304                }
305            }
306        },
307        Fields::Unit => {
308            let field_initializers = std::iter::empty()
309                .chain([
310                    quote! {
311                        #(#field_attrs)*
312                        ctx: err_marks_the_spot::ErrorCtx::new(),
313                    },
314                ]);
315            quote! {
316                #(#ctor_attrs)*
317                #[track_caller]
318                pub fn new() -> Self {
319                    Self {
320                        #(#field_initializers),*
321                    }
322                }
323            }
324        },
325        Fields::Unnamed(u) => {
326            let params = u.unnamed.iter().enumerate()
327                .map(|(i, Field { ident: _, ty, .. })| {
328                    let ident = format!("field{i}");
329                    let ident = Ident2::new(&ident, Span2::call_site());
330                    quote! { #ident : impl Into<#ty> }
331                });
332            let field_initializers = u.unnamed.iter().enumerate()
333                .map(|(i, Field { ident: _, ty: _, .. })| {
334                    let ident = format!("field{i}");
335                    let ident = Ident2::new(&ident, Span2::call_site());
336                    quote! { #ident.into() }
337                })
338                .chain([
339                    quote! {
340                        #(#field_attrs)*
341                        err_marks_the_spot::ErrorCtx::new(),
342                    },
343                ]);
344            quote! {
345                #(#ctor_attrs)*
346                #[track_caller]
347                pub fn new( #(#params),* ) -> Self {
348                    Self(
349                        #(#field_initializers),*
350                    )
351                }
352            }
353        }
354    }
355}
356
357fn generate_enum_ctors(
358    e: &DataEnum,
359    field_attrs: &[Attribute],
360    ctor_attrs: &[Attribute],
361) -> Vec<TokenStream2> {
362    e.variants.iter()
363        .map(|Variant { ident, fields, .. }| {
364            let variant_name = ident;
365            let ctor_name = format!("new_{ident}");
366            let ctor_name = Ident2::new(&ctor_name, Span2::call_site());
367            match fields {
368                Fields::Named(n) => {
369                    let params = n.named.iter()
370                        .map(|Field { ident, ty, .. }| {
371                            quote! { #ident : impl Into<#ty> }
372                        });
373                    let field_initializers = n.named.iter()
374                        .map(|Field { ident, ty: _, .. }| {
375                            quote! { #ident: #ident.into() }
376                        })
377                        .chain([
378                            quote! {
379                                #(#field_attrs)*
380                                ctx: err_marks_the_spot::ErrorCtx::new(),
381                            },
382                        ]);
383                    quote! {
384                        #(#ctor_attrs)*
385                        #[track_caller]
386                        pub fn #ctor_name( #(#params),* ) -> Self {
387                            Self::#variant_name {
388                                #(#field_initializers),*
389                            }
390                        }
391                    }
392                },
393                Fields::Unit => {
394                    let field_initializers = std::iter::empty()
395                        .chain([
396                            quote! {
397                                #(#field_attrs)*
398                                ctx: err_marks_the_spot::ErrorCtx::new(),
399                            },
400                        ]);
401                    quote! {
402                        #(#ctor_attrs)*
403                        #[track_caller]
404                        pub fn #ctor_name() -> Self {
405                            Self::#variant_name {
406                                #(#field_initializers),*
407                            }
408                        }
409                    }
410                },
411                Fields::Unnamed(u) => {
412                    let params = u.unnamed.iter().enumerate()
413                        .map(|(i, Field { ident: _, ty, .. })| {
414                            let ident = format!("field{i}");
415                            let ident = Ident2::new(&ident, Span2::call_site());
416                            quote! { #ident : impl Into<#ty> }
417                        });
418                    let field_initializers = u.unnamed.iter().enumerate()
419                        .map(|(i, Field { ident: _, ty: _, .. })| {
420                            let ident = format!("field{i}");
421                            let ident = Ident2::new(&ident, Span2::call_site());
422                            quote! { #ident.into() }
423                        })
424                        .chain([
425                            quote! {
426                                #(#field_attrs)*
427                                err_marks_the_spot::ErrorCtx::new()
428                            },
429                        ]);
430                    quote! {
431                        #(#ctor_attrs)*
432                        #[track_caller]
433                        pub fn #ctor_name( #(#params),* ) -> Self {
434                            Self::#variant_name(
435                                #(#field_initializers),*
436                            )
437                        }
438                    }
439                },
440            }
441        })
442        .collect()
443}
444
445#[derive(Debug)]
446struct TypeAttrArgs {
447    build_feature: Option<BuildFeatureAttr>,
448    inline_ctors: Option<InlineCtorsAttr>,
449}
450
451impl TypeAttrArgs {
452    fn parse(attr: TokenStream) -> Self {
453        let mut attr_iter = attr.into_iter().peekable();
454        let mut field_attrs = Self {
455            build_feature: None,
456            inline_ctors: None,
457        };
458        let mut loop_count = 0;
459        while let Some(tt) = attr_iter.peek() {
460            let peeked = tt.to_string();
461            // TODO
462            // if loop_count > 0 {
463            //     attr_arg::parse_comma_token(&mut attr_iter);
464            // }
465            match &*peeked {
466                "," if loop_count > 0 => {
467                    attr_arg::parse_comma_token(&mut attr_iter);
468                }
469                "feature" => {
470                    let arg = BuildFeatureAttr::parse(&mut attr_iter);
471                    field_attrs.build_feature = Some(arg);
472                },
473                "inline_ctors" => {
474                    let arg = InlineCtorsAttr::parse(&mut attr_iter);
475                    field_attrs.inline_ctors = Some(arg);
476                },
477                _ => panic!("Expected attr name 'feature', 'inline_ctors', got {peeked}")
478            }
479            loop_count += 1;
480        }
481        field_attrs
482    }
483
484    fn field_attr_vec(&self) -> Vec<Attribute> {
485        let mut vec = vec![];
486        if let Some(build_flag) = &self.build_feature {
487            vec.push(build_flag.to_attr());
488        }
489        vec
490    }
491
492    fn ctor_attr_vec(&self) -> Vec<Attribute> {
493        let mut vec = vec![];
494        if let Some(inline_ctors) = &self.inline_ctors {
495            vec.push(inline_ctors.to_attr());
496        }
497        vec
498    }
499}
500
501// Currently ONLY recognizes the attribute arguments:
502// - feature = "<BUILD_FEATURE_NAME>"
503#[allow(unused)]
504#[derive(Debug)]
505struct BuildFeatureAttr {
506    name: Ident2,
507    name_stream: TokenStream2,
508    value: Expr,
509}
510
511impl BuildFeatureAttr {
512    fn parse(attr_iter: &mut Peekable<IntoIter>) -> Self {
513        let attr_arg_name = "feature";
514        let (name, name_stream) = attr_arg::parse_name(attr_iter, attr_arg_name);
515        attr_arg::parse_eq_token(attr_iter, &attr_arg_name);
516        let value: Expr = attr_arg::parse_value_expr(attr_iter, attr_arg_name);
517        Self { name, name_stream, value }
518    }
519
520    fn to_attr(&self) -> Attribute {
521        Attribute {
522            pound_token: Pound(Span2::call_site()),
523            style: AttrStyle::Outer,
524            bracket_token: Bracket(Span2::call_site()),
525            meta: Meta::List(MetaList {
526                path: Path {
527                    leading_colon: None,
528                    segments: [
529                        PathSegment {
530                            ident: Ident2::new("cfg", Span2::call_site()),
531                            arguments: PathArguments::None,
532                        },
533                    ].into_iter().collect(),
534                },
535                delimiter: MacroDelimiter::Paren(Paren(Span2::call_site())),
536                tokens: {
537                    let mut stream = TokenStream2::new();
538                    stream.extend({
539                        let feature = Ident2::new("feature", Span2::call_site());
540                        let feature = TokenTree2::Ident(feature);
541                        TokenStream2::from(feature.clone())
542                    });
543                    stream.extend({
544                        let eq = Punct2::new('=', Spacing2::Alone);
545                        let eq = TokenTree2::Punct(eq);
546                        TokenStream2::from(eq)
547                    });
548                    let feature = &self.value;
549                    stream.extend(quote! {
550                        #feature
551                    });
552                    stream
553                },
554            }),
555        }
556    }
557}
558
559// Currently ONLY recognizes the attribute arguments:
560// - inline_ctors = true | false
561#[derive(Debug)]
562struct InlineCtorsAttr {
563    #[allow(unused)]
564    name: Ident2,
565    value: Option<Ident2>,
566}
567
568impl InlineCtorsAttr {
569    fn parse(attr_iter: &mut Peekable<IntoIter>) -> Self {
570        let attr_arg_name = "inline_ctors";
571        let (name, _stream) = attr_arg::parse_name(attr_iter, attr_arg_name);
572        let value = attr_arg::parse_parenthesized_value_ident(attr_iter);
573        Self { name, value }
574    }
575
576    fn to_attr(&self) -> Attribute {
577        Attribute {
578            pound_token: Pound(Span2::call_site()),
579            style: AttrStyle::Outer,
580            bracket_token: Bracket(Span2::call_site()),
581            meta: if let Some(value) = self.value.as_ref() {
582                Meta::List(MetaList {
583                    path: Path {
584                        leading_colon: None,
585                        segments: [
586                            PathSegment {
587                                ident: Ident2::new("inline", Span2::call_site()),
588                                arguments: PathArguments::None,
589                            },
590                        ].into_iter().collect(),
591                    },
592                    delimiter: MacroDelimiter::Paren(Paren(Span2::call_site())),
593                    tokens: TokenStream2::from(TokenTree2::Ident(value.clone())),
594                })
595            } else {
596                Meta::Path(Path {
597                    leading_colon: None,
598                    segments: [
599                        PathSegment {
600                            ident: Ident2::new("inline", Span2::call_site()),
601                            arguments: PathArguments::None,
602                        },
603                    ].into_iter().collect(),
604                })
605            },
606        }
607    }
608}
609
610mod attr_arg {
611    use super::*;
612
613    pub fn parse_name(
614        attr_iter: &mut Peekable<IntoIter>,
615        attr_arg_name: &str,
616    ) -> (Ident2, TokenStream2) {
617        let attr_name_tt: TokenTree = attr_iter.next().unwrap();
618        assert_eq!(attr_name_tt.to_string(), attr_arg_name);
619        let name_stream: TokenStream2 = TokenStream::from(attr_name_tt).into();
620        let name: Ident2 = syn::parse2(name_stream.clone()).unwrap_or_else(|_| {
621            panic!("Failed to parse attribute argument: {attr_arg_name}")
622        });
623        (name, name_stream)
624    }
625
626    pub fn parse_eq_token(
627        attr_iter: &mut Peekable<IntoIter>,
628        attr_arg_name: &str,
629    ) {
630        let _eq_tt = attr_iter.next();
631        let _eq = match _eq_tt {
632            Some(TokenTree::Punct(p)) => {
633                assert_eq!(p.as_char(), '=', "{attr_arg_name}");
634            },
635            Some(tt) => panic!("Unrecognized token tree: {tt}"),
636            None => panic!("Expected '='"),
637        };
638    }
639
640    pub fn parse_value_expr(
641        attr_iter: &mut Peekable<IntoIter>,
642        attr_arg_name: &str,
643    ) -> Expr {
644        match attr_iter.next() {
645            Some(tt @ TokenTree::Literal(_)) => {
646                let stream: TokenStream2 = TokenStream::from(tt).into();
647                syn::parse2::<Expr>(stream).unwrap_or_else(|_| panic!(
648                    "Failed to parse value expr of attribute argument {}",
649                    attr_arg_name
650                ))
651            }
652            Some(tt) => panic!(
653                "Expected value expr of attribute argument {}, got token tree {}",
654                attr_arg_name, tt
655            ),
656            None => panic!(
657                "Expected value expr of attribute argument {}",
658                attr_arg_name,
659            ),
660        }
661    }
662
663    pub fn parse_parenthesized_value_ident(
664        attr_iter: &mut Peekable<IntoIter>,
665    ) -> Option<Ident2> {
666        match attr_iter.next() {
667            Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => {
668                let inner_stream = TokenStream2::from(g.stream());
669                let attr_arg_value = syn::parse2::<Ident2>(inner_stream)
670                    .expect("Expected a parenthesized value ident");
671                Some(attr_arg_value)
672            },
673            _ => None,
674        }
675    }
676
677    pub fn parse_comma_token(attr_iter: &mut Peekable<IntoIter>) {
678        let _comma_tt = attr_iter.next();
679        let _comma = match _comma_tt {
680            Some(TokenTree::Punct(p)) => assert_eq!(p.as_char(), ','),
681            Some(tt) => panic!("Unrecognized token tree: {tt}"),
682            None => panic!("Expected ','"),
683        };
684    }
685}
686
687fn gen_impl_Display_for_type(
688    build_feature: Option<&BuildFeatureAttr>,
689    type_item: &DeriveInput,
690) -> TokenStream2 {
691    let type_item_name = &type_item.ident;
692    let type_item_docstrs: Vec<String> = get_docstrs_from_attrs(&type_item.attrs);
693    let item_field_map: FieldMap = match &type_item.data {
694        Data::Union(_) => panic!("Unions are not supported"),
695        Data::Enum(e) => {
696            let enum_fields_map = e.variants
697                .iter()
698                .map(|Variant { ident, fields, .. }| (
699                    ident.clone(),
700                    create_fields_map(&fields)
701                ))
702                .collect();
703            FieldMap::Enum(enum_fields_map)
704        }
705        Data::Struct(s) => FieldMap::Struct(create_fields_map(&s.fields)),
706    };
707    let struct_impl_Display_contents = get_struct_impl_Display_contents(
708        build_feature,
709        type_item,
710        &type_item_docstrs,
711        &item_field_map,
712    );
713    let enum_impl_Display_contents = get_enum_impl_Display_contents(
714        build_feature,
715        type_item,
716        &item_field_map,
717    );
718    let impl_Display_contents = if let FieldMap::Struct(_) = item_field_map {
719        quote! { #struct_impl_Display_contents }
720    } else {
721        quote! { #enum_impl_Display_contents }
722    };
723    quote! {
724        impl std::fmt::Display for #type_item_name {
725            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
726                #impl_Display_contents
727                Ok(())
728            }
729        }
730    }
731}
732
733fn get_struct_impl_Display_contents(
734    build_feature: Option<&BuildFeatureAttr>,
735    type_item: &DeriveInput,
736    type_item_docstrs: &[String],
737    item_field_map: &FieldMap,
738) -> TokenStream2 {
739    let type_item_name = &type_item.ident;
740    let quotes: Vec<TokenStream2> = type_item_docstrs.iter()
741        .filter(|_| matches!(item_field_map, FieldMap::Struct(_)))
742        .map(|item_docstr| {
743            let item_docstr_fields = find_docstring_fields(item_docstr);
744            let modified_item_docstr = modify_docstr(
745                &item_docstr,
746                &item_docstr_fields
747            );
748            let trimmed_item_docstr = LitStr::new(
749                &modified_item_docstr,
750                Span2::call_site()
751            );
752            assert!(matches!(type_item.data, Data::Struct(_)));
753            let FieldMap::Struct(field_map) = &item_field_map else { unreachable!() };
754            let fields: Vec<&FieldIdToken> = item_docstr_fields.iter()
755                .map(|(_pos, field_name)| {
756                    field_map.get(*field_name).unwrap_or_else(|| panic!(
757                        "Type {} no has no field '{}'",
758                        type_item_name, field_name
759                    ))
760                })
761                .collect();
762            quote! {
763                writeln!(
764                    f,
765                    #trimmed_item_docstr,
766                    #(&self . #fields),*
767                )?;
768            }
769        })
770        .chain([
771            if let Some(BuildFeatureAttr { name, value, .. }) = build_feature {
772                assert_eq!(name, &Ident2::new("feature", Span2::call_site()));
773                // Write an empty line between original msg & ErrorCtx, but
774                // only perform the writeln!() call if the consumer crate is
775                // built with the build feature enabled:
776                quote! {
777                    #[cfg(feature = #value)]
778                    writeln!(f, "")?;
779                }
780            } else {
781                // Write an empty line between original msg & ErrorCtx:
782                quote! { writeln!(f, "")?; }
783            },
784        ])
785        .chain(if let Data::Struct(s) = &type_item.data {
786            // ErrorCtx docstring extension:
787            vec![writeln_for_ErrorCtx_field(build_feature, &s.fields)]
788        } else {
789            vec![]
790        })
791        .collect();
792    quote! { #(#quotes)* }
793}
794
795fn get_enum_impl_Display_contents(
796    build_feature: Option<&BuildFeatureAttr>,
797    type_item: &DeriveInput,
798    item_field_map: &FieldMap,
799) -> TokenStream2 {
800    let FieldMap::Enum(field_map) = &item_field_map else { return quote!{} };
801    let Data::Enum(data) = &type_item.data else { return quote!{} };
802    let DataEnum { variants, .. } = data;
803
804    let variant_writelns: Vec<_> = variants.iter()
805        .map(|Variant { attrs, ident: variant_name, fields, .. }| {
806            let vdocstrs = get_docstrs_from_attrs(attrs);
807
808            let vbindings: Vec<TokenStream2> = match fields {
809                Fields::Named(n) => n.named.iter()
810                    .map(|field| field.ident.clone().unwrap())
811                    .map(|field_name| quote! { #field_name })
812                    .collect(),
813                Fields::Unit => vec![],
814                Fields::Unnamed(u) => u.unnamed.iter()
815                    .enumerate()
816                    .map(|(i, field)| {
817                        field.ident.clone().unwrap_or_else(|| {
818                            Ident2::new(
819                                &format!("f{i}"),
820                                Span2::call_site()
821                            )
822                        })
823                    })
824                    .map(|field_name| quote! { #field_name })
825                    .collect(),
826            };
827
828            let vbind_list = match fields {
829                Fields::Named(_)   => quote! { { #(#vbindings ,)*      } },
830                Fields::Unit       => quote! { { #(#vbindings ,)*      } },
831                Fields::Unnamed(_) => quote! { ( #(#vbindings ,)*      ) },
832            };
833            let vbind_list_with_ctx = match fields {
834                Fields::Named(_)   => quote! { { #(#vbindings ,)* ctx, } },
835                Fields::Unit       => quote! { { #(#vbindings ,)* ctx, } },
836                Fields::Unnamed(_) => quote! { ( #(#vbindings ,)* ctx, ) },
837            };
838
839            let vdocstr_writelns: Vec<TokenStream2> = vdocstrs.iter()
840                .map(|variant_docstr| {
841                    let variant_docstr_fields = find_docstring_fields(
842                        variant_docstr
843                    );
844                    let modified_variant_docstr = modify_docstr(
845                        &variant_docstr,
846                        &variant_docstr_fields
847                    );
848                    let trimmed_variant_docstr = LitStr::new(
849                        &modified_variant_docstr,
850                        Span2::call_site()
851                    );
852                    let variant_docstr_fields: Vec<Ident2> =
853                        variant_docstr_fields
854                        .iter()
855                        .map(|(_pos, field_name)| {
856                            let enum_field_map = field_map.get(variant_name)
857                                .unwrap_or_else(|| panic!(
858                                    "Type {} no has no variant '{}'",
859                                    type_item.ident, variant_name
860                                ));
861                            let field_token = enum_field_map
862                                .get(*field_name)
863                                .unwrap_or_else(|| panic!(
864                                    "Type variant {}::{} no has no field '{}'",
865                                    type_item.ident, variant_name, field_name
866                                ));
867                            match field_token {
868                                FieldIdToken::Ident(ident) => ident.clone(),
869                                FieldIdToken::Literal(lit) => Ident2::new(
870                                    &format!("f{lit}"),
871                                    Span2::call_site()
872                                ),
873                            }
874                        })
875                        .collect();
876                    quote! {
877                        writeln!(
878                            f,
879                            #trimmed_variant_docstr,
880                            #(& #variant_docstr_fields),*
881                        )?;
882                    }
883                })
884                .chain([
885                    // Write an empty line between original msg & ErrorCtx,
886                    // but only perform the writeln!() call if the consumer
887                    // crate is built with the build feature enabled, or
888                    // without using the `feature` attribute argument:
889                    if let Some(bf) = build_feature {
890                        assert_eq!(
891                            bf.name,
892                            Ident2::new("feature", Span2::call_site())
893                        );
894                        let feature = &bf.value;
895                        quote! {
896                            #[cfg(feature = #feature)]
897                            writeln!(f, "")?;
898                            #[cfg(feature = #feature)]
899                            writeln!(f, "{}", &ctx)?;
900                        }
901                    } else {
902                        quote! {
903                            writeln!(f, "")?;
904                            writeln!(f, "{}", &ctx)?;
905                        }
906                    }
907                ])
908                .collect();
909
910            if let Some(bf) = build_feature {
911                let feature = &bf.value;
912                quote! {
913                    #[cfg(feature = #feature)]
914                    Self :: #variant_name  #vbind_list_with_ctx  => {
915                        #( #vdocstr_writelns )*
916                    },
917
918                    #[cfg(not(feature = #feature))]
919                    Self :: #variant_name  #vbind_list  => {
920                        #( #vdocstr_writelns )*
921                    },
922                }
923            } else {
924                quote! {
925                    Self :: #variant_name  #vbind_list_with_ctx  => {
926                        #( #vdocstr_writelns )*
927                    },
928                }
929            }
930
931        })
932        .collect();
933
934    quote! {
935        match self {
936            #( #variant_writelns )*
937        }
938    }
939}
940
941fn get_docstrs_from_attrs(attrs: &[Attribute]) -> Vec<String> {
942    attrs.iter()
943        .filter(|Attribute { meta, .. }| {
944            let Meta::NameValue(v) = meta else { return false };
945            let segs = v.path.segments.iter().collect::<Vec<_>>();
946            let [PathSegment { ident, arguments: PathArguments::None }] = &*segs
947            else { return false };
948            ident.to_string() == "doc"
949        })
950        .map(|Attribute { meta, .. }|  {
951            let Meta::NameValue(v) = meta else { unreachable!() };
952            let Expr::Lit(ExprLit { lit, .. }) = &v.value else { unreachable!() };
953            let Lit::Str(s) = lit else { unreachable!() };
954            s.token().to_string()
955                .trim_start_matches(r#"" "#)
956                .trim_end_matches(r#"""#)
957                .to_string()
958        })
959        .collect()
960}
961
962/// Return a modified docstring that has each site where
963/// a field is mentioned (e.g. {foo}) replaced by a {}.
964fn modify_docstr(
965    docstr: &str,
966    fields_sites: &[(Range<usize>, &str)],
967) -> String {
968    fields_sites.iter()
969        .rev(/*replace from last match to first*/)
970        .fold(docstr.to_string(), |docstr, &(Range { start, end }, _)| {
971            const EMPTY_FMT_STR: &str = "{}";
972            let   preamble = &docstr[..start];
973            let  postamble = &docstr[end..];
974            preamble.to_string() + EMPTY_FMT_STR + postamble
975        })
976}
977
978/// Find the struct/enum fields used in a docstring, and return
979/// them in the order thay they occur in within the string.
980fn find_docstring_fields(docstr: &str) -> Vec<(Range<usize>, &str)> {
981    static FIELD_SPECIFIER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
982        Regex::new(r"\{[A-Za-z0-9_]+\}").unwrap()
983    });
984    FIELD_SPECIFIER_REGEX.find_iter(docstr)
985        .map(|m: Match| {
986            let field_name = m.as_str()
987                .strip_prefix('{').unwrap()
988                .strip_suffix('}').unwrap();
989            (m.range(), field_name)
990        })
991        .collect()
992}
993
994fn writeln_for_ErrorCtx_field(
995    build_feature: Option<&BuildFeatureAttr>,
996    fields: &Fields,
997) -> TokenStream2 {
998    match &fields {
999        Fields::Named(_) | Fields::Unit => {
1000            let ident = Ident2::new("ctx", Span2::call_site());
1001            let err_ctx_field = FieldIdToken::Ident(ident);
1002            if let Some(BuildFeatureAttr { name, value, .. }) = build_feature {
1003                assert_eq!(name, &Ident2::new("feature", Span2::call_site()));
1004                // Write the error ctx, but only perform the writeln!() call if
1005                // the consumer crate is built with the build feature enabled:
1006                quote! {
1007                    #[cfg(feature = #value)]
1008                    writeln!(f, "{}", &self . #err_ctx_field)?;
1009                }
1010            } else {
1011                // Write the error ctx
1012                quote! {
1013                    writeln!(f, "{}", &self . #err_ctx_field)?;
1014                }
1015            }
1016        }
1017        Fields::Unnamed(u) => {
1018            let field_num = u.unnamed.iter()
1019                .enumerate()
1020                .last()
1021                .map(|(field_num, _field)| field_num)
1022                .unwrap_or_default();
1023            let lit = format!("{}", field_num + 1);
1024            let lit = LitInt::new(&lit, Span2::call_site());
1025            let err_ctx_field = FieldIdToken::Literal(lit);
1026            if let Some(BuildFeatureAttr { name, value, .. }) = build_feature {
1027                assert_eq!(name, &Ident2::new("feature", Span2::call_site()));
1028                // Write the error ctx, but only perform the writeln!() call if
1029                // the consumer crate is built with the build feature enabled:
1030                quote! {
1031                    #[cfg(feature = #value)]
1032                    writeln!(f, "{}", &self . #err_ctx_field)?;
1033                }
1034            } else {
1035                // Write the error ctx
1036                quote! {
1037                    writeln!(f, "{}", &self . #err_ctx_field)?;
1038                }
1039            }
1040        }
1041    }
1042}
1043
1044/// A quote!()-injectable token representing one of:
1045/// - a name (for named fields), or
1046/// - a number (for unnamd fields)
1047#[derive(Debug)]
1048enum FieldIdToken {
1049    Ident(Ident2),
1050    Literal(LitInt),
1051}
1052
1053impl quote::ToTokens for FieldIdToken {
1054    fn to_tokens(&self, tokens: &mut TokenStream2) {
1055        let ts = match self {
1056            Self::Ident(ident) => quote! { #ident },
1057            Self::Literal(literal) => quote! { #literal },
1058        };
1059        tokens.extend(ts);
1060    }
1061}
1062
1063enum FieldMap {
1064    Struct(HashMap<String, FieldIdToken>),
1065    Enum(HashMap<Ident2, HashMap<String, FieldIdToken>>), // for each variant
1066}
1067
1068fn single_field_mapping(
1069    ident: Option<Ident2>,
1070    num: Option<usize>,
1071) -> (String, FieldIdToken) {
1072    match (ident, num) {
1073        (Some(ident), _) => {
1074            let field_name = format!("{ident}");
1075            (field_name, FieldIdToken::Ident(ident.clone()))
1076        }
1077        (_, Some(num)) => {
1078            let field_num = format!("{num}");
1079            let lit = LitInt::new(&field_num, Span2::call_site());
1080            (field_num, FieldIdToken::Literal(lit))
1081        },
1082        _ => unreachable!(),
1083    }
1084}
1085
1086fn create_fields_map(fields: &Fields) -> HashMap<String, FieldIdToken> {
1087    match fields {
1088        Fields::Named(n) => n.named.iter()
1089            .map(|Field { ident, .. }| {
1090                single_field_mapping(ident.clone(), None)
1091            })
1092            .chain({ // ErrorCtx field
1093                let ident = Ident2::new("ctx", Span2::call_site());
1094                [ single_field_mapping(Some(ident), None) ]
1095            })
1096            .collect(),
1097        Fields::Unit => std::iter::empty()
1098            .chain({ // ErrorCtx field
1099                let ident = Ident2::new("ctx", Span2::call_site());
1100                [ single_field_mapping(Some(ident), None) ]
1101            })
1102            .collect(),
1103        Fields::Unnamed(u) => {
1104            let mut fields: Vec<(String, FieldIdToken)> = u.unnamed.iter()
1105                .enumerate()
1106                .map(|(i, Field { ident, .. })| {
1107                    assert!(ident.is_none());
1108                    single_field_mapping(None, Some(i))
1109                })
1110                .collect();
1111            fields.push({ // ErrorCtx field
1112                single_field_mapping(None, Some(fields.len()))
1113            });
1114            fields.into_iter().collect()
1115        },
1116    }
1117}