Skip to main content

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