Skip to main content

rustc_macros/diagnostics/
subdiagnostic.rs

1#![deny(unused_must_use)]
2
3use proc_macro2::{Ident, TokenStream};
4use quote::{format_ident, quote};
5use syn::parse::ParseStream;
6use syn::spanned::Spanned;
7use syn::{Attribute, Meta, MetaList, Path, Token};
8use synstructure::{BindingInfo, Structure, VariantInfo};
9
10use super::utils::SubdiagnosticVariant;
11use crate::diagnostics::error::{
12    DiagnosticDeriveError, invalid_attr, span_err, throw_invalid_attr, throw_span_err,
13};
14use crate::diagnostics::message::Message;
15use crate::diagnostics::utils::{
16    AllowMultipleAlternatives, FieldInfo, FieldInnerTy, FieldMap, SetOnce, SpannedOption,
17    SubdiagnosticKind, build_field_mapping, build_suggestion_code, is_doc_comment, new_code_ident,
18    report_error_if_not_applied_to_applicability, report_error_if_not_applied_to_span,
19    should_generate_arg,
20};
21
22/// The central struct for constructing the `add_to_diag` method from an annotated struct.
23pub(crate) struct SubdiagnosticDerive {
24    diag: syn::Ident,
25}
26
27impl SubdiagnosticDerive {
28    pub(crate) fn new() -> Self {
29        let diag = format_ident!("diag");
30        Self { diag }
31    }
32
33    pub(crate) fn into_tokens(self, mut structure: Structure<'_>) -> TokenStream {
34        let implementation = {
35            let ast = structure.ast();
36            let span = ast.span().unwrap();
37            match ast.data {
38                syn::Data::Struct(..) | syn::Data::Enum(..) => (),
39                syn::Data::Union(..) => {
40                    span_err(
41                        span,
42                        "`#[derive(Subdiagnostic)]` can only be used on structs and enums",
43                    )
44                    .emit();
45                }
46            }
47
48            let is_enum = matches!(ast.data, syn::Data::Enum(..));
49            if is_enum {
50                for attr in &ast.attrs {
51                    // Always allow documentation comments.
52                    if is_doc_comment(attr) {
53                        continue;
54                    }
55
56                    span_err(
57                        attr.span().unwrap(),
58                        "unsupported type attribute for subdiagnostic enum",
59                    )
60                    .emit();
61                }
62            }
63
64            structure.bind_with(|_| synstructure::BindStyle::Move);
65            let variants_ = structure.each_variant(|variant| {
66                let mut builder = SubdiagnosticDeriveVariantBuilder {
67                    parent: &self,
68                    variant,
69                    span,
70                    formatting_init: TokenStream::new(),
71                    fields: build_field_mapping(variant),
72                    span_field: None,
73                    applicability: None,
74                    has_suggestion_parts: false,
75                    has_subdiagnostic: false,
76                    is_enum,
77                };
78                builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
79            });
80
81            quote! {
82                match self {
83                    #variants_
84                }
85            }
86        };
87
88        let diag = &self.diag;
89
90        // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here?
91        #[allow(keyword_idents_2024)]
92        let ret = structure.gen_impl(quote! {
93            gen impl rustc_errors::Subdiagnostic for @Self {
94                fn add_to_diag<__G>(
95                    self,
96                    #diag: &mut rustc_errors::Diag<'_, __G>,
97                ) where
98                    __G: rustc_errors::EmissionGuarantee,
99                {
100                    #implementation
101                }
102            }
103        });
104
105        ret
106    }
107}
108
109/// Tracks persistent information required for building up the call to add to the diagnostic
110/// for the final generated method. This is a separate struct to `SubdiagnosticDerive`
111/// only to be able to destructure and split `self.builder` and the `self.structure` up to avoid a
112/// double mut borrow later on.
113struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
114    /// The identifier to use for the generated `Diag` instance.
115    parent: &'parent SubdiagnosticDerive,
116
117    /// Info for the current variant (or the type if not an enum).
118    variant: &'a VariantInfo<'a>,
119    /// Span for the entire type.
120    span: proc_macro::Span,
121
122    /// Initialization of format strings for code suggestions.
123    formatting_init: TokenStream,
124
125    /// Store a map of field name to its corresponding field. This is built on construction of the
126    /// derive builder.
127    fields: FieldMap,
128
129    /// Identifier for the binding to the `#[primary_span]` field.
130    span_field: SpannedOption<proc_macro2::Ident>,
131
132    /// The binding to the `#[applicability]` field, if present.
133    applicability: SpannedOption<TokenStream>,
134
135    /// Set to true when a `#[suggestion_part]` field is encountered, used to generate an error
136    /// during finalization if still `false`.
137    has_suggestion_parts: bool,
138
139    /// Set to true when a `#[subdiagnostic]` field is encountered, used to suppress the error
140    /// emitted when no subdiagnostic kinds are specified on the variant itself.
141    has_subdiagnostic: bool,
142
143    /// Set to true when this variant is an enum variant rather than just the body of a struct.
144    is_enum: bool,
145}
146
147/// Provides frequently-needed information about the diagnostic kinds being derived for this type.
148#[derive(Clone, Copy, Debug)]
149struct KindsStatistics {
150    has_multipart_suggestion: bool,
151    all_multipart_suggestions: bool,
152    has_normal_suggestion: bool,
153    all_applicabilities_static: bool,
154}
155
156impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics {
157    fn from_iter<T: IntoIterator<Item = &'a SubdiagnosticKind>>(kinds: T) -> Self {
158        let mut ret = Self {
159            has_multipart_suggestion: false,
160            all_multipart_suggestions: true,
161            has_normal_suggestion: false,
162            all_applicabilities_static: true,
163        };
164
165        for kind in kinds {
166            if let SubdiagnosticKind::MultipartSuggestion { applicability: None, .. }
167            | SubdiagnosticKind::Suggestion { applicability: None, .. } = kind
168            {
169                ret.all_applicabilities_static = false;
170            }
171            if let SubdiagnosticKind::MultipartSuggestion { .. } = kind {
172                ret.has_multipart_suggestion = true;
173            } else {
174                ret.all_multipart_suggestions = false;
175            }
176
177            if let SubdiagnosticKind::Suggestion { .. } = kind {
178                ret.has_normal_suggestion = true;
179            }
180        }
181        ret
182    }
183}
184
185impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
186    fn identify_kind(
187        &mut self,
188    ) -> Result<Vec<(SubdiagnosticKind, Message)>, DiagnosticDeriveError> {
189        let mut kind_messages = vec![];
190
191        for attr in self.variant.ast().attrs {
192            let Some(SubdiagnosticVariant { kind, message }) =
193                SubdiagnosticVariant::from_attr(attr, &self.fields)?
194            else {
195                // Some attributes aren't errors - like documentation comments - but also aren't
196                // subdiagnostics.
197                continue;
198            };
199
200            let Some(message) = message else {
201                let name = attr.path().segments.last().unwrap().ident.to_string();
202                let name = name.as_str();
203
204                throw_span_err!(
205                    attr.span().unwrap(),
206                    format!(
207                        "diagnostic message must be first argument of a `#[{name}(...)]` attribute"
208                    )
209                );
210            };
211
212            kind_messages.push((kind, message));
213        }
214
215        Ok(kind_messages)
216    }
217
218    /// Generates the code for a field with no attributes.
219    fn generate_field_arg(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
220        let diag = &self.parent.diag;
221
222        let field = binding_info.ast();
223        let mut field_binding = binding_info.binding.clone();
224        field_binding.set_span(field.ty.span());
225
226        let ident = field.ident.as_ref().unwrap();
227        let ident = format_ident!("{}", ident); // strip `r#` prefix, if present
228
229        quote! {
230            #diag.arg(
231                stringify!(#ident),
232                #field_binding
233            );
234        }
235    }
236
237    /// Generates the necessary code for all attributes on a field.
238    fn generate_field_attr_code(
239        &mut self,
240        binding: &BindingInfo<'_>,
241        kind_stats: KindsStatistics,
242    ) -> TokenStream {
243        let ast = binding.ast();
244        assert!(ast.attrs.len() > 0, "field without attributes generating attr code");
245
246        // Abstract over `Vec<T>` and `Option<T>` fields using `FieldInnerTy`, which will
247        // apply the generated code on each element in the `Vec` or `Option`.
248        let inner_ty = FieldInnerTy::from_type(&ast.ty);
249        ast.attrs
250            .iter()
251            .map(|attr| {
252                // Always allow documentation comments.
253                if is_doc_comment(attr) {
254                    return quote! {};
255                }
256
257                let info = FieldInfo { binding, ty: inner_ty, span: &ast.span() };
258
259                let generated = self
260                    .generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate())
261                    .unwrap_or_else(|v| v.to_compile_error());
262
263                inner_ty.with(binding, generated)
264            })
265            .collect()
266    }
267
268    fn generate_field_code_inner(
269        &mut self,
270        kind_stats: KindsStatistics,
271        attr: &Attribute,
272        info: FieldInfo<'_>,
273        clone_suggestion_code: bool,
274    ) -> Result<TokenStream, DiagnosticDeriveError> {
275        match &attr.meta {
276            Meta::Path(path) => {
277                self.generate_field_code_inner_path(kind_stats, attr, info, path.clone())
278            }
279            Meta::List(list) => self.generate_field_code_inner_list(
280                kind_stats,
281                attr,
282                info,
283                list,
284                clone_suggestion_code,
285            ),
286            _ => throw_invalid_attr!(attr),
287        }
288    }
289
290    /// Generates the code for a `[Meta::Path]`-like attribute on a field (e.g. `#[primary_span]`).
291    fn generate_field_code_inner_path(
292        &mut self,
293        kind_stats: KindsStatistics,
294        attr: &Attribute,
295        info: FieldInfo<'_>,
296        path: Path,
297    ) -> Result<TokenStream, DiagnosticDeriveError> {
298        let span = attr.span().unwrap();
299        let ident = &path.segments.last().unwrap().ident;
300        let name = ident.to_string();
301        let name = name.as_str();
302
303        match name {
304            "skip_arg" => Ok(quote! {}),
305            "primary_span" => {
306                if kind_stats.has_multipart_suggestion {
307                    invalid_attr(attr)
308                        .help(
309                            "multipart suggestions use one or more `#[suggestion_part]`s rather \
310                            than one `#[primary_span]`",
311                        )
312                        .emit();
313                } else {
314                    report_error_if_not_applied_to_span(attr, &info)?;
315
316                    let binding = info.binding.binding.clone();
317                    // FIXME(#100717): support `Option<Span>` on `primary_span` like in the
318                    // diagnostic derive
319                    if !matches!(info.ty, FieldInnerTy::Plain(_)) {
320                        throw_invalid_attr!(attr, |diag| {
321                            let diag = diag.note("there must be exactly one primary span");
322
323                            if kind_stats.has_normal_suggestion {
324                                diag.help(
325                                    "to create a suggestion with multiple spans, \
326                                     use `#[multipart_suggestion]` instead",
327                                )
328                            } else {
329                                diag
330                            }
331                        });
332                    }
333
334                    self.span_field.set_once(binding, span);
335                }
336
337                Ok(quote! {})
338            }
339            "suggestion_part" => {
340                self.has_suggestion_parts = true;
341
342                if kind_stats.has_multipart_suggestion {
343                    span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
344                        .emit();
345                } else {
346                    invalid_attr(attr)
347                        .help(
348                            "`#[suggestion_part(...)]` is only valid in multipart suggestions, \
349                             use `#[primary_span]` instead",
350                        )
351                        .emit();
352                }
353
354                Ok(quote! {})
355            }
356            "applicability" => {
357                if kind_stats.has_multipart_suggestion || kind_stats.has_normal_suggestion {
358                    report_error_if_not_applied_to_applicability(attr, &info)?;
359
360                    if kind_stats.all_applicabilities_static {
361                        span_err(
362                            span,
363                            "`#[applicability]` has no effect if all `#[suggestion]`/\
364                             `#[multipart_suggestion]` attributes have a static \
365                             `applicability = \"...\"`",
366                        )
367                        .emit();
368                    }
369                    let binding = info.binding.binding.clone();
370                    self.applicability.set_once(quote! { #binding }, span);
371                } else {
372                    span_err(span, "`#[applicability]` is only valid on suggestions").emit();
373                }
374
375                Ok(quote! {})
376            }
377            "subdiagnostic" => {
378                let diag = &self.parent.diag;
379                let binding = &info.binding;
380                self.has_subdiagnostic = true;
381                Ok(quote! { #binding.add_to_diag(#diag); })
382            }
383            _ => {
384                let mut span_attrs = vec![];
385                if kind_stats.has_multipart_suggestion {
386                    span_attrs.push("suggestion_part");
387                }
388                if !kind_stats.all_multipart_suggestions {
389                    span_attrs.push("primary_span")
390                }
391
392                invalid_attr(attr)
393                    .help(format!(
394                        "only `{}`, `applicability` and `skip_arg` are valid field attributes",
395                        span_attrs.join(", ")
396                    ))
397                    .emit();
398
399                Ok(quote! {})
400            }
401        }
402    }
403
404    /// Generates the code for a `[Meta::List]`-like attribute on a field (e.g.
405    /// `#[suggestion_part(code = "...")]`).
406    fn generate_field_code_inner_list(
407        &mut self,
408        kind_stats: KindsStatistics,
409        attr: &Attribute,
410        info: FieldInfo<'_>,
411        list: &MetaList,
412        clone_suggestion_code: bool,
413    ) -> Result<TokenStream, DiagnosticDeriveError> {
414        let span = attr.span().unwrap();
415        let mut ident = list.path.segments.last().unwrap().ident.clone();
416        ident.set_span(info.ty.span());
417        let name = ident.to_string();
418        let name = name.as_str();
419
420        match name {
421            "suggestion_part" => {
422                if !kind_stats.has_multipart_suggestion {
423                    throw_invalid_attr!(attr, |diag| {
424                        diag.help(
425                            "`#[suggestion_part(...)]` is only valid in multipart suggestions",
426                        )
427                    })
428                }
429
430                self.has_suggestion_parts = true;
431
432                report_error_if_not_applied_to_span(attr, &info)?;
433
434                let mut code = None;
435
436                list.parse_args_with(|input: ParseStream<'_>| {
437                    while !input.is_empty() {
438                        let arg_name = input.parse::<Ident>()?;
439                        match arg_name.to_string().as_str() {
440                            "code" => {
441                                let code_field = new_code_ident();
442                                let formatting_init = build_suggestion_code(
443                                    &code_field,
444                                    input,
445                                    &self.fields,
446                                    AllowMultipleAlternatives::No,
447                                )?;
448                                code.set_once(
449                                    (code_field, formatting_init),
450                                    arg_name.span().unwrap(),
451                                );
452                            }
453                            _ => {
454                                span_err(
455                                    arg_name.span().unwrap(),
456                                    "`code` is the only valid nested attribute",
457                                )
458                                .emit();
459                            }
460                        }
461                        if input.is_empty() {
462                            break;
463                        }
464                        input.parse::<Token![,]>()?;
465                    }
466                    Ok(())
467                })?;
468
469                let Some((code_field, formatting_init)) = code.value() else {
470                    span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
471                        .emit();
472                    return Ok(quote! {});
473                };
474                let binding = info.binding;
475
476                self.formatting_init.extend(formatting_init);
477                let code_field = if clone_suggestion_code {
478                    quote! { #code_field.clone() }
479                } else {
480                    quote! { #code_field }
481                };
482                Ok(quote! { suggestions.push((#binding, #code_field)); })
483            }
484            _ => throw_invalid_attr!(attr, |diag| {
485                let mut span_attrs = vec![];
486                if kind_stats.has_multipart_suggestion {
487                    span_attrs.push("suggestion_part");
488                }
489                if !kind_stats.all_multipart_suggestions {
490                    span_attrs.push("primary_span")
491                }
492                diag.help(format!(
493                    "only `{}`, `applicability` and `skip_arg` are valid field attributes",
494                    span_attrs.join(", ")
495                ))
496            }),
497        }
498    }
499
500    pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
501        let kind_messages = self.identify_kind()?;
502
503        let kind_stats: KindsStatistics = kind_messages.iter().map(|(kind, _msg)| kind).collect();
504
505        let init = if kind_stats.has_multipart_suggestion {
506            quote! { let mut suggestions = Vec::new(); }
507        } else {
508            quote! {}
509        };
510
511        let attr_args: TokenStream = self
512            .variant
513            .bindings()
514            .iter()
515            .filter(|binding| !should_generate_arg(binding.ast()))
516            .map(|binding| self.generate_field_attr_code(binding, kind_stats))
517            .collect();
518
519        if kind_messages.is_empty() && !self.has_subdiagnostic {
520            if self.is_enum {
521                // It's okay for a variant to not be a subdiagnostic at all..
522                return Ok(quote! {});
523            } else {
524                // ..but structs should always be _something_.
525                throw_span_err!(
526                    self.variant.ast().ident.span().unwrap(),
527                    "subdiagnostic kind not specified"
528                );
529            }
530        };
531
532        let span_field = self.span_field.value_ref();
533
534        let diag = &self.parent.diag;
535        let mut calls = TokenStream::new();
536        for (kind, messages) in kind_messages {
537            let message = format_ident!("__message");
538            let message_stream = messages.diag_message(None);
539            calls.extend(quote! { let #message = #diag.eagerly_translate(#message_stream); });
540
541            let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
542            let call = match kind {
543                SubdiagnosticKind::Suggestion {
544                    suggestion_kind,
545                    applicability,
546                    code_init,
547                    code_field,
548                } => {
549                    self.formatting_init.extend(code_init);
550
551                    let applicability = applicability
552                        .value()
553                        .map(|a| quote! { #a })
554                        .or_else(|| self.applicability.take().value())
555                        .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
556
557                    if let Some(span) = span_field {
558                        let style = suggestion_kind.to_suggestion_style();
559                        quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
560                    } else {
561                        span_err(self.span, "suggestion without `#[primary_span]` field").emit();
562                        quote! { unreachable!(); }
563                    }
564                }
565                SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
566                    let applicability = applicability
567                        .value()
568                        .map(|a| quote! { #a })
569                        .or_else(|| self.applicability.take().value())
570                        .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
571
572                    if !self.has_suggestion_parts {
573                        span_err(
574                            self.span,
575                            "multipart suggestion without any `#[suggestion_part(...)]` fields",
576                        )
577                        .emit();
578                    }
579
580                    let style = suggestion_kind.to_suggestion_style();
581
582                    quote! { #diag.#name(#message, suggestions, #applicability, #style); }
583                }
584                SubdiagnosticKind::Label => {
585                    if let Some(span) = span_field {
586                        quote! { #diag.#name(#span, #message); }
587                    } else {
588                        span_err(self.span, "label without `#[primary_span]` field").emit();
589                        quote! { unreachable!(); }
590                    }
591                }
592                _ => {
593                    if let Some(span) = span_field {
594                        quote! { #diag.#name(#span, #message); }
595                    } else {
596                        quote! { #diag.#name(#message); }
597                    }
598                }
599            };
600
601            calls.extend(call);
602        }
603        let store_args = quote! {
604            #diag.store_args();
605        };
606        let restore_args = quote! {
607            #diag.restore_args();
608        };
609        let plain_args: TokenStream = self
610            .variant
611            .bindings()
612            .iter()
613            .filter(|binding| should_generate_arg(binding.ast()))
614            .map(|binding| self.generate_field_arg(binding))
615            .collect();
616
617        let formatting_init = &self.formatting_init;
618
619        // For #[derive(Subdiagnostic)]
620        //
621        // - Store args of the main diagnostic for later restore.
622        // - Add args of subdiagnostic.
623        // - Generate the calls, such as note, label, etc.
624        // - Restore the arguments for allowing main and subdiagnostic share the same fields.
625        Ok(quote! {
626            #init
627            #formatting_init
628            #attr_args
629            #store_args
630            #plain_args
631            #calls
632            #restore_args
633        })
634    }
635}