Repository: dtolnay/cxx (Rust)

Issue:
Add support for deriving `Default` for enums
Currently, cxx does not support `#[derive(Default)]` for shared structs, resulting in the following error message:

```
error[cxxbridge]: derive(Default) on shared enum is not supported
   ┌─ ../../path/to/file:line
   │
99 │     #[derive(Debug, Clone, Copy, Default)]
   │                                  ^^^^^^^ derive(Default) on shared enum is not supported
```

It would be nice if the Default derivation case was supported.


Discussion: Thanks for opening an issue.  I guess the expectation here is that the same syntax would work as outside of `#[cxx::bridge]` - e.g. ([Rust playground link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d530c8b71dc31011f9f9f1130c9b27af)):

```
#[derive(Debug, Default)]
pub enum Foo {
    #[default]
    Bar,
    Baz,
}
```



Fix (patch):
diff --git a/macro/src/derive.rs b/macro/src/derive.rs
index 0438bed2d..964704048 100644
--- a/macro/src/derive.rs
+++ b/macro/src/derive.rs
@@ -58,7 +58,7 @@ pub(crate) fn expand_enum(enm: &Enum, actual_derives: &mut Option<TokenStream>)
                 has_clone = true;
             }
             Trait::Debug => expanded.extend(enum_debug(enm, span)),
-            Trait::Default => unreachable!(),
+            Trait::Default => expanded.extend(enum_default(enm, span)),
             Trait::Eq => {
                 traits.push(quote_spanned!(span=> ::cxx::core::cmp::Eq));
                 has_eq = true;
@@ -294,6 +294,28 @@ fn enum_debug(enm: &Enum, span: Span) -> TokenStream {
     }
 }
 
+fn enum_default(enm: &Enum, span: Span) -> TokenStream {
+    let ident = &enm.name.rust;
+    let attrs = &enm.attrs;
+
+    for variant in &enm.variants {
+        if variant.default {
+            let variant = &variant.name.rust;
+            return quote_spanned! {span=>
+                #attrs
+                #[automatically_derived]
+                impl ::cxx::core::default::Default for #ident {
+                    fn default() -> Self {
+                        #ident::#variant
+                    }
+                }
+            };
+        }
+    }
+
+    unreachable!("no #[default] variant");
+}
+
 fn enum_ord(enm: &Enum, span: Span) -> TokenStream {
     let ident = &enm.name.rust;
     let attrs = &enm.attrs;
diff --git a/macro/src/expand.rs b/macro/src/expand.rs
index 46bf65d79..232bb03f5 100644
--- a/macro/src/expand.rs
+++ b/macro/src/expand.rs
@@ -1,9 +1,9 @@
-use crate::message::Message;
 use crate::syntax::atom::Atom::*;
 use crate::syntax::attrs::{self, OtherAttrs};
 use crate::syntax::cfg::{CfgExpr, ComputedCfg};
 use crate::syntax::file::Module;
 use crate::syntax::instantiate::{ImplKey, NamedImplKey};
+use crate::syntax::message::Message;
 use crate::syntax::namespace::Namespace;
 use crate::syntax::qualified::QualifiedName;
 use crate::syntax::report::Errors;
diff --git a/macro/src/lib.rs b/macro/src/lib.rs
index 50f827755..1f72cafd8 100644
--- a/macro/src/lib.rs
+++ b/macro/src/lib.rs
@@ -28,7 +28,6 @@ mod cfg;
 mod derive;
 mod expand;
 mod generics;
-mod message;
 mod syntax;
 mod tokens;
 mod type_id;
diff --git a/syntax/attrs.rs b/syntax/attrs.rs
index 3e4f20f38..b167236e8 100644
--- a/syntax/attrs.rs
+++ b/syntax/attrs.rs
@@ -32,6 +32,7 @@ pub(crate) struct Parser<'a> {
     pub doc: Option<&'a mut Doc>,
     pub derives: Option<&'a mut Vec<Derive>>,
     pub repr: Option<&'a mut Option<Repr>>,
+    pub default: Option<&'a mut bool>,
     pub namespace: Option<&'a mut Namespace>,
     pub cxx_name: Option<&'a mut Option<ForeignName>>,
     pub rust_name: Option<&'a mut Option<Ident>>,
@@ -90,6 +91,19 @@ pub(crate) fn parse(cx: &mut Errors, attrs: Vec<Attribute>, mut parser: Parser)
                     break;
                 }
             }
