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