From 2272b56455732bd682c33de21c66d77c71c10522 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 3 Jan 2024 11:31:02 -0500 Subject: [PATCH] Add support for non-exhaustive enums (#1593) UDL types can now have the `[NonExhaustive]` attribute. If present, we add a default branch arm to make things compile correctly with non-exhaustive enums. I didn't want to make this the default behavior, since in the normal case we want missing variants to result in compile errors. In theory, this also adds the `non_exhaustive` attribute for proc-macros. However, there's no way to test this because `non_exhaustive` only matters for types defined in remote crates and there's currently no proc-macro support for those. One slightly silly part of this is the flow of metadata. It doesn't make much sense as a metadata field, since it doesn't affect bindings generation at all. However, it needs to be because of the way that data flows from `uniffi_udl` -> `uniffi_meta` -> `uniffi_bindgen` -> `uniffi_macros`. --- docs/manual/src/udl/enumerations.md | 23 ++++ fixtures/ext-types/external-crate/src/lib.rs | 6 ++ fixtures/ext-types/lib/src/ext-types-lib.udl | 7 ++ fixtures/ext-types/lib/src/lib.rs | 6 +- fixtures/metadata/src/tests.rs | 5 + uniffi_bindgen/src/interface/enum_.rs | 7 ++ .../src/scaffolding/templates/EnumTemplate.rs | 6 +- .../scaffolding/templates/ErrorTemplate.rs | 3 + uniffi_macros/src/enum_.rs | 100 ++++++++++++++---- uniffi_macros/src/error.rs | 75 +++++++++---- uniffi_macros/src/lib.rs | 14 ++- uniffi_macros/src/util.rs | 1 + uniffi_meta/src/lib.rs | 1 + uniffi_meta/src/reader.rs | 1 + uniffi_udl/src/attributes.rs | 47 +++++++- uniffi_udl/src/converters/enum_.rs | 14 ++- 16 files changed, 266 insertions(+), 50 deletions(-) diff --git a/docs/manual/src/udl/enumerations.md b/docs/manual/src/udl/enumerations.md index f90489ce59..42097d84be 100644 --- a/docs/manual/src/udl/enumerations.md +++ b/docs/manual/src/udl/enumerations.md @@ -18,6 +18,8 @@ enum Animal { }; ``` +## Enums with fields + Enumerations with associated data require a different syntax, due to the limitations of using WebIDL as the basis for UniFFI's interface language. An enum like this in Rust: @@ -40,3 +42,24 @@ interface IpAddr { ``` Only enums with named fields are supported by this syntax. + +## Remote, non-exhaustive enums + +One corner case is an enum that's: + - Defined in another crate. + - Has the [non_exhaustive` attribute](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute). + +In this case, UniFFI needs to generate a default arm when matching against the enum variants, or else a compile error will be generated. +Use the `[NonExhaustive]` attribute to handle this case: + +```idl +[Enum] +[NonExhaustive] +interface Message { + Send(u32 from, u32 to, string contents); + Quit(); +}; +``` + +**Note:** since UniFFI generates a default arm, if you leave out a variant, or if the upstream crate adds a new variant, this won't be caught at compile time. +Any attempt to pass that variant across the FFI will result in a panic. diff --git a/fixtures/ext-types/external-crate/src/lib.rs b/fixtures/ext-types/external-crate/src/lib.rs index 0a9386608f..f27df35da8 100644 --- a/fixtures/ext-types/external-crate/src/lib.rs +++ b/fixtures/ext-types/external-crate/src/lib.rs @@ -8,6 +8,12 @@ pub struct ExternalCrateInterface { pub sval: String, } +#[non_exhaustive] +pub enum ExternalCrateNonExhaustiveEnum { + One, + Two, +} + // This type is referenced as an "external" type as an interface. impl ExternalCrateInterface { pub fn new(sval: String) -> Self { diff --git a/fixtures/ext-types/lib/src/ext-types-lib.udl b/fixtures/ext-types/lib/src/ext-types-lib.udl index caa25a7ce8..9c2abc6574 100644 --- a/fixtures/ext-types/lib/src/ext-types-lib.udl +++ b/fixtures/ext-types/lib/src/ext-types-lib.udl @@ -66,6 +66,12 @@ interface ExternalCrateInterface { string value(); }; +[NonExhaustive] +enum ExternalCrateNonExhaustiveEnum { + "One", + "Two", +}; + // And a new type here to tie them all together. dictionary CombinedType { UniffiOneEnum uoe; @@ -86,4 +92,5 @@ dictionary CombinedType { Handle? maybe_handle; ExternalCrateDictionary ecd; + ExternalCrateNonExhaustiveEnum ecnee; }; diff --git a/fixtures/ext-types/lib/src/lib.rs b/fixtures/ext-types/lib/src/lib.rs index 666709f865..7241858444 100644 --- a/fixtures/ext-types/lib/src/lib.rs +++ b/fixtures/ext-types/lib/src/lib.rs @@ -1,5 +1,7 @@ use custom_types::Handle; -use ext_types_external_crate::{ExternalCrateDictionary, ExternalCrateInterface}; +use ext_types_external_crate::{ + ExternalCrateDictionary, ExternalCrateInterface, ExternalCrateNonExhaustiveEnum, +}; use ext_types_guid::Guid; use std::sync::Arc; use uniffi_one::{ @@ -28,6 +30,7 @@ pub struct CombinedType { pub maybe_handle: Option, pub ecd: ExternalCrateDictionary, + pub ecnee: ExternalCrateNonExhaustiveEnum, } fn get_combined_type(existing: Option) -> CombinedType { @@ -59,6 +62,7 @@ fn get_combined_type(existing: Option) -> CombinedType { maybe_handle: Some(Handle(4)), ecd: ExternalCrateDictionary { sval: "ecd".into() }, + ecnee: ExternalCrateNonExhaustiveEnum::One, }) } diff --git a/fixtures/metadata/src/tests.rs b/fixtures/metadata/src/tests.rs index f4d8ae244f..90111f6a32 100644 --- a/fixtures/metadata/src/tests.rs +++ b/fixtures/metadata/src/tests.rs @@ -221,6 +221,7 @@ mod test_metadata { docstring: None, }, ], + non_exhaustive: false, docstring: None, }, ); @@ -266,6 +267,7 @@ mod test_metadata { docstring: None, }, ], + non_exhaustive: false, docstring: None, }, ); @@ -298,6 +300,7 @@ mod test_metadata { docstring: None, }, ], + non_exhaustive: false, docstring: None, }, ); @@ -325,6 +328,7 @@ mod test_metadata { docstring: None, }, ], + non_exhaustive: false, docstring: None, }, is_flat: true, @@ -373,6 +377,7 @@ mod test_metadata { docstring: None, }, ], + non_exhaustive: false, docstring: None, }, is_flat: false, diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index 61961fbdbf..ddccc4fbd4 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -189,6 +189,7 @@ pub struct Enum { // * For an Enum not used as an error but which has no variants with data, `flat` will be // false when generating the scaffolding but `true` when generating bindings. pub(super) flat: bool, + pub(super) non_exhaustive: bool, #[checksum_ignore] pub(super) docstring: Option, } @@ -233,6 +234,10 @@ impl Enum { self.flat } + pub fn is_non_exhaustive(&self) -> bool { + self.non_exhaustive + } + pub fn iter_types(&self) -> TypeIterator<'_> { Box::new(self.variants.iter().flat_map(Variant::iter_types)) } @@ -257,6 +262,7 @@ impl Enum { .map(TryInto::try_into) .collect::>()?, flat, + non_exhaustive: meta.non_exhaustive, docstring: meta.docstring.clone(), }) } @@ -635,6 +641,7 @@ mod test { name: "test".to_string(), variants: vec![], flat: false, + non_exhaustive: false, docstring: None, }; diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index 6b9f96f224..b09ad18fe8 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -7,7 +7,11 @@ // public so other crates can refer to it via an `[External='crate'] typedef` #} -#[::uniffi::derive_enum_for_udl] +#[::uniffi::derive_enum_for_udl( + {%- if e.is_non_exhaustive() -%} + non_exhaustive, + {%- endif %} +)] enum r#{{ e.name() }} { {%- for variant in e.variants() %} r#{{ variant.name() }} { diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 94538ecaa8..7adbcd4b3c 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -14,6 +14,9 @@ with_try_read, {%- endif %} {%- endif %} + {%- if e.is_non_exhaustive() -%} + non_exhaustive, + {%- endif %} )] enum r#{{ e.name() }} { {%- for variant in e.variants() %} diff --git a/uniffi_macros/src/enum_.rs b/uniffi_macros/src/enum_.rs index 7f17109793..4f040e1dc6 100644 --- a/uniffi_macros/src/enum_.rs +++ b/uniffi_macros/src/enum_.rs @@ -1,13 +1,22 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use syn::{Data, DataEnum, DeriveInput, Expr, Field, Index, Lit, Variant}; +use syn::{ + parse::{Parse, ParseStream}, + Data, DataEnum, DeriveInput, Expr, Field, Index, Lit, Variant, +}; use crate::util::{ - create_metadata_items, derive_all_ffi_traits, extract_docstring, ident_to_string, mod_path, - tagged_impl_header, try_metadata_value_from_usize, try_read_field, + create_metadata_items, derive_all_ffi_traits, either_attribute_arg, extract_docstring, + ident_to_string, kw, mod_path, parse_comma_separated, tagged_impl_header, + try_metadata_value_from_usize, try_read_field, AttributeSliceExt, UniffiAttributeArgs, }; -pub fn expand_enum(input: DeriveInput, udl_mode: bool) -> syn::Result { +pub fn expand_enum( + input: DeriveInput, + // Attributes from #[derive_error_for_udl()], if we are in udl mode + attr_from_udl_mode: Option, + udl_mode: bool, +) -> syn::Result { let enum_ = match input.data { Data::Enum(e) => e, _ => { @@ -19,10 +28,14 @@ pub fn expand_enum(input: DeriveInput, udl_mode: bool) -> syn::Result TokenStream { enum_or_error_ffi_converter_impl( ident, enum_, udl_mode, + attr, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } @@ -49,11 +64,13 @@ pub(crate) fn rich_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, + attr: &EnumAttr, ) -> TokenStream { enum_or_error_ffi_converter_impl( ident, enum_, udl_mode, + attr, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } @@ -62,6 +79,7 @@ fn enum_or_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, + attr: &EnumAttr, metadata_type_code: TokenStream, ) -> TokenStream { let name = ident_to_string(ident); @@ -71,19 +89,29 @@ fn enum_or_error_ffi_converter_impl( Ok(p) => p, Err(e) => return e.into_compile_error(), }; - let write_match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { - let v_ident = &v.ident; - let fields = v.fields.iter().map(|f| &f.ident); - let idx = Index::from(i + 1); - let write_fields = v.fields.iter().map(write_field); + let mut write_match_arms: Vec<_> = enum_ + .variants + .iter() + .enumerate() + .map(|(i, v)| { + let v_ident = &v.ident; + let fields = v.fields.iter().map(|f| &f.ident); + let idx = Index::from(i + 1); + let write_fields = v.fields.iter().map(write_field); - quote! { - Self::#v_ident { #(#fields),* } => { - ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); - #(#write_fields)* + quote! { + Self::#v_ident { #(#fields),* } => { + ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); + #(#write_fields)* + } } - } - }); + }) + .collect(); + if attr.non_exhaustive.is_some() { + write_match_arms.push(quote! { + _ => panic!("Unexpected variant in non-exhaustive enum"), + }) + } let write_impl = quote! { match obj { #(#write_match_arms)* } }; @@ -142,9 +170,11 @@ pub(crate) fn enum_meta_static_var( ident: &Ident, docstring: String, enum_: &DataEnum, + attr: &EnumAttr, ) -> syn::Result { let name = ident_to_string(ident); let module_path = mod_path()?; + let non_exhaustive = attr.non_exhaustive.is_some(); let mut metadata_expr = quote! { ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::ENUM) @@ -152,7 +182,10 @@ pub(crate) fn enum_meta_static_var( .concat_str(#name) }; metadata_expr.extend(variant_metadata(enum_)?); - metadata_expr.extend(quote! { .concat_long_str(#docstring) }); + metadata_expr.extend(quote! { + .concat_bool(#non_exhaustive) + .concat_long_str(#docstring) + }); Ok(create_metadata_items("enum", &name, metadata_expr, None)) } @@ -234,3 +267,34 @@ pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { ) .collect() } + +#[derive(Default)] +pub struct EnumAttr { + pub non_exhaustive: Option, +} + +// So ErrorAttr can be used with `parse_macro_input!` +impl Parse for EnumAttr { + fn parse(input: ParseStream<'_>) -> syn::Result { + parse_comma_separated(input) + } +} + +impl UniffiAttributeArgs for EnumAttr { + fn parse_one(input: ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::non_exhaustive) { + Ok(Self { + non_exhaustive: input.parse()?, + }) + } else { + Err(lookahead.error()) + } + } + + fn merge(self, other: Self) -> syn::Result { + Ok(Self { + non_exhaustive: either_attribute_arg(self.non_exhaustive, other.non_exhaustive)?, + }) + } +} diff --git a/uniffi_macros/src/error.rs b/uniffi_macros/src/error.rs index 601281b8fc..7e2db4c6eb 100644 --- a/uniffi_macros/src/error.rs +++ b/uniffi_macros/src/error.rs @@ -6,7 +6,7 @@ use syn::{ }; use crate::{ - enum_::{rich_error_ffi_converter_impl, variant_metadata}, + enum_::{rich_error_ffi_converter_impl, variant_metadata, EnumAttr}, util::{ chain, create_metadata_items, derive_ffi_traits, either_attribute_arg, extract_docstring, ident_to_string, kw, mod_path, parse_comma_separated, tagged_impl_header, @@ -35,9 +35,9 @@ pub fn expand_error( if let Some(attr_from_udl_mode) = attr_from_udl_mode { attr = attr.merge(attr_from_udl_mode)?; } - let ffi_converter_impl = error_ffi_converter_impl(ident, &enum_, &attr, udl_mode); + let ffi_converter_impl = error_ffi_converter_impl(ident, &enum_, &attr, udl_mode)?; let meta_static_var = (!udl_mode).then(|| { - error_meta_static_var(ident, docstring, &enum_, attr.flat.is_some()) + error_meta_static_var(ident, docstring, &enum_, &attr) .unwrap_or_else(syn::Error::into_compile_error) }); @@ -68,12 +68,12 @@ fn error_ffi_converter_impl( enum_: &DataEnum, attr: &ErrorAttr, udl_mode: bool, -) -> TokenStream { - if attr.flat.is_some() { - flat_error_ffi_converter_impl(ident, enum_, udl_mode, attr.with_try_read.is_some()) +) -> syn::Result { + Ok(if attr.flat.is_some() { + flat_error_ffi_converter_impl(ident, enum_, udl_mode, attr) } else { - rich_error_ffi_converter_impl(ident, enum_, udl_mode) - } + rich_error_ffi_converter_impl(ident, enum_, udl_mode, &attr.clone().try_into()?) + }) } // FfiConverters for "flat errors" @@ -84,7 +84,7 @@ fn flat_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - implement_lift: bool, + attr: &ErrorAttr, ) -> TokenStream { let name = ident_to_string(ident); let lower_impl_spec = tagged_impl_header("Lower", ident, udl_mode); @@ -96,7 +96,7 @@ fn flat_error_ffi_converter_impl( }; let lower_impl = { - let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { + let mut match_arms: Vec<_> = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let idx = Index::from(i + 1); @@ -106,7 +106,12 @@ fn flat_error_ffi_converter_impl( <::std::string::String as ::uniffi::Lower>::write(error_msg, buf); } } - }); + }).collect(); + if attr.non_exhaustive.is_some() { + match_arms.push(quote! { + _ => panic!("Unexpected variant in non-exhaustive enum"), + }) + } quote! { #[automatically_derived] @@ -129,7 +134,7 @@ fn flat_error_ffi_converter_impl( } }; - let lift_impl = if implement_lift { + let lift_impl = if attr.with_try_read.is_some() { let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let idx = Index::from(i + 1); @@ -195,10 +200,12 @@ pub(crate) fn error_meta_static_var( ident: &Ident, docstring: String, enum_: &DataEnum, - flat: bool, + attr: &ErrorAttr, ) -> syn::Result { let name = ident_to_string(ident); let module_path = mod_path()?; + let flat = attr.flat.is_some(); + let non_exhaustive = attr.non_exhaustive.is_some(); let mut metadata_expr = quote! { ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::ERROR) // first our is-flat flag @@ -212,7 +219,10 @@ pub(crate) fn error_meta_static_var( } else { metadata_expr.extend(variant_metadata(enum_)?); } - metadata_expr.extend(quote! { .concat_long_str(#docstring) }); + metadata_expr.extend(quote! { + .concat_bool(#non_exhaustive) + .concat_long_str(#docstring) + }); Ok(create_metadata_items("error", &name, metadata_expr, None)) } @@ -231,10 +241,11 @@ pub fn flat_error_variant_metadata(enum_: &DataEnum) -> syn::Result, - with_try_read: Option, + pub flat: Option, + pub with_try_read: Option, + pub non_exhaustive: Option, } impl UniffiAttributeArgs for ErrorAttr { @@ -250,8 +261,13 @@ impl UniffiAttributeArgs for ErrorAttr { with_try_read: input.parse()?, ..Self::default() }) + } else if lookahead.peek(kw::non_exhaustive) { + Ok(Self { + non_exhaustive: input.parse()?, + ..Self::default() + }) } else if lookahead.peek(kw::handle_unknown_callback_error) { - // Not used anymore, but still lallowed + // Not used anymore, but still allowed Ok(Self::default()) } else { Err(lookahead.error()) @@ -262,6 +278,7 @@ impl UniffiAttributeArgs for ErrorAttr { Ok(Self { flat: either_attribute_arg(self.flat, other.flat)?, with_try_read: either_attribute_arg(self.with_try_read, other.with_try_read)?, + non_exhaustive: either_attribute_arg(self.non_exhaustive, other.non_exhaustive)?, }) } } @@ -272,3 +289,25 @@ impl Parse for ErrorAttr { parse_comma_separated(input) } } + +impl TryFrom for EnumAttr { + type Error = syn::Error; + + fn try_from(error_attr: ErrorAttr) -> Result { + if error_attr.flat.is_some() { + Err(syn::Error::new( + Span::call_site(), + "flat attribute not valid for rich enum errors", + )) + } else if error_attr.with_try_read.is_some() { + Err(syn::Error::new( + Span::call_site(), + "with_try_read attribute not valid for rich enum errors", + )) + } else { + Ok(EnumAttr { + non_exhaustive: error_attr.non_exhaustive, + }) + } + } +} diff --git a/uniffi_macros/src/lib.rs b/uniffi_macros/src/lib.rs index bf91ba7c77..3a8a20dd86 100644 --- a/uniffi_macros/src/lib.rs +++ b/uniffi_macros/src/lib.rs @@ -115,7 +115,7 @@ pub fn derive_record(input: TokenStream) -> TokenStream { #[proc_macro_derive(Enum)] pub fn derive_enum(input: TokenStream) -> TokenStream { - expand_enum(parse_macro_input!(input), false) + expand_enum(parse_macro_input!(input), None, false) .unwrap_or_else(syn::Error::into_compile_error) .into() } @@ -211,10 +211,14 @@ pub fn derive_record_for_udl(_attrs: TokenStream, input: TokenStream) -> TokenSt #[doc(hidden)] #[proc_macro_attribute] -pub fn derive_enum_for_udl(_attrs: TokenStream, input: TokenStream) -> TokenStream { - expand_enum(syn::parse_macro_input!(input), true) - .unwrap_or_else(syn::Error::into_compile_error) - .into() +pub fn derive_enum_for_udl(attrs: TokenStream, input: TokenStream) -> TokenStream { + expand_enum( + syn::parse_macro_input!(input), + Some(syn::parse_macro_input!(attrs)), + true, + ) + .unwrap_or_else(syn::Error::into_compile_error) + .into() } #[doc(hidden)] diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index d419d80b70..148f7e8f76 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -252,6 +252,7 @@ pub mod kw { syn::custom_keyword!(flat_error); syn::custom_keyword!(None); syn::custom_keyword!(with_try_read); + syn::custom_keyword!(non_exhaustive); syn::custom_keyword!(Debug); syn::custom_keyword!(Display); syn::custom_keyword!(Eq); diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 0da2fbfd13..d1a274cfcb 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -309,6 +309,7 @@ pub struct EnumMetadata { pub module_path: String, pub name: String, pub variants: Vec, + pub non_exhaustive: bool, pub docstring: Option, } diff --git a/uniffi_meta/src/reader.rs b/uniffi_meta/src/reader.rs index fa7f4447e9..b03303de10 100644 --- a/uniffi_meta/src/reader.rs +++ b/uniffi_meta/src/reader.rs @@ -304,6 +304,7 @@ impl<'a> MetadataReader<'a> { module_path, name, variants, + non_exhaustive: self.read_bool()?, docstring: self.read_optional_long_string()?, }) } diff --git a/uniffi_udl/src/attributes.rs b/uniffi_udl/src/attributes.rs index 1df337c9e6..3d9433ff55 100644 --- a/uniffi_udl/src/attributes.rs +++ b/uniffi_udl/src/attributes.rs @@ -45,6 +45,7 @@ pub(super) enum Attribute { // The interface described is implemented as a trait. Trait, Async, + NonExhaustive, } // A type defined in Rust via procmacros but which should be available @@ -83,6 +84,7 @@ impl TryFrom<&weedle::attribute::ExtendedAttribute<'_>> for Attribute { "Custom" => Ok(Attribute::Custom), "Trait" => Ok(Attribute::Trait), "Async" => Ok(Attribute::Async), + "NonExhaustive" => Ok(Attribute::NonExhaustive), _ => anyhow::bail!("ExtendedAttributeNoArgs not supported: {:?}", (attr.0).0), }, // Matches assignment-style attributes like ["Throws=Error"] @@ -209,7 +211,6 @@ where } /// Attributes that can be attached to an `enum` definition in the UDL. -/// There's only one case here: using `[Error]` to mark an enum as an error class. #[derive(Debug, Clone, Checksum, Default)] pub(super) struct EnumAttributes(Vec); @@ -217,6 +218,12 @@ impl EnumAttributes { pub fn contains_error_attr(&self) -> bool { self.0.iter().any(|attr| attr.is_error()) } + + pub fn contains_non_exhaustive_attr(&self) -> bool { + self.0 + .iter() + .any(|attr| matches!(attr, Attribute::NonExhaustive)) + } } impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for EnumAttributes { @@ -226,6 +233,10 @@ impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for EnumAttributes { ) -> Result { let attrs = parse_attributes(weedle_attributes, |attr| match attr { Attribute::Error => Ok(()), + Attribute::NonExhaustive => Ok(()), + // Allow `[Enum]`, since we may be parsing an attribute list from an interface with the + // `[Enum]` attribute. + Attribute::Enum => Ok(()), _ => bail!(format!("{attr:?} not supported for enums")), })?; Ok(Self(attrs)) @@ -846,7 +857,7 @@ mod test { } #[test] - fn test_enum_attribute() { + fn test_enum_attribute_on_interface() { let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Enum]").unwrap(); let attrs = InterfaceAttributes::try_from(&node).unwrap(); assert!(matches!(attrs.contains_enum_attr(), true)); @@ -867,6 +878,38 @@ mod test { ); } + // Test parsing attributes for enum definitions + #[test] + fn test_enum_attributes() { + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Error, NonExhaustive]").unwrap(); + let attrs = EnumAttributes::try_from(&node).unwrap(); + assert!(attrs.contains_error_attr()); + assert!(attrs.contains_non_exhaustive_attr()); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Trait]").unwrap(); + let err = EnumAttributes::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "Trait not supported for enums"); + } + + // Test parsing attributes for interface definitions with the `[Enum]` attribute + #[test] + fn test_enum_attributes_from_interface() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Enum]").unwrap(); + assert!(EnumAttributes::try_from(&node).is_ok()); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Enum, Error, NonExhaustive]") + .unwrap(); + let attrs = EnumAttributes::try_from(&node).unwrap(); + assert!(attrs.contains_error_attr()); + assert!(attrs.contains_non_exhaustive_attr()); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Enum, Trait]").unwrap(); + let err = EnumAttributes::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "Trait not supported for enums"); + } + #[test] fn test_other_attributes_not_supported_for_interfaces() { let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Trait, ByRef]").unwrap(); diff --git a/uniffi_udl/src/converters/enum_.rs b/uniffi_udl/src/converters/enum_.rs index 37ca11ac4a..2fe01e6aae 100644 --- a/uniffi_udl/src/converters/enum_.rs +++ b/uniffi_udl/src/converters/enum_.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use super::APIConverter; -use crate::{converters::convert_docstring, InterfaceCollector}; +use crate::{attributes::EnumAttributes, converters::convert_docstring, InterfaceCollector}; use anyhow::{bail, Result}; use uniffi_meta::{EnumMetadata, ErrorMetadata, VariantMetadata}; @@ -13,6 +13,7 @@ use uniffi_meta::{EnumMetadata, ErrorMetadata, VariantMetadata}; // and one for the `[Error] interface` case. impl APIConverter for weedle::EnumDefinition<'_> { fn convert(&self, ci: &mut InterfaceCollector) -> Result { + let attributes = EnumAttributes::try_from(self.attributes.as_ref())?; Ok(EnumMetadata { module_path: ci.module_path(), name: self.identifier.0.to_string(), @@ -30,6 +31,7 @@ impl APIConverter for weedle::EnumDefinition<'_> { }) }) .collect::>>()?, + non_exhaustive: attributes.contains_non_exhaustive_attr(), docstring: self.docstring.as_ref().map(|v| convert_docstring(&v.0)), }) } @@ -37,6 +39,7 @@ impl APIConverter for weedle::EnumDefinition<'_> { impl APIConverter for weedle::EnumDefinition<'_> { fn convert(&self, ci: &mut InterfaceCollector) -> Result { + let attributes = EnumAttributes::try_from(self.attributes.as_ref())?; Ok(ErrorMetadata::Enum { enum_: EnumMetadata { module_path: ci.module_path(), @@ -55,6 +58,7 @@ impl APIConverter for weedle::EnumDefinition<'_> { }) }) .collect::>>()?, + non_exhaustive: attributes.contains_non_exhaustive_attr(), docstring: self.docstring.as_ref().map(|v| convert_docstring(&v.0)), }, is_flat: true, @@ -67,8 +71,7 @@ impl APIConverter for weedle::InterfaceDefinition<'_> { if self.inheritance.is_some() { bail!("interface inheritance is not supported for enum interfaces"); } - // We don't need to check `self.attributes` here; if calling code has dispatched - // to this impl then we already know there was an `[Enum]` attribute. + let attributes = EnumAttributes::try_from(self.attributes.as_ref())?; Ok(EnumMetadata { module_path: ci.module_path(), name: self.identifier.0.to_string(), @@ -84,6 +87,7 @@ impl APIConverter for weedle::InterfaceDefinition<'_> { ), }) .collect::>>()?, + non_exhaustive: attributes.contains_non_exhaustive_attr(), docstring: self.docstring.as_ref().map(|v| convert_docstring(&v.0)), // Enums declared using the `[Enum] interface` syntax might have variants with fields. //flat: false, @@ -96,8 +100,7 @@ impl APIConverter for weedle::InterfaceDefinition<'_> { if self.inheritance.is_some() { bail!("interface inheritance is not supported for enum interfaces"); } - // We don't need to check `self.attributes` here; callers have already checked them - // to work out which version to dispatch to. + let attributes = EnumAttributes::try_from(self.attributes.as_ref())?; Ok(ErrorMetadata::Enum { enum_: EnumMetadata { module_path: ci.module_path(), @@ -114,6 +117,7 @@ impl APIConverter for weedle::InterfaceDefinition<'_> { ), }) .collect::>>()?, + non_exhaustive: attributes.contains_non_exhaustive_attr(), docstring: self.docstring.as_ref().map(|v| convert_docstring(&v.0)), }, is_flat: false,