1#![allow(clippy::all)]
2#![deny(unused_must_use)]
3
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use syn::spanned::Spanned;
7use syn::{Attribute, Meta, MetaList, Path};
8use synstructure::{BindingInfo, Structure, VariantInfo};
9
10use super::utils::SubdiagnosticVariant;
11use crate::diagnostics::error::{
12 DiagnosticDeriveError, invalid_attr, span_err, throw_invalid_attr, throw_span_err,
13};
14use crate::diagnostics::utils::{
15 AllowMultipleAlternatives, FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce,
16 SpannedOption, SubdiagnosticKind, build_field_mapping, build_suggestion_code, is_doc_comment,
17 new_code_ident, report_error_if_not_applied_to_applicability,
18 report_error_if_not_applied_to_span, should_generate_arg,
19};
20
21pub(crate) struct SubdiagnosticDerive {
23 diag: syn::Ident,
24}
25
26impl SubdiagnosticDerive {
27 pub(crate) fn new() -> Self {
28 let diag = format_ident!("diag");
29 Self { diag }
30 }
31
32 pub(crate) fn into_tokens(self, mut structure: Structure<'_>) -> TokenStream {
33 let implementation = {
34 let ast = structure.ast();
35 let span = ast.span().unwrap();
36 match ast.data {
37 syn::Data::Struct(..) | syn::Data::Enum(..) => (),
38 syn::Data::Union(..) => {
39 span_err(
40 span,
41 "`#[derive(Subdiagnostic)]` can only be used on structs and enums",
42 )
43 .emit();
44 }
45 }
46
47 let is_enum = matches!(ast.data, syn::Data::Enum(..));
48 if is_enum {
49 for attr in &ast.attrs {
50 if is_doc_comment(attr) {
52 continue;
53 }
54
55 span_err(
56 attr.span().unwrap(),
57 "unsupported type attribute for subdiagnostic enum",
58 )
59 .emit();
60 }
61 }
62
63 structure.bind_with(|_| synstructure::BindStyle::Move);
64 let variants_ = structure.each_variant(|variant| {
65 let mut builder = SubdiagnosticDeriveVariantBuilder {
66 parent: &self,
67 variant,
68 span,
69 formatting_init: TokenStream::new(),
70 fields: build_field_mapping(variant),
71 span_field: None,
72 applicability: None,
73 has_suggestion_parts: false,
74 has_subdiagnostic: false,
75 is_enum,
76 };
77 builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
78 });
79
80 quote! {
81 match self {
82 #variants_
83 }
84 }
85 };
86
87 let diag = &self.diag;
88
89 #[allow(keyword_idents_2024)]
91 let ret = structure.gen_impl(quote! {
92 gen impl rustc_errors::Subdiagnostic for @Self {
93 fn add_to_diag<__G>(
94 self,
95 #diag: &mut rustc_errors::Diag<'_, __G>,
96 ) where
97 __G: rustc_errors::EmissionGuarantee,
98 {
99 #implementation
100 }
101 }
102 });
103
104 ret
105 }
106}
107
108struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
113 parent: &'parent SubdiagnosticDerive,
115
116 variant: &'a VariantInfo<'a>,
118 span: proc_macro::Span,
120
121 formatting_init: TokenStream,
123
124 fields: FieldMap,
127
128 span_field: SpannedOption<proc_macro2::Ident>,
130
131 applicability: SpannedOption<TokenStream>,
133
134 has_suggestion_parts: bool,
137
138 has_subdiagnostic: bool,
141
142 is_enum: bool,
144}
145
146impl<'parent, 'a> HasFieldMap for SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
147 fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
148 self.fields.get(field)
149 }
150}
151
152#[derive(Clone, Copy, Debug)]
154struct KindsStatistics {
155 has_multipart_suggestion: bool,
156 all_multipart_suggestions: bool,
157 has_normal_suggestion: bool,
158 all_applicabilities_static: bool,
159}
160
161impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics {
162 fn from_iter<T: IntoIterator<Item = &'a SubdiagnosticKind>>(kinds: T) -> Self {
163 let mut ret = Self {
164 has_multipart_suggestion: false,
165 all_multipart_suggestions: true,
166 has_normal_suggestion: false,
167 all_applicabilities_static: true,
168 };
169
170 for kind in kinds {
171 if let SubdiagnosticKind::MultipartSuggestion { applicability: None, .. }
172 | SubdiagnosticKind::Suggestion { applicability: None, .. } = kind
173 {
174 ret.all_applicabilities_static = false;
175 }
176 if let SubdiagnosticKind::MultipartSuggestion { .. } = kind {
177 ret.has_multipart_suggestion = true;
178 } else {
179 ret.all_multipart_suggestions = false;
180 }
181
182 if let SubdiagnosticKind::Suggestion { .. } = kind {
183 ret.has_normal_suggestion = true;
184 }
185 }
186 ret
187 }
188}
189
190impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
191 fn identify_kind(
192 &mut self,
193 ) -> Result<Vec<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
194 let mut kind_slugs = vec![];
195
196 for attr in self.variant.ast().attrs {
197 let Some(SubdiagnosticVariant { kind, slug, no_span }) =
198 SubdiagnosticVariant::from_attr(attr, self)?
199 else {
200 continue;
203 };
204
205 let Some(slug) = slug else {
206 let name = attr.path().segments.last().unwrap().ident.to_string();
207 let name = name.as_str();
208
209 throw_span_err!(
210 attr.span().unwrap(),
211 format!(
212 "diagnostic slug must be first argument of a `#[{name}(...)]` attribute"
213 )
214 );
215 };
216
217 kind_slugs.push((kind, slug, no_span));
218 }
219
220 Ok(kind_slugs)
221 }
222
223 fn generate_field_arg(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
225 let diag = &self.parent.diag;
226
227 let field = binding_info.ast();
228 let mut field_binding = binding_info.binding.clone();
229 field_binding.set_span(field.ty.span());
230
231 let ident = field.ident.as_ref().unwrap();
232 let ident = format_ident!("{}", ident); quote! {
235 #diag.arg(
236 stringify!(#ident),
237 #field_binding
238 );
239 }
240 }
241
242 fn generate_field_attr_code(
244 &mut self,
245 binding: &BindingInfo<'_>,
246 kind_stats: KindsStatistics,
247 ) -> TokenStream {
248 let ast = binding.ast();
249 assert!(ast.attrs.len() > 0, "field without attributes generating attr code");
250
251 let inner_ty = FieldInnerTy::from_type(&ast.ty);
254 ast.attrs
255 .iter()
256 .map(|attr| {
257 if is_doc_comment(attr) {
259 return quote! {};
260 }
261
262 let info = FieldInfo { binding, ty: inner_ty, span: &ast.span() };
263
264 let generated = self
265 .generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate())
266 .unwrap_or_else(|v| v.to_compile_error());
267
268 inner_ty.with(binding, generated)
269 })
270 .collect()
271 }
272
273 fn generate_field_code_inner(
274 &mut self,
275 kind_stats: KindsStatistics,
276 attr: &Attribute,
277 info: FieldInfo<'_>,
278 clone_suggestion_code: bool,
279 ) -> Result<TokenStream, DiagnosticDeriveError> {
280 match &attr.meta {
281 Meta::Path(path) => {
282 self.generate_field_code_inner_path(kind_stats, attr, info, path.clone())
283 }
284 Meta::List(list) => self.generate_field_code_inner_list(
285 kind_stats,
286 attr,
287 info,
288 list,
289 clone_suggestion_code,
290 ),
291 _ => throw_invalid_attr!(attr),
292 }
293 }
294
295 fn generate_field_code_inner_path(
297 &mut self,
298 kind_stats: KindsStatistics,
299 attr: &Attribute,
300 info: FieldInfo<'_>,
301 path: Path,
302 ) -> Result<TokenStream, DiagnosticDeriveError> {
303 let span = attr.span().unwrap();
304 let ident = &path.segments.last().unwrap().ident;
305 let name = ident.to_string();
306 let name = name.as_str();
307
308 match name {
309 "skip_arg" => Ok(quote! {}),
310 "primary_span" => {
311 if kind_stats.has_multipart_suggestion {
312 invalid_attr(attr)
313 .help(
314 "multipart suggestions use one or more `#[suggestion_part]`s rather \
315 than one `#[primary_span]`",
316 )
317 .emit();
318 } else {
319 report_error_if_not_applied_to_span(attr, &info)?;
320
321 let binding = info.binding.binding.clone();
322 if !matches!(info.ty, FieldInnerTy::Plain(_)) {
325 throw_invalid_attr!(attr, |diag| {
326 let diag = diag.note("there must be exactly one primary span");
327
328 if kind_stats.has_normal_suggestion {
329 diag.help(
330 "to create a suggestion with multiple spans, \
331 use `#[multipart_suggestion]` instead",
332 )
333 } else {
334 diag
335 }
336 });
337 }
338
339 self.span_field.set_once(binding, span);
340 }
341
342 Ok(quote! {})
343 }
344 "suggestion_part" => {
345 self.has_suggestion_parts = true;
346
347 if kind_stats.has_multipart_suggestion {
348 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
349 .emit();
350 } else {
351 invalid_attr(attr)
352 .help(
353 "`#[suggestion_part(...)]` is only valid in multipart suggestions, \
354 use `#[primary_span]` instead",
355 )
356 .emit();
357 }
358
359 Ok(quote! {})
360 }
361 "applicability" => {
362 if kind_stats.has_multipart_suggestion || kind_stats.has_normal_suggestion {
363 report_error_if_not_applied_to_applicability(attr, &info)?;
364
365 if kind_stats.all_applicabilities_static {
366 span_err(
367 span,
368 "`#[applicability]` has no effect if all `#[suggestion]`/\
369 `#[multipart_suggestion]` attributes have a static \
370 `applicability = \"...\"`",
371 )
372 .emit();
373 }
374 let binding = info.binding.binding.clone();
375 self.applicability.set_once(quote! { #binding }, span);
376 } else {
377 span_err(span, "`#[applicability]` is only valid on suggestions").emit();
378 }
379
380 Ok(quote! {})
381 }
382 "subdiagnostic" => {
383 let diag = &self.parent.diag;
384 let binding = &info.binding;
385 self.has_subdiagnostic = true;
386 Ok(quote! { #binding.add_to_diag(#diag); })
387 }
388 _ => {
389 let mut span_attrs = vec![];
390 if kind_stats.has_multipart_suggestion {
391 span_attrs.push("suggestion_part");
392 }
393 if !kind_stats.all_multipart_suggestions {
394 span_attrs.push("primary_span");
395 }
396
397 invalid_attr(attr)
398 .help(format!(
399 "only `{}`, `applicability` and `skip_arg` are valid field attributes",
400 span_attrs.join(", ")
401 ))
402 .emit();
403
404 Ok(quote! {})
405 }
406 }
407 }
408
409 fn generate_field_code_inner_list(
412 &mut self,
413 kind_stats: KindsStatistics,
414 attr: &Attribute,
415 info: FieldInfo<'_>,
416 list: &MetaList,
417 clone_suggestion_code: bool,
418 ) -> Result<TokenStream, DiagnosticDeriveError> {
419 let span = attr.span().unwrap();
420 let mut ident = list.path.segments.last().unwrap().ident.clone();
421 ident.set_span(info.ty.span());
422 let name = ident.to_string();
423 let name = name.as_str();
424
425 match name {
426 "suggestion_part" => {
427 if !kind_stats.has_multipart_suggestion {
428 throw_invalid_attr!(attr, |diag| {
429 diag.help(
430 "`#[suggestion_part(...)]` is only valid in multipart suggestions",
431 )
432 })
433 }
434
435 self.has_suggestion_parts = true;
436
437 report_error_if_not_applied_to_span(attr, &info)?;
438
439 let mut code = None;
440
441 list.parse_nested_meta(|nested| {
442 if nested.path.is_ident("code") {
443 let code_field = new_code_ident();
444 let span = nested.path.span().unwrap();
445 let formatting_init = build_suggestion_code(
446 &code_field,
447 nested,
448 self,
449 AllowMultipleAlternatives::No,
450 );
451 code.set_once((code_field, formatting_init), span);
452 } else {
453 span_err(
454 nested.path.span().unwrap(),
455 "`code` is the only valid nested attribute",
456 )
457 .emit();
458 }
459 Ok(())
460 })?;
461
462 let Some((code_field, formatting_init)) = code.value() else {
463 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
464 .emit();
465 return Ok(quote! {});
466 };
467 let binding = info.binding;
468
469 self.formatting_init.extend(formatting_init);
470 let code_field = if clone_suggestion_code {
471 quote! { #code_field.clone() }
472 } else {
473 quote! { #code_field }
474 };
475 Ok(quote! { suggestions.push((#binding, #code_field)); })
476 }
477 _ => throw_invalid_attr!(attr, |diag| {
478 let mut span_attrs = vec![];
479 if kind_stats.has_multipart_suggestion {
480 span_attrs.push("suggestion_part");
481 }
482 if !kind_stats.all_multipart_suggestions {
483 span_attrs.push("primary_span");
484 }
485 diag.help(format!(
486 "only `{}`, `applicability` and `skip_arg` are valid field attributes",
487 span_attrs.join(", ")
488 ))
489 }),
490 }
491 }
492
493 pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
494 let kind_slugs = self.identify_kind()?;
495
496 let kind_stats: KindsStatistics =
497 kind_slugs.iter().map(|(kind, _slug, _no_span)| kind).collect();
498
499 let init = if kind_stats.has_multipart_suggestion {
500 quote! { let mut suggestions = Vec::new(); }
501 } else {
502 quote! {}
503 };
504
505 let attr_args: TokenStream = self
506 .variant
507 .bindings()
508 .iter()
509 .filter(|binding| !should_generate_arg(binding.ast()))
510 .map(|binding| self.generate_field_attr_code(binding, kind_stats))
511 .collect();
512
513 if kind_slugs.is_empty() && !self.has_subdiagnostic {
514 if self.is_enum {
515 return Ok(quote! {});
517 } else {
518 throw_span_err!(
520 self.variant.ast().ident.span().unwrap(),
521 "subdiagnostic kind not specified"
522 );
523 }
524 };
525
526 let span_field = self.span_field.value_ref();
527
528 let diag = &self.parent.diag;
529 let mut calls = TokenStream::new();
530 for (kind, slug, no_span) in kind_slugs {
531 let message = format_ident!("__message");
532 calls.extend(
533 quote! { let #message = #diag.eagerly_translate(crate::fluent_generated::#slug); },
534 );
535
536 let name = format_ident!(
537 "{}{}",
538 if span_field.is_some() && !no_span { "span_" } else { "" },
539 kind
540 );
541 let call = match kind {
542 SubdiagnosticKind::Suggestion {
543 suggestion_kind,
544 applicability,
545 code_init,
546 code_field,
547 } => {
548 self.formatting_init.extend(code_init);
549
550 let applicability = applicability
551 .value()
552 .map(|a| quote! { #a })
553 .or_else(|| self.applicability.take().value())
554 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
555
556 if let Some(span) = span_field {
557 let style = suggestion_kind.to_suggestion_style();
558 quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
559 } else {
560 span_err(self.span, "suggestion without `#[primary_span]` field").emit();
561 quote! { unreachable!(); }
562 }
563 }
564 SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
565 let applicability = applicability
566 .value()
567 .map(|a| quote! { #a })
568 .or_else(|| self.applicability.take().value())
569 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
570
571 if !self.has_suggestion_parts {
572 span_err(
573 self.span,
574 "multipart suggestion without any `#[suggestion_part(...)]` fields",
575 )
576 .emit();
577 }
578
579 let style = suggestion_kind.to_suggestion_style();
580
581 quote! { #diag.#name(#message, suggestions, #applicability, #style); }
582 }
583 SubdiagnosticKind::Label => {
584 if let Some(span) = span_field {
585 quote! { #diag.#name(#span, #message); }
586 } else {
587 span_err(self.span, "label without `#[primary_span]` field").emit();
588 quote! { unreachable!(); }
589 }
590 }
591 _ => {
592 if let Some(span) = span_field
593 && !no_span
594 {
595 quote! { #diag.#name(#span, #message); }
596 } else {
597 quote! { #diag.#name(#message); }
598 }
599 }
600 };
601
602 calls.extend(call);
603 }
604 let store_args = quote! {
605 #diag.store_args();
606 };
607 let restore_args = quote! {
608 #diag.restore_args();
609 };
610 let plain_args: TokenStream = self
611 .variant
612 .bindings()
613 .iter()
614 .filter(|binding| should_generate_arg(binding.ast()))
615 .map(|binding| self.generate_field_arg(binding))
616 .collect();
617
618 let formatting_init = &self.formatting_init;
619
620 Ok(quote! {
627 #init
628 #formatting_init
629 #attr_args
630 #store_args
631 #plain_args
632 #calls
633 #restore_args
634 })
635 }
636}