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<__G>(
98                    self,
99                    #diag: &mut rustc_errors::Diag<'_, __G>,
100                ) where
101                    __G: rustc_errors::EmissionGuarantee,
102                {
103                    #implementation
104                }
105            }
106        });
107
108        ret
109    }
110}
111
112/// Tracks persistent information required for building up the call to add to the diagnostic
113/// for the final generated method. This is a separate struct to `SubdiagnosticDerive`
114/// only to be able to destructure and split `self.builder` and the `self.structure` up to avoid a
115/// double mut borrow later on.
116struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
117    /// The identifier to use for the generated `Diag` instance.
118    parent: &'parent SubdiagnosticDerive,
119
120    /// Info for the current variant (or the type if not an enum).
121    variant: &'a VariantInfo<'a>,
122    /// Span for the entire type.
123    span: proc_macro::Span,
124
125    /// Initialization of format strings for code suggestions.
126    formatting_init: TokenStream,
127
128    /// Store a map of field name to its corresponding field. This is built on construction of the
129    /// derive builder.
130    fields: FieldMap,
131
132    /// Identifier for the binding to the `#[primary_span]` field.
133    span_field: SpannedOption<proc_macro2::Ident>,
134
135    /// The binding to the `#[applicability]` field, if present.
136    applicability: SpannedOption<TokenStream>,
137
138    /// Set to true when a `#[suggestion_part]` field is encountered, used to generate an error
139    /// during finalization if still `false`.
140    has_suggestion_parts: bool,
141
142    /// Set to true when a `#[subdiagnostic]` field is encountered, used to suppress the error
143    /// emitted when no subdiagnostic kinds are specified on the variant itself.
144    has_subdiagnostic: bool,
145
146    /// Set to true when this variant is an enum variant rather than just the body of a struct.
147    is_enum: bool,
148
149    used_fields: &'parent mut HashSet<proc_macro2::Ident>,
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, Message)>, DiagnosticDeriveError> {
194        let mut kind_messages = vec![];
195
196        for attr in self.variant.ast().attrs {
197            let Some(SubdiagnosticVariant { kind, message }) =
198                SubdiagnosticVariant::from_attr(attr, &self.fields, &mut self.used_fields)?
199            else {
200                // Some attributes aren't errors - like documentation comments - but also aren't
201                // subdiagnostics.
202                continue;
203            };
204
205            let Some(message) = message 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 message must be first argument of a `#[{name}(...)]` attribute"
213                    )
214                );
215            };
216
217            kind_messages.push((kind, message));
218        }
219
220        Ok(kind_messages)
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            sub_args.insert(
236                stringify!(#ident).into(),
237                rustc_errors::IntoDiagArg::into_diag_arg(#field_binding, &mut #diag.long_ty_path)
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            "primary_span" => {
310                if kind_stats.has_multipart_suggestion {
311                    invalid_attr(attr)
312                        .help(
313                            "multipart suggestions use one or more `#[suggestion_part]`s rather \
314                            than one `#[primary_span]`",
315                        )
316                        .emit();
317                } else {
318                    report_error_if_not_applied_to_span(attr, &info)?;
319
320                    let binding = info.binding.binding.clone();
321                    // FIXME(#100717): support `Option<Span>` on `primary_span` like in the
322                    // diagnostic derive
323                    if !matches!(info.ty, FieldInnerTy::Plain(_)) {
324                        throw_invalid_attr!(attr, |diag| {
325                            let diag = diag.note("there must be exactly one primary span");
326
327                            if kind_stats.has_normal_suggestion {
328                                diag.help(
329                                    "to create a suggestion with multiple spans, \
330                                     use `#[multipart_suggestion]` instead",
331                                )
332                            } else {
333                                diag
334                            }
335                        });
336                    }
337
338                    self.span_field.set_once(binding, span);
339                }
340
341                Ok(quote! {})
342            }
343            "suggestion_part" => {
344                self.has_suggestion_parts = true;
345
346                if kind_stats.has_multipart_suggestion {
347                    span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
348                        .emit();
349                } else {
350                    invalid_attr(attr)
351                        .help(
352                            "`#[suggestion_part(...)]` is only valid in multipart suggestions, \
353                             use `#[primary_span]` instead",
354                        )
355                        .emit();
356                }
357
358                Ok(quote! {})
359            }
360            "applicability" => {
361                if kind_stats.has_multipart_suggestion || kind_stats.has_normal_suggestion {
362                    report_error_if_not_applied_to_applicability(attr, &info)?;
363
364                    if kind_stats.all_applicabilities_static {
365                        span_err(
366                            span,
367                            "`#[applicability]` has no effect if all `#[suggestion]`/\
368                             `#[multipart_suggestion]` attributes have a static \
369                             `applicability = \"...\"`",
370                        )
371                        .emit();
372                    }
373                    let binding = info.binding.binding.clone();
374                    self.applicability.set_once(quote! { #binding }, span);
375                } else {
376                    span_err(span, "`#[applicability]` is only valid on suggestions").emit();
377                }
378
379                Ok(quote! {})
380            }
381            "subdiagnostic" => {
382                let diag = &self.parent.diag;
383                let binding = &info.binding;
384                self.has_subdiagnostic = true;
385                Ok(quote! { #binding.add_to_diag(#diag); })
386            }
387            _ => {
388                let mut span_attrs = vec![];
389                if kind_stats.has_multipart_suggestion {
390                    span_attrs.push("suggestion_part");
391                }
392                if !kind_stats.all_multipart_suggestions {
393                    span_attrs.push("primary_span")
394                }
395
396                invalid_attr(attr)
397                    .help(format!(
398                        "only `{}`, `applicability` is a valid field attribute",
399                        span_attrs.join(", ")
400                    ))
401                    .emit();
402
403                Ok(quote! {})
404            }
405        }
406    }
407
408    /// Generates the code for a `[Meta::List]`-like attribute on a field (e.g.
409    /// `#[suggestion_part(code = "...")]`).
410    fn generate_field_code_inner_list(
411        &mut self,
412        kind_stats: KindsStatistics,
413        attr: &Attribute,
414        info: FieldInfo<'_>,
415        list: &MetaList,
416        clone_suggestion_code: bool,
417    ) -> Result<TokenStream, DiagnosticDeriveError> {
418        let span = attr.span().unwrap();
419        let mut ident = list.path.segments.last().unwrap().ident.clone();
420        ident.set_span(info.ty.span());
421        let name = ident.to_string();
422        let name = name.as_str();
423
424        match name {
425            "suggestion_part" => {
426                if !kind_stats.has_multipart_suggestion {
427                    throw_invalid_attr!(attr, |diag| {
428                        diag.help(
429                            "`#[suggestion_part(...)]` is only valid in multipart suggestions",
430                        )
431                    })
432                }
433
434                self.has_suggestion_parts = true;
435
436                report_error_if_not_applied_to_span(attr, &info)?;
437
438                let mut code = None;
439
440                list.parse_args_with(|input: ParseStream<'_>| {
441                    while !input.is_empty() {
442                        let arg_name = input.parse::<Ident>()?;
443                        match arg_name.to_string().as_str() {
444                            "code" => {
445                                let code_field = new_code_ident();
446                                let formatting_init = build_suggestion_code(
447                                    &code_field,
448                                    input,
449                                    &self.fields,
450                                    AllowMultipleAlternatives::No,
451                                )?;
452                                code.set_once(
453                                    (code_field, formatting_init),
454                                    arg_name.span().unwrap(),
455                                );
456                            }
457                            _ => {
458                                span_err(
459                                    arg_name.span().unwrap(),
460                                    "`code` is the only valid nested attribute",
461                                )
462                                .emit();
463                            }
464                        }
465                        if input.is_empty() {
466                            break;
467                        }
468                        input.parse::<Token![,]>()?;
469                    }
470                    Ok(())
471                })?;
472
473                let Some((code_field, formatting_init)) = code.value() else {
474                    span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
475                        .emit();
476                    return Ok(quote! {});
477                };
478                let binding = info.binding;
479
480                self.formatting_init.extend(formatting_init);
481                let code_field = if clone_suggestion_code {
482                    quote! { #code_field.clone() }
483                } else {
484                    quote! { #code_field }
485                };
486                Ok(quote! { suggestions.push((#binding, #code_field)); })
487            }
488            _ => throw_invalid_attr!(attr, |diag| {
489                let mut span_attrs = vec![];
490                if kind_stats.has_multipart_suggestion {
491                    span_attrs.push("suggestion_part");
492                }
493                if !kind_stats.all_multipart_suggestions {
494                    span_attrs.push("primary_span")
495                }
496                diag.help(format!(
497                    "only `{}`, `applicability` is a valid field attribute",
498                    span_attrs.join(", ")
499                ))
500            }),
501        }
502    }
503
504    fn is_used_in_message(&self, binding: &BindingInfo<'_>) -> bool {
505        binding.ast().ident.as_ref().is_some_and(|ident| self.used_fields.contains(ident))
506    }
507
508    pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
509        let kind_messages = self.identify_kind()?;
510
511        let kind_stats: KindsStatistics = kind_messages.iter().map(|(kind, _msg)| kind).collect();
512
513        let init = if kind_stats.has_multipart_suggestion {
514            quote! { let mut suggestions = Vec::new(); }
515        } else {
516            quote! {}
517        };
518
519        let attr_args: TokenStream = self
520            .variant
521            .bindings()
522            .iter()
523            .filter(|binding| !should_generate_arg(binding.ast()))
524            .map(|binding| self.generate_field_attr_code(binding, kind_stats))
525            .collect();
526
527        if kind_messages.is_empty() && !self.has_subdiagnostic {
528            if self.is_enum {
529                // It's okay for a variant to not be a subdiagnostic at all..
530                return Ok(quote! {});
531            } else {
532                // ..but structs should always be _something_.
533                throw_span_err!(
534                    self.variant.ast().ident.span().unwrap(),
535                    "subdiagnostic kind not specified"
536                );
537            }
538        };
539
540        let plain_args: TokenStream = self
541            .variant
542            .bindings()
543            .iter()
544            .filter_map(|binding| {
545                if should_generate_arg(binding.ast()) && self.is_used_in_message(binding) {
546                    Some(self.generate_field_arg(binding))
547                } else {
548                    None
549                }
550            })
551            .collect();
552        let plain_args = quote! {
553            let mut sub_args = rustc_errors::DiagArgMap::default();
554            #plain_args
555        };
556
557        let span_field = self.span_field.value_ref();
558        let diag = &self.parent.diag;
559        let mut calls = TokenStream::new();
560        for (kind, messages) in kind_messages {
561            let message = format_ident!("__message");
562            let message_stream = messages.diag_message();
563            calls.extend(quote! { let #message = rustc_errors::format_diag_message(&#message_stream, &sub_args); });
564
565            let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
566            let call = match kind {
567                SubdiagnosticKind::Suggestion {
568                    suggestion_kind,
569                    applicability,
570                    code_init,
571                    code_field,
572                } => {
573                    self.formatting_init.extend(code_init);
574
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 let Some(span) = span_field {
582                        let style = suggestion_kind.to_suggestion_style();
583                        quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
584                    } else {
585                        span_err(self.span, "suggestion without `#[primary_span]` field").emit();
586                        quote! { unreachable!(); }
587                    }
588                }
589                SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
590                    let applicability = applicability
591                        .value()
592                        .map(|a| quote! { #a })
593                        .or_else(|| self.applicability.take().value())
594                        .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
595
596                    if !self.has_suggestion_parts {
597                        span_err(
598                            self.span,
599                            "multipart suggestion without any `#[suggestion_part(...)]` fields",
600                        )
601                        .emit();
602                    }
603
604                    let style = suggestion_kind.to_suggestion_style();
605
606                    quote! { #diag.#name(#message, suggestions, #applicability, #style); }
607                }
608                SubdiagnosticKind::Label => {
609                    if let Some(span) = span_field {
610                        quote! { #diag.#name(#span, #message); }
611                    } else {
612                        span_err(self.span, "label without `#[primary_span]` field").emit();
613                        quote! { unreachable!(); }
614                    }
615                }
616                _ => {
617                    if let Some(span) = span_field {
618                        quote! { #diag.#name(#span, #message); }
619                    } else {
620                        quote! { #diag.#name(#message); }
621                    }
622                }
623            };
624
625            calls.extend(call);
626        }
627
628        let formatting_init = &self.formatting_init;
629
630        // For #[derive(Subdiagnostic)]
631        //
632        // - Store args of the main diagnostic for later restore.
633        // - Add args of subdiagnostic.
634        // - Generate the calls, such as note, label, etc.
635        // - Restore the arguments for allowing main and subdiagnostic share the same fields.
636        Ok(quote! {
637            #init
638            #formatting_init
639            #attr_args
640            // #store_args
641            #plain_args
642            #calls
643            // #restore_args
644        })
645    }
646}