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