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