flux_macros/diagnostics/
utils.rs

1use std::{
2    cell::RefCell,
3    collections::{BTreeSet, HashMap},
4    fmt,
5    str::FromStr,
6};
7
8use proc_macro::Span;
9use proc_macro2::{Ident, TokenStream};
10use quote::{ToTokens, format_ident, quote};
11use syn::{
12    Attribute, Field, LitStr, Meta, Path, Token, Type, TypeTuple, meta::ParseNestedMeta,
13    parenthesized, punctuated::Punctuated, spanned::Spanned,
14};
15use synstructure::{BindingInfo, VariantInfo};
16
17use super::error::invalid_attr;
18use crate::diagnostics::error::{
19    DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
20};
21
22thread_local! {
23    pub(crate) static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0);
24}
25
26/// Returns an ident of the form `__code_N` where `N` is incremented once with every call.
27pub(crate) fn new_code_ident() -> syn::Ident {
28    CODE_IDENT_COUNT.with(|count| {
29        let ident = format_ident!("__code_{}", *count.borrow());
30        *count.borrow_mut() += 1;
31        ident
32    })
33}
34
35/// Checks whether the type name of `ty` matches `name`.
36///
37/// Given some struct at `a::b::c::Foo`, this will return true for `c::Foo`, `b::c::Foo`, or
38/// `a::b::c::Foo`. This reasonably allows qualified names to be used in the macro.
39pub(crate) fn type_matches_path(ty: &Type, name: &[&str]) -> bool {
40    if let Type::Path(ty) = ty {
41        ty.path
42            .segments
43            .iter()
44            .map(|s| s.ident.to_string())
45            .rev()
46            .zip(name.iter().rev())
47            .all(|(x, y)| &x.as_str() == y)
48    } else {
49        false
50    }
51}
52
53/// Checks whether the type `ty` is `()`.
54pub(crate) fn type_is_unit(ty: &Type) -> bool {
55    if let Type::Tuple(TypeTuple { elems, .. }) = ty { elems.is_empty() } else { false }
56}
57
58/// Checks whether the type `ty` is `bool`.
59pub(crate) fn type_is_bool(ty: &Type) -> bool {
60    type_matches_path(ty, &["bool"])
61}
62
63/// Reports a type error for field with `attr`.
64pub(crate) fn report_type_error(
65    attr: &Attribute,
66    ty_name: &str,
67) -> Result<!, DiagnosticDeriveError> {
68    let name = attr.path().segments.last().unwrap().ident.to_string();
69    let meta = &attr.meta;
70
71    throw_span_err!(
72        attr.span().unwrap(),
73        &format!(
74            "the `#[{}{}]` attribute can only be applied to fields of type {}",
75            name,
76            match meta {
77                Meta::Path(_) => "",
78                Meta::NameValue(_) => " = ...",
79                Meta::List(_) => "(...)",
80            },
81            ty_name
82        )
83    );
84}
85
86/// Reports an error if the field's type does not match `path`.
87fn report_error_if_not_applied_to_ty(
88    attr: &Attribute,
89    info: &FieldInfo<'_>,
90    path: &[&str],
91    ty_name: &str,
92) -> Result<(), DiagnosticDeriveError> {
93    if !type_matches_path(info.ty.inner_type(), path) {
94        report_type_error(attr, ty_name)?;
95    }
96
97    Ok(())
98}
99
100/// Reports an error if the field's type is not `Applicability`.
101pub(crate) fn report_error_if_not_applied_to_applicability(
102    attr: &Attribute,
103    info: &FieldInfo<'_>,
104) -> Result<(), DiagnosticDeriveError> {
105    report_error_if_not_applied_to_ty(
106        attr,
107        info,
108        &["rustc_errors", "Applicability"],
109        "`Applicability`",
110    )
111}
112
113/// Reports an error if the field's type is not `Span`.
114pub(crate) fn report_error_if_not_applied_to_span(
115    attr: &Attribute,
116    info: &FieldInfo<'_>,
117) -> Result<(), DiagnosticDeriveError> {
118    if !type_matches_path(info.ty.inner_type(), &["rustc_span", "Span"])
119        && !type_matches_path(info.ty.inner_type(), &["rustc_errors", "MultiSpan"])
120    {
121        report_type_error(attr, "`Span` or `MultiSpan`")?;
122    }
123
124    Ok(())
125}
126
127/// Inner type of a field and type of wrapper.
128#[derive(Copy, Clone)]
129pub(crate) enum FieldInnerTy<'ty> {
130    /// Field is wrapped in a `Option<$inner>`.
131    Option(&'ty Type),
132    /// Field is wrapped in a `Vec<$inner>`.
133    Vec(&'ty Type),
134    /// Field isn't wrapped in an outer type.
135    Plain(&'ty Type),
136}
137
138impl<'ty> FieldInnerTy<'ty> {
139    /// Returns inner type for a field, if there is one.
140    ///
141    /// - If `ty` is an `Option<Inner>`, returns `FieldInnerTy::Option(Inner)`.
142    /// - If `ty` is a `Vec<Inner>`, returns `FieldInnerTy::Vec(Inner)`.
143    /// - Otherwise returns `FieldInnerTy::Plain(ty)`.
144    pub(crate) fn from_type(ty: &'ty Type) -> Self {
145        fn single_generic_type(ty: &Type) -> &Type {
146            let Type::Path(ty_path) = ty else {
147                panic!("expected path type");
148            };
149
150            let path = &ty_path.path;
151            let ty = path.segments.iter().last().unwrap();
152            let syn::PathArguments::AngleBracketed(bracketed) = &ty.arguments else {
153                panic!("expected bracketed generic arguments")
154            };
155
156            assert_eq!(bracketed.args.len(), 1);
157
158            let syn::GenericArgument::Type(ty) = &bracketed.args[0] else {
159                panic!("expected generic parameter to be a type generic");
160            };
161
162            ty
163        }
164
165        if type_matches_path(ty, &["std", "option", "Option"]) {
166            FieldInnerTy::Option(single_generic_type(ty))
167        } else if type_matches_path(ty, &["std", "vec", "Vec"]) {
168            FieldInnerTy::Vec(single_generic_type(ty))
169        } else {
170            FieldInnerTy::Plain(ty)
171        }
172    }
173
174    /// Returns `true` if `FieldInnerTy::with` will result in iteration for this inner type (i.e.
175    /// that cloning might be required for values moved in the loop body).
176    pub(crate) fn will_iterate(&self) -> bool {
177        match self {
178            FieldInnerTy::Vec(..) => true,
179            FieldInnerTy::Option(..) | FieldInnerTy::Plain(_) => false,
180        }
181    }
182
183    /// Returns the inner type.
184    pub(crate) fn inner_type(&self) -> &'ty Type {
185        match self {
186            FieldInnerTy::Option(inner) | FieldInnerTy::Vec(inner) | FieldInnerTy::Plain(inner) => {
187                inner
188            }
189        }
190    }
191
192    /// Surrounds `inner` with destructured wrapper type, exposing inner type as `binding`.
193    pub(crate) fn with(&self, binding: impl ToTokens, inner: impl ToTokens) -> TokenStream {
194        match self {
195            FieldInnerTy::Option(..) => {
196                quote! {
197                    if let Some(#binding) = #binding {
198                        #inner
199                    }
200                }
201            }
202            FieldInnerTy::Vec(..) => {
203                quote! {
204                    for #binding in #binding {
205                        #inner
206                    }
207                }
208            }
209            FieldInnerTy::Plain(t) if type_is_bool(t) => {
210                quote! {
211                    if #binding {
212                        #inner
213                    }
214                }
215            }
216            FieldInnerTy::Plain(..) => quote! { #inner },
217        }
218    }
219
220    pub(crate) fn span(&self) -> proc_macro2::Span {
221        match self {
222            FieldInnerTy::Option(ty) | FieldInnerTy::Vec(ty) | FieldInnerTy::Plain(ty) => ty.span(),
223        }
224    }
225}
226
227/// Field information passed to the builder. Deliberately omits attrs to discourage the
228/// `generate_*` methods from walking the attributes themselves.
229pub(crate) struct FieldInfo<'a> {
230    pub(crate) binding: &'a BindingInfo<'a>,
231    pub(crate) ty: FieldInnerTy<'a>,
232    pub(crate) span: &'a proc_macro2::Span,
233}
234
235/// Small helper trait for abstracting over `Option` fields that contain a value and a `Span`
236/// for error reporting if they are set more than once.
237pub(crate) trait SetOnce<T> {
238    fn set_once(&mut self, value: T, span: Span);
239
240    fn value(self) -> Option<T>;
241    fn value_ref(&self) -> Option<&T>;
242}
243
244/// An [`Option<T>`] that keeps track of the span that caused it to be set; used with [`SetOnce`].
245pub(super) type SpannedOption<T> = Option<(T, Span)>;
246
247impl<T> SetOnce<T> for SpannedOption<T> {
248    fn set_once(&mut self, value: T, span: Span) {
249        match self {
250            None => {
251                *self = Some((value, span));
252            }
253            Some((_, prev_span)) => {
254                span_err(span, "specified multiple times")
255                    .span_note(*prev_span, "previously specified here")
256                    .emit();
257            }
258        }
259    }
260
261    fn value(self) -> Option<T> {
262        self.map(|(v, _)| v)
263    }
264
265    fn value_ref(&self) -> Option<&T> {
266        self.as_ref().map(|(v, _)| v)
267    }
268}
269
270pub(super) type FieldMap = HashMap<String, TokenStream>;
271
272pub(crate) trait HasFieldMap {
273    /// Returns the binding for the field with the given name, if it exists on the type.
274    fn get_field_binding(&self, field: &String) -> Option<&TokenStream>;
275
276    /// In the strings in the attributes supplied to this macro, we want callers to be able to
277    /// reference fields in the format string. For example:
278    ///
279    /// ```ignore (not-usage-example)
280    /// /// Suggest `==` when users wrote `===`.
281    /// #[suggestion(slug = "parser-not-javascript-eq", code = "{lhs} == {rhs}")]
282    /// struct NotJavaScriptEq {
283    ///     #[primary_span]
284    ///     span: Span,
285    ///     lhs: Ident,
286    ///     rhs: Ident,
287    /// }
288    /// ```
289    ///
290    /// We want to automatically pick up that `{lhs}` refers `self.lhs` and `{rhs}` refers to
291    /// `self.rhs`, then generate this call to `format!`:
292    ///
293    /// ```ignore (not-usage-example)
294    /// format!("{lhs} == {rhs}", lhs = self.lhs, rhs = self.rhs)
295    /// ```
296    ///
297    /// This function builds the entire call to `format!`.
298    fn build_format(&self, input: &str, span: proc_macro2::Span) -> TokenStream {
299        // This set is used later to generate the final format string. To keep builds reproducible,
300        // the iteration order needs to be deterministic, hence why we use a `BTreeSet` here
301        // instead of a `HashSet`.
302        let mut referenced_fields: BTreeSet<String> = BTreeSet::new();
303
304        // At this point, we can start parsing the format string.
305        let mut it = input.chars().peekable();
306
307        // Once the start of a format string has been found, process the format string and spit out
308        // the referenced fields. Leaves `it` sitting on the closing brace of the format string, so
309        // the next call to `it.next()` retrieves the next character.
310        while let Some(c) = it.next() {
311            if c != '{' {
312                continue;
313            }
314            if *it.peek().unwrap_or(&'\0') == '{' {
315                assert_eq!(it.next().unwrap(), '{');
316                continue;
317            }
318            let mut eat_argument = || -> Option<String> {
319                let mut result = String::new();
320                // Format specifiers look like:
321                //
322                //   format   := '{' [ argument ] [ ':' format_spec ] '}' .
323                //
324                // Therefore, we only need to eat until ':' or '}' to find the argument.
325                while let Some(c) = it.next() {
326                    result.push(c);
327                    let next = *it.peek().unwrap_or(&'\0');
328                    if next == '}' {
329                        break;
330                    } else if next == ':' {
331                        // Eat the ':' character.
332                        assert_eq!(it.next().unwrap(), ':');
333                        break;
334                    }
335                }
336                // Eat until (and including) the matching '}'
337                while it.next()? != '}' {
338                    continue;
339                }
340                Some(result)
341            };
342
343            if let Some(referenced_field) = eat_argument() {
344                referenced_fields.insert(referenced_field);
345            }
346        }
347
348        // At this point, `referenced_fields` contains a set of the unique fields that were
349        // referenced in the format string. Generate the corresponding "x = self.x" format
350        // string parameters:
351        let args = referenced_fields.into_iter().map(|field: String| {
352            let field_ident = format_ident!("{}", field);
353            let value = match self.get_field_binding(&field) {
354                Some(value) => value.clone(),
355                // This field doesn't exist. Emit a diagnostic.
356                None => {
357                    span_err(
358                        span.unwrap(),
359                        format!("`{field}` doesn't refer to a field on this type"),
360                    )
361                    .emit();
362                    quote! {
363                        "{#field}"
364                    }
365                }
366            };
367            quote! {
368                #field_ident = #value
369            }
370        });
371        quote! {
372            format!(#input #(,#args)*)
373        }
374    }
375}
376
377/// `Applicability` of a suggestion - mirrors `rustc_errors::Applicability` - and used to represent
378/// the user's selection of applicability if specified in an attribute.
379#[derive(Clone, Copy)]
380pub(crate) enum Applicability {
381    MachineApplicable,
382    MaybeIncorrect,
383    HasPlaceholders,
384    Unspecified,
385}
386
387impl FromStr for Applicability {
388    type Err = ();
389
390    fn from_str(s: &str) -> Result<Self, Self::Err> {
391        match s {
392            "machine-applicable" => Ok(Applicability::MachineApplicable),
393            "maybe-incorrect" => Ok(Applicability::MaybeIncorrect),
394            "has-placeholders" => Ok(Applicability::HasPlaceholders),
395            "unspecified" => Ok(Applicability::Unspecified),
396            _ => Err(()),
397        }
398    }
399}
400
401impl quote::ToTokens for Applicability {
402    fn to_tokens(&self, tokens: &mut TokenStream) {
403        tokens.extend(match self {
404            Applicability::MachineApplicable => {
405                quote! { rustc_errors::Applicability::MachineApplicable }
406            }
407            Applicability::MaybeIncorrect => {
408                quote! { rustc_errors::Applicability::MaybeIncorrect }
409            }
410            Applicability::HasPlaceholders => {
411                quote! { rustc_errors::Applicability::HasPlaceholders }
412            }
413            Applicability::Unspecified => {
414                quote! { rustc_errors::Applicability::Unspecified }
415            }
416        });
417    }
418}
419
420/// Build the mapping of field names to fields. This allows attributes to peek values from
421/// other fields.
422pub(super) fn build_field_mapping(variant: &VariantInfo<'_>) -> HashMap<String, TokenStream> {
423    let mut fields_map = FieldMap::new();
424    for binding in variant.bindings() {
425        if let Some(ident) = &binding.ast().ident {
426            fields_map.insert(ident.to_string(), quote! { #binding });
427        }
428    }
429    fields_map
430}
431
432#[derive(Copy, Clone, Debug)]
433pub(super) enum AllowMultipleAlternatives {
434    No,
435    Yes,
436}
437
438fn parse_suggestion_values(
439    nested: ParseNestedMeta<'_>,
440    allow_multiple: AllowMultipleAlternatives,
441) -> syn::Result<Vec<LitStr>> {
442    let values = if let Ok(val) = nested.value() {
443        vec![val.parse()?]
444    } else {
445        let content;
446        parenthesized!(content in nested.input);
447
448        if let AllowMultipleAlternatives::No = allow_multiple {
449            span_err(
450                nested.input.span().unwrap(),
451                "expected exactly one string literal for `code = ...`",
452            )
453            .emit();
454            vec![]
455        } else {
456            let literals = Punctuated::<LitStr, Token![,]>::parse_terminated(&content);
457
458            match literals {
459                Ok(p) if p.is_empty() => {
460                    span_err(
461                        content.span().unwrap(),
462                        "expected at least one string literal for `code(...)`",
463                    )
464                    .emit();
465                    vec![]
466                }
467                Ok(p) => p.into_iter().collect(),
468                Err(_) => {
469                    span_err(
470                        content.span().unwrap(),
471                        "`code(...)` must contain only string literals",
472                    )
473                    .emit();
474                    vec![]
475                }
476            }
477        }
478    };
479
480    Ok(values)
481}
482
483/// Constructs the `format!()` invocation(s) necessary for a `#[suggestion*(code = "foo")]` or
484/// `#[suggestion*(code("foo", "bar"))]` attribute field
485pub(super) fn build_suggestion_code(
486    code_field: &Ident,
487    nested: ParseNestedMeta<'_>,
488    fields: &impl HasFieldMap,
489    allow_multiple: AllowMultipleAlternatives,
490) -> TokenStream {
491    let values = match parse_suggestion_values(nested, allow_multiple) {
492        Ok(x) => x,
493        Err(e) => return e.into_compile_error(),
494    };
495
496    if let AllowMultipleAlternatives::Yes = allow_multiple {
497        let formatted_strings: Vec<_> = values
498            .into_iter()
499            .map(|value| fields.build_format(&value.value(), value.span()))
500            .collect();
501        quote! { let #code_field = [#(#formatted_strings),*].into_iter(); }
502    } else if let [value] = values.as_slice() {
503        let formatted_str = fields.build_format(&value.value(), value.span());
504        quote! { let #code_field = #formatted_str; }
505    } else {
506        // error handled previously
507        quote! { let #code_field = String::new(); }
508    }
509}
510
511/// Possible styles for suggestion subdiagnostics.
512#[derive(Clone, Copy, PartialEq)]
513pub(super) enum SuggestionKind {
514    Normal,
515    Short,
516    Hidden,
517    Verbose,
518    ToolOnly,
519}
520
521impl FromStr for SuggestionKind {
522    type Err = ();
523
524    fn from_str(s: &str) -> Result<Self, Self::Err> {
525        match s {
526            "normal" => Ok(SuggestionKind::Normal),
527            "short" => Ok(SuggestionKind::Short),
528            "hidden" => Ok(SuggestionKind::Hidden),
529            "verbose" => Ok(SuggestionKind::Verbose),
530            "tool-only" => Ok(SuggestionKind::ToolOnly),
531            _ => Err(()),
532        }
533    }
534}
535
536impl fmt::Display for SuggestionKind {
537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538        match self {
539            SuggestionKind::Normal => write!(f, "normal"),
540            SuggestionKind::Short => write!(f, "short"),
541            SuggestionKind::Hidden => write!(f, "hidden"),
542            SuggestionKind::Verbose => write!(f, "verbose"),
543            SuggestionKind::ToolOnly => write!(f, "tool-only"),
544        }
545    }
546}
547
548impl SuggestionKind {
549    pub(crate) fn to_suggestion_style(&self) -> TokenStream {
550        match self {
551            SuggestionKind::Normal => {
552                quote! { rustc_errors::SuggestionStyle::ShowCode }
553            }
554            SuggestionKind::Short => {
555                quote! { rustc_errors::SuggestionStyle::HideCodeInline }
556            }
557            SuggestionKind::Hidden => {
558                quote! { rustc_errors::SuggestionStyle::HideCodeAlways }
559            }
560            SuggestionKind::Verbose => {
561                quote! { rustc_errors::SuggestionStyle::ShowAlways }
562            }
563            SuggestionKind::ToolOnly => {
564                quote! { rustc_errors::SuggestionStyle::CompletelyHidden }
565            }
566        }
567    }
568
569    fn from_suffix(s: &str) -> Option<Self> {
570        match s {
571            "" => Some(SuggestionKind::Normal),
572            "_short" => Some(SuggestionKind::Short),
573            "_hidden" => Some(SuggestionKind::Hidden),
574            "_verbose" => Some(SuggestionKind::Verbose),
575            _ => None,
576        }
577    }
578}
579
580/// Types of subdiagnostics that can be created using attributes
581#[derive(Clone)]
582pub(super) enum SubdiagnosticKind {
583    /// `#[label(...)]`
584    Label,
585    /// `#[note(...)]`
586    Note,
587    /// `#[note_once(...)]`
588    NoteOnce,
589    /// `#[help(...)]`
590    Help,
591    /// `#[help_once(...)]`
592    HelpOnce,
593    /// `#[warning(...)]`
594    Warn,
595    /// `#[suggestion{,_short,_hidden,_verbose}]`
596    Suggestion {
597        suggestion_kind: SuggestionKind,
598        applicability: SpannedOption<Applicability>,
599        /// Identifier for variable used for formatted code, e.g. `___code_0`. Enables separation
600        /// of formatting and diagnostic emission so that `arg` calls can happen in-between..
601        code_field: syn::Ident,
602        /// Initialization logic for `code_field`'s variable, e.g.
603        /// `let __formatted_code = /* whatever */;`
604        code_init: TokenStream,
605    },
606    /// `#[multipart_suggestion{,_short,_hidden,_verbose}]`
607    MultipartSuggestion {
608        suggestion_kind: SuggestionKind,
609        applicability: SpannedOption<Applicability>,
610    },
611}
612
613pub(super) struct SubdiagnosticVariant {
614    pub(super) kind: SubdiagnosticKind,
615    pub(super) slug: Option<Path>,
616    pub(super) no_span: bool,
617}
618
619impl SubdiagnosticVariant {
620    /// Constructs a `SubdiagnosticVariant` from a field or type attribute such as `#[note]`,
621    /// `#[error(parser::add_paren, no_span)]` or `#[suggestion(code = "...")]`. Returns the
622    /// `SubdiagnosticKind` and the diagnostic slug, if specified.
623    pub(super) fn from_attr(
624        attr: &Attribute,
625        fields: &impl HasFieldMap,
626    ) -> Result<Option<SubdiagnosticVariant>, DiagnosticDeriveError> {
627        // Always allow documentation comments.
628        if is_doc_comment(attr) {
629            return Ok(None);
630        }
631
632        let span = attr.span().unwrap();
633
634        let name = attr.path().segments.last().unwrap().ident.to_string();
635        let name = name.as_str();
636
637        let mut kind = match name {
638            "label" => SubdiagnosticKind::Label,
639            "note" => SubdiagnosticKind::Note,
640            "note_once" => SubdiagnosticKind::NoteOnce,
641            "help" => SubdiagnosticKind::Help,
642            "help_once" => SubdiagnosticKind::HelpOnce,
643            "warning" => SubdiagnosticKind::Warn,
644            _ => {
645                // Recover old `#[(multipart_)suggestion_*]` syntaxes
646                // FIXME(#100717): remove
647                if let Some(suggestion_kind) = name
648                    .strip_prefix("suggestion")
649                    .and_then(SuggestionKind::from_suffix)
650                {
651                    if suggestion_kind != SuggestionKind::Normal {
652                        invalid_attr(attr)
653                            .help(format!(
654                                r#"Use `#[suggestion(..., style = "{suggestion_kind}")]` instead"#
655                            ))
656                            .emit();
657                    }
658
659                    SubdiagnosticKind::Suggestion {
660                        suggestion_kind: SuggestionKind::Normal,
661                        applicability: None,
662                        code_field: new_code_ident(),
663                        code_init: TokenStream::new(),
664                    }
665                } else if let Some(suggestion_kind) = name
666                    .strip_prefix("multipart_suggestion")
667                    .and_then(SuggestionKind::from_suffix)
668                {
669                    if suggestion_kind != SuggestionKind::Normal {
670                        invalid_attr(attr)
671                            .help(format!(
672                                r#"Use `#[multipart_suggestion(..., style = "{suggestion_kind}")]` instead"#
673                            ))
674                            .emit();
675                    }
676
677                    SubdiagnosticKind::MultipartSuggestion {
678                        suggestion_kind: SuggestionKind::Normal,
679                        applicability: None,
680                    }
681                } else {
682                    throw_invalid_attr!(attr);
683                }
684            }
685        };
686
687        let list = match &attr.meta {
688            Meta::List(list) => {
689                // An attribute with properties, such as `#[suggestion(code = "...")]` or
690                // `#[error(some::slug)]`
691                list
692            }
693            Meta::Path(_) => {
694                // An attribute without a slug or other properties, such as `#[note]` - return
695                // without further processing.
696                //
697                // Only allow this if there are no mandatory properties, such as `code = "..."` in
698                // `#[suggestion(...)]`
699                match kind {
700                    SubdiagnosticKind::Label
701                    | SubdiagnosticKind::Note
702                    | SubdiagnosticKind::NoteOnce
703                    | SubdiagnosticKind::Help
704                    | SubdiagnosticKind::HelpOnce
705                    | SubdiagnosticKind::Warn
706                    | SubdiagnosticKind::MultipartSuggestion { .. } => {
707                        return Ok(Some(SubdiagnosticVariant { kind, slug: None, no_span: false }));
708                    }
709                    SubdiagnosticKind::Suggestion { .. } => {
710                        throw_span_err!(span, "suggestion without `code = \"...\"`")
711                    }
712                }
713            }
714            _ => {
715                throw_invalid_attr!(attr)
716            }
717        };
718
719        let mut code = None;
720        let mut suggestion_kind = None;
721
722        let mut first = true;
723        let mut slug = None;
724        let mut no_span = false;
725
726        list.parse_nested_meta(|nested| {
727            if nested.input.is_empty() || nested.input.peek(Token![,]) {
728                if first {
729                    slug = Some(nested.path);
730                } else if nested.path.is_ident("no_span") {
731                    no_span = true;
732                } else {
733                    span_err(nested.input.span().unwrap(), "a diagnostic slug must be the first argument to the attribute").emit();
734                }
735
736                first = false;
737                return Ok(());
738            }
739
740            first = false;
741
742            let nested_name = nested.path.segments.last().unwrap().ident.to_string();
743            let nested_name = nested_name.as_str();
744
745            let path_span = nested.path.span().unwrap();
746            let val_span = nested.input.span().unwrap();
747
748            macro_rules! get_string {
749                () => {{
750                    let Ok(value) = nested.value().and_then(|x| x.parse::<LitStr>()) else {
751                        span_err(val_span, "expected `= \"xxx\"`").emit();
752                        return Ok(());
753                    };
754                    value
755                }};
756            }
757
758            let mut has_errors = false;
759            let input = nested.input;
760
761            match (nested_name, &mut kind) {
762                ("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
763                    let code_init = build_suggestion_code(
764                        code_field,
765                        nested,
766                        fields,
767                        AllowMultipleAlternatives::Yes,
768                    );
769                    code.set_once(code_init, path_span);
770                }
771                (
772                    "applicability",
773                    SubdiagnosticKind::Suggestion { applicability, .. }
774                    | SubdiagnosticKind::MultipartSuggestion { applicability, .. },
775                ) => {
776                    let value = get_string!();
777                    let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
778                        span_err(value.span().unwrap(), "invalid applicability").emit();
779                        has_errors = true;
780                        Applicability::Unspecified
781                    });
782                    applicability.set_once(value, span);
783                }
784                (
785                    "style",
786                    SubdiagnosticKind::Suggestion { .. }
787                    | SubdiagnosticKind::MultipartSuggestion { .. },
788                ) => {
789                    let value = get_string!();
790
791                    let value = value.value().parse().unwrap_or_else(|()| {
792                        span_err(value.span().unwrap(), "invalid suggestion style")
793                            .help("valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`")
794                            .emit();
795                        has_errors = true;
796                        SuggestionKind::Normal
797                    });
798
799                    suggestion_kind.set_once(value, span);
800                }
801
802                // Invalid nested attribute
803                (_, SubdiagnosticKind::Suggestion { .. }) => {
804                    span_err(path_span, "invalid nested attribute")
805                        .help(
806                            "only `no_span`, `style`, `code` and `applicability` are valid nested attributes",
807                        )
808                        .emit();
809                    has_errors = true;
810                }
811                (_, SubdiagnosticKind::MultipartSuggestion { .. }) => {
812                    span_err(path_span, "invalid nested attribute")
813                        .help("only `no_span`, `style` and `applicability` are valid nested attributes")
814                        .emit();
815                    has_errors = true;
816                }
817                _ => {
818                    span_err(path_span, "only `no_span` is a valid nested attribute").emit();
819                    has_errors = true;
820                }
821            }
822
823            if has_errors {
824                // Consume the rest of the input to avoid spamming errors
825                let _ = input.parse::<TokenStream>();
826            }
827
828            Ok(())
829        })?;
830
831        match kind {
832            SubdiagnosticKind::Suggestion {
833                ref code_field,
834                ref mut code_init,
835                suggestion_kind: ref mut kind_field,
836                ..
837            } => {
838                if let Some(kind) = suggestion_kind.value() {
839                    *kind_field = kind;
840                }
841
842                *code_init = if let Some(init) = code.value() {
843                    init
844                } else {
845                    span_err(span, "suggestion without `code = \"...\"`").emit();
846                    quote! { let #code_field = std::iter::empty(); }
847                };
848            }
849            SubdiagnosticKind::MultipartSuggestion {
850                suggestion_kind: ref mut kind_field, ..
851            } => {
852                if let Some(kind) = suggestion_kind.value() {
853                    *kind_field = kind;
854                }
855            }
856            SubdiagnosticKind::Label
857            | SubdiagnosticKind::Note
858            | SubdiagnosticKind::NoteOnce
859            | SubdiagnosticKind::Help
860            | SubdiagnosticKind::HelpOnce
861            | SubdiagnosticKind::Warn => {}
862        }
863
864        Ok(Some(SubdiagnosticVariant { kind, slug, no_span }))
865    }
866}
867
868impl quote::IdentFragment for SubdiagnosticKind {
869    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
870        match self {
871            SubdiagnosticKind::Label => write!(f, "label"),
872            SubdiagnosticKind::Note => write!(f, "note"),
873            SubdiagnosticKind::NoteOnce => write!(f, "note_once"),
874            SubdiagnosticKind::Help => write!(f, "help"),
875            SubdiagnosticKind::HelpOnce => write!(f, "help_once"),
876            SubdiagnosticKind::Warn => write!(f, "warn"),
877            SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"),
878            SubdiagnosticKind::MultipartSuggestion { .. } => {
879                write!(f, "multipart_suggestion_with_style")
880            }
881        }
882    }
883
884    fn span(&self) -> Option<proc_macro2::Span> {
885        None
886    }
887}
888
889/// Returns `true` if `field` should generate a `arg` call rather than any other diagnostic
890/// call (like `span_label`).
891pub(super) fn should_generate_arg(field: &Field) -> bool {
892    // Perhaps this should be an exhaustive list...
893    field.attrs.iter().all(|attr| is_doc_comment(attr))
894}
895
896pub(super) fn is_doc_comment(attr: &Attribute) -> bool {
897    attr.path().segments.last().unwrap().ident == "doc"
898}