+        } else if attr_path.is_ident("default") {
+            match parse_default_attribute(&attr.meta) {
+                Ok(()) => {
+                    if let Some(default) = &mut parser.default {
+                        **default = true;
+                        continue;
+                    }
+                }
+                Err(err) => {
+                    cx.push(err);
+                    break;
+                }
+            }
         } else if attr_path.is_ident("namespace") {
             match Namespace::parse_meta(&attr.meta) {
                 Ok(attr) => {
@@ -230,6 +244,18 @@ fn parse_derive_attribute(cx: &mut Errors, input: ParseStream) -> Result<Vec<Der
     Ok(derives)
 }
 
+fn parse_default_attribute(meta: &Meta) -> Result<()> {
+    let error_span = match meta {
+        Meta::Path(_) => return Ok(()),
+        Meta::List(meta) => meta.delimiter.span().open(),
+        Meta::NameValue(meta) => meta.eq_token.span,
+    };
+    Err(Error::new(
+        error_span,
+        "#[default] attribute does not accept an argument",
+    ))
+}
+
 fn parse_cxx_name_attribute(meta: &Meta) -> Result<ForeignName> {
     if let Meta::NameValue(meta) = meta {
         match &meta.value {
diff --git a/syntax/check.rs b/syntax/check.rs
index fea0fbdb0..96cd87442 100644
--- a/syntax/check.rs
+++ b/syntax/check.rs
@@ -1,4 +1,5 @@
 use crate::syntax::atom::Atom::{self, *};
+use crate::syntax::message::Message;
 use crate::syntax::report::Errors;
 use crate::syntax::visit::{self, Visit};
 use crate::syntax::{
@@ -375,8 +376,18 @@ fn check_api_enum(cx: &mut Check, enm: &Enum) {
     }
 
     for derive in &enm.derives {
-        if derive.what == Trait::Default || derive.what == Trait::ExternType {
-            let msg = format!("derive({}) on shared enum is not supported", derive);
+        if derive.what == Trait::Default {
+            let default_variants = enm.variants.iter().filter(|v| v.default).count();
+            if default_variants != 1 {
+                let mut msg = Message::new();
+                write!(msg, "derive(Default) on enum requires exactly one variant to be marked with #[default]");
+                if default_variants > 0 {
+                    write!(msg, " (found {})", default_variants);
+                }
+                cx.error(derive, msg);
+            }
+        } else if derive.what == Trait::ExternType {
+            let msg = "derive(ExternType) on shared enum is not supported";
             cx.error(derive, msg);
         }
     }
diff --git a/macro/src/message.rs b/syntax/message.rs
similarity index 71%
rename from macro/src/message.rs
rename to syntax/message.rs
index d9ab56d79..244ee3bd9 100644
--- a/macro/src/message.rs
+++ b/syntax/message.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::ToTokens;
-use std::fmt;
+use std::fmt::{self, Display};
 
 pub(crate) struct Message(String);
 
@@ -14,6 +14,12 @@ impl Message {
     }
 }
 
+impl Display for Message {
+    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(formatter)
+    }
+}
+
 impl ToTokens for Message {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         self.0.to_tokens(tokens);
diff --git a/syntax/mod.rs b/syntax/mod.rs
index b1cb19dfe..6015a15e4 100644
--- a/syntax/mod.rs
+++ b/syntax/mod.rs
@@ -15,6 +15,7 @@ mod improper;
 pub(crate) mod instantiate;
 pub(crate) mod mangle;
 pub(crate) mod map;
+pub(crate) mod message;
 mod names;
 pub(crate) mod namespace;
 mod parse;
@@ -249,6 +250,7 @@ pub(crate) struct Variant {
     #[cfg_attr(proc_macro, expect(dead_code))]
     pub cfg: CfgExpr,
     pub doc: Doc,
+    pub default: bool,
     #[cfg_attr(not(proc_macro), expect(dead_code))]
     pub attrs: OtherAttrs,
     pub name: Pair,
diff --git a/syntax/parse.rs b/syntax/parse.rs
index 6f114a142..4195d68c3 100644
--- a/syntax/parse.rs
+++ b/syntax/parse.rs
@@ -303,6 +303,7 @@ fn parse_variant(
 ) -> Result<Variant> {
     let mut cfg = CfgExpr::Unconditional;
     let mut doc = Doc::new();
+    let mut default = false;
     let mut cxx_name = None;
     let mut rust_name = None;
     let attrs = attrs::parse(
@@ -311,6 +312,7 @@ fn parse_variant(
         attrs::Parser {
             cfg: Some(&mut cfg),
             doc: Some(&mut doc),
+            default: Some(&mut default),
             cxx_name: Some(&mut cxx_name),
             rust_name: Some(&mut rust_name),
             ..Default::default()
@@ -341,6 +343,7 @@ fn parse_variant(
     Ok(Variant {
         cfg,
         doc,
+        default,
         attrs,
         name,
         discriminant,