miette_derive/
label.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    parenthesized,
5    parse::{Parse, ParseStream},
6    spanned::Spanned,
7    Token,
8};
9
10use crate::{
11    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
12    fmt::{self, Display},
13    forward::WhichFn,
14    utils::{display_pat_members, gen_all_variants_with},
15};
16
17pub struct Labels(Vec<Label>);
18
19#[derive(PartialEq, Eq)]
20enum LabelType {
21    Default,
22    Primary,
23    Collection,
24}
25
26struct Label {
27    label: Option<Display>,
28    ty: syn::Type,
29    span: syn::Member,
30    lbl_ty: LabelType,
31}
32
33struct LabelAttr {
34    label: Option<Display>,
35    lbl_ty: LabelType,
36}
37
38impl Parse for LabelAttr {
39    fn parse(input: ParseStream) -> syn::Result<Self> {
40        // Skip a token.
41        // This should receive one of:
42        // - label = "..."
43        // - label("...")
44        let _ = input.step(|cursor| {
45            if let Some((_, next)) = cursor.token_tree() {
46                Ok(((), next))
47            } else {
48                Err(cursor.error("unexpected empty attribute"))
49            }
50        });
51        let la = input.lookahead1();
52        let (lbl_ty, label) = if la.peek(syn::token::Paren) {
53            // #[label(primary?, "{}", x)]
54            let content;
55            parenthesized!(content in input);
56
57            let attr = match content.parse::<Option<syn::Ident>>()? {
58                Some(ident) if ident == "primary" => {
59                    let _ = content.parse::<Token![,]>();
60                    LabelType::Primary
61                }
62                Some(ident) if ident == "collection" => {
63                    let _ = content.parse::<Token![,]>();
64                    LabelType::Collection
65                }
66                Some(_) => {
67                    return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
68                }
69                _ => LabelType::Default,
70            };
71
72            if content.peek(syn::LitStr) {
73                let fmt = content.parse()?;
74                let args = if content.is_empty() {
75                    TokenStream::new()
76                } else {
77                    fmt::parse_token_expr(&content, false)?
78                };
79                let display = Display {
80                    fmt,
81                    args,
82                    has_bonus_display: false,
83                };
84                (attr, Some(display))
85            } else if !content.is_empty() {
86                return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
87            } else {
88                (attr, None)
89            }
90        } else if la.peek(Token![=]) {
91            // #[label = "blabla"]
92            input.parse::<Token![=]>()?;
93            (
94                LabelType::Default,
95                Some(Display {
96                    fmt: input.parse()?,
97                    args: TokenStream::new(),
98                    has_bonus_display: false,
99                }),
100            )
101        } else {
102            (LabelType::Default, None)
103        };
104        Ok(LabelAttr { label, lbl_ty })
105    }
106}
107
108impl Labels {
109    pub fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
110        match fields {
111            syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
112            syn::Fields::Unnamed(unnamed) => {
113                Self::from_fields_vec(unnamed.unnamed.iter().collect())
114            }
115            syn::Fields::Unit => Ok(None),
116        }
117    }
118
119    fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
120        let mut labels = Vec::new();
121        for (i, field) in fields.iter().enumerate() {
122            for attr in &field.attrs {
123                if attr.path().is_ident("label") {
124                    let span = if let Some(ident) = field.ident.clone() {
125                        syn::Member::Named(ident)
126                    } else {
127                        syn::Member::Unnamed(syn::Index {
128                            index: i as u32,
129                            span: field.span(),
130                        })
131                    };
132                    use quote::ToTokens;
133                    let LabelAttr { label, lbl_ty } =
134                        syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
135
136                    if lbl_ty == LabelType::Primary
137                        && labels
138                            .iter()
139                            .any(|l: &Label| l.lbl_ty == LabelType::Primary)
140                    {
141                        return Err(syn::Error::new(
142                            field.span(),
143                            "Cannot have more than one primary label.",
144                        ));
145                    }
146
147                    labels.push(Label {
148                        label,
149                        span,
150                        ty: field.ty.clone(),
151                        lbl_ty,
152                    });
153                }
154            }
155        }
156        if labels.is_empty() {
157            Ok(None)
158        } else {
159            Ok(Some(Labels(labels)))
160        }
161    }
162
163    pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
164        let (display_pat, display_members) = display_pat_members(fields);
165        let labels = self.0.iter().filter_map(|highlight| {
166            let Label {
167                span,
168                label,
169                ty,
170                lbl_ty,
171            } = highlight;
172            if *lbl_ty == LabelType::Collection {
173                return None;
174            }
175            let var = quote! { __miette_internal_var };
176            let display = if let Some(display) = label {
177                let (fmt, args) = display.expand_shorthand_cloned(&display_members);
178                quote! { std::option::Option::Some(format!(#fmt #args)) }
179            } else {
180                quote! { std::option::Option::None }
181            };
182            let ctor = if *lbl_ty == LabelType::Primary {
183                quote! { miette::LabeledSpan::new_primary_with_span }
184            } else {
185                quote! { miette::LabeledSpan::new_with_span }
186            };
187
188            Some(quote! {
189                miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
190                .map(|#var| #ctor(
191                    #display,
192                    #var.clone(),
193                ))
194            })
195        });
196        let collections_chain = self.0.iter().filter_map(|label| {
197            let Label {
198                span,
199                label,
200                ty: _,
201                lbl_ty,
202            } = label;
203            if *lbl_ty != LabelType::Collection {
204                return None;
205            }
206            let display = if let Some(display) = label {
207                let (fmt, args) = display.expand_shorthand_cloned(&display_members);
208                quote! { std::option::Option::Some(format!(#fmt #args)) }
209            } else {
210                quote! { std::option::Option::None }
211            };
212            Some(quote! {
213                .chain({
214                    let display = #display;
215                    self.#span.iter().map(move |span| {
216                        use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
217                        let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
218                        if display.is_some() && labeled_span.label().is_none() {
219                            labeled_span.set_label(display.clone())
220                        }
221                        Some(labeled_span)
222                    })
223                })
224            })
225        });
226
227        Some(quote! {
228            #[allow(unused_variables)]
229            fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>> {
230                use miette::macro_helpers::ToOption;
231                let Self #display_pat = self;
232
233                let labels_iter = vec![
234                    #(#labels),*
235                ]
236                .into_iter()
237                #(#collections_chain)*;
238
239                std::option::Option::Some(Box::new(labels_iter.filter(Option::is_some).map(Option::unwrap)))
240            }
241        })
242    }
243
244    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
245        gen_all_variants_with(
246            variants,
247            WhichFn::Labels,
248            |ident, fields, DiagnosticConcreteArgs { labels, .. }| {
249                let (display_pat, display_members) = display_pat_members(fields);
250                labels.as_ref().and_then(|labels| {
251                    let variant_labels = labels.0.iter().filter_map(|label| {
252                        let Label { span, label, ty, lbl_ty } = label;
253                        if *lbl_ty == LabelType::Collection {
254                            return None;
255                        }
256                        let field = match &span {
257                            syn::Member::Named(ident) => ident.clone(),
258                            syn::Member::Unnamed(syn::Index { index, .. }) => {
259                                format_ident!("_{}", index)
260                            }
261                        };
262                        let var = quote! { __miette_internal_var };
263                        let display = if let Some(display) = label {
264                            let (fmt, args) = display.expand_shorthand_cloned(&display_members);
265                            quote! { std::option::Option::Some(format!(#fmt #args)) }
266                        } else {
267                            quote! { std::option::Option::None }
268                        };
269                        let ctor = if *lbl_ty == LabelType::Primary {
270                            quote! { miette::LabeledSpan::new_primary_with_span }
271                        } else {
272                            quote! { miette::LabeledSpan::new_with_span }
273                        };
274
275                        Some(quote! {
276                            miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
277                            .map(|#var| #ctor(
278                                #display,
279                                #var.clone(),
280                            ))
281                        })
282                    });
283                    let collections_chain = labels.0.iter().filter_map(|label| {
284                        let Label { span, label, ty: _, lbl_ty } = label;
285                        if *lbl_ty != LabelType::Collection {
286                            return None;
287                        }
288                        let field = match &span {
289                            syn::Member::Named(ident) => ident.clone(),
290                            syn::Member::Unnamed(syn::Index { index, .. }) => {
291                                format_ident!("_{}", index)
292                            }
293                        };
294                        let display = if let Some(display) = label {
295                            let (fmt, args) = display.expand_shorthand_cloned(&display_members);
296                            quote! { std::option::Option::Some(format!(#fmt #args)) }
297                        } else {
298                            quote! { std::option::Option::None }
299                        };
300                        Some(quote! {
301                            .chain({
302                                let display = #display;
303                                #field.iter().map(move |span| {
304                                    use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
305                                    let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
306                                    if display.is_some() && labeled_span.label().is_none() {
307                                        labeled_span.set_label(display.clone());
308                                    }
309                                    Some(labeled_span)
310                                })
311                            })
312                        })
313                    });
314                    let variant_name = ident.clone();
315                    match &fields {
316                        syn::Fields::Unit => None,
317                        _ => Some(quote! {
318                            Self::#variant_name #display_pat => {
319                                use miette::macro_helpers::ToOption;
320                                let labels_iter = vec![
321                                    #(#variant_labels),*
322                                ]
323                                .into_iter()
324                                #(#collections_chain)*;
325                                std::option::Option::Some(std::boxed::Box::new(labels_iter.filter(Option::is_some).map(Option::unwrap)))
326                            }
327                        }),
328                    }
329                })
330            },
331        )
332    }
333}