1#![deny(unused_must_use)]
2
3use std::collections::HashSet;
4
5use proc_macro2::{Ident, Span, TokenStream};
6use quote::{format_ident, quote, quote_spanned};
7use syn::parse::ParseStream;
8use syn::spanned::Spanned;
9use syn::{Attribute, LitStr, Meta, Path, Token, Type};
10use synstructure::{BindingInfo, Structure, VariantInfo};
11
12use super::utils::SubdiagnosticVariant;
13use crate::diagnostics::error::{
14 DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
15};
16use crate::diagnostics::message::Message;
17use crate::diagnostics::utils::{
18 FieldInfo, FieldInnerTy, FieldMap, SetOnce, SpannedOption, SubdiagnosticKind,
19 build_field_mapping, is_doc_comment, report_error_if_not_applied_to_span, report_type_error,
20 should_generate_arg, type_is_bool, type_is_unit, type_matches_path,
21};
22
23pub(crate) fn each_variant<'s, F>(structure: &mut Structure<'s>, f: F) -> TokenStream
24where
25 F: for<'v> Fn(DiagnosticDeriveVariantBuilder, &VariantInfo<'v>) -> TokenStream,
26{
27 let ast = structure.ast();
28 let span = ast.span().unwrap();
29 match ast.data {
30 syn::Data::Struct(..) | syn::Data::Enum(..) => (),
31 syn::Data::Union(..) => {
32 span_err(span, "diagnostic derives can only be used on structs and enums").emit();
33 }
34 }
35
36 if matches!(ast.data, syn::Data::Enum(..)) {
37 for attr in &ast.attrs {
38 span_err(attr.span().unwrap(), "unsupported type attribute for diagnostic derive enum")
39 .emit();
40 }
41 }
42
43 structure.bind_with(|_| synstructure::BindStyle::Move);
44 let variants = structure.each_variant(|variant| {
45 let span = match structure.ast().data {
46 syn::Data::Struct(..) => span,
47 _ => variant.ast().ident.span().unwrap(),
50 };
51 let builder = DiagnosticDeriveVariantBuilder {
52 span,
53 field_map: build_field_mapping(variant),
54 formatting_init: TokenStream::new(),
55 message: None,
56 code: None,
57 used_fields: HashSet::new(),
58 };
59 f(builder, variant)
60 });
61
62 quote! {
63 match self {
64 #variants
65 }
66 }
67}
68
69pub(crate) struct DiagnosticDeriveVariantBuilder {
72 pub formatting_init: TokenStream,
74
75 pub span: proc_macro::Span,
77
78 pub field_map: FieldMap,
81
82 pub message: Option<Message>,
85
86 pub code: SpannedOption<()>,
89
90 pub used_fields: HashSet<proc_macro2::Ident>,
91}
92
93impl DiagnosticDeriveVariantBuilder {
94 pub(crate) fn primary_message(&self) -> Option<&Message> {
95 match self.message.as_ref() {
96 None => {
97 span_err(self.span, "diagnostic message not specified")
98 .help(
99 "specify the message as the first argument to the `#[diag(...)]` \
100 attribute, such as `#[diag(\"Example error\")]`",
101 )
102 .emit();
103 None
104 }
105 Some(msg) => Some(msg),
106 }
107 }
108
109 pub(crate) fn preamble(&mut self, variant: &VariantInfo<'_>) -> TokenStream {
112 let ast = variant.ast();
113 let attrs = &ast.attrs;
114 let preamble = attrs.iter().map(|attr| {
115 self.generate_structure_code_for_attr(attr).unwrap_or_else(|v| v.to_compile_error())
116 });
117
118 quote! {
119 #(#preamble)*;
120 }
121 }
122
123 pub(crate) fn body(&mut self, variant: &VariantInfo<'_>) -> TokenStream {
126 let mut body = quote! {};
127 let mut second_part = quote! {};
128
129 for binding in variant.bindings().iter().filter(|bi| !should_generate_arg(bi.ast())) {
131 second_part.extend(self.generate_field_attrs_code(binding));
132 }
133 for binding in variant.bindings().iter().filter(|bi| should_generate_arg(bi.ast())) {
135 if self.is_used_in_message(binding) {
136 body.extend(self.generate_field_code(binding));
137 }
138 }
139 body.extend(second_part);
140 body
141 }
142
143 fn is_used_in_message(&self, binding: &BindingInfo<'_>) -> bool {
144 binding.ast().ident.as_ref().is_some_and(|ident| self.used_fields.contains(ident))
145 }
146
147 fn parse_subdiag_attribute(
149 &mut self,
150 attr: &Attribute,
151 ) -> Result<Option<(SubdiagnosticKind, Message, bool)>, DiagnosticDeriveError> {
152 let Some(subdiag) =
153 SubdiagnosticVariant::from_attr(attr, &self.field_map, &mut self.used_fields)?
154 else {
155 return Ok(None);
158 };
159
160 if let SubdiagnosticKind::MultipartSuggestion { .. } = subdiag.kind {
161 throw_invalid_attr!(attr, |diag| diag
162 .help("consider creating a `Subdiagnostic` instead"));
163 }
164
165 let Some(message) = subdiag.message else {
166 throw_invalid_attr!(attr, |diag| diag.help("subdiagnostic message is missing"))
167 };
168
169 Ok(Some((subdiag.kind, message, false)))
170 }
171
172 fn generate_structure_code_for_attr(
176 &mut self,
177 attr: &Attribute,
178 ) -> Result<TokenStream, DiagnosticDeriveError> {
179 if is_doc_comment(attr) {
181 return Ok(quote! {});
182 }
183
184 let name = attr.path().segments.last().unwrap().ident.to_string();
185 let name = name.as_str();
186
187 if name == "diag" {
188 let mut tokens = TokenStream::new();
189 attr.parse_args_with(|input: ParseStream<'_>| {
190 if input.peek(LitStr) {
191 let message = input.parse::<LitStr>()?;
193 if !message.suffix().is_empty() {
194 span_err(
195 message.span().unwrap(),
196 "Inline message is not allowed to have a suffix",
197 )
198 .emit();
199 }
200 let message = Message::new(
201 attr.span(),
202 message.span(),
203 message.value(),
204 &self.field_map,
205 &mut self.used_fields,
206 );
207 self.message = Some(message);
208 }
209
210 while !input.is_empty() {
212 input.parse::<Token![,]>()?;
213 if input.is_empty() {
215 break;
216 }
217 let arg_name: Path = input.parse::<Path>()?;
218 if input.peek(Token![,]) {
219 span_err(
220 arg_name.span().unwrap(),
221 "diagnostic message must be the first argument",
222 )
223 .emit();
224 continue;
225 }
226 let arg_name = arg_name.require_ident()?;
227 input.parse::<Token![=]>()?;
228 let arg_value = input.parse::<syn::Expr>()?;
229 match arg_name.to_string().as_str() {
230 "code" => {
231 self.code.set_once((), arg_name.span().unwrap());
232 tokens.extend(quote! {
233 diag.code(#arg_value);
234 });
235 }
236 _ => {
237 span_err(arg_name.span().unwrap(), "unknown argument")
238 .note("only the `code` parameter is valid after the message")
239 .emit();
240 }
241 }
242 }
243 Ok(())
244 })?;
245
246 return Ok(tokens);
247 }
248
249 let Some((subdiag, message, _no_span)) = self.parse_subdiag_attribute(attr)? else {
250 return Ok(quote! {});
253 };
254 let fn_ident = format_ident!("{}", subdiag);
255 match subdiag {
256 SubdiagnosticKind::Note
257 | SubdiagnosticKind::NoteOnce
258 | SubdiagnosticKind::Help
259 | SubdiagnosticKind::HelpOnce
260 | SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, message)),
261 SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => {
262 throw_invalid_attr!(attr, |diag| diag
263 .help("`#[label]` and `#[suggestion]` can only be applied to fields"));
264 }
265 SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
266 }
267 }
268
269 fn generate_field_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
270 let field = binding_info.ast();
271 let mut field_binding = binding_info.binding.clone();
272 field_binding.set_span(field.ty.span());
273
274 let Some(ident) = field.ident.as_ref() else {
275 span_err(field.span().unwrap(), "tuple structs are not supported").emit();
276 return TokenStream::new();
277 };
278 let ident = format_ident!("{}", ident); quote! {
281 diag.arg(
282 stringify!(#ident),
283 #field_binding
284 );
285 }
286 }
287
288 fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
289 let field = binding_info.ast();
290 let field_binding = &binding_info.binding;
291
292 let inner_ty = FieldInnerTy::from_type(&field.ty);
293 let mut seen_label = false;
294
295 field
296 .attrs
297 .iter()
298 .map(move |attr| {
299 if is_doc_comment(attr) {
301 return quote! {};
302 }
303
304 let name = attr.path().segments.last().unwrap().ident.to_string();
305
306 if name == "primary_span" && seen_label {
307 span_err(attr.span().unwrap(), format!("`#[primary_span]` must be placed before labels, since it overwrites the span of the diagnostic")).emit();
308 }
309 if name == "label" {
310 seen_label = true;
311 }
312
313 let needs_clone =
314 name == "primary_span" && matches!(inner_ty, FieldInnerTy::Vec(_));
315 let (binding, needs_destructure) = if needs_clone {
316 (quote_spanned! {inner_ty.span()=> #field_binding.clone() }, false)
318 } else {
319 (quote_spanned! {inner_ty.span()=> #field_binding }, true)
320 };
321
322 let generated_code = self
323 .generate_inner_field_code(
324 attr,
325 FieldInfo { binding: binding_info, ty: inner_ty, span: &field.span() },
326 binding,
327 )
328 .unwrap_or_else(|v| v.to_compile_error());
329
330 if needs_destructure {
331 inner_ty.with(field_binding, generated_code)
332 } else {
333 generated_code
334 }
335 })
336 .collect()
337 }
338
339 fn generate_inner_field_code(
340 &mut self,
341 attr: &Attribute,
342 info: FieldInfo<'_>,
343 binding: TokenStream,
344 ) -> Result<TokenStream, DiagnosticDeriveError> {
345 let ident = &attr.path().segments.last().unwrap().ident;
346 let name = ident.to_string();
347 match (&attr.meta, name.as_str()) {
348 (Meta::Path(_), "primary_span") => {
349 report_error_if_not_applied_to_span(attr, &info)?;
350
351 return Ok(quote! {
352 diag.span(#binding);
353 });
354 }
355 (Meta::Path(_), "subdiagnostic") => {
356 return Ok(quote! { diag.subdiagnostic(#binding); });
357 }
358 _ => (),
359 }
360
361 let Some((subdiag, message, _no_span)) = self.parse_subdiag_attribute(attr)? else {
362 return Ok(quote! {});
365 };
366 let fn_ident = format_ident!("{}", subdiag);
367 match subdiag {
368 SubdiagnosticKind::Label => {
369 report_error_if_not_applied_to_span(attr, &info)?;
370 Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, message))
371 }
372 SubdiagnosticKind::Note
373 | SubdiagnosticKind::NoteOnce
374 | SubdiagnosticKind::Help
375 | SubdiagnosticKind::HelpOnce
376 | SubdiagnosticKind::Warn => {
377 let inner = info.ty.inner_type();
378 if type_matches_path(inner, &["rustc_span", "Span"])
379 || type_matches_path(inner, &["rustc_span", "MultiSpan"])
380 {
381 Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, message))
382 } else if type_is_unit(inner)
383 || (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
384 {
385 Ok(self.add_subdiagnostic(&fn_ident, message))
386 } else {
387 report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
388 }
389 }
390 SubdiagnosticKind::Suggestion {
391 suggestion_kind,
392 applicability: static_applicability,
393 code_field,
394 code_init,
395 } => {
396 if let FieldInnerTy::Vec(_) = info.ty {
397 throw_invalid_attr!(attr, |diag| {
398 diag
399 .note("`#[suggestion(...)]` applied to `Vec` field is ambiguous")
400 .help("to show a suggestion consisting of multiple parts, use a `Subdiagnostic` annotated with `#[multipart_suggestion(...)]`")
401 .help("to show a variable set of suggestions, use a `Vec` of `Subdiagnostic`s annotated with `#[suggestion(...)]`")
402 });
403 }
404
405 let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?;
406
407 if let Some((static_applicability, span)) = static_applicability {
408 applicability.set_once(quote! { #static_applicability }, span);
409 }
410
411 let message = message.diag_message();
412 let applicability = applicability
413 .value()
414 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
415 let style = suggestion_kind.to_suggestion_style();
416
417 self.formatting_init.extend(code_init);
418 Ok(quote! {
419 diag.span_suggestions_with_style(
420 #span_field,
421 #message,
422 #code_field,
423 #applicability,
424 #style
425 );
426 })
427 }
428 SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
429 }
430 }
431
432 fn add_spanned_subdiagnostic(
435 &self,
436 field_binding: TokenStream,
437 kind: &Ident,
438 message: Message,
439 ) -> TokenStream {
440 let fn_name = format_ident!("span_{}", kind);
441 let message = message.diag_message();
442 quote! {
443 diag.#fn_name(
444 #field_binding,
445 #message
446 );
447 }
448 }
449
450 fn add_subdiagnostic(&self, kind: &Ident, message: Message) -> TokenStream {
453 let message = message.diag_message();
454 quote! {
455 diag.#kind(#message);
456 }
457 }
458
459 fn span_and_applicability_of_ty(
460 &self,
461 info: FieldInfo<'_>,
462 ) -> Result<(TokenStream, SpannedOption<TokenStream>), DiagnosticDeriveError> {
463 match &info.ty.inner_type() {
464 ty @ Type::Path(..) if type_matches_path(ty, &["rustc_span", "Span"]) => {
466 let binding = &info.binding.binding;
467 Ok((quote!(#binding), None))
468 }
469 Type::Tuple(tup) => {
471 let mut span_idx = None;
472 let mut applicability_idx = None;
473
474 fn type_err(span: &Span) -> Result<!, DiagnosticDeriveError> {
475 span_err(span.unwrap(), "wrong types for suggestion")
476 .help(
477 "`#[suggestion(...)]` on a tuple field must be applied to fields \
478 of type `(Span, Applicability)`",
479 )
480 .emit();
481 Err(DiagnosticDeriveError::ErrorHandled)
482 }
483
484 for (idx, elem) in tup.elems.iter().enumerate() {
485 if type_matches_path(elem, &["rustc_span", "Span"]) {
486 span_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
487 } else if type_matches_path(elem, &["rustc_errors", "Applicability"]) {
488 applicability_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
489 } else {
490 type_err(&elem.span())?;
491 }
492 }
493
494 let Some((span_idx, _)) = span_idx else {
495 type_err(&tup.span())?;
496 };
497 let Some((applicability_idx, applicability_span)) = applicability_idx else {
498 type_err(&tup.span())?;
499 };
500 let binding = &info.binding.binding;
501 let span = quote!(#binding.#span_idx);
502 let applicability = quote!(#binding.#applicability_idx);
503
504 Ok((span, Some((applicability, applicability_span))))
505 }
506 _ => throw_span_err!(info.span.unwrap(), "wrong field type for suggestion", |diag| {
508 diag.help(
509 "`#[suggestion(...)]` should be applied to fields of type `Span` or \
510 `(Span, Applicability)`",
511 )
512 }),
513 }
514 }
515}