flux_macros/diagnostics/
subdiagnostic.rs

1#![allow(clippy::pedantic)]
2#![deny(unused_must_use)]
3
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use syn::{Attribute, Meta, MetaList, Path, spanned::Spanned};
7use synstructure::{BindingInfo, Structure, VariantInfo};
8
9use super::utils::SubdiagnosticVariant;
10use crate::diagnostics::{
11    error::{DiagnosticDeriveError, invalid_attr, span_err, throw_invalid_attr, throw_span_err},
12    utils::{
13        AllowMultipleAlternatives, FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce,
14        SpannedOption, SubdiagnosticKind, build_field_mapping, build_suggestion_code,
15        is_doc_comment, new_code_ident, report_error_if_not_applied_to_applicability,
16        report_error_if_not_applied_to_span, should_generate_arg,
17    },
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
79                    .into_tokens()
80                    .unwrap_or_else(|v| v.to_compile_error())
81            });
82
83            quote! {
84                match self {
85                    #variants_
86                }
87            }
88        };
89
90        let diag = &self.diag;
91        let f = &self.f;
92        let ret = structure.gen_impl(quote! {
93            gen impl rustc_errors::Subdiagnostic for @Self {
94                fn add_to_diag_with<__G, __F>(
95                    self,
96                    #diag: &mut rustc_errors::Diag<'_, __G>,
97                    #f: &__F
98                ) where
99                    __G: rustc_errors::EmissionGuarantee,
100                    __F: rustc_errors::SubdiagMessageOp<__G>,
101                {
102                    #implementation
103                }
104            }
105        });
106        ret
107    }
108}
109
110/// Tracks persistent information required for building up the call to add to the diagnostic
111/// for the final generated method. This is a separate struct to `SubdiagnosticDerive`
112/// only to be able to destructure and split `self.builder` and the `self.structure` up to avoid a
113/// double mut borrow later on.
114struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
115    /// The identifier to use for the generated `Diag` instance.
116    parent: &'parent SubdiagnosticDerive,
117
118    /// Info for the current variant (or the type if not an enum).
119    variant: &'a VariantInfo<'a>,
120    /// Span for the entire type.
121    span: proc_macro::Span,
122
123    /// Initialization of format strings for code suggestions.
124    formatting_init: TokenStream,
125
126    /// Store a map of field name to its corresponding field. This is built on construction of the
127    /// derive builder.
128    fields: FieldMap,
129
130    /// Identifier for the binding to the `#[primary_span]` field.
131    span_field: SpannedOption<proc_macro2::Ident>,
132
133    /// The binding to the `#[applicability]` field, if present.
134    applicability: SpannedOption<TokenStream>,
135
136    /// Set to true when a `#[suggestion_part]` field is encountered, used to generate an error
137    /// during finalization if still `false`.
138    has_suggestion_parts: bool,
139
140    /// Set to true when a `#[subdiagnostic]` field is encountered, used to suppress the error
141    /// emitted when no subdiagnostic kinds are specified on the variant itself.
142    has_subdiagnostic: bool,
143
144    /// Set to true when this variant is an enum variant rather than just the body of a struct.
145    is_enum: bool,
146}
147
148impl<'parent, 'a> HasFieldMap for SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
149    fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
150        self.fields.get(field)
151    }
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, Path, bool)>, DiagnosticDeriveError> {
196        let mut kind_slugs = vec![];
197
198        for attr in self.variant.ast().attrs {
199            let Some(SubdiagnosticVariant { kind, slug, no_span }) =
200                SubdiagnosticVariant::from_attr(attr, self)?
201            else {
202                // Some attributes aren't errors - like documentation comments - but also aren't
203                // subdiagnostics.
204                continue;
205            };
206
207            let Some(slug) = slug 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 slug must be first argument of a `#[{name}(...)]` attribute"
215                    )
216                );
217            };
218
219            kind_slugs.push((kind, slug, no_span));
220        }
221
222        Ok(kind_slugs)
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            #diag.arg(
238                stringify!(#ident),
239                #field_binding
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) => {
287                self.generate_field_code_inner_list(
288                    kind_stats,
289                    attr,
290                    info,
291                    list,
292                    clone_suggestion_code,
293                )
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            _ => {
483                throw_invalid_attr!(attr, |diag| {
484                    let mut span_attrs = vec![];
485                    if kind_stats.has_multipart_suggestion {
486                        span_attrs.push("suggestion_part");
487                    }
488                    if !kind_stats.all_multipart_suggestions {
489                        span_attrs.push("primary_span")
490                    }
491                    diag.help(format!(
492                        "only `{}`, `applicability` and `skip_arg` are valid field attributes",
493                        span_attrs.join(", ")
494                    ))
495                })
496            }
497        }
498    }
499
500    pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
501        let kind_slugs = self.identify_kind()?;
502
503        let kind_stats: KindsStatistics = kind_slugs
504            .iter()
505            .map(|(kind, _slug, _no_span)| kind)
506            .collect();
507
508        let init = if kind_stats.has_multipart_suggestion {
509            quote! { let mut suggestions = Vec::new(); }
510        } else {
511            quote! {}
512        };
513
514        let attr_args: TokenStream = self
515            .variant
516            .bindings()
517            .iter()
518            .filter(|binding| !should_generate_arg(binding.ast()))
519            .map(|binding| self.generate_field_attr_code(binding, kind_stats))
520            .collect();
521
522        if kind_slugs.is_empty() && !self.has_subdiagnostic {
523            if self.is_enum {
524                // It's okay for a variant to not be a subdiagnostic at all..
525                return Ok(quote! {});
526            } else {
527                // ..but structs should always be _something_.
528                throw_span_err!(
529                    self.variant.ast().ident.span().unwrap(),
530                    "subdiagnostic kind not specified"
531                );
532            }
533        };
534
535        let span_field = self.span_field.value_ref();
536
537        let diag = &self.parent.diag;
538        let f = &self.parent.f;
539        let mut calls = TokenStream::new();
540        for (kind, slug, no_span) in kind_slugs {
541            let message = format_ident!("__message");
542            calls.extend(
543                quote! { let #message = #f(#diag, crate::fluent_generated::#slug.into()); },
544            );
545
546            let name = format_ident!(
547                "{}{}",
548                if span_field.is_some() && !no_span { "span_" } else { "" },
549                kind
550            );
551            let call = match kind {
552                SubdiagnosticKind::Suggestion {
553                    suggestion_kind,
554                    applicability,
555                    code_init,
556                    code_field,
557                } => {
558                    self.formatting_init.extend(code_init);
559
560                    let applicability = applicability
561                        .value()
562                        .map(|a| quote! { #a })
563                        .or_else(|| self.applicability.take().value())
564                        .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
565
566                    if let Some(span) = span_field {
567                        let style = suggestion_kind.to_suggestion_style();
568                        quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
569                    } else {
570                        span_err(self.span, "suggestion without `#[primary_span]` field").emit();
571                        quote! { unreachable!(); }
572                    }
573                }
574                SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
575                    let applicability = applicability
576                        .value()
577                        .map(|a| quote! { #a })
578                        .or_else(|| self.applicability.take().value())
579                        .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
580
581                    if !self.has_suggestion_parts {
582                        span_err(
583                            self.span,
584                            "multipart suggestion without any `#[suggestion_part(...)]` fields",
585                        )
586                        .emit();
587                    }
588
589                    let style = suggestion_kind.to_suggestion_style();
590
591                    quote! { #diag.#name(#message, suggestions, #applicability, #style); }
592                }
593                SubdiagnosticKind::Label => {
594                    if let Some(span) = span_field {
595                        quote! { #diag.#name(#span, #message); }
596                    } else {
597                        span_err(self.span, "label without `#[primary_span]` field").emit();
598                        quote! { unreachable!(); }
599                    }
600                }
601                _ => {
602                    if let Some(span) = span_field
603                        && !no_span
604                    {
605                        quote! { #diag.#name(#span, #message); }
606                    } else {
607                        quote! { #diag.#name(#message); }
608                    }
609                }
610            };
611
612            calls.extend(call);
613        }
614
615        let plain_args: TokenStream = self
616            .variant
617            .bindings()
618            .iter()
619            .filter(|binding| should_generate_arg(binding.ast()))
620            .map(|binding| self.generate_field_arg(binding))
621            .collect();
622
623        let formatting_init = &self.formatting_init;
624        Ok(quote! {
625            #init
626            #formatting_init
627            #attr_args
628            #plain_args
629            #calls
630        })
631    }
632}