Skip to main content

miniextendr_macros/
match_arg_derive.rs

1//! # `#[derive(MatchArg)]` - Enum ↔ R String with `match.arg` Support
2//!
3//! This module implements the `#[derive(MatchArg)]` macro which generates
4//! the `MatchArg` trait implementation for C-style enums, enabling automatic
5//! conversion between Rust enums and R character strings with partial matching.
6//!
7//! ## Tradeoff
8//!
9//! Without `#[derive(MatchArg)]`, an enum-shaped argument lands in the Rust
10//! function as a raw `String` that you match on by hand — invalid values
11//! surface as Rust `Err` deep in the body, with no R-side defaulting and no
12//! partial matching. `#[derive(MatchArg)]` shifts that work to the wrapper
13//! boundary: the generated R wrapper picks up `match.arg`-style **partial
14//! string matching** against the enum's variant list, fills in the first
15//! variant as the **R-visible default**, and rejects unknown values before
16//! the Rust function is even called. The variant list reaches the R side
17//! via a write-time placeholder (`MX_MATCH_ARG_CHOICES`) rather than a
18//! string baked into the proc macro, so adding a variant only requires
19//! re-running the build.
20//!
21//! ## Usage
22//!
23//! ```ignore
24//! #[derive(Copy, Clone, MatchArg)]
25//! enum Mode {
26//!     Fast,
27//!     Safe,
28//!     Debug,
29//! }
30//!
31//! // Generates impl MatchArg for Mode, TryFromSexp for Mode, IntoR for Mode.
32//! ```
33//!
34//! ## Attributes
35//!
36//! - `#[match_arg(rename = "name")]` - Rename a variant's choice string
37//! - `#[match_arg(rename_all = "snake_case")]` - Rename all variants (snake_case, kebab-case, lower, upper)
38//! - `#[match_arg(alias = "name")]` - Another spelling of a variant's choice, matched only when
39//!   typed in full (`MatchArg::ALIASES`, #1843). Repeatable, variants only, never renamed by
40//!   `rename_all`. An alias that is empty, `"NA"`, a choice, given twice, or a prefix of another
41//!   variant's choice is a compile error. Every error that lists the choices names it after them
42//!   (`(or "name" for "choice")`, #2148); the formal, the usage and the `@param` line don't.
43//! - `#[match_arg(alias = "name", listed = false)]` - Keep the aliases of this attribute out of
44//!   every error. `listed` applies to the aliases of its own `#[match_arg(...)]` only; one
45//!   without an alias, a repeated one, or a value other than `true` / `false` is a compile error.
46
47use proc_macro2::TokenStream;
48use quote::quote;
49use syn::{Data, DeriveInput, Fields};
50
51use crate::naming::apply_rename_all;
52
53/// Parsed `#[match_arg(...)]` attributes from an enum or variant.
54#[derive(#[automatically_derived]
impl ::core::default::Default for MatchArgAttrs {
    #[inline]
    fn default() -> Self {
        MatchArgAttrs {
            rename: ::core::default::Default::default(),
            rename_all: ::core::default::Default::default(),
            aliases: ::core::default::Default::default(),
        }
    }
}Default)]
55struct MatchArgAttrs {
56    /// Per-variant rename: `#[match_arg(rename = "custom")]`.
57    rename: Option<String>,
58    /// Enum-level rename-all: `#[match_arg(rename_all = "snake_case")]`.
59    /// Applied to all variants that don't have an explicit `rename`.
60    rename_all: Option<String>,
61    /// Per-variant aliases, in written order: `#[match_arg(alias = "grey")]`,
62    /// repeatable. Each literal is kept for the span of its error, with
63    /// whether the errors list it (`listed = false` in the same attribute
64    /// opts out).
65    aliases: Vec<(syn::LitStr, bool)>,
66}
67
68/// Parse `#[match_arg(...)]` attributes from a list of `syn::Attribute`.
69///
70/// Extracts `rename`, `rename_all`, `alias` and `listed` keys. Validates that
71/// `rename_all` uses one of the supported modes: `snake_case`, `kebab-case`,
72/// `lower`, `upper`. Returns `Err` for unknown attribute keys or unsupported
73/// `rename_all` values. `listed = <bool>` applies to every alias of the same
74/// `#[match_arg(...)]` (#2148), so it needs one there, once, with a `bool`
75/// value. Where `alias` may appear (variants only) is the caller's check.
76fn parse_match_arg_attrs(attrs: &[syn::Attribute]) -> syn::Result<MatchArgAttrs> {
77    let mut result = MatchArgAttrs::default();
78
79    for attr in attrs {
80        if attr.path().is_ident("match_arg") {
81            let mut aliases: Vec<syn::LitStr> = Vec::new();
82            // The value of this attribute's `listed`, and its key for the
83            // span of an error.
84            let mut listed: Option<(bool, syn::Path)> = None;
85            attr.parse_nested_meta(|meta| {
86                if meta.path.is_ident("rename") {
87                    let value: syn::LitStr = meta.value()?.parse()?;
88                    result.rename = Some(value.value());
89                } else if meta.path.is_ident("alias") {
90                    aliases.push(meta.value()?.parse()?);
91                } else if meta.path.is_ident("listed") {
92                    if listed.is_some() {
93                        return Err(meta
94                            .error("`listed` is given more than once in this #[match_arg(...)]"));
95                    }
96                    let value: syn::Expr = meta.value()?.parse()?;
97                    let syn::Expr::Lit(syn::ExprLit {
98                        lit: syn::Lit::Bool(value),
99                        ..
100                    }) = value
101                    else {
102                        return Err(syn::Error::new_spanned(
103                            value,
104                            "expected `listed = true` or `listed = false`",
105                        ));
106                    };
107                    listed = Some((value.value, meta.path.clone()));
108                } else if meta.path.is_ident("rename_all") {
109                    let value: syn::LitStr = meta.value()?.parse()?;
110                    let val = value.value();
111                    match val.as_str() {
112                        "snake_case" | "kebab-case" | "lower" | "upper" => {}
113                        _ => {
114                            return Err(meta.error(
115                                "unsupported rename_all value; expected one of: \
116                                 snake_case, kebab-case, lower, upper",
117                            ));
118                        }
119                    }
120                    result.rename_all = Some(val);
121                } else {
122                    return Err(meta.error(
123                        "unknown match_arg attribute; expected `rename`, `rename_all`, \
124                         `alias` or `listed`",
125                    ));
126                }
127                Ok(())
128            })?;
129            if let Some((_, key)) = listed.as_ref().filter(|_| aliases.is_empty()) {
130                return Err(syn::Error::new_spanned(
131                    key,
132                    "`listed` applies to the aliases of its own #[match_arg(...)], and this \
133                     one has none: write it next to `alias = \"...\"`",
134                ));
135            }
136            let listed = listed.is_none_or(|(listed, _)| listed);
137            result
138                .aliases
139                .extend(aliases.into_iter().map(|alias| (alias, listed)));
140        }
141    }
142
143    Ok(result)
144}
145
146/// Main entry point for `#[derive(MatchArg)]`.
147///
148/// Generates three trait implementations:
149/// - `impl MatchArg` -- provides `CHOICES` (static string slice), `ALIASES` (from the
150///   variants' `alias = "..."`), `from_choice`, `to_choice`
151/// - `impl TryFromSexp` -- converts R character scalar to enum variant via `match_arg_from_sexp`
152/// - `impl IntoR` -- converts enum variant to R character scalar via `to_choice().into_sexp()`
153///
154/// `impl IntoR for Vec<Self>` is provided automatically by the blanket
155/// `impl<T: MatchArg> IntoR for Vec<T>` in `miniextendr-api::match_arg`,
156/// so returning `Vec<EnumName>` from a `#[miniextendr]` function works
157/// without any extra code in the user's crate.
158///
159/// Validates:
160/// - Only enums are accepted (not structs or unions)
161/// - Generic enums are rejected
162/// - At least one variant is required
163/// - Only fieldless (C-style) variants are allowed
164/// - No duplicate choice names after renaming
165/// - No `alias` on the enum itself, and no alias [`check_aliases`] refuses
166///
167/// Choice names default to variant identifiers, optionally transformed by
168/// `#[match_arg(rename_all = "...")]` or overridden per-variant with
169/// `#[match_arg(rename = "...")]`.
170pub fn derive_match_arg(input: DeriveInput) -> syn::Result<TokenStream> {
171    let name = &input.ident;
172    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
173
174    // Reject generics for v1
175    if !input.generics.params.is_empty() {
176        return Err(syn::Error::new_spanned(
177            &input.generics,
178            "#[derive(MatchArg)] does not support generic enums",
179        ));
180    }
181
182    // Parse enum-level attributes
183    let attrs = parse_match_arg_attrs(&input.attrs)?;
184    if let Some((alias, _)) = attrs.aliases.first() {
185        return Err(syn::Error::new_spanned(
186            alias,
187            "`alias` names another spelling of one choice: put \
188             #[match_arg(alias = \"...\")] on that choice's variant",
189        ));
190    }
191
192    // Get enum variants
193    let variants = match &input.data {
194        Data::Enum(data) => &data.variants,
195        Data::Struct(_) => {
196            return Err(syn::Error::new_spanned(
197                &input,
198                "#[derive(MatchArg)] can only be applied to enums",
199            ));
200        }
201        Data::Union(_) => {
202            return Err(syn::Error::new_spanned(
203                &input,
204                "#[derive(MatchArg)] can only be applied to enums",
205            ));
206        }
207    };
208
209    if variants.is_empty() {
210        return Err(syn::Error::new_spanned(
211            &input,
212            "#[derive(MatchArg)] requires at least one variant",
213        ));
214    }
215
216    let mut choice_names = Vec::new();
217    let mut variant_idents = Vec::new();
218    // (alias literal, index of its variant's choice in `choice_names`,
219    // whether the errors list it)
220    let mut variant_aliases: Vec<(syn::LitStr, usize, bool)> = Vec::new();
221
222    for variant in variants {
223        // Only allow unit variants (fieldless)
224        if !#[allow(non_exhaustive_omitted_patterns)] match variant.fields {
    Fields::Unit => true,
    _ => false,
}matches!(variant.fields, Fields::Unit) {
225            return Err(syn::Error::new_spanned(
226                variant,
227                "#[derive(MatchArg)] only supports fieldless (C-style) enum variants",
228            ));
229        }
230
231        // Parse variant-level attributes
232        let var_attrs = parse_match_arg_attrs(&variant.attrs)?;
233
234        // Determine choice name
235        let choice_name = if let Some(r) = var_attrs.rename {
236            r
237        } else {
238            apply_rename_all(
239                &crate::naming::ident_name(&variant.ident),
240                attrs.rename_all.as_deref(),
241            )
242        };
243
244        let at = choice_names.len();
245        variant_aliases.extend(
246            var_attrs
247                .aliases
248                .into_iter()
249                .map(|(alias, listed)| (alias, at, listed)),
250        );
251        choice_names.push(choice_name);
252        variant_idents.push(&variant.ident);
253    }
254
255    // Check for duplicate choice names
256    {
257        let mut seen = std::collections::HashSet::new();
258        for (i, name) in choice_names.iter().enumerate() {
259            if !seen.insert(name.as_str()) {
260                return Err(syn::Error::new_spanned(
261                    &variants.iter().nth(i).unwrap().ident,
262                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("duplicate choice name {0:?} in #[derive(MatchArg)]",
                name))
    })format!("duplicate choice name {:?} in #[derive(MatchArg)]", name),
263                ));
264            }
265        }
266    }
267
268    check_aliases(&choice_names, &variant_aliases)?;
269
270    let choice_strs: Vec<&str> = choice_names.iter().map(|s| s.as_str()).collect();
271    // In the order of the variants, then as written: the order the errors
272    // list them in.
273    let alias_entries = variant_aliases.iter().map(|(alias, at, listed)| {
274        let alias = alias.value();
275        let target = choice_names[*at].as_str();
276        if *listed {
277            {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "match_arg");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "MatchArgAlias");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "new");
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Parenthesis,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::ToTokens::to_tokens(&alias, &mut _s);
            ::quote::__private::push_comma(&mut _s);
            ::quote::ToTokens::to_tokens(&target, &mut _s);
            _s
        });
    _s
}quote! { ::miniextendr_api::match_arg::MatchArgAlias::new(#alias, #target) }
278        } else {
279            {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "match_arg");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "MatchArgAlias");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "unlisted");
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Parenthesis,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::ToTokens::to_tokens(&alias, &mut _s);
            ::quote::__private::push_comma(&mut _s);
            ::quote::ToTokens::to_tokens(&target, &mut _s);
            _s
        });
    _s
}quote! { ::miniextendr_api::match_arg::MatchArgAlias::unlisted(#alias, #target) }
280        }
281    });
282
283    Ok({
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_ident(&mut _s, "impl");
    ::quote::ToTokens::to_tokens(&impl_generics, &mut _s);
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "match_arg");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "MatchArg");
    ::quote::__private::push_ident(&mut _s, "for");
    ::quote::ToTokens::to_tokens(&name, &mut _s);
    ::quote::ToTokens::to_tokens(&ty_generics, &mut _s);
    ::quote::ToTokens::to_tokens(&where_clause, &mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Brace,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "const");
            ::quote::__private::push_ident(&mut _s, "CHOICES");
            ::quote::__private::push_colon(&mut _s);
            ::quote::__private::push_and(&mut _s);
            ::quote::__private::push_lifetime(&mut _s, "\'static");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Bracket,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_and(&mut _s);
                    ::quote::__private::push_lifetime(&mut _s, "\'static");
                    ::quote::__private::push_ident(&mut _s, "str");
                    _s
                });
            ::quote::__private::push_eq(&mut _s);
            ::quote::__private::push_and(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Bracket,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    {
                        use ::quote::__private::ext::*;
                        let mut _first = true;
                        let has_iter = ::quote::__private::HasIterator::<false>;
                        #[allow(unused_mut)]
                        let (mut choice_strs, i) = choice_strs.quote_into_iter();
                        let has_iter = has_iter | i;
                        <_ as
                                ::quote::__private::CheckHasIterator<true>>::check(has_iter);
                        while true {
                            let choice_strs =
                                match choice_strs.next() {
                                    Some(_x) => ::quote::__private::RepInterp(_x),
                                    None => break,
                                };
                            if !_first { ::quote::__private::push_comma(&mut _s); }
                            _first = false;
                            ::quote::ToTokens::to_tokens(&choice_strs, &mut _s);
                        }
                    }
                    _s
                });
            ::quote::__private::push_semi(&mut _s);
            ::quote::__private::push_ident(&mut _s, "const");
            ::quote::__private::push_ident(&mut _s, "ALIASES");
            ::quote::__private::push_colon(&mut _s);
            ::quote::__private::push_and(&mut _s);
            ::quote::__private::push_lifetime(&mut _s, "\'static");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Bracket,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "match_arg");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "MatchArgAlias");
                    _s
                });
            ::quote::__private::push_eq(&mut _s);
            ::quote::__private::push_and(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Bracket,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    {
                        use ::quote::__private::ext::*;
                        let mut _first = true;
                        let has_iter = ::quote::__private::HasIterator::<false>;
                        #[allow(unused_mut)]
                        let (mut alias_entries, i) =
                            alias_entries.quote_into_iter();
                        let has_iter = has_iter | i;
                        <_ as
                                ::quote::__private::CheckHasIterator<true>>::check(has_iter);
                        while true {
                            let alias_entries =
                                match alias_entries.next() {
                                    Some(_x) => ::quote::__private::RepInterp(_x),
                                    None => break,
                                };
                            if !_first { ::quote::__private::push_comma(&mut _s); }
                            _first = false;
                            ::quote::ToTokens::to_tokens(&alias_entries, &mut _s);
                        }
                    }
                    _s
                });
            ::quote::__private::push_semi(&mut _s);
            ::quote::__private::push_ident(&mut _s, "fn");
            ::quote::__private::push_ident(&mut _s, "from_choice");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "choice");
                    ::quote::__private::push_colon(&mut _s);
                    ::quote::__private::push_and(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "str");
                    _s
                });
            ::quote::__private::push_rarrow(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "option");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Option");
            ::quote::__private::push_lt(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Self");
            ::quote::__private::push_gt(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Brace,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "match");
                    ::quote::__private::push_ident(&mut _s, "choice");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Brace,
                        {
                            let mut _s = ::quote::__private::TokenStream::new();
                            {
                                use ::quote::__private::ext::*;
                                let has_iter = ::quote::__private::HasIterator::<false>;
                                #[allow(unused_mut)]
                                let (mut choice_strs, i) = choice_strs.quote_into_iter();
                                let has_iter = has_iter | i;
                                #[allow(unused_mut)]
                                let (mut variant_idents, i) =
                                    variant_idents.quote_into_iter();
                                let has_iter = has_iter | i;
                                <_ as
                                        ::quote::__private::CheckHasIterator<true>>::check(has_iter);
                                while true {
                                    let choice_strs =
                                        match choice_strs.next() {
                                            Some(_x) => ::quote::__private::RepInterp(_x),
                                            None => break,
                                        };
                                    let variant_idents =
                                        match variant_idents.next() {
                                            Some(_x) => ::quote::__private::RepInterp(_x),
                                            None => break,
                                        };
                                    ::quote::ToTokens::to_tokens(&choice_strs, &mut _s);
                                    ::quote::__private::push_fat_arrow(&mut _s);
                                    ::quote::__private::push_colon2(&mut _s);
                                    ::quote::__private::push_ident(&mut _s, "core");
                                    ::quote::__private::push_colon2(&mut _s);
                                    ::quote::__private::push_ident(&mut _s, "option");
                                    ::quote::__private::push_colon2(&mut _s);
                                    ::quote::__private::push_ident(&mut _s, "Option");
                                    ::quote::__private::push_colon2(&mut _s);
                                    ::quote::__private::push_ident(&mut _s, "Some");
                                    ::quote::__private::push_group(&mut _s,
                                        ::quote::__private::Delimiter::Parenthesis,
                                        {
                                            let mut _s = ::quote::__private::TokenStream::new();
                                            ::quote::__private::push_ident(&mut _s, "Self");
                                            ::quote::__private::push_colon2(&mut _s);
                                            ::quote::ToTokens::to_tokens(&variant_idents, &mut _s);
                                            _s
                                        });
                                    ::quote::__private::push_comma(&mut _s);
                                }
                            }
                            ::quote::__private::push_underscore(&mut _s);
                            ::quote::__private::push_fat_arrow(&mut _s);
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "core");
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "option");
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "Option");
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "None");
                            ::quote::__private::push_comma(&mut _s);
                            _s
                        });
                    _s
                });
            ::quote::__private::push_ident(&mut _s, "fn");
            ::quote::__private::push_ident(&mut _s, "to_choice");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "self");
                    _s
                });
            ::quote::__private::push_rarrow(&mut _s);
            ::quote::__private::push_and(&mut _s);
            ::quote::__private::push_lifetime(&mut _s, "\'static");
            ::quote::__private::push_ident(&mut _s, "str");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Brace,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "match");
                    ::quote::__private::push_ident(&mut _s, "self");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Brace,
                        {
                            let mut _s = ::quote::__private::TokenStream::new();
                            {
                                use ::quote::__private::ext::*;
                                let has_iter = ::quote::__private::HasIterator::<false>;
                                #[allow(unused_mut)]
                                let (mut variant_idents, i) =
                                    variant_idents.quote_into_iter();
                                let has_iter = has_iter | i;
                                #[allow(unused_mut)]
                                let (mut choice_strs, i) = choice_strs.quote_into_iter();
                                let has_iter = has_iter | i;
                                <_ as
                                        ::quote::__private::CheckHasIterator<true>>::check(has_iter);
                                while true {
                                    let variant_idents =
                                        match variant_idents.next() {
                                            Some(_x) => ::quote::__private::RepInterp(_x),
                                            None => break,
                                        };
                                    let choice_strs =
                                        match choice_strs.next() {
                                            Some(_x) => ::quote::__private::RepInterp(_x),
                                            None => break,
                                        };
                                    ::quote::__private::push_ident(&mut _s, "Self");
                                    ::quote::__private::push_colon2(&mut _s);
                                    ::quote::ToTokens::to_tokens(&variant_idents, &mut _s);
                                    ::quote::__private::push_fat_arrow(&mut _s);
                                    ::quote::ToTokens::to_tokens(&choice_strs, &mut _s);
                                    ::quote::__private::push_comma(&mut _s);
                                }
                            }
                            _s
                        });
                    _s
                });
            _s
        });
    ::quote::__private::push_ident(&mut _s, "impl");
    ::quote::ToTokens::to_tokens(&impl_generics, &mut _s);
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "TryFromSexp");
    ::quote::__private::push_ident(&mut _s, "for");
    ::quote::ToTokens::to_tokens(&name, &mut _s);
    ::quote::ToTokens::to_tokens(&ty_generics, &mut _s);
    ::quote::ToTokens::to_tokens(&where_clause, &mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Brace,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "type");
            ::quote::__private::push_ident(&mut _s, "Error");
            ::quote::__private::push_eq(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "miniextendr_api");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "SexpError");
            ::quote::__private::push_semi(&mut _s);
            ::quote::__private::push_ident(&mut _s, "const");
            ::quote::__private::push_ident(&mut _s, "CHARACTER_ONLY");
            ::quote::__private::push_colon(&mut _s);
            ::quote::__private::push_ident(&mut _s, "bool");
            ::quote::__private::push_eq(&mut _s);
            ::quote::__private::push_ident(&mut _s, "true");
            ::quote::__private::push_semi(&mut _s);
            ::quote::__private::push_ident(&mut _s, "fn");
            ::quote::__private::push_ident(&mut _s, "try_from_sexp");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "sexp");
                    ::quote::__private::push_colon(&mut _s);
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "SEXP");
                    _s
                });
            ::quote::__private::push_rarrow(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "result");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Result");
            ::quote::__private::push_lt(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Self");
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "miniextendr_api");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "SexpError");
            ::quote::__private::push_gt(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Brace,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s,
                        "match_arg_from_sexp");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        {
                            let mut _s = ::quote::__private::TokenStream::new();
                            ::quote::__private::push_ident(&mut _s, "sexp");
                            _s
                        });
                    ::quote::__private::push_dot(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "map_err");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        {
                            let mut _s = ::quote::__private::TokenStream::new();
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "core");
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "convert");
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "Into");
                            ::quote::__private::push_colon2(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "into");
                            _s
                        });
                    _s
                });
            _s
        });
    ::quote::__private::push_ident(&mut _s, "impl");
    ::quote::ToTokens::to_tokens(&impl_generics, &mut _s);
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
    ::quote::__private::push_colon2(&mut _s);
    ::quote::__private::push_ident(&mut _s, "IntoR");
    ::quote::__private::push_ident(&mut _s, "for");
    ::quote::ToTokens::to_tokens(&name, &mut _s);
    ::quote::ToTokens::to_tokens(&ty_generics, &mut _s);
    ::quote::ToTokens::to_tokens(&where_clause, &mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Brace,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "type");
            ::quote::__private::push_ident(&mut _s, "Error");
            ::quote::__private::push_eq(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "convert");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Infallible");
            ::quote::__private::push_semi(&mut _s);
            ::quote::__private::push_ident(&mut _s, "fn");
            ::quote::__private::push_ident(&mut _s, "try_into_sexp");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "self");
                    _s
                });
            ::quote::__private::push_rarrow(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "result");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Result");
            ::quote::__private::push_lt(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "miniextendr_api");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "SEXP");
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "convert");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Infallible");
            ::quote::__private::push_gt(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Brace,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "core");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "result");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "Result");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "Ok");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        {
                            let mut _s = ::quote::__private::TokenStream::new();
                            ::quote::__private::push_ident(&mut _s, "self");
                            ::quote::__private::push_dot(&mut _s);
                            ::quote::__private::push_ident(&mut _s, "into_sexp");
                            ::quote::__private::push_group(&mut _s,
                                ::quote::__private::Delimiter::Parenthesis,
                                ::quote::__private::TokenStream::new());
                            _s
                        });
                    _s
                });
            ::quote::__private::push_ident(&mut _s, "unsafe");
            ::quote::__private::push_ident(&mut _s, "fn");
            ::quote::__private::push_ident(&mut _s,
                "try_into_sexp_unchecked");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "self");
                    _s
                });
            ::quote::__private::push_rarrow(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "result");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Result");
            ::quote::__private::push_lt(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "miniextendr_api");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "SEXP");
            ::quote::__private::push_comma(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "core");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "convert");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "Infallible");
            ::quote::__private::push_gt(&mut _s);
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Brace,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "self");
                    ::quote::__private::push_dot(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "try_into_sexp");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        ::quote::__private::TokenStream::new());
                    _s
                });
            ::quote::__private::push_ident(&mut _s, "fn");
            ::quote::__private::push_ident(&mut _s, "into_sexp");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "self");
                    _s
                });
            ::quote::__private::push_rarrow(&mut _s);
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "miniextendr_api");
            ::quote::__private::push_colon2(&mut _s);
            ::quote::__private::push_ident(&mut _s, "SEXP");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Brace,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_ident(&mut _s, "use");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "match_arg");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "MatchArg");
                    ::quote::__private::push_semi(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "self");
                    ::quote::__private::push_dot(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "to_choice");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        ::quote::__private::TokenStream::new());
                    ::quote::__private::push_dot(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "into_sexp");
                    ::quote::__private::push_group(&mut _s,
                        ::quote::__private::Delimiter::Parenthesis,
                        ::quote::__private::TokenStream::new());
                    _s
                });
            _s
        });
    _s
}quote! {
284        impl #impl_generics ::miniextendr_api::match_arg::MatchArg for #name #ty_generics #where_clause {
285            const CHOICES: &'static [&'static str] = &[#(#choice_strs),*];
286            const ALIASES: &'static [::miniextendr_api::match_arg::MatchArgAlias] =
287                &[#(#alias_entries),*];
288
289            fn from_choice(choice: &str) -> ::core::option::Option<Self> {
290                match choice {
291                    #(#choice_strs => ::core::option::Option::Some(Self::#variant_idents),)*
292                    _ => ::core::option::Option::None,
293                }
294            }
295
296            fn to_choice(self) -> &'static str {
297                match self {
298                    #(Self::#variant_idents => #choice_strs,)*
299                }
300            }
301        }
302
303        impl #impl_generics ::miniextendr_api::TryFromSexp for #name #ty_generics #where_clause {
304            type Error = ::miniextendr_api::SexpError;
305            const CHARACTER_ONLY: bool = true;
306
307            fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> ::core::result::Result<Self, ::miniextendr_api::SexpError> {
308                ::miniextendr_api::match_arg_from_sexp(sexp).map_err(::core::convert::Into::into)
309            }
310        }
311
312        impl #impl_generics ::miniextendr_api::IntoR for #name #ty_generics #where_clause {
313            type Error = ::core::convert::Infallible;
314
315            fn try_into_sexp(self) -> ::core::result::Result<::miniextendr_api::SEXP, ::core::convert::Infallible> {
316                ::core::result::Result::Ok(self.into_sexp())
317            }
318
319            unsafe fn try_into_sexp_unchecked(self) -> ::core::result::Result<::miniextendr_api::SEXP, ::core::convert::Infallible> {
320                self.try_into_sexp()
321            }
322
323            fn into_sexp(self) -> ::miniextendr_api::SEXP {
324                use ::miniextendr_api::match_arg::MatchArg;
325                self.to_choice().into_sexp()
326            }
327        }
328
329
330    })
331}
332
333/// Refuse the aliases `#[match_arg(alias = "...")]` can't have, all at once
334/// (one error per bad alias, spanned on its literal): an alias that is empty
335/// (`pmatch()` matches `""` to nothing, while the R check's `match()` would
336/// map it), `"NA"` (it would read as a missing value), a choice, given twice
337/// (on any variant), or a prefix of another variant's choice, since the alias
338/// would change what typing it selects. A prefix of its own choice only is
339/// harmless. The wrappers writer applies the same
340/// rules to a hand-written `MatchArg::ALIASES`
341/// (`miniextendr_api::registry::match_arg_aliases_arg`).
342fn check_aliases(choices: &[String], aliases: &[(syn::LitStr, usize, bool)]) -> syn::Result<()> {
343    let mut errors: Option<syn::Error> = None;
344    let mut seen = std::collections::HashSet::new();
345    for (lit, at, _) in aliases {
346        let alias = lit.value();
347        let target = &choices[*at];
348        let problem = if alias.is_empty() {
349            Some("an alias can't be empty: the empty string matches no choice".to_string())
350        } else if alias == "NA" {
351            Some("alias \"NA\" would read as a missing value".to_string())
352        } else if choices.contains(&alias) {
353            Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} is already a choice",
                alias))
    })format!("alias {alias:?} is already a choice"))
