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