1#![deny(unused_must_use)]
2
3use proc_macro2::{Ident, Span, TokenStream};
4use quote::{format_ident, quote, quote_spanned};
5use syn::spanned::Spanned;
6use syn::{Attribute, Meta, Path, Token, Type, parse_quote};
7use synstructure::{BindingInfo, Structure, VariantInfo};
8
9use super::utils::SubdiagnosticVariant;
10use crate::diagnostics::error::{
11 DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
12};
13use crate::diagnostics::utils::{
14 FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce, SpannedOption, SubdiagnosticKind,
15 build_field_mapping, is_doc_comment, report_error_if_not_applied_to_span, report_type_error,
16 should_generate_arg, type_is_bool, type_is_unit, type_matches_path,
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).unwrap_or_else(|v| v.to_compile_error())
120 });
121
122 quote! {
123 #(#preamble)*;
124 }
125 }
126
127 pub(crate) fn body(&mut self, variant: &VariantInfo<'_>) -> TokenStream {
130 let mut body = quote! {};
131 for binding in variant.bindings().iter().filter(|bi| should_generate_arg(bi.ast())) {
133 body.extend(self.generate_field_code(binding));
134 }
135 for binding in variant.bindings().iter().filter(|bi| !should_generate_arg(bi.ast())) {
137 body.extend(self.generate_field_attrs_code(binding));
138 }
139 body
140 }
141
142 fn parse_subdiag_attribute(
144 &self,
145 attr: &Attribute,
146 ) -> Result<Option<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
147 let Some(subdiag) = SubdiagnosticVariant::from_attr(attr, self)? else {
148 return Ok(None);
151 };
152
153 if let SubdiagnosticKind::MultipartSuggestion { .. } = subdiag.kind {
154 throw_invalid_attr!(attr, |diag| diag
155 .help("consider creating a `Subdiagnostic` instead"));
156 }
157
158 let slug = subdiag.slug.unwrap_or_else(|| match subdiag.kind {
159 SubdiagnosticKind::Label => parse_quote! { _subdiag::label },
160 SubdiagnosticKind::Note => parse_quote! { _subdiag::note },
161 SubdiagnosticKind::NoteOnce => parse_quote! { _subdiag::note_once },
162 SubdiagnosticKind::Help => parse_quote! { _subdiag::help },
163 SubdiagnosticKind::HelpOnce => parse_quote! { _subdiag::help_once },
164 SubdiagnosticKind::Warn => parse_quote! { _subdiag::warn },
165 SubdiagnosticKind::Suggestion { .. } => parse_quote! { _subdiag::suggestion },
166 SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
167 });
168
169 Ok(Some((subdiag.kind, slug, subdiag.no_span)))
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 let mut first = true;
188
189 if name == "diag" {
190 let mut tokens = TokenStream::new();
191 attr.parse_nested_meta(|nested| {
192 let path = &nested.path;
193
194 if first && (nested.input.is_empty() || nested.input.peek(Token![,])) {
195 self.slug.set_once(path.clone(), path.span().unwrap());
196 first = false;
197 return Ok(());
198 }
199
200 first = false;
201
202 let Ok(nested) = nested.value() else {
203 span_err(
204 nested.input.span().unwrap(),
205 "diagnostic slug must be the first argument",
206 )
207 .emit();
208 return Ok(());
209 };
210
211 if path.is_ident("code") {
212 self.code.set_once((), path.span().unwrap());
213
214 let code = nested.parse::<syn::Expr>()?;
215 tokens.extend(quote! {
216 diag.code(#code);
217 });
218 } else {
219 span_err(path.span().unwrap(), "unknown argument")
220 .note("only the `code` parameter is valid after the slug")
221 .emit();
222
223 let _ = nested.parse::<TokenStream>();
225 }
226 Ok(())
227 })?;
228 return Ok(tokens);
229 }
230
231 let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else {
232 return Ok(quote! {});
235 };
236 let fn_ident = format_ident!("{}", subdiag);
237 match subdiag {
238 SubdiagnosticKind::Note
239 | SubdiagnosticKind::NoteOnce
240 | SubdiagnosticKind::Help
241 | SubdiagnosticKind::HelpOnce
242 | SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug)),
243 SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => {
244 throw_invalid_attr!(attr, |diag| diag
245 .help("`#[label]` and `#[suggestion]` can only be applied to fields"));
246 }
247 SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
248 }
249 }
250
251 fn generate_field_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
252 let field = binding_info.ast();
253 let mut field_binding = binding_info.binding.clone();
254 field_binding.set_span(field.ty.span());
255
256 let Some(ident) = field.ident.as_ref() else {
257 span_err(field.span().unwrap(), "tuple structs are not supported").emit();
258 return TokenStream::new();
259 };
260 let ident = format_ident!("{}", ident); quote! {
263 diag.arg(
264 stringify!(#ident),
265 #field_binding
266 );
267 }
268 }
269
270 fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
271 let field = binding_info.ast();
272 let field_binding = &binding_info.binding;
273
274 let inner_ty = FieldInnerTy::from_type(&field.ty);
275 let mut seen_label = false;
276
277 field
278 .attrs
279 .iter()
280 .map(move |attr| {
281 if is_doc_comment(attr) {
283 return quote! {};
284 }
285
286 let name = attr.path().segments.last().unwrap().ident.to_string();
287
288 if name == "primary_span" && seen_label {
289 span_err(attr.span().unwrap(), format!("`#[primary_span]` must be placed before labels, since it overwrites the span of the diagnostic")).emit();
290 }
291 if name == "label" {
292 seen_label = true;
293 }
294
295 let needs_clone =
296 name == "primary_span" && matches!(inner_ty, FieldInnerTy::Vec(_));
297 let (binding, needs_destructure) = if needs_clone {
298 (quote_spanned! {inner_ty.span()=> #field_binding.clone() }, false)
300 } else {
301 (quote_spanned! {inner_ty.span()=> #field_binding }, true)
302 };
303
304 let generated_code = self
305 .generate_inner_field_code(
306 attr,
307 FieldInfo { binding: binding_info, ty: inner_ty, span: &field.span() },
308 binding,
309 )
310 .unwrap_or_else(|v| v.to_compile_error());
311
312 if needs_destructure {
313 inner_ty.with(field_binding, generated_code)
314 } else {
315 generated_code
316 }
317 })
318 .collect()
319 }
320
321 fn generate_inner_field_code(
322 &mut self,
323 attr: &Attribute,
324 info: FieldInfo<'_>,
325 binding: TokenStream,
326 ) -> Result<TokenStream, DiagnosticDeriveError> {
327 let ident = &attr.path().segments.last().unwrap().ident;
328 let name = ident.to_string();
329 match (&attr.meta, name.as_str()) {
330 (Meta::Path(_), "skip_arg") => return Ok(quote! {}),
333 (Meta::Path(_), "primary_span") => {
334 match self.kind {
335 DiagnosticDeriveKind::Diagnostic => {
336 report_error_if_not_applied_to_span(attr, &info)?;
337
338 return Ok(quote! {
339 diag.span(#binding);
340 });
341 }
342 DiagnosticDeriveKind::LintDiagnostic => {
343 throw_invalid_attr!(attr, |diag| {
344 diag.help("the `primary_span` field attribute is not valid for lint diagnostics")
345 })
346 }
347 }
348 }
349 (Meta::Path(_), "subdiagnostic") => {
350 return Ok(quote! { diag.subdiagnostic(#binding); });
351 }
352 _ => (),
353 }
354
355 let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else {
356 return Ok(quote! {});
359 };
360 let fn_ident = format_ident!("{}", subdiag);
361 match subdiag {
362 SubdiagnosticKind::Label => {
363 report_error_if_not_applied_to_span(attr, &info)?;
364 Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
365 }
366 SubdiagnosticKind::Note
367 | SubdiagnosticKind::NoteOnce
368 | SubdiagnosticKind::Help
369 | SubdiagnosticKind::HelpOnce
370 | SubdiagnosticKind::Warn => {
371 let inner = info.ty.inner_type();
372 if type_matches_path(inner, &["rustc_span", "Span"])
373 || type_matches_path(inner, &["rustc_span", "MultiSpan"])
374 {
375 Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
376 } else if type_is_unit(inner)
377 || (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
378 {
379 Ok(self.add_subdiagnostic(&fn_ident, slug))
380 } else {
381 report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
382 }
383 }
384 SubdiagnosticKind::Suggestion {
385 suggestion_kind,
386 applicability: static_applicability,
387 code_field,
388 code_init,
389 } => {
390 if let FieldInnerTy::Vec(_) = info.ty {
391 throw_invalid_attr!(attr, |diag| {
392 diag
393 .note("`#[suggestion(...)]` applied to `Vec` field is ambiguous")
394 .help("to show a suggestion consisting of multiple parts, use a `Subdiagnostic` annotated with `#[multipart_suggestion(...)]`")
395 .help("to show a variable set of suggestions, use a `Vec` of `Subdiagnostic`s annotated with `#[suggestion(...)]`")
396 });
397 }
398
399 let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?;
400
401 if let Some((static_applicability, span)) = static_applicability {
402 applicability.set_once(quote! { #static_applicability }, span);
403 }
404
405 let applicability = applicability
406 .value()
407 .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
408 let style = suggestion_kind.to_suggestion_style();
409
410 self.formatting_init.extend(code_init);
411 Ok(quote! {
412 diag.span_suggestions_with_style(
413 #span_field,
414 crate::fluent_generated::#slug,
415 #code_field,
416 #applicability,
417 #style
418 );
419 })
420 }
421 SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(),
422 }
423 }
424
425 fn add_spanned_subdiagnostic(
428 &self,
429 field_binding: TokenStream,
430 kind: &Ident,
431 fluent_attr_identifier: Path,
432 ) -> TokenStream {
433 let fn_name = format_ident!("span_{}", kind);
434 quote! {
435 diag.#fn_name(
436 #field_binding,
437 crate::fluent_generated::#fluent_attr_identifier
438 );
439 }
440 }
441
442 fn add_subdiagnostic(&self, kind: &Ident, fluent_attr_identifier: Path) -> TokenStream {
445 quote! {
446 diag.#kind(crate::fluent_generated::#fluent_attr_identifier);
447 }
448 }
449
450 fn span_and_applicability_of_ty(
451 &self,
452 info: FieldInfo<'_>,
453 ) -> Result<(TokenStream, SpannedOption<TokenStream>), DiagnosticDeriveError> {
454 match &info.ty.inner_type() {
455 ty @ Type::Path(..) if type_matches_path(ty, &["rustc_span", "Span"]) => {
457 let binding = &info.binding.binding;
458 Ok((quote!(#binding), None))
459 }
460 Type::Tuple(tup) => {
462 let mut span_idx = None;
463 let mut applicability_idx = None;
464
465 fn type_err(span: &Span) -> Result<!, DiagnosticDeriveError> {
466 span_err(span.unwrap(), "wrong types for suggestion")
467 .help(
468 "`#[suggestion(...)]` on a tuple field must be applied to fields \
469 of type `(Span, Applicability)`",
470 )
471 .emit();
472 Err(DiagnosticDeriveError::ErrorHandled)
473 }
474
475 for (idx, elem) in tup.elems.iter().enumerate() {
476 if type_matches_path(elem, &["rustc_span", "Span"]) {
477 span_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
478 } else if type_matches_path(elem, &["rustc_errors", "Applicability"]) {
479 applicability_idx.set_once(syn::Index::from(idx), elem.span().unwrap());
480 } else {
481 type_err(&elem.span())?;
482 }
483 }
484
485 let Some((span_idx, _)) = span_idx else {
486 type_err(&tup.span())?;
487 };
488 let Some((applicability_idx, applicability_span)) = applicability_idx else {
489 type_err(&tup.span())?;
490 };
491 let binding = &info.binding.binding;
492 let span = quote!(#binding.#span_idx);
493 let applicability = quote!(#binding.#applicability_idx);
494
495 Ok((span, Some((applicability, applicability_span))))
496 }
497 _ => throw_span_err!(info.span.unwrap(), "wrong field type for suggestion", |diag| {
499 diag.help(
500 "`#[suggestion(...)]` should be applied to fields of type `Span` or \
501 `(Span, Applicability)`",
502 )
503 }),
504 }
505 }
506}