Skip to main content

rustc_macros/diagnostics/
subdiagnostic.rs

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