From db7f0f09cafdfb3fa0e7d2c30b6008d5468135d4 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 15 Oct 2025 08:29:23 +0100 Subject: [PATCH 1/2] Allow user to set the crate path of num_enum Add a `#[num_enum(crate = ..)]` attribute, which lets the user set where the num_enum crate is located. --- .../custom_error_type_parsing.stderr | 6 +- .../compile_fail/variants_with_fields.stderr | 6 +- num_enum_derive/src/enum_attributes.rs | 72 ++++++++++++++++++- num_enum_derive/src/lib.rs | 23 +++--- num_enum_derive/src/parsing.rs | 60 ++++++++-------- 5 files changed, 118 insertions(+), 49 deletions(-) diff --git a/num_enum/tests/try_build/compile_fail/custom_error_type_parsing.stderr b/num_enum/tests/try_build/compile_fail/custom_error_type_parsing.stderr index fef9618..456050e 100644 --- a/num_enum/tests/try_build/compile_fail/custom_error_type_parsing.stderr +++ b/num_enum/tests/try_build/compile_fail/custom_error_type_parsing.stderr @@ -22,8 +22,8 @@ error: num_enum attribute must have at most one error_type 29 | #[num_enum(error_type(name = CustomError, constructor = CustomError::new), error_type(name = CustomError, constructor = CustomError::new))] | ^^^^^^^^^^ -error: At most one num_enum error_type attribute may be specified - --> tests/try_build/compile_fail/custom_error_type_parsing.rs:39:1 +error: num_enum attribute must have at most one error_type + --> tests/try_build/compile_fail/custom_error_type_parsing.rs:39:12 | 39 | #[num_enum(error_type(name = CustomError, constructor = CustomError::new))] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^ diff --git a/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr b/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr index 093cc90..a85d043 100644 --- a/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr +++ b/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr @@ -1,16 +1,16 @@ -error: `num_enum` only supports unit variants (with no associated data), but `Number::NonZero` was not a unit variant. +error: `:: num_enum` only supports unit variants (with no associated data), but `Number::NonZero` was not a unit variant. --> $DIR/variants_with_fields.rs:7:5 | 7 | NonZero(u8), | ^^^^^^^^^^^ -error: `num_enum` only supports unit variants (with no associated data), but `Colour::Red` was not a unit variant. +error: `:: num_enum` only supports unit variants (with no associated data), but `Colour::Red` was not a unit variant. --> $DIR/variants_with_fields.rs:13:5 | 13 | Red { intensity: u8 }, | ^^^^^^^^^^^^^^^^^^^^^ -error: `num_enum` only supports unit variants (with no associated data), but `Meaningless::Beep` was not a unit variant. +error: `:: num_enum` only supports unit variants (with no associated data), but `Meaningless::Beep` was not a unit variant. --> $DIR/variants_with_fields.rs:19:5 | 19 | Beep(), diff --git a/num_enum_derive/src/enum_attributes.rs b/num_enum_derive/src/enum_attributes.rs index fd19a31..5bf5515 100644 --- a/num_enum_derive/src/enum_attributes.rs +++ b/num_enum_derive/src/enum_attributes.rs @@ -13,20 +13,50 @@ mod kw { // Example: error_type(name = Foo, constructor = Foo::new) #[cfg_attr(test, derive(Debug))] +#[derive(Default)] pub(crate) struct Attributes { pub(crate) error_type: Option, + pub(crate) crate_path: Option, } // Example: error_type(name = Foo, constructor = Foo::new) #[cfg_attr(test, derive(Debug))] pub(crate) enum AttributeItem { ErrorType(ErrorTypeAttribute), + CratePath(CrateAttribute), +} + +impl Attributes { + pub(crate) fn exclusive_union(&mut self, other: Self) -> Result<()> { + if self.crate_path.is_some() { + if let Some(other) = &other.crate_path { + return Err(Error::new( + other.span, + "num_enum attribute must have at most one crate", + )); + } + } else { + self.crate_path = other.crate_path; + } + if self.error_type.is_some() { + if let Some(other) = &other.error_type { + return Err(Error::new( + other.span, + "num_enum attribute must have at most one error_type", + )); + } + } else { + self.error_type = other.error_type; + } + Ok(()) + } } impl Parse for Attributes { fn parse(input: ParseStream<'_>) -> Result { let attribute_items = input.parse_terminated(AttributeItem::parse, syn::Token![,])?; let mut maybe_error_type = None; + let mut maybe_krate_path = None; for attribute_item in &attribute_items { match attribute_item { AttributeItem::ErrorType(error_type) => { @@ -38,10 +68,20 @@ impl Parse for Attributes { } maybe_error_type = Some(error_type.clone()); } + AttributeItem::CratePath(krate_path) => { + if maybe_krate_path.is_some() { + return Err(Error::new( + krate_path.span, + "num_enum attribute must have at most one crate", + )); + } + maybe_krate_path = Some(krate_path.clone()); + } } } Ok(Self { error_type: maybe_error_type, + crate_path: maybe_krate_path, }) } } @@ -51,6 +91,8 @@ impl Parse for AttributeItem { let lookahead = input.lookahead1(); if lookahead.peek(kw::error_type) { input.parse().map(Self::ErrorType) + } else if lookahead.peek(syn::token::Crate) { + input.parse().map(Self::CratePath) } else { Err(lookahead.error()) } @@ -168,6 +210,24 @@ impl Parse for ErrorTypeConstructorAttribute { } } +#[derive(Clone)] +#[cfg_attr(test, derive(Debug))] +pub(crate) struct CrateAttribute { + pub(crate) path: syn::Path, + + span: Span, +} + +impl Parse for CrateAttribute { + fn parse(input: ParseStream) -> Result { + let span = input.span(); + let _: syn::token::Crate = input.parse()?; + let _: syn::token::Eq = input.parse()?; + let path = syn::Path::parse_mod_style(input)?; + Ok(Self { path, span }) + } +} + #[cfg(test)] mod test { use crate::enum_attributes::Attributes; @@ -178,11 +238,15 @@ mod test { fn parse_num_enum_attr() { let expected_name: Path = parse_quote! { Foo }; let expected_constructor: Path = parse_quote! { ::foo::Foo::::new }; + let expected_krate: Path = parse_quote! { ::num_enum }; - let attributes: Attributes = - syn::parse_str("error_type(name = Foo, constructor = ::foo::Foo::::new)").unwrap(); + let attributes: Attributes = syn::parse_str( + "error_type(name = Foo, constructor = ::foo::Foo::::new), crate = ::num_enum", + ) + .unwrap(); assert!(attributes.error_type.is_some()); let error_type = attributes.error_type.unwrap(); + let krate_path = attributes.crate_path.unwrap(); assert_eq!( error_type.name.path.to_token_stream().to_string(), expected_name.to_token_stream().to_string() @@ -191,6 +255,10 @@ mod test { error_type.constructor.path.to_token_stream().to_string(), expected_constructor.to_token_stream().to_string() ); + assert_eq!( + krate_path.path.to_token_stream().to_string(), + expected_krate.to_token_stream().to_string() + ); } #[test] diff --git a/num_enum_derive/src/lib.rs b/num_enum_derive/src/lib.rs index 4fc4927..075ca56 100644 --- a/num_enum_derive/src/lib.rs +++ b/num_enum_derive/src/lib.rs @@ -10,7 +10,7 @@ use syn::{parse_macro_input, Expr, Ident}; mod enum_attributes; mod parsing; -use parsing::{get_crate_name, EnumInfo}; +use parsing::{get_crate_path, EnumInfo}; mod utils; mod variant_attributes; @@ -90,7 +90,7 @@ pub fn derive_into_primitive(input: TokenStream) -> TokenStream { #[proc_macro_derive(FromPrimitive, attributes(num_enum, default, catch_all))] pub fn derive_from_primitive(input: TokenStream) -> TokenStream { let enum_info: EnumInfo = parse_macro_input!(input); - let krate = Ident::new(&get_crate_name(), Span::call_site()); + let krate = get_crate_path(enum_info.crate_path.clone()); let is_naturally_exhaustive = enum_info.is_naturally_exhaustive(); let catch_all_body = match is_naturally_exhaustive { @@ -124,7 +124,7 @@ pub fn derive_from_primitive(input: TokenStream) -> TokenStream { debug_assert_eq!(variant_idents.len(), variant_expressions.len()); TokenStream::from(quote! { - impl ::#krate::FromPrimitive for #name { + impl #krate::FromPrimitive for #name { type Primitive = #repr; fn from_primitive(number: Self::Primitive) -> Self { @@ -153,12 +153,12 @@ pub fn derive_from_primitive(input: TokenStream) -> TokenStream { fn from ( number: #repr, ) -> Self { - ::#krate::FromPrimitive::from_primitive(number) + #krate::FromPrimitive::from_primitive(number) } } #[doc(hidden)] - impl ::#krate::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for #name {} + impl #krate::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for #name {} }) } @@ -190,8 +190,7 @@ pub fn derive_from_primitive(input: TokenStream) -> TokenStream { #[proc_macro_derive(TryFromPrimitive, attributes(num_enum))] pub fn derive_try_from_primitive(input: TokenStream) -> TokenStream { let enum_info: EnumInfo = parse_macro_input!(input); - let krate = Ident::new(&get_crate_name(), Span::call_site()); - + let krate = get_crate_path(enum_info.crate_path.clone()); let EnumInfo { ref name, ref repr, @@ -209,7 +208,7 @@ pub fn derive_try_from_primitive(input: TokenStream) -> TokenStream { let error_constructor = &error_type_info.constructor; TokenStream::from(quote! { - impl ::#krate::TryFromPrimitive for #name { + impl #krate::TryFromPrimitive for #name { type Primitive = #repr; type Error = #error_type; @@ -251,12 +250,12 @@ pub fn derive_try_from_primitive(input: TokenStream) -> TokenStream { number: #repr, ) -> ::core::result::Result { - ::#krate::TryFromPrimitive::try_from_primitive(number) + #krate::TryFromPrimitive::try_from_primitive(number) } } #[doc(hidden)] - impl ::#krate::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for #name {} + impl #krate::CannotDeriveBothFromPrimitiveAndTryFromPrimitive for #name {} }) } @@ -302,14 +301,14 @@ pub fn derive_try_from_primitive(input: TokenStream) -> TokenStream { #[proc_macro_derive(UnsafeFromPrimitive, attributes(num_enum))] pub fn derive_unsafe_from_primitive(stream: TokenStream) -> TokenStream { let enum_info = parse_macro_input!(stream as EnumInfo); - let krate = Ident::new(&get_crate_name(), Span::call_site()); + let krate = get_crate_path(enum_info.crate_path); let EnumInfo { ref name, ref repr, .. } = enum_info; TokenStream::from(quote! { - impl ::#krate::UnsafeFromPrimitive for #name { + impl #krate::UnsafeFromPrimitive for #name { type Primitive = #repr; unsafe fn unchecked_transmute_from(number: Self::Primitive) -> Self { diff --git a/num_enum_derive/src/parsing.rs b/num_enum_derive/src/parsing.rs index 1a24b0c..3fe1edc 100644 --- a/num_enum_derive/src/parsing.rs +++ b/num_enum_derive/src/parsing.rs @@ -13,6 +13,7 @@ use syn::{ pub(crate) struct EnumInfo { pub(crate) name: Ident, pub(crate) repr: Ident, + pub(crate) crate_path: Option, pub(crate) variants: Vec, pub(crate) error_type_info: ErrorType, } @@ -89,9 +90,9 @@ impl EnumInfo { fn parse_attrs>( attrs: Attrs, - ) -> Result<(Ident, Option)> { + ) -> Result<(Ident, crate::enum_attributes::Attributes)> { let mut maybe_repr = None; - let mut maybe_error_type = None; + let mut attributes = crate::enum_attributes::Attributes::default(); for attr in attrs { if let Meta::List(meta_list) = &attr.meta { if let Some(ident) = meta_list.path.get_ident() { @@ -114,14 +115,9 @@ impl EnumInfo { maybe_repr = Some(repr_ident); } } else if ident == "num_enum" { - let attributes = + let new_attributes = attr.parse_args_with(crate::enum_attributes::Attributes::parse)?; - if let Some(error_type) = attributes.error_type { - if maybe_error_type.is_some() { - die!(attr => "At most one num_enum error_type attribute may be specified"); - } - maybe_error_type = Some(error_type.into()); - } + attributes.exclusive_union(new_attributes)?; } } } @@ -129,7 +125,7 @@ impl EnumInfo { if maybe_repr.is_none() { die!("Missing `#[repr({Integer})]` attribute"); } - Ok((maybe_repr.unwrap(), maybe_error_type)) + Ok((maybe_repr.unwrap(), attributes)) } } @@ -144,7 +140,8 @@ impl Parse for EnumInfo { Data::Struct(data) => die!(data.struct_token => "Expected enum but found struct"), }; - let (repr, maybe_error_type) = Self::parse_attrs(input.attrs.into_iter())?; + let (repr, attributes) = Self::parse_attrs(input.attrs.into_iter())?; + let crate_path = attributes.crate_path.clone().map(|k| k.path); let mut variants: Vec = vec![]; let mut has_default_variant: bool = false; @@ -266,7 +263,7 @@ impl Parse for EnumInfo { if !is_catch_all { match &variant.fields { Fields::Named(_) | Fields::Unnamed(_) => { - die!(variant => format!("`{}` only supports unit variants (with no associated data), but `{}::{}` was not a unit variant.", get_crate_name(), name, ident)); + die!(variant => format!("`{}` only supports unit variants (with no associated data), but `{}::{}` was not a unit variant.", get_crate_path(crate_path).to_token_stream(), name, ident)); } Fields::Unit => {} } @@ -385,14 +382,14 @@ impl Parse for EnumInfo { } } - let error_type_info = maybe_error_type.unwrap_or_else(|| { - let crate_name = Ident::new(&get_crate_name(), Span::call_site()); + let error_type_info = attributes.error_type.map(Into::into).unwrap_or_else(|| { + let crate_name = get_crate_path(crate_path.clone()); ErrorType { name: parse_quote! { - ::#crate_name::TryFromPrimitiveError + #crate_name::TryFromPrimitiveError }, constructor: parse_quote! { - ::#crate_name::TryFromPrimitiveError::::new + #crate_name::TryFromPrimitiveError::::new }, } }); @@ -401,6 +398,7 @@ impl Parse for EnumInfo { name, repr, variants, + crate_path, error_type_info, } }) @@ -530,18 +528,22 @@ impl From for ErrorType { } #[cfg(feature = "proc-macro-crate")] -pub(crate) fn get_crate_name() -> String { - let found_crate = proc_macro_crate::crate_name("num_enum").unwrap_or_else(|err| { - eprintln!("Warning: {}\n => defaulting to `num_enum`", err,); - proc_macro_crate::FoundCrate::Itself - }); - - match found_crate { - proc_macro_crate::FoundCrate::Itself => String::from("num_enum"), - proc_macro_crate::FoundCrate::Name(name) => name, - } +pub(crate) fn get_crate_path(path: Option) -> syn::Path { + path.unwrap_or_else(|| { + let found_crate = proc_macro_crate::crate_name("num_enum").unwrap_or_else(|err| { + eprintln!("Warning: {}\n => defaulting to `num_enum`", err,); + proc_macro_crate::FoundCrate::Itself + }); + + match found_crate { + proc_macro_crate::FoundCrate::Itself => parse_quote!(::num_enum), + proc_macro_crate::FoundCrate::Name(name) => { + let krate = format_ident!("{}", name); + parse_quote!( ::#krate ) + } + } + }) } - // Don't depend on proc-macro-crate in no_std environments because it causes an awkward dependency // on serde with std. // @@ -549,6 +551,6 @@ pub(crate) fn get_crate_name() -> String { // // See https://github.com/illicitonion/num_enum/issues/18 #[cfg(not(feature = "proc-macro-crate"))] -pub(crate) fn get_crate_name() -> String { - String::from("num_enum") +pub(crate) fn get_crate_path(path: Option) -> syn::Path { + path.unwrap_or_else(|| parse_quote!(::num_enum)) } From ae0cc9e84a25836afa0771de23833710843de027 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Sun, 19 Oct 2025 17:31:40 +0100 Subject: [PATCH 2/2] Format crate name more clearly --- .../compile_fail/variants_with_fields.stderr | 6 +++--- num_enum_derive/Cargo.toml | 2 +- num_enum_derive/src/parsing.rs | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr b/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr index a85d043..ce72b3c 100644 --- a/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr +++ b/num_enum/tests/try_build/compile_fail/variants_with_fields.stderr @@ -1,16 +1,16 @@ -error: `:: num_enum` only supports unit variants (with no associated data), but `Number::NonZero` was not a unit variant. +error: `::num_enum` only supports unit variants (with no associated data), but `Number::NonZero` was not a unit variant. --> $DIR/variants_with_fields.rs:7:5 | 7 | NonZero(u8), | ^^^^^^^^^^^ -error: `:: num_enum` only supports unit variants (with no associated data), but `Colour::Red` was not a unit variant. +error: `::num_enum` only supports unit variants (with no associated data), but `Colour::Red` was not a unit variant. --> $DIR/variants_with_fields.rs:13:5 | 13 | Red { intensity: u8 }, | ^^^^^^^^^^^^^^^^^^^^^ -error: `:: num_enum` only supports unit variants (with no associated data), but `Meaningless::Beep` was not a unit variant. +error: `::num_enum` only supports unit variants (with no associated data), but `Meaningless::Beep` was not a unit variant. --> $DIR/variants_with_fields.rs:19:5 | 19 | Beep(), diff --git a/num_enum_derive/Cargo.toml b/num_enum_derive/Cargo.toml index 4645fb0..fe17af2 100644 --- a/num_enum_derive/Cargo.toml +++ b/num_enum_derive/Cargo.toml @@ -35,7 +35,7 @@ features = ["external_doc"] proc-macro2 = "1.0.60" proc-macro-crate = { version = ">= 1, <= 3", optional = true } quote = "1" -syn = { version = "2", features = ["parsing"] } +syn = { version = "2", features = ["derive", "extra-traits", "parsing"] } [dev-dependencies] syn = { version = "2", features = ["extra-traits", "parsing"] } diff --git a/num_enum_derive/src/parsing.rs b/num_enum_derive/src/parsing.rs index 3fe1edc..2a4826f 100644 --- a/num_enum_derive/src/parsing.rs +++ b/num_enum_derive/src/parsing.rs @@ -263,7 +263,7 @@ impl Parse for EnumInfo { if !is_catch_all { match &variant.fields { Fields::Named(_) | Fields::Unnamed(_) => { - die!(variant => format!("`{}` only supports unit variants (with no associated data), but `{}::{}` was not a unit variant.", get_crate_path(crate_path).to_token_stream(), name, ident)); + die!(variant => format!("`{}` only supports unit variants (with no associated data), but `{}::{}` was not a unit variant.", crate_path_as_string(&get_crate_path(crate_path))?, name, ident)); } Fields::Unit => {} } @@ -554,3 +554,17 @@ pub(crate) fn get_crate_path(path: Option) -> syn::Path { pub(crate) fn get_crate_path(path: Option) -> syn::Path { path.unwrap_or_else(|| parse_quote!(::num_enum)) } + +fn crate_path_as_string(path: &syn::Path) -> Result { + let mut string = String::new(); + for (index, part) in path.segments.iter().enumerate() { + if index != 0 || path.leading_colon.is_some() { + string.push_str("::"); + } + if !part.arguments.is_none() { + die!(part => format!("Crate paths should never contain arguments")); + } + string.push_str(&format!("{}", part.ident)); + } + Ok(string) +}