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