1#![deny(unused_must_use)]
2
3use std::collections::HashSet;
4
5use proc_macro2::{Ident, TokenStream};
6use quote::{format_ident, quote};
7use syn::parse::ParseStream;
8use syn::spanned::Spanned;
9use syn::{Attribute, Meta, MetaList, Path, Token};
10use synstructure::{BindingInfo, Structure, VariantInfo};
11
12use super::utils::SubdiagnosticVariant;
13use crate::diagnostics::error::{
14 DiagnosticDeriveError, invalid_attr, span_err, throw_invalid_attr, throw_span_err,
15};
16use crate::diagnostics::message::Message;
17use crate::diagnostics::utils::{
18 AllowMultipleAlternatives, FieldInfo, FieldInnerTy, FieldMap, SetOnce, SpannedOption,
19 SubdiagnosticKind, build_field_mapping, build_suggestion_code, is_doc_comment, new_code_ident,
20 report_error_if_not_applied_to_applicability, report_error_if_not_applied_to_span,
21 should_generate_arg,
22};
23
24pub(crate) struct SubdiagnosticDerive {
26 diag: syn::Ident,
27}
28
29impl SubdiagnosticDerive {
30 pub(crate) fn new() -> Self {
31 let diag = format_ident!("diag");
32 Self { diag }
33 }
34
35 pub(crate) fn into_tokens(self, mut structure: Structure<'_>) -> TokenStream {
36 let implementation = {
37 let ast = structure.ast();
38 let span = ast.span().unwrap();
39 match ast.data {
40 syn::Data::Struct(..) | syn::Data::Enum(..) => (),
41 syn::Data::Union(..) => {
42 span_err(
43 span,
44 "`#[derive(Subdiagnostic)]` can only be used on structs and enums",
45 )
46 .emit();
47 }
48 }
49
50 let is_enum = matches!(ast.data, syn::Data::Enum(..));
51 if is_enum {
52 for attr in &ast.attrs {
53 if is_doc_comment(attr) {
55 continue;
56 }
57
58 span_err(
59 attr.span().unwrap(),
60 "unsupported type attribute for subdiagnostic enum",
61 )
62 .emit();
63 }
64 }
65
66 let mut used_fields: HashSet<proc_macro2::Ident> = HashSet::new();
67
68 structure.bind_with(|_| synstructure::BindStyle::Move);
69 let variants_ = structure.each_variant(|variant| {
70 let mut builder = SubdiagnosticDeriveVariantBuilder {
71 parent: &self,
72 variant,
73 span,
74 formatting_init: TokenStream::new(),
75 fields: build_field_mapping(variant),
76 span_field: None,
77 applicability: None,
78 has_suggestion_parts: false,
79 has_subdiagnostic: false,
80 is_enum,
81 used_fields: &mut used_fields,
82 };
83 builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
84 });
85
86 quote! {
87 match self {
88 #variants_
89 }
90 }
91 };
92
93 let diag = &self.diag;
94
95 #[allow(keyword_idents_2024)]
97 let ret = structure.gen_impl(quote! {
98 gen impl rustc_errors::Subdiagnostic for @Self {
99 fn add_to_diag<__G>(
100 self,
101 #diag: &mut rustc_errors::Diag<'_, __G>,
102 ) where
103 __G: rustc_errors::EmissionGuarantee,
104 {
105 #implementation
106 }
107 }
108 });
109
110 ret
111 }
112}
113
114struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
119 parent: &'parent SubdiagnosticDerive,
121
122 variant: &'a VariantInfo<'a>,
124 span: proc_macro::Span,
126
127 formatting_init: TokenStream,
129
130 fields: FieldMap,
133
134 span_field: SpannedOption<proc_macro2::Ident>,
136
137 applicability: SpannedOption<TokenStream>,
139
140 has_suggestion_parts: bool,
143
144 has_subdiagnostic: bool,
147
148 is_enum: bool,
150
151 used_fields: &'parent mut HashSet<proc_macro2::Ident>,
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, Message)>, DiagnosticDeriveError> {
196 let mut kind_messages = vec![];
197
198 for attr in self.variant.ast().attrs {
199 let Some(SubdiagnosticVariant { kind, message }) =
200 SubdiagnosticVariant::from_attr(attr, &self.fields, &mut self.used_fields)?
201 else {
202 continue;
205 };
206
207 let Some(message) = message 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 message must be first argument of a `#[{name}(...)]` attribute"
215 )
216 );
217 };
218
219 kind_messages.push((kind, message));
220 }
221
222 Ok(kind_messages)
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 sub_args.insert(
238 stringify!(#ident).into(),
239 rustc_errors::IntoDiagArg::into_diag_arg(#field_binding, &mut #diag.long_ty_path)
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) => self.generate_field_code_inner_list(
287 kind_stats,
288 attr,
289 info,
290 list,
291 clone_suggestion_code,
292 ),
293 _ => throw_invalid_attr!(attr),
294 }
295 }
296
297 fn generate_field_code_inner_path(
299 &mut self,
300 kind_stats: KindsStatistics,
301 attr: &Attribute,
302 info: FieldInfo<'_>,
303 path: Path,
304 ) -> Result<TokenStream, DiagnosticDeriveError> {
305 let span = attr.span().unwrap();
306 let ident = &path.segments.last().unwrap().ident;
307 let name = ident.to_string();
308 let name = name.as_str();
309
310 match name {
311 "primary_span" => {
312 if kind_stats.has_multipart_suggestion {
313 invalid_attr(attr)
314 .help(
315 "multipart suggestions use one or more `#[suggestion_part]`s rather \
316 than one `#[primary_span]`",
317 )
318 .emit();
319 } else {
320 report_error_if_not_applied_to_span(attr, &info)?;
321
322 let binding = info.binding.binding.clone();
323 if !matches!(info.ty, FieldInnerTy::Plain(_)) {
326 throw_invalid_attr!(attr, |diag| {
327 let diag = diag.note("there must be exactly one primary span");
328
329 if kind_stats.has_normal_suggestion {
330 diag.help(
331 "to create a suggestion with multiple spans, \
332 use `#[multipart_suggestion]` instead",
333 )
334 } else {
335 diag
336 }
337 });
338 }
339
340 self.span_field.set_once(binding, span);
341 }
342
343 Ok(quote! {})
344 }
345 "suggestion_part" => {
346 self.has_suggestion_parts = true;
347
348 if kind_stats.has_multipart_suggestion {
349 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
350 .emit();
351 } else {
352 invalid_attr(attr)
353 .help(
354 "`#[suggestion_part(...)]` is only valid in multipart suggestions, \
355 use `#[primary_span]` instead",
356 )
357 .emit();
358 }
359
360 Ok(quote! {})
361 }
362 "applicability" => {
363 if kind_stats.has_multipart_suggestion || kind_stats.has_normal_suggestion {
364 report_error_if_not_applied_to_applicability(attr, &info)?;
365
366 if kind_stats.all_applicabilities_static {
367 span_err(
368 span,
369 "`#[applicability]` has no effect if all `#[suggestion]`/\
370 `#[multipart_suggestion]` attributes have a static \
371 `applicability = \"...\"`",
372 )
373 .emit();
374 }
375 let binding = info.binding.binding.clone();
376 self.applicability.set_once(quote! { #binding }, span);
377 } else {
378 span_err(span, "`#[applicability]` is only valid on suggestions").emit();
379 }
380
381 Ok(quote! {})
382 }
383 "subdiagnostic" => {
384 let diag = &self.parent.diag;
385 let binding = &info.binding;
386 self.has_subdiagnostic = true;
387 Ok(quote! { #binding.add_to_diag(#diag); })
388 }
389 _ => {
390 let mut span_attrs = vec![];
391 if kind_stats.has_multipart_suggestion {
392 span_attrs.push("suggestion_part");
393 }
394 if !kind_stats.all_multipart_suggestions {
395 span_attrs.push("primary_span")
396 }
397
398 invalid_attr(attr)
399 .help(format!(
400 "only `{}`, `applicability` is a valid field attribute",
401 span_attrs.join(", ")
402 ))
403 .emit();
404
405 Ok(quote! {})
406 }
407 }
408 }
409
410 fn generate_field_code_inner_list(
413 &mut self,
414 kind_stats: KindsStatistics,
415 attr: &Attribute,
416 info: FieldInfo<'_>,
417 list: &MetaList,
418 clone_suggestion_code: bool,
419 ) -> Result<TokenStream, DiagnosticDeriveError> {
420 let span = attr.span().unwrap();
421 let mut ident = list.path.segments.last().unwrap().ident.clone();
422 ident.set_span(info.ty.span());
423 let name = ident.to_string();
424 let name = name.as_str();
425
426 match name {
427 "suggestion_part" => {
428 if !kind_stats.has_multipart_suggestion {
429 throw_invalid_attr!(attr, |diag| {
430 diag.help(
431 "`#[suggestion_part(...)]` is only valid in multipart suggestions",
432 )
433 })
434 }
435
436 self.has_suggestion_parts = true;
437
438 report_error_if_not_applied_to_span(attr, &info)?;
439
440 let mut code = None;
441
442 list.parse_args_with(|input: ParseStream<'_>| {
443 while !input.is_empty() {
444 let arg_name = input.parse::<Ident>()?;
445 match arg_name.to_string().as_str() {
446 "code" => {
447 let code_field = new_code_ident();
448 let formatting_init = build_suggestion_code(
449 &code_field,
450 input,
451 &self.fields,
452 AllowMultipleAlternatives::No,
453 )?;
454 code.set_once(
455 (code_field, formatting_init),
456 arg_name.span().unwrap(),
457 );
458 }
459 _ => {
460 span_err(
461 arg_name.span().unwrap(),
462 "`code` is the only valid nested attribute",
463 )
464 .emit();
465 }
466 }
467 if input.is_empty() {
468 break;
469 }
470 input.parse::<Token![,]>()?;
471 }
472 Ok(())
473 })?;
474
475 let Some((code_field, formatting_init)) = code.value() else {
476 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
477 .emit();
478 return Ok(quote! {});
479 };
480 let binding = info.binding;
481
482 self.formatting_init.extend(formatting_init);
483 let code_field = if clone_suggestion_code {
484 quote! { #code_field.clone() }
485 } else {
486 quote! { #code_field }
487 };
488 Ok(quote! { suggestions.push((#binding, #code_field)); })
489 }
490 _ => throw_invalid_attr!(attr, |diag| {
491 let mut span_attrs = vec![];
492 if kind_stats.has_multipart_suggestion {
493 span_attrs.push("suggestion_part");
494 }
495 if !kind_stats.all_multipart_suggestions {
496 span_attrs.push("primary_span")
497 }
498 diag.help(format!(
499 "only `{}`, `applicability` is a valid field attribute",
500 span_attrs.join(", ")
501 ))
502 }),
503 }
504 }
505
506 fn is_used_in_message(&self, binding: &BindingInfo<'_>) -> bool {
507 binding.ast().ident.as_ref().is_some_and(|ident| self.used_fields.contains(ident))
508 }
509
510 pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
511 let kind_messages = self.identify_kind()?;
512
513 let kind_stats: KindsStatistics = kind_messages.iter().map(|(kind, _msg)| kind).collect();
514
515 let init = if kind_stats.has_multipart_suggestion {
516 quote! { let mut suggestions = Vec::new(); }
517 } else {
518 quote! {}
519 };
520
521 let attr_args: TokenStream = self
522 .variant
523 .bindings()
524 .iter()
525 .filter(|binding| !should_generate_arg(binding.ast()))
526 .map(|binding| self.generate_field_attr_code(binding, kind_stats))
527 .collect();
528
529 if kind_messages.is_empty() && !self.has_subdiagnostic {
530 if self.is_enum {
531 return Ok(quote! {});
533 } else {
534 throw_span_err!(
536 self.variant.ast().ident.span().unwrap(),
537 "subdiagnostic kind not specified"
538 );
539 }
540 };
541
542 let plain_args: TokenStream = self
543 .variant
544 .bindings()
545 .iter()
546 .filter_map(|binding| {
547 if should_generate_arg(binding.ast()) && self.is_used_in_message(binding) {
548 Some(self.generate_field_arg(binding))
549 } else {
550 None
551 }
552 })
553 .collect();
554 let plain_args = quote! {
555 let mut sub_args = rustc_errors::DiagArgMap::default();
556 #plain_args
557 };
558
559 let span_field = self.span_field.value_ref();
560 let diag = &self.parent.diag;
561 let mut calls = TokenStream::new();
562 for (kind, messages) in kind_messages {
563 let message = format_ident!("__message");
564 let message_stream = messages.diag_message();
565 calls.extend(quote! { let #message = rustc_errors::format_diag_message(&#message_stream, &sub_args); });
566
567 let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
568 let call = match kind {
569 SubdiagnosticKind::Suggestion {
570 suggestion_kind,
571 applicability,
572 code_init,
573 code_field,
574 } => {
575 self.formatting_init.extend(code_init);
576
577 let applicability = applicability
578 .value()
579 .map(|a| quote! { #a })
580 .or_else(|| self.applicability.take().value())
581 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
582
583 if let Some(span) = span_field {
584 let style = suggestion_kind.to_suggestion_style();
585 quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
586 } else {
587 span_err(self.span, "suggestion without `#[primary_span]` field").emit();
588 quote! { unreachable!(); }
589 }
590 }
591 SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
592 let applicability = applicability
593 .value()
594 .map(|a| quote! { #a })
595 .or_else(|| self.applicability.take().value())
596 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
597
598 if !self.has_suggestion_parts {
599 span_err(
600 self.span,
601 "multipart suggestion without any `#[suggestion_part(...)]` fields",
602 )
603 .emit();
604 }
605
606 let style = suggestion_kind.to_suggestion_style();
607
608 quote! { #diag.#name(#message, suggestions, #applicability, #style); }
609 }
610 SubdiagnosticKind::Label => {
611 if let Some(span) = span_field {
612 quote! { #diag.#name(#span, #message); }
613 } else {
614 span_err(self.span, "label without `#[primary_span]` field").emit();
615 quote! { unreachable!(); }
616 }
617 }
618 _ => {
619 if let Some(span) = span_field {
620 quote! { #diag.#name(#span, #message); }
621 } else {
622 quote! { #diag.#name(#message); }
623 }
624 }
625 };
626
627 calls.extend(call);
628 }
629
630 let formatting_init = &self.formatting_init;
631
632 Ok(quote! {
639 #init
640 #formatting_init
641 #attr_args
642 #plain_args
644 #calls
645 })
647 }
648}