Skip to main content

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