rustc_macros/diagnostics/
diagnostic_builder.rs

1#![deny(unused_must_use)]
2
3use proc_macro2::{Ident, Span, TokenStream};
4use quote::{format_ident, quote, quote_spanned};
5use syn::spanned::Spanned;
6use syn::{Attribute, Meta, Path, Token, Type, parse_quote};
7use synstructure::{BindingInfo, Structure, VariantInfo};
8
9use super::utils::SubdiagnosticVariant;
10use crate::diagnostics::error::{
11    DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
12};
13use crate::diagnostics::utils::{
14    FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce, SpannedOption, SubdiagnosticKind,
15    build_field_mapping, is_doc_comment, report_error_if_not_applied_to_span, report_type_error,
16    should_generate_arg, type_is_bool, type_is_unit, type_matches_path,
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).unwrap_or_else(|v| v.to_compile_error())
120        });
121
122        quote! {
123            #(#preamble)*;
124        }
125    }
126
127    /// Generates calls to `span_label` and similar functions based on the attributes on fields or
128    /// calls to `arg` when no attributes are present.
129    pub(crate) fn body(&mut self, variant: &VariantInfo<'_>) -> TokenStream {
130        let mut body = quote! {};
131        // Generate `arg` calls first..
132        for binding in variant.bindings().iter().filter(|bi| should_generate_arg(bi.ast())) {
133            body.extend(self.generate_field_code(binding));
134        }
135        // ..and then subdiagnostic additions.
136        for binding in variant.bindings().iter().filter(|bi| !should_generate_arg(bi.ast())) {
137            body.extend(self.generate_field_attrs_code(binding));
138        }
139        body
140    }
141
142    /// Parse a `SubdiagnosticKind` from an `Attribute`.
143    fn parse_subdiag_attribute(
144        &self,
145        attr: &Attribute,
146    ) -> Result<Option<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
147        let Some(subdiag) = SubdiagnosticVariant::from_attr(attr, self)? else {
148            // Some attributes aren't errors - like documentation comments - but also aren't
149            // subdiagnostics.
150            return Ok(None);
151        };
152
153        if let SubdiagnosticKind::MultipartSuggestion { .. } = subdiag.kind {
154            throw_invalid_attr!(attr, |diag| diag
155                .help("consider creating a `Subdiagnostic` instead"));
156        }
157
158        let slug = subdiag.slug.unwrap_or_else(|| match subdiag.kind {
159            SubdiagnosticKind::Label => parse_quote! { _subdiag::label },
160            SubdiagnosticKind::Note => parse_quote! { _subdiag::note },
161            SubdiagnosticKind::NoteOnce => parse_quote! { _subdiag::note_once },
162            SubdiagnosticKind::Help => parse_quote! { _subdiag::help },
163            SubdiagnosticKind::HelpOnce => parse_quote! { _subdiag::help_once },
164            SubdiagnosticKind::Warn => parse_quote! { _subdiag::warn },
165            SubdiagnosticKind::Suggestion { .. } => parse_quote! { _subdiag::suggestion },
166            SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
167        });
168
169        Ok(Some((subdiag.kind, slug, subdiag.no_span)))
170    }
171
172    /// Establishes state in the `DiagnosticDeriveBuilder` resulting from the struct
173    /// attributes like `#[diag(..)]`, such as the slug and error code. Generates
174    /// diagnostic builder calls for setting error code and creating note/help messages.
175    fn generate_structure_code_for_attr(
176        &mut self,
177        attr: &Attribute,
178    ) -> Result<TokenStream, DiagnosticDeriveError> {
179        // Always allow documentation comments.
180        if is_doc_comment(attr) {
181            return Ok(quote! {});
182        }
183
184        let name = attr.path().segments.last().unwrap().ident.to_string();
185        let name = name.as_str();
186
187        let mut first = true;
188
189        if name == "diag" {
190            let mut tokens = TokenStream::new();
191            attr.parse_nested_meta(|nested| {
192                let path = &nested.path;
193
194                if first && (nested.input.is_empty() || nested.input.peek(Token![,])) {
195                    self.slug.set_once(path.clone(), path.span().unwrap());
196                    first = false;
197                    return Ok(());
198                }
199
200                first = false;
201
202                let Ok(nested) = nested.value() else {
203                    span_err(
204                        nested.input.span().unwrap(),
205                        "diagnostic slug must be the first argument",
206                    )
207                    .emit();
208                    return Ok(());
209                };
210
211                if path.is_ident("code") {
212                    self.code.set_once((), path.span().unwrap());
213
214                    let code = nested.parse::<syn::Expr>()?;
215                    tokens.extend(quote! {
216                        diag.code(#code);
217                    });
218                } else {
219                    span_err(path.span().unwrap(), "unknown argument")
220                        .note("only the `code` parameter is valid after the slug")
221                        .emit();
222
223                    // consume the buffer so we don't have syntax errors from syn
224                    let _ = nested.parse::<TokenStream>();
225                }
226                Ok(())
227            })?;
228            return Ok(tokens);
229        }
230
231        let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else {
232            // Some attributes aren't errors - like documentation comments - but also aren't
233            // subdiagnostics.
234            return Ok(quote! {});
235        };
236        let fn_ident = format_ident!("{}", subdiag);
237        match subdiag {
238            SubdiagnosticKind::Note
239            | SubdiagnosticKind::NoteOnce
240            | SubdiagnosticKind::Help
241            | SubdiagnosticKind::HelpOnce
242            | SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug)),
243            SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => {
244                throw_invalid_attr!(attr, |diag| diag
245                    .help("`#[label]` and `#[suggestion]` can only be applied to fields"));
246            }
247            SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
248        }
249    }
250
251    fn generate_field_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
252        let field = binding_info.ast();
253        let mut field_binding = binding_info.binding.clone();
254        field_binding.set_span(field.ty.span());
255
256        let Some(ident) = field.ident.as_ref() else {
257            span_err(field.span().unwrap(), "tuple structs are not supported").emit();
258            return TokenStream::new();
259        };
260        let ident = format_ident!("{}", ident); // strip `r#` prefix, if present
261
262        quote! {
263            diag.arg(
264                stringify!(#ident),
265                #field_binding
266            );
267        }
268    }
269
270    fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
271        let field = binding_info.ast();
272        let field_binding = &binding_info.binding;
273
274        let inner_ty = FieldInnerTy::from_type(&field.ty);
275        let mut seen_label = false;
276
277        field
278            .attrs
279            .iter()
280            .map(move |attr| {
281                // Always allow documentation comments.
282                if is_doc_comment(attr) {
283                    return quote! {};
284                }
285
286                let name = attr.path().segments.last().unwrap().ident.to_string();
287
288                if name == "primary_span" && seen_label {
289                    span_err(attr.span().unwrap(), format!("`#[primary_span]` must be placed before labels, since it overwrites the span of the diagnostic")).emit();
290                }
291                if name == "label" {
292                    seen_label = true;
293                }
294
295                let needs_clone =
296                    name == "primary_span" && matches!(inner_ty, FieldInnerTy::Vec(_));
297                let (binding, needs_destructure) = if needs_clone {
298                    // `primary_span` can accept a `Vec<Span>` so don't destructure that.
299                    (quote_spanned! {inner_ty.span()=> #field_binding.clone() }, false)
300                } else {
301                    (quote_spanned! {inner_ty.span()=> #field_binding }, true)
302                };
303
304                let generated_code = self
305                    .generate_inner_field_code(
306                        attr,
307                        FieldInfo { binding: binding_info, ty: inner_ty, span: &field.span() },
308                        binding,
309                    )
310                    .unwrap_or_else(|v| v.to_compile_error());
311
312                if needs_destructure {
313                    inner_ty.with(field_binding, generated_code)
314                } else {
315                    generated_code
316                }
317            })
318            .collect()
319    }
320
321    fn generate_inner_field_code(
322        &mut self,
323        attr: &Attribute,
324        info: FieldInfo<'_>,
325        binding: TokenStream,
326    ) -> Result<TokenStream, DiagnosticDeriveError> {
327        let ident = &attr.path().segments.last().unwrap().ident;
328        let name = ident.to_string();
329        match (&attr.meta, name.as_str()) {
330            // Don't need to do anything - by virtue of the attribute existing, the
331            // `arg` call will not be generated.
332            (Meta::Path(_), "skip_arg") => return Ok(quote! {}),
333            (Meta::Path(_), "primary_span") => {
334                match self.kind {
335                    DiagnosticDeriveKind::Diagnostic => {
336                        report_error_if_not_applied_to_span(attr, &info)?;
337
338                        return Ok(quote! {
339                            diag.span(#binding);
340                        });
341                    }
342                    DiagnosticDeriveKind::LintDiagnostic => {
343                        throw_invalid_attr!(attr, |diag| {
344                            diag.help("the `primary_span` field attribute is not valid for lint diagnostics")
345                        })
346                    }
347                }
348            }
349            (Meta::Path(_), "subdiagnostic") => {
350                return Ok(quote! { diag.subdiagnostic(#binding); });
351            }
352            _ => (),
353        }
354
355        let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else {
356            // Some attributes aren't errors - like documentation comments - but also aren't
357            // subdiagnostics.
358            return Ok(quote! {});
359        };
360        let fn_ident = format_ident!("{}", subdiag);
361        match subdiag {
362            SubdiagnosticKind::Label => {
363                report_error_if_not_applied_to_span(attr, &info)?;
364                Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
365            }
366            SubdiagnosticKind::Note
367            | SubdiagnosticKind::NoteOnce
368            | SubdiagnosticKind::Help
369            | SubdiagnosticKind::HelpOnce
370            | SubdiagnosticKind::Warn => {
371                let inner = info.ty.inner_type();
372                if type_matches_path(inner, &["rustc_span", "Span"])
373                    || type_matches_path(inner, &["rustc_span", "MultiSpan"])
374                {
375                    Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
376                } else if type_is_unit(inner)
377                    || (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
378                {
379                    Ok(self.add_subdiagnostic(&fn_ident, slug))
380                } else {
381                    report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
382                }
383            }
384            SubdiagnosticKind::Suggestion {
385                suggestion_kind,
386                applicability: static_applicability,
387                code_field,
388                code_init,
389            } => {
390                if let FieldInnerTy::Vec(_) = info.ty {
391                    throw_invalid_attr!(attr, |diag| {
392                        diag
393                        .note("`#[suggestion(...)]` applied to `Vec` field is ambiguous")
394                        .help("to show a suggestion consisting of multiple parts, use a `Subdiagnostic` annotated with `#[multipart_suggestion(...)]`")
395                        .help("to show a variable set of suggestions, use a `Vec` of `Subdiagnostic`s annotated with `#[suggestion(...)]`")
396                    });
397                }
398
399                let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?;
400
401                if let Some((static_applicability, span)) = static_applicability {
402                    applicability.set_once(quote! { #static_applicability }, span);
403                }
404
405                let applicability = applicability
406                    .value()
407                    .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
408                let style = suggestion_kind.to_suggestion_style();
409
410                self.formatting_init.extend(code_init);
411                Ok(quote! {
412                    diag.span_suggestions_with_style(
413                        #span_field,
414                        crate::fluent_generated::#slug,
415                        #code_field,
416                        #applicability,
417                        #style
418                    );
419                })
420            }
421            SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
422        }
423    }
424
425    /// Adds a spanned subdiagnostic by generating a `diag.span_$kind` call with the current slug
426    /// and `fluent_attr_identifier`.
427    fn add_spanned_subdiagnostic(
428        &self,
429        field_binding: TokenStream,
430        kind: &Ident,
431        fluent_attr_identifier: Path,
432    ) -> TokenStream {
433        let fn_name = format_ident!("span_{}", kind);
434        quote! {
435            diag.#fn_name(
436                #field_binding,
437                crate::fluent_generated::#fluent_attr_identifier
438            );
439        }
440    }
441
442    /// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug
443    /// and `fluent_attr_identifier`.
444    fn add_subdiagnostic(&self, kind: &Ident, fluent_attr_identifier: Path) -> TokenStream {
445        quote! {
446            diag.#kind(crate::fluent_generated::#fluent_attr_identifier);
447        }
448    }
449
450    fn span_and_applicability_of_ty(
451        &self,
452        info: FieldInfo<'_>,
453    ) -> Result<(TokenStream, SpannedOption<TokenStream>), DiagnosticDeriveError> {
454        match &info.ty.inner_type() {
455            // If `ty` is `Span` w/out applicability, then use `Applicability::Unspecified`.
456            ty @ Type::Path(..) if type_matches_path(ty, &["rustc_span", "Span"]) => {
457                let binding = &info.binding.binding;
458                Ok((quote!(#binding), None))
459            }
460            // If `ty` is `(Span, Applicability)` then return tokens accessing those.
461            Type::Tuple(tup) => {
462                let mut span_idx = None;
463                let mut applicability_idx = None;
464
465                fn type_err(span: &Span) -> Result<!, DiagnosticDeriveError> {
466                    span_err(span.unwrap(), "wrong types for suggestion")
467                        .help(
468                            "`#[suggestion(...)]` on a tuple field must be applied to fields \
469                             of type `(Span, Applicability)`",
470                        )
471                        .emit();
472                    Err(DiagnosticDeriveError::ErrorHandled)
473                }
474
475                for (idx, elem) in tup.elems.iter().enumerate() {
476                    if type_matches_path(elem, &["rustc_span", "Span"]) {
477                        span_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
478                    } else if type_matches_path(elem, &["rustc_errors", "Applicability"]) {
479                        applicability_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
480                    } else {
481                        type_err(&elem.span())?;
482                    }
483                }
484
485                let Some((span_idx, _)) = span_idx else {
486                    type_err(&tup.span())?;
487                };
488                let Some((applicability_idx, applicability_span)) = applicability_idx else {
489                    type_err(&tup.span())?;
490                };
491                let binding = &info.binding.binding;
492                let span = quote!(#binding.#span_idx);
493                let applicability = quote!(#binding.#applicability_idx);
494
495                Ok((span, Some((applicability, applicability_span))))
496            }
497            // If `ty` isn't a `Span` or `(Span, Applicability)` then emit an error.
498            _ => throw_span_err!(info.span.unwrap(), "wrong field type for suggestion", |diag| {
499                diag.help(
500                    "`#[suggestion(...)]` should be applied to fields of type `Span` or \
501                     `(Span, Applicability)`",
502                )
503            }),
504        }
505    }
506}