Skip to main content

open_enum_derive/
lib.rs

1extern crate proc_macro;
2
3mod config;
4mod discriminant;
5mod repr;
6
7use config::Config;
8
9use discriminant::Discriminant;
10use proc_macro2::{Span, TokenStream};
11use quote::{format_ident, quote, ToTokens};
12use repr::Repr;
13use std::collections::HashSet;
14use syn::Attribute;
15use syn::{
16    parse_macro_input, punctuated::Punctuated, spanned::Spanned, Error, Ident, ItemEnum, Visibility,
17};
18
19/// Sets the span for every token tree in the token stream
20fn set_token_stream_span(tokens: TokenStream, span: Span) -> TokenStream {
21    tokens
22        .into_iter()
23        .map(|mut tt| {
24            tt.set_span(span);
25            tt
26        })
27        .collect()
28}
29
30/// Checks that there are no duplicate discriminant values. If all variants are literals, return an `Err` so we can have
31/// more clear error messages. Otherwise, emit a static check that ensures no duplicates.
32fn check_no_alias<'a>(
33    enum_: &ItemEnum,
34    variants: impl Iterator<Item = (&'a Ident, &'a Discriminant, Span)> + Clone,
35) -> syn::Result<TokenStream> {
36    // If they're all literals, we can give better error messages by checking at proc macro time.
37    let mut values: HashSet<i128> = HashSet::new();
38    for (_, variant, span) in variants {
39        if let &Discriminant::Literal(value) = variant {
40            if !values.insert(value) {
41                return Err(Error::new(
42                    span,
43                    format!("discriminant value `{value}` assigned more than once"),
44                ));
45            }
46        } else {
47            let mut checking_enum = syn::ItemEnum {
48                ident: format_ident!("_Check{}", enum_.ident, span = Span::call_site()),
49                vis: Visibility::Inherited,
50                ..enum_.clone()
51            };
52            checking_enum.attrs.retain(|attr| {
53                matches!(
54                    attr.path().to_token_stream().to_string().as_str(),
55                    "repr" | "allow" | "warn" | "deny" | "forbid"
56                )
57            });
58            return Ok(quote!(
59                #[allow(dead_code)]
60                #checking_enum
61            ));
62        }
63    }
64    Ok(TokenStream::default())
65}
66
67fn emit_debug_impl<'a>(
68    ident: &Ident,
69    variants: impl Iterator<Item = &'a Ident> + Clone,
70    attrs: impl Iterator<Item = &'a Vec<Attribute>> + Clone,
71) -> TokenStream {
72    let attrs = attrs.map(|attrs| {
73        // Only allow "#[cfg(...)]" attributes
74        let iter = attrs.iter().filter(|attr| attr.path().is_ident("cfg"));
75        quote!(#(#iter)*)
76    });
77    quote!(impl ::core::fmt::Debug for #ident {
78        fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
79            #![allow(unreachable_patterns)]
80            let s = match *self {
81                #( #attrs Self::#variants => stringify!(#variants), )*
82                _ => {
83                    return fmt.debug_tuple(stringify!(#ident)).field(&self.0).finish();
84                }
85            };
86            fmt.pad(s)
87        }
88    })
89}
90
91fn path_matches_prelude_derive(
92    got_path: &syn::Path,
93    expected_path_after_std: &[&'static str],
94) -> bool {
95    let &[a, b] = expected_path_after_std else {
96        unimplemented!("checking against stdlib paths with != 2 parts");
97    };
98    let segments: Vec<&syn::PathSegment> = got_path.segments.iter().collect();
99    if segments
100        .iter()
101        .any(|segment| !matches!(segment.arguments, syn::PathArguments::None))
102    {
103        return false;
104    }
105    match &segments[..] {
106        // `core::fmt::Debug` or `some_crate::module::Name`
107        [maybe_core_or_std, maybe_a, maybe_b] => {
108            (maybe_core_or_std.ident == "core" || maybe_core_or_std.ident == "std")
109                && maybe_a.ident == a
110                && maybe_b.ident == b
111        }
112        // `fmt::Debug` or `module::Name`
113        [maybe_a, maybe_b] => {
114            maybe_a.ident == a && maybe_b.ident == b && got_path.leading_colon.is_none()
115        }
116        // `Debug` or `Name``
117        [maybe_b] => maybe_b.ident == b && got_path.leading_colon.is_none(),
118        _ => false,
119    }
120}
121
122fn open_enum_impl(
123    enum_: ItemEnum,
124    Config {
125        allow_alias,
126        repr_visibility,
127    }: Config,
128) -> Result<TokenStream, Error> {
129    // Does the enum define a `#[repr()]`?
130    let mut struct_attrs: Vec<TokenStream> = Vec::with_capacity(enum_.attrs.len() + 5);
131    struct_attrs.push(quote!(#[allow(clippy::exhaustive_structs)]));
132
133    if !enum_.generics.params.is_empty() {
134        return Err(Error::new(enum_.generics.span(), "enum cannot be generic"));
135    }
136    let mut variants = Vec::with_capacity(enum_.variants.len());
137    let mut last_field = Discriminant::Literal(-1);
138    for variant in &enum_.variants {
139        if !matches!(variant.fields, syn::Fields::Unit) {
140            return Err(Error::new(variant.span(), "enum cannot contain fields"));
141        }
142
143        let (value, value_span) = if let Some((_, discriminant)) = &variant.discriminant {
144            let span = discriminant.span();
145            (Discriminant::new(discriminant.clone())?, span)
146        } else {
147            last_field = last_field
148                .next_value()
149                .ok_or_else(|| Error::new(variant.span(), "enum discriminant overflowed"))?;
150            (last_field.clone(), variant.ident.span())
151        };
152        last_field = value.clone();
153        variants.push((&variant.ident, value, value_span, &variant.attrs))
154    }
155
156    let mut impl_attrs: Vec<TokenStream> = vec![quote!(#[allow(non_upper_case_globals)])];
157    let mut explicit_repr: Option<Repr> = None;
158
159    // To make `match` seamless, derive(PartialEq, Eq) if they aren't already.
160    let mut extra_derives = vec![quote!(::core::cmp::PartialEq), quote!(::core::cmp::Eq)];
161    let mut extra_attrs: Vec<TokenStream> = Vec::with_capacity(enum_.attrs.len());
162
163    let mut make_custom_debug_impl = false;
164    for attr in &enum_.attrs {
165        let mut include_in_struct = true;
166        // Turns out `is_ident` does a `to_string` every time
167        match attr.path().to_token_stream().to_string().as_str() {
168            "derive" => {
169                if let Ok(derive_paths) =
170                    attr.parse_args_with(Punctuated::<syn::Path, syn::Token![,]>::parse_terminated)
171                {
172                    for derive in &derive_paths {
173                        // These derives are treated specially
174                        const PARTIAL_EQ_PATH: &[&str] = &["cmp", "PartialEq"];
175                        const EQ_PATH: &[&str] = &["cmp", "Eq"];
176                        const DEBUG_PATH: &[&str] = &["fmt", "Debug"];
177
178                        if path_matches_prelude_derive(derive, PARTIAL_EQ_PATH)
179                            || path_matches_prelude_derive(derive, EQ_PATH)
180                        {
181                            // This derive is always included, exclude it.
182                            continue;
183                        }
184                        if path_matches_prelude_derive(derive, DEBUG_PATH) && !allow_alias {
185                            make_custom_debug_impl = true;
186                            // Don't include this derive since we're generating a special one.
187                            continue;
188                        }
189                        extra_derives.push(derive.to_token_stream());
190                    }
191                    include_in_struct = false;
192                }
193            }
194            // Copy linting attribute to the impl.
195            "allow" | "warn" | "deny" | "forbid" => impl_attrs.push(attr.to_token_stream()),
196            "repr" => {
197                assert!(explicit_repr.is_none(), "duplicate explicit repr");
198                explicit_repr = Some(attr.parse_args()?);
199                include_in_struct = false;
200            }
201            "non_exhaustive" => {
202                // technically it's exhaustive if the enum covers the full integer range
203                return Err(Error::new(attr.path().span(), "`non_exhaustive` cannot be applied to an open enum; it is already non-exhaustive"));
204            }
205            _ => {}
206        }
207        if include_in_struct {
208            extra_attrs.push(attr.to_token_stream());
209        }
210    }
211
212    // The proper repr to type-check against
213    let typecheck_repr: Repr = explicit_repr.unwrap_or(Repr::Isize);
214
215    // The actual representation of the value.
216    let inner_repr = match explicit_repr {
217        Some(explicit_repr) => {
218            // If there is an explicit repr, emit #[repr(transparent)].
219            struct_attrs.push(quote!(#[repr(transparent)]));
220            explicit_repr
221        }
222        None => {
223            // If there isn't an explicit repr, determine an appropriate sized integer that will fit.
224            // Interpret all discriminant expressions as isize.
225            repr::autodetect_inner_repr(variants.iter().map(|v| &v.1))
226        }
227    };
228
229    if !extra_derives.is_empty() {
230        struct_attrs.push(quote!(#[derive(#(#extra_derives),*)]));
231    }
232    struct_attrs.append(&mut extra_attrs);
233
234    let alias_check = if allow_alias {
235        TokenStream::default()
236    } else {
237        check_no_alias(&enum_, variants.iter().map(|(i, v, s, _)| (*i, v, *s)))?
238    };
239
240    let syn::ItemEnum { ident, vis, .. } = enum_;
241
242    // If we use the original ident's span in more than one location,
243    // rust-analyzer will duplicate `pub struct EnumName` items when hovering
244    // over the `enum EnumName`. By only using the input name's span once, we
245    // only get one expansion. This is in exchange for marginally worse errors.
246    let ident_nospan = {
247        let mut ident_nospan = ident.clone();
248        ident_nospan.set_span(Span::call_site());
249        ident_nospan
250    };
251
252    let debug_impl = if make_custom_debug_impl {
253        emit_debug_impl(
254            &ident_nospan,
255            variants.iter().map(|(i, _, _, _)| *i),
256            variants.iter().map(|(_, _, _, a)| *a),
257        )
258    } else {
259        TokenStream::default()
260    };
261
262    let fields = variants
263        .into_iter()
264        .map(|(name, value, value_span, attrs)| {
265            let mut value = value.into_token_stream();
266            value = set_token_stream_span(value, value_span);
267            let inner = if typecheck_repr == inner_repr {
268                value
269            } else {
270                quote!(::core::convert::identity::<#typecheck_repr>(#value) as #inner_repr)
271            };
272            quote!(
273                #(#attrs)*
274                pub const #name: #ident_nospan = #ident_nospan(#inner);
275            )
276        });
277
278    Ok(quote! {
279        #(#struct_attrs)*
280        #vis struct #ident(#repr_visibility #inner_repr);
281
282        #(#impl_attrs)*
283        impl #ident_nospan {
284            #(
285                #fields
286            )*
287        }
288        #debug_impl
289        #alias_check
290    })
291}
292
293#[proc_macro_attribute]
294pub fn open_enum(
295    attrs: proc_macro::TokenStream,
296    input: proc_macro::TokenStream,
297) -> proc_macro::TokenStream {
298    let enum_ = parse_macro_input!(input as syn::ItemEnum);
299    let config = parse_macro_input!(attrs as Config);
300    open_enum_impl(enum_, config)
301        .unwrap_or_else(Error::into_compile_error)
302        .into()
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_path_matches_stdlib_derive() {
311        const DEBUG_PATH: &[&str] = &["fmt", "Debug"];
312
313        for success_case in [
314            "::core::fmt::Debug",
315            "::std::fmt::Debug",
316            "core::fmt::Debug",
317            "std::fmt::Debug",
318            "fmt::Debug",
319            "Debug",
320        ] {
321            assert!(
322                path_matches_prelude_derive(&syn::parse_str(success_case).unwrap(), DEBUG_PATH),
323                "{success_case}"
324            );
325        }
326
327        for fail_case in [
328            "::fmt::Debug",
329            "::Debug",
330            "zerocopy::IntoBytes",
331            "::zerocopy::IntoBytes",
332            "PartialEq",
333            "core::cmp::Eq",
334        ] {
335            assert!(
336                !path_matches_prelude_derive(&syn::parse_str(fail_case).unwrap(), DEBUG_PATH),
337                "{fail_case}"
338            );
339        }
340    }
341}