rustc_macros/diagnostics/
subdiagnostic.rs

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