flux_macros/diagnostics/
subdiagnostic.rs

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