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