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