]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
c1b82abc1e064a8da1b8dd62e3610232f7722393
[rust.git] / compiler / rustc_macros / src / diagnostics / subdiagnostic.rs
1 #![deny(unused_must_use)]
2
3 use crate::diagnostics::error::{
4     span_err, throw_invalid_attr, throw_invalid_nested_attr, throw_span_err, DiagnosticDeriveError,
5 };
6 use crate::diagnostics::utils::{
7     report_error_if_not_applied_to_applicability, report_error_if_not_applied_to_span,
8     Applicability, FieldInfo, FieldInnerTy, HasFieldMap, SetOnce,
9 };
10 use proc_macro2::TokenStream;
11 use quote::{format_ident, quote};
12 use std::collections::HashMap;
13 use std::fmt;
14 use std::str::FromStr;
15 use syn::{spanned::Spanned, Meta, MetaList, MetaNameValue, NestedMeta, Path};
16 use synstructure::{BindingInfo, Structure, VariantInfo};
17
18 /// Which kind of suggestion is being created?
19 #[derive(Clone, Copy)]
20 enum SubdiagnosticSuggestionKind {
21     /// `#[suggestion]`
22     Normal,
23     /// `#[suggestion_short]`
24     Short,
25     /// `#[suggestion_hidden]`
26     Hidden,
27     /// `#[suggestion_verbose]`
28     Verbose,
29 }
30
31 /// Which kind of subdiagnostic is being created from a variant?
32 #[derive(Clone, Copy)]
33 enum SubdiagnosticKind {
34     /// `#[label(...)]`
35     Label,
36     /// `#[note(...)]`
37     Note,
38     /// `#[help(...)]`
39     Help,
40     /// `#[warning(...)]`
41     Warn,
42     /// `#[suggestion{,_short,_hidden,_verbose}]`
43     Suggestion(SubdiagnosticSuggestionKind),
44 }
45
46 impl FromStr for SubdiagnosticKind {
47     type Err = ();
48
49     fn from_str(s: &str) -> Result<Self, Self::Err> {
50         match s {
51             "label" => Ok(SubdiagnosticKind::Label),
52             "note" => Ok(SubdiagnosticKind::Note),
53             "help" => Ok(SubdiagnosticKind::Help),
54             "warning" => Ok(SubdiagnosticKind::Warn),
55             "suggestion" => Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Normal)),
56             "suggestion_short" => {
57                 Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Short))
58             }
59             "suggestion_hidden" => {
60                 Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Hidden))
61             }
62             "suggestion_verbose" => {
63                 Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Verbose))
64             }
65             _ => Err(()),
66         }
67     }
68 }
69
70 impl quote::IdentFragment for SubdiagnosticKind {
71     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72         match self {
73             SubdiagnosticKind::Label => write!(f, "label"),
74             SubdiagnosticKind::Note => write!(f, "note"),
75             SubdiagnosticKind::Help => write!(f, "help"),
76             SubdiagnosticKind::Warn => write!(f, "warn"),
77             SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Normal) => {
78                 write!(f, "suggestion")
79             }
80             SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Short) => {
81                 write!(f, "suggestion_short")
82             }
83             SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Hidden) => {
84                 write!(f, "suggestion_hidden")
85             }
86             SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Verbose) => {
87                 write!(f, "suggestion_verbose")
88             }
89         }
90     }
91
92     fn span(&self) -> Option<proc_macro2::Span> {
93         None
94     }
95 }
96
97 /// The central struct for constructing the `add_to_diagnostic` method from an annotated struct.
98 pub(crate) struct SessionSubdiagnosticDerive<'a> {
99     structure: Structure<'a>,
100     diag: syn::Ident,
101 }
102
103 impl<'a> SessionSubdiagnosticDerive<'a> {
104     pub(crate) fn new(structure: Structure<'a>) -> Self {
105         let diag = format_ident!("diag");
106         Self { structure, diag }
107     }
108
109     pub(crate) fn into_tokens(self) -> TokenStream {
110         let SessionSubdiagnosticDerive { mut structure, diag } = self;
111         let implementation = {
112             let ast = structure.ast();
113             let span = ast.span().unwrap();
114             match ast.data {
115                 syn::Data::Struct(..) | syn::Data::Enum(..) => (),
116                 syn::Data::Union(..) => {
117                     span_err(
118                         span,
119                         "`#[derive(SessionSubdiagnostic)]` can only be used on structs and enums",
120                     );
121                 }
122             }
123
124             if matches!(ast.data, syn::Data::Enum(..)) {
125                 for attr in &ast.attrs {
126                     span_err(
127                         attr.span().unwrap(),
128                         "unsupported type attribute for subdiagnostic enum",
129                     )
130                     .emit();
131                 }
132             }
133
134             structure.bind_with(|_| synstructure::BindStyle::Move);
135             let variants_ = structure.each_variant(|variant| {
136                 // Build the mapping of field names to fields. This allows attributes to peek
137                 // values from other fields.
138                 let mut fields_map = HashMap::new();
139                 for binding in variant.bindings() {
140                     let field = binding.ast();
141                     if let Some(ident) = &field.ident {
142                         fields_map.insert(ident.to_string(), quote! { #binding });
143                     }
144                 }
145
146                 let mut builder = SessionSubdiagnosticDeriveBuilder {
147                     diag: &diag,
148                     variant,
149                     span,
150                     fields: fields_map,
151                     kinds: Vec::new(),
152                     slugs: Vec::new(),
153                     code: None,
154                     span_field: None,
155                     applicability: None,
156                 };
157                 builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
158             });
159
160             quote! {
161                 match self {
162                     #variants_
163                 }
164             }
165         };
166
167         let ret = structure.gen_impl(quote! {
168             gen impl rustc_errors::AddSubdiagnostic for @Self {
169                 fn add_to_diagnostic(self, #diag: &mut rustc_errors::Diagnostic) {
170                     use rustc_errors::{Applicability, IntoDiagnosticArg};
171                     #implementation
172                 }
173             }
174         });
175         ret
176     }
177 }
178
179 /// Tracks persistent information required for building up the call to add to the diagnostic
180 /// for the final generated method. This is a separate struct to `SessionSubdiagnosticDerive`
181 /// only to be able to destructure and split `self.builder` and the `self.structure` up to avoid a
182 /// double mut borrow later on.
183 struct SessionSubdiagnosticDeriveBuilder<'a> {
184     /// The identifier to use for the generated `DiagnosticBuilder` instance.
185     diag: &'a syn::Ident,
186
187     /// Info for the current variant (or the type if not an enum).
188     variant: &'a VariantInfo<'a>,
189     /// Span for the entire type.
190     span: proc_macro::Span,
191
192     /// Store a map of field name to its corresponding field. This is built on construction of the
193     /// derive builder.
194     fields: HashMap<String, TokenStream>,
195
196     /// Subdiagnostic kind of the type/variant.
197     kinds: Vec<(SubdiagnosticKind, proc_macro::Span)>,
198
199     /// Slugs of the subdiagnostic - corresponds to the Fluent identifier for the message - from the
200     /// `#[kind(slug)]` attribute on the type or variant.
201     slugs: Vec<(Path, proc_macro::Span)>,
202     /// If a suggestion, the code to suggest as a replacement - from the `#[kind(code = "...")]`
203     /// attribute on the type or variant.
204     code: Option<(TokenStream, proc_macro::Span)>,
205
206     /// Identifier for the binding to the `#[primary_span]` field.
207     span_field: Option<(proc_macro2::Ident, proc_macro::Span)>,
208     /// If a suggestion, the identifier for the binding to the `#[applicability]` field or a
209     /// `rustc_errors::Applicability::*` variant directly.
210     applicability: Option<(TokenStream, proc_macro::Span)>,
211 }
212
213 impl<'a> HasFieldMap for SessionSubdiagnosticDeriveBuilder<'a> {
214     fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
215         self.fields.get(field)
216     }
217 }
218
219 impl<'a> SessionSubdiagnosticDeriveBuilder<'a> {
220     fn identify_kind(&mut self) -> Result<(), DiagnosticDeriveError> {
221         for (i, attr) in self.variant.ast().attrs.iter().enumerate() {
222             let span = attr.span().unwrap();
223
224             let name = attr.path.segments.last().unwrap().ident.to_string();
225             let name = name.as_str();
226
227             let meta = attr.parse_meta()?;
228             let kind = match meta {
229                 Meta::List(MetaList { ref nested, .. }) => {
230                     let mut nested_iter = nested.into_iter();
231                     if let Some(nested_attr) = nested_iter.next() {
232                         match nested_attr {
233                             NestedMeta::Meta(Meta::Path(path)) => {
234                                 self.slugs.push((path.clone(), span));
235                             }
236                             NestedMeta::Meta(meta @ Meta::NameValue(_))
237                                 if matches!(
238                                     meta.path().segments.last().unwrap().ident.to_string().as_str(),
239                                     "code" | "applicability"
240                                 ) =>
241                             {
242                                 // don't error for valid follow-up attributes
243                             }
244                             nested_attr => {
245                                 throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
246                                     diag.help(
247                                         "first argument of the attribute should be the diagnostic \
248                                          slug",
249                                     )
250                                 })
251                             }
252                         };
253                     }
254
255                     for nested_attr in nested_iter {
256                         let meta = match nested_attr {
257                             NestedMeta::Meta(ref meta) => meta,
258                             _ => throw_invalid_nested_attr!(attr, &nested_attr),
259                         };
260
261                         let span = meta.span().unwrap();
262                         let nested_name = meta.path().segments.last().unwrap().ident.to_string();
263                         let nested_name = nested_name.as_str();
264
265                         match meta {
266                             Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(s), .. }) => {
267                                 match nested_name {
268                                     "code" => {
269                                         let formatted_str = self.build_format(&s.value(), s.span());
270                                         self.code.set_once((formatted_str, span));
271                                     }
272                                     "applicability" => {
273                                         let value = match Applicability::from_str(&s.value()) {
274                                             Ok(v) => v,
275                                             Err(()) => {
276                                                 span_err(span, "invalid applicability").emit();
277                                                 Applicability::Unspecified
278                                             }
279                                         };
280                                         self.applicability.set_once((quote! { #value }, span));
281                                     }
282                                     _ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
283                                         diag.help(
284                                             "only `code` and `applicability` are valid nested \
285                                              attributes",
286                                         )
287                                     }),
288                                 }
289                             }
290                             _ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
291                                 if matches!(meta, Meta::Path(_)) {
292                                     diag.help(
293                                         "a diagnostic slug must be the first argument to the \
294                                          attribute",
295                                     )
296                                 } else {
297                                     diag
298                                 }
299                             }),
300                         }
301                     }
302
303                     let Ok(kind) = SubdiagnosticKind::from_str(name) else {
304                         throw_invalid_attr!(attr, &meta)
305                     };
306
307                     kind
308                 }
309                 _ => throw_invalid_attr!(attr, &meta),
310             };
311
312             if matches!(
313                 kind,
314                 SubdiagnosticKind::Label | SubdiagnosticKind::Help | SubdiagnosticKind::Note
315             ) && self.code.is_some()
316             {
317                 throw_span_err!(
318                     span,
319                     &format!("`code` is not a valid nested attribute of a `{}` attribute", name)
320                 );
321             }
322
323             if matches!(
324                 kind,
325                 SubdiagnosticKind::Label | SubdiagnosticKind::Help | SubdiagnosticKind::Note
326             ) && self.applicability.is_some()
327             {
328                 throw_span_err!(
329                     span,
330                     &format!(
331                         "`applicability` is not a valid nested attribute of a `{}` attribute",
332                         name
333                     )
334                 );
335             }
336
337             if self.slugs.len() != i + 1 {
338                 throw_span_err!(
339                     span,
340                     &format!(
341                         "diagnostic slug must be first argument of a `#[{}(...)]` attribute",
342                         name
343                     )
344                 );
345             }
346
347             self.kinds.push((kind, span));
348         }
349
350         Ok(())
351     }
352
353     fn generate_field_code(
354         &mut self,
355         binding: &BindingInfo<'_>,
356         have_suggestion: bool,
357     ) -> Result<TokenStream, DiagnosticDeriveError> {
358         let ast = binding.ast();
359
360         let inner_ty = FieldInnerTy::from_type(&ast.ty);
361         let info = FieldInfo {
362             binding: binding,
363             ty: inner_ty.inner_type().unwrap_or(&ast.ty),
364             span: &ast.span(),
365         };
366
367         for attr in &ast.attrs {
368             let name = attr.path.segments.last().unwrap().ident.to_string();
369             let name = name.as_str();
370             let span = attr.span().unwrap();
371
372             let meta = attr.parse_meta()?;
373             match meta {
374                 Meta::Path(_) => match name {
375                     "primary_span" => {
376                         report_error_if_not_applied_to_span(attr, &info)?;
377                         self.span_field.set_once((binding.binding.clone(), span));
378                         return Ok(quote! {});
379                     }
380                     "applicability" if have_suggestion => {
381                         report_error_if_not_applied_to_applicability(attr, &info)?;
382                         let binding = binding.binding.clone();
383                         self.applicability.set_once((quote! { #binding }, span));
384                         return Ok(quote! {});
385                     }
386                     "applicability" => {
387                         span_err(span, "`#[applicability]` is only valid on suggestions").emit();
388                         return Ok(quote! {});
389                     }
390                     "skip_arg" => {
391                         return Ok(quote! {});
392                     }
393                     _ => throw_invalid_attr!(attr, &meta, |diag| {
394                         diag.help(
395                             "only `primary_span`, `applicability` and `skip_arg` are valid field \
396                              attributes",
397                         )
398                     }),
399                 },
400                 _ => throw_invalid_attr!(attr, &meta),
401             }
402         }
403
404         let ident = ast.ident.as_ref().unwrap();
405
406         let diag = &self.diag;
407         let generated = quote! {
408             #diag.set_arg(
409                 stringify!(#ident),
410                 #binding
411             );
412         };
413
414         Ok(inner_ty.with(binding, generated))
415     }
416
417     fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
418         self.identify_kind()?;
419         if self.kinds.is_empty() {
420             throw_span_err!(
421                 self.variant.ast().ident.span().unwrap(),
422                 "subdiagnostic kind not specified"
423             );
424         };
425         let have_suggestion =
426             self.kinds.iter().any(|(k, _)| matches!(k, SubdiagnosticKind::Suggestion(_)));
427         let mut args = TokenStream::new();
428         for binding in self.variant.bindings() {
429             let arg = self
430                 .generate_field_code(binding, have_suggestion)
431                 .unwrap_or_else(|v| v.to_compile_error());
432             args.extend(arg);
433         }
434         let mut tokens = TokenStream::new();
435         for ((kind, _), (slug, _)) in self.kinds.iter().zip(&self.slugs) {
436             let code = match self.code.as_ref() {
437                 Some((code, _)) => Some(quote! { #code }),
438                 None if have_suggestion => {
439                     span_err(self.span, "suggestion without `code = \"...\"`").emit();
440                     Some(quote! { /* macro error */ "..." })
441                 }
442                 None => None,
443             };
444
445             let span_field = self.span_field.as_ref().map(|(span, _)| span);
446             let applicability = match self.applicability.clone() {
447                 Some((applicability, _)) => Some(applicability),
448                 None if have_suggestion => {
449                     span_err(self.span, "suggestion without `applicability`").emit();
450                     Some(quote! { rustc_errors::Applicability::Unspecified })
451                 }
452                 None => None,
453             };
454
455             let diag = &self.diag;
456             let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
457             let message = quote! { rustc_errors::fluent::#slug };
458             let call = if matches!(kind, SubdiagnosticKind::Suggestion(..)) {
459                 if let Some(span) = span_field {
460                     quote! { #diag.#name(#span, #message, #code, #applicability); }
461                 } else {
462                     span_err(self.span, "suggestion without `#[primary_span]` field").emit();
463                     quote! { unreachable!(); }
464                 }
465             } else if matches!(kind, SubdiagnosticKind::Label) {
466                 if let Some(span) = span_field {
467                     quote! { #diag.#name(#span, #message); }
468                 } else {
469                     span_err(self.span, "label without `#[primary_span]` field").emit();
470                     quote! { unreachable!(); }
471                 }
472             } else {
473                 if let Some(span) = span_field {
474                     quote! { #diag.#name(#span, #message); }
475                 } else {
476                     quote! { #diag.#name(#message); }
477                 }
478             };
479             tokens.extend(quote! {
480                 #call
481                 #args
482             });
483         }
484
485         Ok(tokens)
486     }
487 }