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