354        } else if !seen.insert(alias.clone()) {
355            Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} is given more than once",
                alias))
    })format!("alias {alias:?} is given more than once"))
356        } else {
357            choices
358                .iter()
359                .find(|choice| *choice != target && choice.starts_with(alias.as_str()))
360                .map(|other| {
361                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} for {1:?} is a prefix of the choice {2:?}, so it would change what typing {0:?} selects",
                alias, target, other))
    })format!(
362                        "alias {alias:?} for {target:?} is a prefix of the choice {other:?}, \
363                         so it would change what typing {alias:?} selects"
364                    )
365                })
366        };
367        if let Some(problem) = problem {
368            let error = syn::Error::new_spanned(lit, problem);
369            match errors.as_mut() {
370                Some(errors) => errors.combine(error),
371                None => errors = Some(error),
372            }
373        }
374    }
375    match errors {
376        Some(errors) => Err(errors),
377        None => Ok(()),
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_simple_derive() {
387        let input: DeriveInput = syn::parse_quote! {
388            enum Mode {
389                Fast,
390                Safe,
391                Debug,
392            }
393        };
394
395        let result = derive_match_arg(input).unwrap();
396        let code = result.to_string();
397        assert!(code.contains("Fast"));
398        assert!(code.contains("Safe"));
399        assert!(code.contains("Debug"));
400        assert!(code.contains("CHOICES"));
401        assert!(code.contains("from_choice"));
402        assert!(code.contains("to_choice"));
403    }
404
405    #[test]
406    fn test_rename_all() {
407        let input: DeriveInput = syn::parse_quote! {
408            #[match_arg(rename_all = "snake_case")]
409            enum Mode {
410                FastMode,
411                SafeMode,
412            }
413        };
414
415        let result = derive_match_arg(input).unwrap();
416        let code = result.to_string();
417        assert!(code.contains("fast_mode"));
418        assert!(code.contains("safe_mode"));
419    }
420
421    #[test]
422    fn test_rename_variant() {
423        let input: DeriveInput = syn::parse_quote! {
424            enum Priority {
425                #[match_arg(rename = "lo")]
426                Low,
427                #[match_arg(rename = "hi")]
428                High,
429            }
430        };
431
432        let result = derive_match_arg(input).unwrap();
433        let code = result.to_string();
434        assert!(code.contains("\"lo\""));
435        assert!(code.contains("\"hi\""));
436    }
437
438    #[test]
439    fn test_reject_fields() {
440        let input: DeriveInput = syn::parse_quote! {
441            enum Bad {
442                A(i32),
443            }
444        };
445
446        let result = derive_match_arg(input);
447        assert!(result.is_err());
448    }
449
450    #[test]
451    fn test_reject_struct() {
452        let input: DeriveInput = syn::parse_quote! {
453            struct Bad;
454        };
455
456        let result = derive_match_arg(input);
457        assert!(result.is_err());
458    }
459
460    #[test]
461    fn test_reject_empty() {
462        let input: DeriveInput = syn::parse_quote! {
463            enum Empty {}
464        };
465
466        let result = derive_match_arg(input);
467        assert!(result.is_err());
468    }
469
470    #[test]
471    fn test_into_r_impl_present() {
472        // The derive emits IntoR for the scalar EnumName.
473        // Vec<EnumName> IntoR is covered by the blanket impl<T: MatchArg> IntoR for Vec<T>
474        // in miniextendr-api — it is NOT emitted by the derive.
475        let input: DeriveInput = syn::parse_quote! {
476            enum Mode {
477                Fast,
478                Safe,
479                Debug,
480            }
481        };
482
483        let result = derive_match_arg(input).unwrap();
484        let code = result.to_string();
485        // Scalar IntoR impl must be present
486        assert!(code.contains("IntoR for Mode"));
487        // Vec<Mode> IntoR must NOT be emitted by the derive (covered by blanket in miniextendr-api)
488        assert!(!code.contains("IntoR for :: std :: vec :: Vec < Mode >"));
489        assert!(!code.contains("match_arg_vec_into_sexp"));
490    }
491
492    /// `alias = "..."` fills `ALIASES` with (alias, the variant's choice after
493    /// renaming) entries in written order, is repeatable, is not renamed by
494    /// `rename_all`, and leaves `CHOICES` alone (#1843). An alias is listed
495    /// unless its own attribute says `listed = false` (#2148).
496    #[test]
497    fn test_aliases() {
498        let input: DeriveInput = syn::parse_quote! {
499            #[match_arg(rename_all = "lower")]
500            enum Shade {
501                Red,
502                #[match_arg(alias = "grey", alias = "Grau")]
503                #[match_arg(alias = "gris", listed = false)]
504                Gray,
505                #[match_arg(rename = "navy", alias = "Blue", listed = true)]
506                Blue,
507            }
508        };
509        let code = derive_match_arg(input).unwrap().to_string();
510        assert!(
511            code.contains(
512                r#"const CHOICES : & 'static [& 'static str] = & ["red" , "gray" , "navy"] ;"#
513            ),
514            "{code}"
515        );
516        let alias = |kind: &str, alias: &str, choice: &str| {
517            format!(
518                ":: miniextendr_api :: match_arg :: MatchArgAlias :: {kind} ({alias:?} , {choice:?})"
519            )
520        };
521        let entries = [
522            alias("new", "grey", "gray"),
523            alias("new", "Grau", "gray"),
524            alias("unlisted", "gris", "gray"),
525            alias("new", "Blue", "navy"),
526        ];
527        assert!(
528            code.contains(&format!(
529                "const ALIASES : & 'static [:: miniextendr_api :: match_arg :: MatchArgAlias] = & [{}] ;",
530                entries.join(" , ")
531            )),
532            "{code}"
533        );
534        // An alias is not a choice: `from_choice` matches the choices only.
535        assert!(!code.contains(r#""grey" => Some"#), "{code}");
536    }
537
538    /// Without aliases `ALIASES` is empty.
539    #[test]
540    fn test_no_aliases() {
541        let input: DeriveInput = syn::parse_quote! {
542            enum Mode { Fast, Safe }
543        };
544        let code = derive_match_arg(input).unwrap().to_string();
545        assert!(
546            code.contains(
547                r#"const ALIASES : & 'static [:: miniextendr_api :: match_arg :: MatchArgAlias] = & [] ;"#
548            ),
549            "{code}"
550        );
551    }
552
553    /// `listed` qualifies the aliases of its own attribute: one with none, a
554    /// repeated one, and a value that is not `true` / `false` are refused.
555    #[test]
556    fn test_reject_listed() {
557        let error = |input: DeriveInput| match derive_match_arg(input) {
558            Ok(_) => String::new(),
559            Err(e) => e.to_string(),
560        };
561        let no_alias = "`listed` applies to the aliases of its own #[match_arg(...)], and this \
562                        one has none: write it next to `alias = \"...\"`";
563        assert_eq!(
564            error(syn::parse_quote! {
565                enum Shade {
566                    #[match_arg(alias = "grey")]
567                    #[match_arg(listed = false)]
568                    Gray,
569                }
570            }),
571            no_alias
572        );
573        assert_eq!(
574            error(syn::parse_quote! {
575                #[match_arg(listed = false)]
576                enum Shade { Gray }
577            }),
578            no_alias
579        );
580        assert_eq!(
581            error(syn::parse_quote! {
582                enum Shade {
583                    #[match_arg(alias = "grey", listed = false, listed = true)]
584                    Gray,
585                }
586            }),
587            "`listed` is given more than once in this #[match_arg(...)]"
588        );
589        for value in [quote!("false"), quote!(no), quote!(0)] {
590            let input: DeriveInput = syn::parse_quote! {
591                enum Shade {
592                    #[match_arg(alias = "grey", listed = #value)]
593                    Gray,
594                }
595            };
596            assert_eq!(
597                error(input),
598                "expected `listed = true` or `listed = false`",
599                "{value}"
600            );
601        }
602    }
603
604    /// The error messages of the aliases the derive refuses, all reported at
605    /// once.
606    #[test]
607    fn test_reject_aliases() {
608        let errors = |input: DeriveInput| -> Vec<String> {
609            match derive_match_arg(input) {
610                Ok(_) => Vec::new(),
611                Err(e) => e.into_iter().map(|e| e.to_string()).collect(),
612            }
613        };
614        assert_eq!(
615            errors(syn::parse_quote! {
616                #[match_arg(rename_all = "lower")]
617                enum Shade {
618                    #[match_arg(alias = "")]
619                    Red,
620                    #[match_arg(alias = "NA", alias = "blue", alias = "grey")]
621                    Gray,
622                    #[match_arg(alias = "grey", alias = "bl")]
623                    Blue,
624                    #[match_arg(alias = "gr")]
625                    Green,
626                }
627            }),
628            [
629                "an alias can't be empty: the empty string matches no choice",
630                "alias \"NA\" would read as a missing value",
631                "alias \"blue\" is already a choice",
632                "alias \"grey\" is given more than once",
633                "alias \"gr\" for \"green\" is a prefix of the choice \"gray\", \
634                 so it would change what typing \"gr\" selects",
635            ]
636        );
637        // A prefix of its own choice only is allowed ("bl" above).
638        assert!(
639            errors(syn::parse_quote! {
640                enum Shade {
641                    Red,
642                    #[match_arg(alias = "Gr")]
643                    Gray,
644                }
645            })
646            .is_empty()
647        );
648        // Aliases go on variants.
649        assert_eq!(
650            errors(syn::parse_quote! {
651                #[match_arg(alias = "grey")]
652                enum Shade { Red, Gray }
653            }),
654            ["`alias` names another spelling of one choice: put \
655              #[match_arg(alias = \"...\")] on that choice's variant"]
656        );
657    }
658
659    #[test]
660    fn test_duplicate_choice_names() {
661        let input: DeriveInput = syn::parse_quote! {
662            enum Dup {
663                #[match_arg(rename = "same")]
664                A,
665                #[match_arg(rename = "same")]
666                B,
667            }
668        };
669
670        let result = derive_match_arg(input);
671        assert!(result.is_err());
672    }
673}