1#![deny(unused_must_use)]
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5use syn::spanned::Spanned;
6use syn::{Attribute, Meta, MetaList, Path};
7use synstructure::{BindingInfo, Structure, VariantInfo};
8
9use super::utils::SubdiagnosticVariant;
10use crate::diagnostics::error::{
11 DiagnosticDeriveError, invalid_attr, span_err, throw_invalid_attr, throw_span_err,
12};
13use crate::diagnostics::utils::{
14 AllowMultipleAlternatives, FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce,
15 SpannedOption, SubdiagnosticKind, build_field_mapping, build_suggestion_code, is_doc_comment,
16 new_code_ident, report_error_if_not_applied_to_applicability,
17 report_error_if_not_applied_to_span, should_generate_arg,
18};
19
20pub(crate) struct SubdiagnosticDerive {
22 diag: syn::Ident,
23}
24
25impl SubdiagnosticDerive {
26 pub(crate) fn new() -> Self {
27 let diag = format_ident!("diag");
28 Self { diag }
29 }
30
31 pub(crate) fn into_tokens(self, mut structure: Structure<'_>) -> TokenStream {
32 let implementation = {
33 let ast = structure.ast();
34 let span = ast.span().unwrap();
35 match ast.data {
36 syn::Data::Struct(..) | syn::Data::Enum(..) => (),
37 syn::Data::Union(..) => {
38 span_err(
39 span,
40 "`#[derive(Subdiagnostic)]` can only be used on structs and enums",
41 )
42 .emit();
43 }
44 }
45
46 let is_enum = matches!(ast.data, syn::Data::Enum(..));
47 if is_enum {
48 for attr in &ast.attrs {
49 if is_doc_comment(attr) {
51 continue;
52 }
53
54 span_err(
55 attr.span().unwrap(),
56 "unsupported type attribute for subdiagnostic enum",
57 )
58 .emit();
59 }
60 }
61
62 structure.bind_with(|_| synstructure::BindStyle::Move);
63 let variants_ = structure.each_variant(|variant| {
64 let mut builder = SubdiagnosticDeriveVariantBuilder {
65 parent: &self,
66 variant,
67 span,
68 formatting_init: TokenStream::new(),
69 fields: build_field_mapping(variant),
70 span_field: None,
71 applicability: None,
72 has_suggestion_parts: false,
73 has_subdiagnostic: false,
74 is_enum,
75 };
76 builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
77 });
78
79 quote! {
80 match self {
81 #variants_
82 }
83 }
84 };
85
86 let diag = &self.diag;
87
88 #[allow(keyword_idents_2024)]
90 let ret = structure.gen_impl(quote! {
91 gen impl rustc_errors::Subdiagnostic for @Self {
92 fn add_to_diag<__G>(
93 self,
94 #diag: &mut rustc_errors::Diag<'_, __G>,
95 ) where
96 __G: rustc_errors::EmissionGuarantee,
97 {
98 #implementation
99 }
100 }
101 });
102
103 ret
104 }
105}
106
107struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
112 parent: &'parent SubdiagnosticDerive,
114
115 variant: &'a VariantInfo<'a>,
117 span: proc_macro::Span,
119
120 formatting_init: TokenStream,
122
123 fields: FieldMap,
126
127 span_field: SpannedOption<proc_macro2::Ident>,
129
130 applicability: SpannedOption<TokenStream>,
132
133 has_suggestion_parts: bool,
136
137 has_subdiagnostic: bool,
140
141 is_enum: bool,
143}
144
145impl<'parent, 'a> HasFieldMap for SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
146 fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
147 self.fields.get(field)
148 }
149}
150
151#[derive(Clone, Copy, Debug)]
153struct KindsStatistics {
154 has_multipart_suggestion: bool,
155 all_multipart_suggestions: bool,
156 has_normal_suggestion: bool,
157 all_applicabilities_static: bool,
158}
159
160impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics {
161 fn from_iter<T: IntoIterator<Item = &'a SubdiagnosticKind>>(kinds: T) -> Self {
162 let mut ret = Self {
163 has_multipart_suggestion: false,
164 all_multipart_suggestions: true,
165 has_normal_suggestion: false,
166 all_applicabilities_static: true,
167 };
168
169 for kind in kinds {
170 if let SubdiagnosticKind::MultipartSuggestion { applicability: None, .. }
171 | SubdiagnosticKind::Suggestion { applicability: None, .. } = kind
172 {
173 ret.all_applicabilities_static = false;
174 }
175 if let SubdiagnosticKind::MultipartSuggestion { .. } = kind {
176 ret.has_multipart_suggestion = true;
177 } else {
178 ret.all_multipart_suggestions = false;
179 }
180
181 if let SubdiagnosticKind::Suggestion { .. } = kind {
182 ret.has_normal_suggestion = true;
183 }
184 }
185 ret
186 }
187}
188
189impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
190 fn identify_kind(
191 &mut self,
192 ) -> Result<Vec<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
193 let mut kind_slugs = vec![];
194
195 for attr in self.variant.ast().attrs {
196 let Some(SubdiagnosticVariant { kind, slug, no_span }) =
197 SubdiagnosticVariant::from_attr(attr, self)?
198 else {
199 continue;
202 };
203
204 let Some(slug) = slug else {
205 let name = attr.path().segments.last().unwrap().ident.to_string();
206 let name = name.as_str();
207
208 throw_span_err!(
209 attr.span().unwrap(),
210 format!(
211 "diagnostic slug must be first argument of a `#[{name}(...)]` attribute"
212 )
213 );
214 };
215
216 kind_slugs.push((kind, slug, no_span));
217 }
218
219 Ok(kind_slugs)
220 }
221
222 fn generate_field_arg(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
224 let diag = &self.parent.diag;
225
226 let field = binding_info.ast();
227 let mut field_binding = binding_info.binding.clone();
228 field_binding.set_span(field.ty.span());
229
230 let ident = field.ident.as_ref().unwrap();
231 let ident = format_ident!("{}", ident); quote! {
234 #diag.arg(
235 stringify!(#ident),
236 #field_binding
237 );
238 }
239 }
240
241 fn generate_field_attr_code(
243 &mut self,
244 binding: &BindingInfo<'_>,
245 kind_stats: KindsStatistics,
246 ) -> TokenStream {
247 let ast = binding.ast();
248 assert!(ast.attrs.len() > 0, "field without attributes generating attr code");
249
250 let inner_ty = FieldInnerTy::from_type(&ast.ty);
253 ast.attrs
254 .iter()
255 .map(|attr| {
256 if is_doc_comment(attr) {
258 return quote! {};
259 }
260
261 let info = FieldInfo { binding, ty: inner_ty, span: &ast.span() };
262
263 let generated = self
264 .generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate())
265 .unwrap_or_else(|v| v.to_compile_error());
266
267 inner_ty.with(binding, generated)
268 })
269 .collect()
270 }
271
272 fn generate_field_code_inner(
273 &mut self,
274 kind_stats: KindsStatistics,
275 attr: &Attribute,
276 info: FieldInfo<'_>,
277 clone_suggestion_code: bool,
278 ) -> Result<TokenStream, DiagnosticDeriveError> {
279 match &attr.meta {
280 Meta::Path(path) => {
281 self.generate_field_code_inner_path(kind_stats, attr, info, path.clone())
282 }
283 Meta::List(list) => self.generate_field_code_inner_list(
284 kind_stats,
285 attr,
286 info,
287 list,
288 clone_suggestion_code,
289 ),
290 _ => throw_invalid_attr!(attr),
291 }
292 }
293
294 fn generate_field_code_inner_path(
296 &mut self,
297 kind_stats: KindsStatistics,
298 attr: &Attribute,
299 info: FieldInfo<'_>,
300 path: Path,
301 ) -> Result<TokenStream, DiagnosticDeriveError> {
302 let span = attr.span().unwrap();
303 let ident = &path.segments.last().unwrap().ident;
304 let name = ident.to_string();
305 let name = name.as_str();
306
307 match name {
308 "skip_arg" => Ok(quote! {}),
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 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` and `skip_arg` are valid field attributes",
399 span_attrs.join(", ")
400 ))
401 .emit();
402
403 Ok(quote! {})
404 }
405 }
406 }
407
408 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_nested_meta(|nested| {
441 if nested.path.is_ident("code") {
442 let code_field = new_code_ident();
443 let span = nested.path.span().unwrap();
444 let formatting_init = build_suggestion_code(
445 &code_field,
446 nested,
447 self,
448 AllowMultipleAlternatives::No,
449 );
450 code.set_once((code_field, formatting_init), span);
451 } else {
452 span_err(
453 nested.path.span().unwrap(),
454 "`code` is the only valid nested attribute",
455 )
456 .emit();
457 }
458 Ok(())
459 })?;
460
461 let Some((code_field, formatting_init)) = code.value() else {
462 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
463 .emit();
464 return Ok(quote! {});
465 };
466 let binding = info.binding;
467
468 self.formatting_init.extend(formatting_init);
469 let code_field = if clone_suggestion_code {
470 quote! { #code_field.clone() }
471 } else {
472 quote! { #code_field }
473 };
474 Ok(quote! { suggestions.push((#binding, #code_field)); })
475 }
476 _ => throw_invalid_attr!(attr, |diag| {
477 let mut span_attrs = vec![];
478 if kind_stats.has_multipart_suggestion {
479 span_attrs.push("suggestion_part");
480 }
481 if !kind_stats.all_multipart_suggestions {
482 span_attrs.push("primary_span")
483 }
484 diag.help(format!(
485 "only `{}`, `applicability` and `skip_arg` are valid field attributes",
486 span_attrs.join(", ")
487 ))
488 }),
489 }
490 }
491
492 pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
493 let kind_slugs = self.identify_kind()?;
494
495 let kind_stats: KindsStatistics =
496 kind_slugs.iter().map(|(kind, _slug, _no_span)| kind).collect();
497
498 let init = if kind_stats.has_multipart_suggestion {
499 quote! { let mut suggestions = Vec::new(); }
500 } else {
501 quote! {}
502 };
503
504 let attr_args: TokenStream = self
505 .variant
506 .bindings()
507 .iter()
508 .filter(|binding| !should_generate_arg(binding.ast()))
509 .map(|binding| self.generate_field_attr_code(binding, kind_stats))
510 .collect();
511
512 if kind_slugs.is_empty() && !self.has_subdiagnostic {
513 if self.is_enum {
514 return Ok(quote! {});
516 } else {
517 throw_span_err!(
519 self.variant.ast().ident.span().unwrap(),
520 "subdiagnostic kind not specified"
521 );
522 }
523 };
524
525 let span_field = self.span_field.value_ref();
526
527 let diag = &self.parent.diag;
528 let mut calls = TokenStream::new();
529 for (kind, slug, no_span) in kind_slugs {
530 let message = format_ident!("__message");
531 calls.extend(
532 quote! { let #message = #diag.eagerly_translate(crate::fluent_generated::#slug); },
533 );
534
535 let name = format_ident!(
536 "{}{}",
537 if span_field.is_some() && !no_span { "span_" } else { "" },
538 kind
539 );
540 let call = match kind {
541 SubdiagnosticKind::Suggestion {
542 suggestion_kind,
543 applicability,
544 code_init,
545 code_field,
546 } => {
547 self.formatting_init.extend(code_init);
548
549 let applicability = applicability
550 .value()
551 .map(|a| quote! { #a })
552 .or_else(|| self.applicability.take().value())
553 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
554
555 if let Some(span) = span_field {
556 let style = suggestion_kind.to_suggestion_style();
557 quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
558 } else {
559 span_err(self.span, "suggestion without `#[primary_span]` field").emit();
560 quote! { unreachable!(); }
561 }
562 }
563 SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
564 let applicability = applicability
565 .value()
566 .map(|a| quote! { #a })
567 .or_else(|| self.applicability.take().value())
568 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
569
570 if !self.has_suggestion_parts {
571 span_err(
572 self.span,
573 "multipart suggestion without any `#[suggestion_part(...)]` fields",
574 )
575 .emit();
576 }
577
578 let style = suggestion_kind.to_suggestion_style();
579
580 quote! { #diag.#name(#message, suggestions, #applicability, #style); }
581 }
582 SubdiagnosticKind::Label => {
583 if let Some(span) = span_field {
584 quote! { #diag.#name(#span, #message); }
585 } else {
586 span_err(self.span, "label without `#[primary_span]` field").emit();
587 quote! { unreachable!(); }
588 }
589 }
590 _ => {
591 if let Some(span) = span_field
592 && !no_span
593 {
594 quote! { #diag.#name(#span, #message); }
595 } else {
596 quote! { #diag.#name(#message); }
597 }
598 }
599 };
600
601 calls.extend(call);
602 }
603 let store_args = quote! {
604 #diag.store_args();
605 };
606 let restore_args = quote! {
607 #diag.restore_args();
608 };
609 let plain_args: TokenStream = self
610 .variant
611 .bindings()
612 .iter()
613 .filter(|binding| should_generate_arg(binding.ast()))
614 .map(|binding| self.generate_field_arg(binding))
615 .collect();
616
617 let formatting_init = &self.formatting_init;
618
619 Ok(quote! {
626 #init
627 #formatting_init
628 #attr_args
629 #store_args
630 #plain_args
631 #calls
632 #restore_args
633 })
634 }
635}