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 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
79 .into_tokens()
80 .unwrap_or_else(|v| v.to_compile_error())
81 });
82
83 quote! {
84 match self {
85 #variants_
86 }
87 }
88 };
89
90 let diag = &self.diag;
91 let f = &self.f;
92 let ret = structure.gen_impl(quote! {
93 gen impl rustc_errors::Subdiagnostic for @Self {
94 fn add_to_diag_with<__G, __F>(
95 self,
96 #diag: &mut rustc_errors::Diag<'_, __G>,
97 #f: &__F
98 ) where
99 __G: rustc_errors::EmissionGuarantee,
100 __F: rustc_errors::SubdiagMessageOp<__G>,
101 {
102 #implementation
103 }
104 }
105 });
106 ret
107 }
108}
109
110struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
115 parent: &'parent SubdiagnosticDerive,
117
118 variant: &'a VariantInfo<'a>,
120 span: proc_macro::Span,
122
123 formatting_init: TokenStream,
125
126 fields: FieldMap,
129
130 span_field: SpannedOption<proc_macro2::Ident>,
132
133 applicability: SpannedOption<TokenStream>,
135
136 has_suggestion_parts: bool,
139
140 has_subdiagnostic: bool,
143
144 is_enum: bool,
146}
147
148impl<'parent, 'a> HasFieldMap for SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
149 fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
150 self.fields.get(field)
151 }
152}
153
154#[derive(Clone, Copy, Debug)]
156struct KindsStatistics {
157 has_multipart_suggestion: bool,
158 all_multipart_suggestions: bool,
159 has_normal_suggestion: bool,
160 all_applicabilities_static: bool,
161}
162
163impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics {
164 fn from_iter<T: IntoIterator<Item = &'a SubdiagnosticKind>>(kinds: T) -> Self {
165 let mut ret = Self {
166 has_multipart_suggestion: false,
167 all_multipart_suggestions: true,
168 has_normal_suggestion: false,
169 all_applicabilities_static: true,
170 };
171
172 for kind in kinds {
173 if let SubdiagnosticKind::MultipartSuggestion { applicability: None, .. }
174 | SubdiagnosticKind::Suggestion { applicability: None, .. } = kind
175 {
176 ret.all_applicabilities_static = false;
177 }
178 if let SubdiagnosticKind::MultipartSuggestion { .. } = kind {
179 ret.has_multipart_suggestion = true;
180 } else {
181 ret.all_multipart_suggestions = false;
182 }
183
184 if let SubdiagnosticKind::Suggestion { .. } = kind {
185 ret.has_normal_suggestion = true;
186 }
187 }
188 ret
189 }
190}
191
192impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
193 fn identify_kind(
194 &mut self,
195 ) -> Result<Vec<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
196 let mut kind_slugs = vec![];
197
198 for attr in self.variant.ast().attrs {
199 let Some(SubdiagnosticVariant { kind, slug, no_span }) =
200 SubdiagnosticVariant::from_attr(attr, self)?
201 else {
202 continue;
205 };
206
207 let Some(slug) = slug else {
208 let name = attr.path().segments.last().unwrap().ident.to_string();
209 let name = name.as_str();
210
211 throw_span_err!(
212 attr.span().unwrap(),
213 format!(
214 "diagnostic slug must be first argument of a `#[{name}(...)]` attribute"
215 )
216 );
217 };
218
219 kind_slugs.push((kind, slug, no_span));
220 }
221
222 Ok(kind_slugs)
223 }
224
225 fn generate_field_arg(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
227 let diag = &self.parent.diag;
228
229 let field = binding_info.ast();
230 let mut field_binding = binding_info.binding.clone();
231 field_binding.set_span(field.ty.span());
232
233 let ident = field.ident.as_ref().unwrap();
234 let ident = format_ident!("{}", ident); quote! {
237 #diag.arg(
238 stringify!(#ident),
239 #field_binding
240 );
241 }
242 }
243
244 fn generate_field_attr_code(
246 &mut self,
247 binding: &BindingInfo<'_>,
248 kind_stats: KindsStatistics,
249 ) -> TokenStream {
250 let ast = binding.ast();
251 assert!(ast.attrs.len() > 0, "field without attributes generating attr code");
252
253 let inner_ty = FieldInnerTy::from_type(&ast.ty);
256 ast.attrs
257 .iter()
258 .map(|attr| {
259 if is_doc_comment(attr) {
261 return quote! {};
262 }
263
264 let info = FieldInfo { binding, ty: inner_ty, span: &ast.span() };
265
266 let generated = self
267 .generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate())
268 .unwrap_or_else(|v| v.to_compile_error());
269
270 inner_ty.with(binding, generated)
271 })
272 .collect()
273 }
274
275 fn generate_field_code_inner(
276 &mut self,
277 kind_stats: KindsStatistics,
278 attr: &Attribute,
279 info: FieldInfo<'_>,
280 clone_suggestion_code: bool,
281 ) -> Result<TokenStream, DiagnosticDeriveError> {
282 match &attr.meta {
283 Meta::Path(path) => {
284 self.generate_field_code_inner_path(kind_stats, attr, info, path.clone())
285 }
286 Meta::List(list) => {
287 self.generate_field_code_inner_list(
288 kind_stats,
289 attr,
290 info,
291 list,
292 clone_suggestion_code,
293 )
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 _ => {
483 throw_invalid_attr!(attr, |diag| {
484 let mut span_attrs = vec![];
485 if kind_stats.has_multipart_suggestion {
486 span_attrs.push("suggestion_part");
487 }
488 if !kind_stats.all_multipart_suggestions {
489 span_attrs.push("primary_span")
490 }
491 diag.help(format!(
492 "only `{}`, `applicability` and `skip_arg` are valid field attributes",
493 span_attrs.join(", ")
494 ))
495 })
496 }
497 }
498 }
499
500 pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
501 let kind_slugs = self.identify_kind()?;
502
503 let kind_stats: KindsStatistics = kind_slugs
504 .iter()
505 .map(|(kind, _slug, _no_span)| kind)
506 .collect();
507
508 let init = if kind_stats.has_multipart_suggestion {
509 quote! { let mut suggestions = Vec::new(); }
510 } else {
511 quote! {}
512 };
513
514 let attr_args: TokenStream = self
515 .variant
516 .bindings()
517 .iter()
518 .filter(|binding| !should_generate_arg(binding.ast()))
519 .map(|binding| self.generate_field_attr_code(binding, kind_stats))
520 .collect();
521
522 if kind_slugs.is_empty() && !self.has_subdiagnostic {
523 if self.is_enum {
524 return Ok(quote! {});
526 } else {
527 throw_span_err!(
529 self.variant.ast().ident.span().unwrap(),
530 "subdiagnostic kind not specified"
531 );
532 }
533 };
534
535 let span_field = self.span_field.value_ref();
536
537 let diag = &self.parent.diag;
538 let f = &self.parent.f;
539 let mut calls = TokenStream::new();
540 for (kind, slug, no_span) in kind_slugs {
541 let message = format_ident!("__message");
542 calls.extend(
543 quote! { let #message = #f(#diag, crate::fluent_generated::#slug.into()); },
544 );
545
546 let name = format_ident!(
547 "{}{}",
548 if span_field.is_some() && !no_span { "span_" } else { "" },
549 kind
550 );
551 let call = match kind {
552 SubdiagnosticKind::Suggestion {
553 suggestion_kind,
554 applicability,
555 code_init,
556 code_field,
557 } => {
558 self.formatting_init.extend(code_init);
559
560 let applicability = applicability
561 .value()
562 .map(|a| quote! { #a })
563 .or_else(|| self.applicability.take().value())
564 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
565
566 if let Some(span) = span_field {
567 let style = suggestion_kind.to_suggestion_style();
568 quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
569 } else {
570 span_err(self.span, "suggestion without `#[primary_span]` field").emit();
571 quote! { unreachable!(); }
572 }
573 }
574 SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
575 let applicability = applicability
576 .value()
577 .map(|a| quote! { #a })
578 .or_else(|| self.applicability.take().value())
579 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
580
581 if !self.has_suggestion_parts {
582 span_err(
583 self.span,
584 "multipart suggestion without any `#[suggestion_part(...)]` fields",
585 )
586 .emit();
587 }
588
589 let style = suggestion_kind.to_suggestion_style();
590
591 quote! { #diag.#name(#message, suggestions, #applicability, #style); }
592 }
593 SubdiagnosticKind::Label => {
594 if let Some(span) = span_field {
595 quote! { #diag.#name(#span, #message); }
596 } else {
597 span_err(self.span, "label without `#[primary_span]` field").emit();
598 quote! { unreachable!(); }
599 }
600 }
601 _ => {
602 if let Some(span) = span_field
603 && !no_span
604 {
605 quote! { #diag.#name(#span, #message); }
606 } else {
607 quote! { #diag.#name(#message); }
608 }
609 }
610 };
611
612 calls.extend(call);
613 }
614
615 let plain_args: TokenStream = self
616 .variant
617 .bindings()
618 .iter()
619 .filter(|binding| should_generate_arg(binding.ast()))
620 .map(|binding| self.generate_field_arg(binding))
621 .collect();
622
623 let formatting_init = &self.formatting_init;
624 Ok(quote! {
625 #init
626 #formatting_init
627 #attr_args
628 #plain_args
629 #calls
630 })
631 }
632}