flux_macros/diagnostics/
diagnostic_builder.rs

1#![allow(clippy::pedantic)]
2#![deny(unused_must_use)]
3
4use proc_macro2::{Ident, Span, TokenStream};
5use quote::{format_ident, quote, quote_spanned};
6use syn::{Attribute, Meta, Path, Token, Type, parse_quote, spanned::Spanned};
7use synstructure::{BindingInfo, Structure, VariantInfo};
8
9use super::utils::SubdiagnosticVariant;
10use crate::diagnostics::{
11    error::{DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err},
12    utils::{
13        FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce, SpannedOption, SubdiagnosticKind,
14        build_field_mapping, is_doc_comment, report_error_if_not_applied_to_span,
15        report_type_error, should_generate_arg, type_is_bool, type_is_unit, type_matches_path,
16    },
17};
18
19/// What kind of diagnostic is being derived - a fatal/error/warning or a lint?
20#[derive(Clone, Copy, PartialEq, Eq)]
21pub(crate) enum DiagnosticDeriveKind {
22    Diagnostic,
23    LintDiagnostic,
24}
25
26/// Tracks persistent information required for a specific variant when building up individual calls
27/// to diagnostic methods for generated diagnostic derives - both `Diagnostic` for
28/// fatal/errors/warnings and `LintDiagnostic` for lints.
29pub(crate) struct DiagnosticDeriveVariantBuilder {
30    /// The kind for the entire type.
31    pub kind: DiagnosticDeriveKind,
32
33    /// Initialization of format strings for code suggestions.
34    pub formatting_init: TokenStream,
35
36    /// Span of the struct or the enum variant.
37    pub span: proc_macro::Span,
38
39    /// Store a map of field name to its corresponding field. This is built on construction of the
40    /// derive builder.
41    pub field_map: FieldMap,
42
43    /// Slug is a mandatory part of the struct attribute as corresponds to the Fluent message that
44    /// has the actual diagnostic message.
45    pub slug: SpannedOption<Path>,
46
47    /// Error codes are a optional part of the struct attribute - this is only set to detect
48    /// multiple specifications.
49    pub code: SpannedOption<()>,
50}
51
52impl HasFieldMap for DiagnosticDeriveVariantBuilder {
53    fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
54        self.field_map.get(field)
55    }
56}
57
58impl DiagnosticDeriveKind {
59    /// Call `f` for the struct or for each variant of the enum, returning a `TokenStream` with the
60    /// tokens from `f` wrapped in an `match` expression. Emits errors for use of derive on unions
61    /// or attributes on the type itself when input is an enum.
62    pub(crate) fn each_variant<'s, F>(self, structure: &mut Structure<'s>, f: F) -> TokenStream
63    where
64        F: for<'v> Fn(DiagnosticDeriveVariantBuilder, &VariantInfo<'v>) -> TokenStream,
65    {
66        let ast = structure.ast();
67        let span = ast.span().unwrap();
68        match ast.data {
69            syn::Data::Struct(..) | syn::Data::Enum(..) => (),
70            syn::Data::Union(..) => {
71                span_err(span, "diagnostic derives can only be used on structs and enums").emit();
72            }
73        }
74
75        if matches!(ast.data, syn::Data::Enum(..)) {
76            for attr in &ast.attrs {
77                span_err(
78                    attr.span().unwrap(),
79                    "unsupported type attribute for diagnostic derive enum",
80                )
81                .emit();
82            }
83        }
84
85        structure.bind_with(|_| synstructure::BindStyle::Move);
86        let variants = structure.each_variant(|variant| {
87            let span = match structure.ast().data {
88                syn::Data::Struct(..) => span,
89                // There isn't a good way to get the span of the variant, so the variant's
90                // name will need to do.
91                _ => variant.ast().ident.span().unwrap(),
92            };
93            let builder = DiagnosticDeriveVariantBuilder {
94                kind: self,
95                span,
96                field_map: build_field_mapping(variant),
97                formatting_init: TokenStream::new(),
98                slug: None,
99                code: None,
100            };
101            f(builder, variant)
102        });
103
104        quote! {
105            match self {
106                #variants
107            }
108        }
109    }
110}
111
112impl DiagnosticDeriveVariantBuilder {
113    /// Generates calls to `code` and similar functions based on the attributes on the type or
114    /// variant.
115    pub(crate) fn preamble(&mut self, variant: &VariantInfo<'_>) -> TokenStream {
116        let ast = variant.ast();
117        let attrs = &ast.attrs;
118        let preamble = attrs.iter().map(|attr| {
119            self.generate_structure_code_for_attr(attr)
120                .unwrap_or_else(|v| v.to_compile_error())
121        });
122
123        quote! {
124            #(#preamble)*;
125        }
126    }
127
128    /// Generates calls to `span_label` and similar functions based on the attributes on fields or
129    /// calls to `arg` when no attributes are present.
130    pub(crate) fn body(&mut self, variant: &VariantInfo<'_>) -> TokenStream {
131        let mut body = quote! {};
132        // Generate `arg` calls first..
133        for binding in variant
134            .bindings()
135            .iter()
136            .filter(|bi| should_generate_arg(bi.ast()))
137        {
138            body.extend(self.generate_field_code(binding));
139        }
140        // ..and then subdiagnostic additions.
141        for binding in variant
142            .bindings()
143            .iter()
144            .filter(|bi| !should_generate_arg(bi.ast()))
145        {
146            body.extend(self.generate_field_attrs_code(binding));
147        }
148        body
149    }
150
151    /// Parse a `SubdiagnosticKind` from an `Attribute`.
152    fn parse_subdiag_attribute(
153        &self,
154        attr: &Attribute,
155    ) -> Result<Option<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
156        let Some(subdiag) = SubdiagnosticVariant::from_attr(attr, self)? else {
157            // Some attributes aren't errors - like documentation comments - but also aren't
158            // subdiagnostics.
159            return Ok(None);
160        };
161
162        if let SubdiagnosticKind::MultipartSuggestion { .. } = subdiag.kind {
163            throw_invalid_attr!(attr, |diag| {
164                diag.help("consider creating a `Subdiagnostic` instead")
165            });
166        }
167
168        let slug = subdiag.slug.unwrap_or_else(|| {
169            match subdiag.kind {
170                SubdiagnosticKind::Label => parse_quote! { _subdiag::label },
171                SubdiagnosticKind::Note => parse_quote! { _subdiag::note },
172                SubdiagnosticKind::NoteOnce => parse_quote! { _subdiag::note_once },
173                SubdiagnosticKind::Help => parse_quote! { _subdiag::help },
174                SubdiagnosticKind::HelpOnce => parse_quote! { _subdiag::help_once },
175                SubdiagnosticKind::Warn => parse_quote! { _subdiag::warn },
176                SubdiagnosticKind::Suggestion { .. } => parse_quote! { _subdiag::suggestion },
177                SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
178            }
179        });
180
181        Ok(Some((subdiag.kind, slug, subdiag.no_span)))
182    }
183
184    /// Establishes state in the `DiagnosticDeriveBuilder` resulting from the struct
185    /// attributes like `#[diag(..)]`, such as the slug and error code. Generates
186    /// diagnostic builder calls for setting error code and creating note/help messages.
187    fn generate_structure_code_for_attr(
188        &mut self,
189        attr: &Attribute,
190    ) -> Result<TokenStream, DiagnosticDeriveError> {
191        // Always allow documentation comments.
192        if is_doc_comment(attr) {
193            return Ok(quote! {});
194        }
195
196        let name = attr.path().segments.last().unwrap().ident.to_string();
197        let name = name.as_str();
198
199        let mut first = true;
200
201        if name == "diag" {
202            let mut tokens = TokenStream::new();
203            attr.parse_nested_meta(|nested| {
204                let path = &nested.path;
205
206                if first && (nested.input.is_empty() || nested.input.peek(Token![,])) {
207                    self.slug.set_once(path.clone(), path.span().unwrap());
208                    first = false;
209                    return Ok(());
210                }
211
212                first = false;
213
214                let Ok(nested) = nested.value() else {
215                    span_err(
216                        nested.input.span().unwrap(),
217                        "diagnostic slug must be the first argument",
218                    )
219                    .emit();
220                    return Ok(());
221                };
222
223                if path.is_ident("code") {
224                    self.code.set_once((), path.span().unwrap());
225
226                    let code = nested.parse::<syn::Expr>()?;
227                    tokens.extend(quote! {
228                        diag.code(#code);
229                    });
230                } else {
231                    span_err(path.span().unwrap(), "unknown argument")
232                        .note("only the `code` parameter is valid after the slug")
233                        .emit();
234
235                    // consume the buffer so we don't have syntax errors from syn
236                    let _ = nested.parse::<TokenStream>();
237                }
238                Ok(())
239            })?;
240            return Ok(tokens);
241        }
242
243        let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else {
244            // Some attributes aren't errors - like documentation comments - but also aren't
245            // subdiagnostics.
246            return Ok(quote! {});
247        };
248        let fn_ident = format_ident!("{}", subdiag);
249        match subdiag {
250            SubdiagnosticKind::Note
251            | SubdiagnosticKind::NoteOnce
252            | SubdiagnosticKind::Help
253            | SubdiagnosticKind::HelpOnce
254            | SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug)),
255            SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => {
256                throw_invalid_attr!(attr, |diag| {
257                    diag.help("`#[label]` and `#[suggestion]` can only be applied to fields")
258                });
259            }
260            SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
261        }
262    }
263
264    fn generate_field_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
265        let field = binding_info.ast();
266        let mut field_binding = binding_info.binding.clone();
267        field_binding.set_span(field.ty.span());
268
269        let ident = field.ident.as_ref().unwrap();
270        let ident = format_ident!("{}", ident); // strip `r#` prefix, if present
271
272        quote! {
273            diag.arg(
274                stringify!(#ident),
275                #field_binding
276            );
277        }
278    }
279
280    fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
281        let field = binding_info.ast();
282        let field_binding = &binding_info.binding;
283
284        let inner_ty = FieldInnerTy::from_type(&field.ty);
285
286        field
287            .attrs
288            .iter()
289            .map(move |attr| {
290                // Always allow documentation comments.
291                if is_doc_comment(attr) {
292                    return quote! {};
293                }
294
295                let name = attr.path().segments.last().unwrap().ident.to_string();
296                let needs_clone =
297                    name == "primary_span" && matches!(inner_ty, FieldInnerTy::Vec(_));
298                let (binding, needs_destructure) = if needs_clone {
299                    // `primary_span` can accept a `Vec<Span>` so don't destructure that.
300                    (quote_spanned! {inner_ty.span()=> #field_binding.clone() }, false)
301                } else {
302                    (quote_spanned! {inner_ty.span()=> #field_binding }, true)
303                };
304
305                let generated_code = self
306                    .generate_inner_field_code(
307                        attr,
308                        FieldInfo { binding: binding_info, ty: inner_ty, span: &field.span() },
309                        binding,
310                    )
311                    .unwrap_or_else(|v| v.to_compile_error());
312
313                if needs_destructure {
314                    inner_ty.with(field_binding, generated_code)
315                } else {
316                    generated_code
317                }
318            })
319            .collect()
320    }
321
322    fn generate_inner_field_code(
323        &mut self,
324        attr: &Attribute,
325        info: FieldInfo<'_>,
326        binding: TokenStream,
327    ) -> Result<TokenStream, DiagnosticDeriveError> {
328        let ident = &attr.path().segments.last().unwrap().ident;
329        let name = ident.to_string();
330        match (&attr.meta, name.as_str()) {
331            // Don't need to do anything - by virtue of the attribute existing, the
332            // `arg` call will not be generated.
333            (Meta::Path(_), "skip_arg") => return Ok(quote! {}),
334            (Meta::Path(_), "primary_span") => {
335                match self.kind {
336                    DiagnosticDeriveKind::Diagnostic => {
337                        report_error_if_not_applied_to_span(attr, &info)?;
338
339                        return Ok(quote! {
340                            diag.span(#binding);
341                        });
342                    }
343                    DiagnosticDeriveKind::LintDiagnostic => {
344                        throw_invalid_attr!(attr, |diag| {
345                            diag.help("the `primary_span` field attribute is not valid for lint diagnostics")
346                        })
347                    }
348                }
349            }
350            (Meta::Path(_), "subdiagnostic") => {
351                return Ok(quote! { diag.subdiagnostic(#binding); });
352            }
353            _ => (),
354        }
355
356        let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else {
357            // Some attributes aren't errors - like documentation comments - but also aren't
358            // subdiagnostics.
359            return Ok(quote! {});
360        };
361        let fn_ident = format_ident!("{}", subdiag);
362        match subdiag {
363            SubdiagnosticKind::Label => {
364                report_error_if_not_applied_to_span(attr, &info)?;
365                Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
366            }
367            SubdiagnosticKind::Note
368            | SubdiagnosticKind::NoteOnce
369            | SubdiagnosticKind::Help
370            | SubdiagnosticKind::HelpOnce
371            | SubdiagnosticKind::Warn => {
372                let inner = info.ty.inner_type();
373                if type_matches_path(inner, &["rustc_span", "Span"])
374                    || type_matches_path(inner, &["rustc_span", "MultiSpan"])
375                {
376                    Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
377                } else if type_is_unit(inner)
378                    || (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
379                {
380                    Ok(self.add_subdiagnostic(&fn_ident, slug))
381                } else {
382                    report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
383                }
384            }
385            SubdiagnosticKind::Suggestion {
386                suggestion_kind,
387                applicability: static_applicability,
388                code_field,
389                code_init,
390            } => {
391                if let FieldInnerTy::Vec(_) = info.ty {
392                    throw_invalid_attr!(attr, |diag| {
393                        diag
394                        .note("`#[suggestion(...)]` applied to `Vec` field is ambiguous")
395                        .help("to show a suggestion consisting of multiple parts, use a `Subdiagnostic` annotated with `#[multipart_suggestion(...)]`")
396                        .help("to show a variable set of suggestions, use a `Vec` of `Subdiagnostic`s annotated with `#[suggestion(...)]`")
397                    });
398                }
399
400                let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?;
401
402                if let Some((static_applicability, span)) = static_applicability {
403                    applicability.set_once(quote! { #static_applicability }, span);
404                }
405
406                let applicability = applicability
407                    .value()
408                    .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
409                let style = suggestion_kind.to_suggestion_style();
410
411                self.formatting_init.extend(code_init);
412                Ok(quote! {
413                    diag.span_suggestions_with_style(
414                        #span_field,
415                        crate::fluent_generated::#slug,
416                        #code_field,
417                        #applicability,
418                        #style
419                    );
420                })
421            }
422            SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
423        }
424    }
425
426    /// Adds a spanned subdiagnostic by generating a `diag.span_$kind` call with the current slug
427    /// and `fluent_attr_identifier`.
428    fn add_spanned_subdiagnostic(
429        &self,
430        field_binding: TokenStream,
431        kind: &Ident,
432        fluent_attr_identifier: Path,
433    ) -> TokenStream {
434        let fn_name = format_ident!("span_{}", kind);
435        quote! {
436            diag.#fn_name(
437                #field_binding,
438                crate::fluent_generated::#fluent_attr_identifier
439            );
440        }
441    }
442
443    /// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug
444    /// and `fluent_attr_identifier`.
445    fn add_subdiagnostic(&self, kind: &Ident, fluent_attr_identifier: Path) -> TokenStream {
446        quote! {
447            diag.#kind(crate::fluent_generated::#fluent_attr_identifier);
448        }
449    }
450
451    fn span_and_applicability_of_ty(
452        &self,
453        info: FieldInfo<'_>,
454    ) -> Result<(TokenStream, SpannedOption<TokenStream>), DiagnosticDeriveError> {
455        match &info.ty.inner_type() {
456            // If `ty` is `Span` w/out applicability, then use `Applicability::Unspecified`.
457            ty @ Type::Path(..) if type_matches_path(ty, &["rustc_span", "Span"]) => {
458                let binding = &info.binding.binding;
459                Ok((quote!(#binding), None))
460            }
461            // If `ty` is `(Span, Applicability)` then return tokens accessing those.
462            Type::Tuple(tup) => {
463                let mut span_idx = None;
464                let mut applicability_idx = None;
465
466                fn type_err(span: &Span) -> Result<!, DiagnosticDeriveError> {
467                    span_err(span.unwrap(), "wrong types for suggestion")
468                        .help(
469                            "`#[suggestion(...)]` on a tuple field must be applied to fields \
470                             of type `(Span, Applicability)`",
471                        )
472                        .emit();
473                    Err(DiagnosticDeriveError::ErrorHandled)
474                }
475
476                for (idx, elem) in tup.elems.iter().enumerate() {
477                    if type_matches_path(elem, &["rustc_span", "Span"]) {
478                        span_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
479                    } else if type_matches_path(elem, &["rustc_errors", "Applicability"]) {
480                        applicability_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
481                    } else {
482                        type_err(&elem.span())?;
483                    }
484                }
485
486                let Some((span_idx, _)) = span_idx else {
487                    type_err(&tup.span())?;
488                };
489                let Some((applicability_idx, applicability_span)) = applicability_idx else {
490                    type_err(&tup.span())?;
491                };
492                let binding = &info.binding.binding;
493                let span = quote!(#binding.#span_idx);
494                let applicability = quote!(#binding.#applicability_idx);
495
496                Ok((span, Some((applicability, applicability_span))))
497            }
498            // If `ty` isn't a `Span` or `(Span, Applicability)` then emit an error.
499            _ => {
500                throw_span_err!(info.span.unwrap(), "wrong field type for suggestion", |diag| {
501                    diag.help(
502                        "`#[suggestion(...)]` should be applied to fields of type `Span` or \
503                     `(Span, Applicability)`",
504                    )
505                })
506            }
507        }
508    }
509}