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 let ret = structure.gen_impl(quote! {
96 gen impl rustc_errors::Subdiagnostic for @Self {
97 fn add_to_diag(self, #diag: &mut rustc_errors::Diag<'_>) {
98 #implementation
99 }
100 }
101 });
102
103 ret
104 }
105}
106
107struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
112 parent: &'parent SubdiagnosticDerive,
114
115 variant: &'a VariantInfo<'a>,
117 span: proc_macro::Span,
119
120 formatting_init: TokenStream,
122
123 fields: FieldMap,
126
127 span_field: SpannedOption<proc_macro2::Ident>,
129
130 applicability: SpannedOption<TokenStream>,
132
133 has_suggestion_parts: bool,
136
137 has_subdiagnostic: bool,
140
141 is_enum: bool,
143
144 used_fields: &'parent mut HashSet<proc_macro2::Ident>,
145}
146
147#[derive(Clone, Copy, Debug)]
149struct KindsStatistics {
150 has_multipart_suggestion: bool,
151 all_multipart_suggestions: bool,
152 has_normal_suggestion: bool,
153 all_applicabilities_static: bool,
154}
155
156impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics {
157 fn from_iter<T: IntoIterator<Item = &'a SubdiagnosticKind>>(kinds: T) -> Self {
158 let mut ret = Self {
159 has_multipart_suggestion: false,
160 all_multipart_suggestions: true,
161 has_normal_suggestion: false,
162 all_applicabilities_static: true,
163 };
164
165 for kind in kinds {
166 if let SubdiagnosticKind::MultipartSuggestion { applicability: None, .. }
167 | SubdiagnosticKind::Suggestion { applicability: None, .. } = kind
168 {
169 ret.all_applicabilities_static = false;
170 }
171 if let SubdiagnosticKind::MultipartSuggestion { .. } = kind {
172 ret.has_multipart_suggestion = true;
173 } else {
174 ret.all_multipart_suggestions = false;
175 }
176
177 if let SubdiagnosticKind::Suggestion { .. } = kind {
178 ret.has_normal_suggestion = true;
179 }
180 }
181 ret
182 }
183}
184
185impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
186 fn identify_kind(
187 &mut self,
188 ) -> Result<Vec<(SubdiagnosticKind, Message)>, DiagnosticDeriveError> {
189 let mut kind_messages = vec![];
190
191 for attr in self.variant.ast().attrs {
192 let Some(SubdiagnosticVariant { kind, message }) =
193 SubdiagnosticVariant::from_attr(attr, &self.fields, &mut self.used_fields)?
194 else {
195 continue;
198 };
199
200 let Some(message) = message else {
201 let name = attr.path().segments.last().unwrap().ident.to_string();
202 let name = name.as_str();
203
204 throw_span_err!(
205 attr.span().unwrap(),
206 format!(
207 "diagnostic message must be first argument of a `#[{name}(...)]` attribute"
208 )
209 );
210 };
211
212 kind_messages.push((kind, message));
213 }
214
215 Ok(kind_messages)
216 }
217
218 fn generate_field_arg(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
220 let diag = &self.parent.diag;
221
222 let field = binding_info.ast();
223 let mut field_binding = binding_info.binding.clone();
224 field_binding.set_span(field.ty.span());
225
226 let ident = field.ident.as_ref().unwrap();
227 let ident = format_ident!("{}", ident); quote! {
230 sub_args.insert(
231 stringify!(#ident).into(),
232 rustc_errors::IntoDiagArg::into_diag_arg(#field_binding, &mut #diag.long_ty_path)
233 );
234 }
235 }
236
237 fn generate_field_attr_code(
239 &mut self,
240 binding: &BindingInfo<'_>,
241 kind_stats: KindsStatistics,
242 ) -> TokenStream {
243 let ast = binding.ast();
244 assert!(ast.attrs.len() > 0, "field without attributes generating attr code");
245
246 let inner_ty = FieldInnerTy::from_type(&ast.ty);
249 ast.attrs
250 .iter()
251 .map(|attr| {
252 if is_doc_comment(attr) {
254 return quote! {};
255 }
256
257 let info = FieldInfo { binding, ty: inner_ty, span: &ast.span() };
258
259 let generated = self
260 .generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate())
261 .unwrap_or_else(|v| v.to_compile_error());
262
263 inner_ty.with(binding, generated)
264 })
265 .collect()
266 }
267
268 fn generate_field_code_inner(
269 &mut self,
270 kind_stats: KindsStatistics,
271 attr: &Attribute,
272 info: FieldInfo<'_>,
273 clone_suggestion_code: bool,
274 ) -> Result<TokenStream, DiagnosticDeriveError> {
275 match &attr.meta {
276 Meta::Path(path) => {
277 self.generate_field_code_inner_path(kind_stats, attr, info, path.clone())
278 }
279 Meta::List(list) => self.generate_field_code_inner_list(
280 kind_stats,
281 attr,
282 info,
283 list,
284 clone_suggestion_code,
285 ),
286 _ => throw_invalid_attr!(attr),
287 }
288 }
289
290 fn generate_field_code_inner_path(
292 &mut self,
293 kind_stats: KindsStatistics,
294 attr: &Attribute,
295 info: FieldInfo<'_>,
296 path: Path,
297 ) -> Result<TokenStream, DiagnosticDeriveError> {
298 let span = attr.span().unwrap();
299 let ident = &path.segments.last().unwrap().ident;
300 let name = ident.to_string();
301 let name = name.as_str();
302
303 match name {
304 "primary_span" => {
305 if kind_stats.has_multipart_suggestion {
306 invalid_attr(attr)
307 .help(
308 "multipart suggestions use one or more `#[suggestion_part]`s rather \
309 than one `#[primary_span]`",
310 )
311 .emit();
312 } else {
313 report_error_if_not_applied_to_span(attr, &info)?;
314
315 let binding = info.binding.binding.clone();
316 if !matches!(info.ty, FieldInnerTy::Plain(_)) {
319 throw_invalid_attr!(attr, |diag| {
320 let diag = diag.note("there must be exactly one primary span");
321
322 if kind_stats.has_normal_suggestion {
323 diag.help(
324 "to create a suggestion with multiple spans, \
325 use `#[multipart_suggestion]` instead",
326 )
327 } else {
328 diag
329 }
330 });
331 }
332
333 self.span_field.set_once(binding, span);
334 }
335
336 Ok(quote! {})
337 }
338 "suggestion_part" => {
339 self.has_suggestion_parts = true;
340
341 if kind_stats.has_multipart_suggestion {
342 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
343 .emit();
344 } else {
345 invalid_attr(attr)
346 .help(
347 "`#[suggestion_part(...)]` is only valid in multipart suggestions, \
348 use `#[primary_span]` instead",
349 )
350 .emit();
351 }
352
353 Ok(quote! {})
354 }
355 "applicability" => {
356 if kind_stats.has_multipart_suggestion || kind_stats.has_normal_suggestion {
357 report_error_if_not_applied_to_applicability(attr, &info)?;
358
359 if kind_stats.all_applicabilities_static {
360 span_err(
361 span,
362 "`#[applicability]` has no effect if all `#[suggestion]`/\
363 `#[multipart_suggestion]` attributes have a static \
364 `applicability = \"...\"`",
365 )
366 .emit();
367 }
368 let binding = info.binding.binding.clone();
369 self.applicability.set_once(quote! { #binding }, span);
370 } else {
371 span_err(span, "`#[applicability]` is only valid on suggestions").emit();
372 }
373
374 Ok(quote! {})
375 }
376 "subdiagnostic" => {
377 let diag = &self.parent.diag;
378 let binding = &info.binding;
379 self.has_subdiagnostic = true;
380 Ok(quote! { #binding.add_to_diag(#diag); })
381 }
382 _ => {
383 let mut span_attrs = vec![];
384 if kind_stats.has_multipart_suggestion {
385 span_attrs.push("suggestion_part");
386 }
387 if !kind_stats.all_multipart_suggestions {
388 span_attrs.push("primary_span")
389 }
390
391 invalid_attr(attr)
392 .help(format!(
393 "only `{}`, `applicability` is a valid field attribute",
394 span_attrs.join(", ")
395 ))
396 .emit();
397
398 Ok(quote! {})
399 }
400 }
401 }
402
403 fn generate_field_code_inner_list(
406 &mut self,
407 kind_stats: KindsStatistics,
408 attr: &Attribute,
409 info: FieldInfo<'_>,
410 list: &MetaList,
411 clone_suggestion_code: bool,
412 ) -> Result<TokenStream, DiagnosticDeriveError> {
413 let span = attr.span().unwrap();
414 let mut ident = list.path.segments.last().unwrap().ident.clone();
415 ident.set_span(info.ty.span());
416 let name = ident.to_string();
417 let name = name.as_str();
418
419 match name {
420 "suggestion_part" => {
421 if !kind_stats.has_multipart_suggestion {
422 throw_invalid_attr!(attr, |diag| {
423 diag.help(
424 "`#[suggestion_part(...)]` is only valid in multipart suggestions",
425 )
426 })
427 }
428
429 self.has_suggestion_parts = true;
430
431 report_error_if_not_applied_to_span(attr, &info)?;
432
433 let mut code = None;
434
435 list.parse_args_with(|input: ParseStream<'_>| {
436 while !input.is_empty() {
437 let arg_name = input.parse::<Ident>()?;
438 match arg_name.to_string().as_str() {
439 "code" => {
440 let code_field = new_code_ident();
441 let formatting_init = build_suggestion_code(
442 &code_field,
443 input,
444 &self.fields,
445 AllowMultipleAlternatives::No,
446 )?;
447 code.set_once(
448 (code_field, formatting_init),
449 arg_name.span().unwrap(),
450 );
451 }
452 _ => {
453 span_err(
454 arg_name.span().unwrap(),
455 "`code` is the only valid nested attribute",
456 )
457 .emit();
458 }
459 }
460 if input.is_empty() {
461 break;
462 }
463 input.parse::<Token![,]>()?;
464 }
465 Ok(())
466 })?;
467
468 let Some((code_field, formatting_init)) = code.value() else {
469 span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
470 .emit();
471 return Ok(quote! {});
472 };
473 let binding = info.binding;
474
475 self.formatting_init.extend(formatting_init);
476 let code_field = if clone_suggestion_code {
477 quote! { #code_field.clone() }
478 } else {
479 quote! { #code_field }
480 };
481 Ok(quote! { suggestions.push((#binding, #code_field)); })
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` is a valid field attribute",
493 span_attrs.join(", ")
494 ))
495 }),
496 }
497 }
498
499 fn is_used_in_message(&self, binding: &BindingInfo<'_>) -> bool {
500 binding.ast().ident.as_ref().is_some_and(|ident| self.used_fields.contains(ident))
501 }
502
503 pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
504 let kind_messages = self.identify_kind()?;
505
506 let kind_stats: KindsStatistics = kind_messages.iter().map(|(kind, _msg)| kind).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_messages.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 plain_args: TokenStream = self
536 .variant
537 .bindings()
538 .iter()
539 .filter_map(|binding| {
540 if should_generate_arg(binding.ast()) && self.is_used_in_message(binding) {
541 Some(self.generate_field_arg(binding))
542 } else {
543 None
544 }
545 })
546 .collect();
547 let plain_args = quote! {
548 let mut sub_args = rustc_errors::DiagArgMap::default();
549 #plain_args
550 };
551
552 let span_field = self.span_field.value_ref();
553 let diag = &self.parent.diag;
554 let mut calls = TokenStream::new();
555 for (kind, messages) in kind_messages {
556 let message = format_ident!("__message");
557 let message_stream = messages.diag_message();
558 calls.extend(quote! { let #message = rustc_errors::format_diag_message(&#message_stream, &sub_args); });
559
560 let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
561 let call = match kind {
562 SubdiagnosticKind::Suggestion {
563 suggestion_kind,
564 applicability,
565 code_init,
566 code_field,
567 } => {
568 self.formatting_init.extend(code_init);
569
570 let applicability = applicability
571 .value()
572 .map(|a| quote! { #a })
573 .or_else(|| self.applicability.take().value())
574 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
575
576 if let Some(span) = span_field {
577 let style = suggestion_kind.to_suggestion_style();
578 quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
579 } else {
580 span_err(self.span, "suggestion without `#[primary_span]` field").emit();
581 quote! { unreachable!(); }
582 }
583 }
584 SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => {
585 let applicability = applicability
586 .value()
587 .map(|a| quote! { #a })
588 .or_else(|| self.applicability.take().value())
589 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
590
591 if !self.has_suggestion_parts {
592 span_err(
593 self.span,
594 "multipart suggestion without any `#[suggestion_part(...)]` fields",
595 )
596 .emit();
597 }
598
599 let style = suggestion_kind.to_suggestion_style();
600
601 quote! { #diag.#name(#message, suggestions, #applicability, #style); }
602 }
603 SubdiagnosticKind::Label => {
604 if let Some(span) = span_field {
605 quote! { #diag.#name(#span, #message); }
606 } else {
607 span_err(self.span, "label without `#[primary_span]` field").emit();
608 quote! { unreachable!(); }
609 }
610 }
611 _ => {
612 if let Some(span) = span_field {
613 quote! { #diag.#name(#span, #message); }
614 } else {
615 quote! { #diag.#name(#message); }
616 }
617 }
618 };
619
620 calls.extend(call);
621 }
622
623 let formatting_init = &self.formatting_init;
624
625 Ok(quote! {
632 #init
633 #formatting_init
634 #attr_args
635 #plain_args
637 #calls
638 })
640 }
641}