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