Repository: extendr/extendr
Pull request #634 (2023-10-04 14:36:26)

Use error instead of panic! in extendr-macros
Make the `#[extendr]` macro return clear error like this (See `extendr-macros/tests/cases/case-extendr-macro.stderr` for more examples).

```
error: Unexpected key
 --> tests/cases/case-extendr-macro.rs:4:11
  |
4 | #[extendr(foo = true)]
  |   
```

JosiahParry: This looks like a rather cut and dry PR. Is there anything preventing this PR from being merged?
CGMossa: I don't think there is any reason not to merge this. I liked it when I first saw it.. And I thought we had merged it..

@yutannihilation please forgive me for not approving this right away. Mind taken an extra look, and then we'll merge it? 
yutannihilation: Yeah, I'm fine and no hurry at all. I think this is low-priority as this is just a small improvement.
CGMossa: Okay. I'll rebase this and approve soon then 
CGMossa: @yutannihilation Hello! I tried to rebase this. Have you made commits this moment that may have been overwritten by the rebase?

I'm sorry, i'm just getting a bit paranoid here.
yutannihilation: Thanks. I think nothing is overwritten.

Patch:
diff --git a/extendr-macros/Cargo.toml b/extendr-macros/Cargo.toml
index 939f13f9e..c1a77c4ac 100644
--- a/extendr-macros/Cargo.toml
+++ b/extendr-macros/Cargo.toml
@@ -19,3 +19,4 @@ proc-macro2 = { version = "1.0" }
 extendr-api = { path = "../extendr-api" }
 extendr-engine = { path = "../extendr-engine" }
 libR-sys = { workspace = true }
+trybuild = "1.0"
diff --git a/extendr-macros/src/extendr_function.rs b/extendr-macros/src/extendr_function.rs
index 2c841ac7c..c220781ee 100644
--- a/extendr-macros/src/extendr_function.rs
+++ b/extendr-macros/src/extendr_function.rs
@@ -6,7 +6,12 @@ use syn::{meta::ParseNestedMeta, ItemFn, Lit, LitBool};
 /// Generate bindings for a single function.
 pub fn extendr_function(mut func: ItemFn, opts: &wrappers::ExtendrOptions) -> TokenStream {
     let mut wrappers: Vec<ItemFn> = Vec::new();
-    wrappers::make_function_wrappers(opts, &mut wrappers, "", &func.attrs, &mut func.sig, None);
+
+    let res =
+        wrappers::make_function_wrappers(opts, &mut wrappers, "", &func.attrs, &mut func.sig, None);
+    if let Err(e) = res {
+        return e.into_compile_error().into();
+    };
 
     TokenStream::from(quote! {
         #func
@@ -25,45 +30,46 @@ impl wrappers::ExtendrOptions {
     /// - `use_rng = bool` ensures the RNG-state is pulled and pushed
     ///
     pub fn parse(&mut self, meta: ParseNestedMeta) -> syn::parse::Result<()> {
-        fn help_message() -> ! {
-            panic!("expected #[extendr(use_try_from = bool, r_name = \"name\", mod_name = \"r_mod_name\", use_rng = bool)]");
-        }
-
-        let value = match meta.value() {
-            Ok(value) => value,
-            Err(_) => help_message(),
-        };
+        let value = meta.value()?;
+        let path = meta
+            .path
+            .get_ident()
+            .ok_or(meta.error("Unexpected syntax"))?;
 
-        if meta.path.is_ident("use_try_from") {
-            if let Ok(LitBool { value, .. }) = value.parse() {
-                self.use_try_from = value;
-                Ok(())
-            } else {
-                help_message();
+        match path.to_string().as_str() {
+            "use_try_from" => {
+                if let Ok(LitBool { value, .. }) = value.parse() {
+                    self.use_try_from = value;
+                    Ok(())
+                } else {
+                    Err(value.error("`use_try_from` must be `true` or `false`"))
+                }
             }
-        } else if meta.path.is_ident("r_name") {
-            if let Ok(Lit::Str(litstr)) = value.parse() {
-                self.r_name = Some(litstr.value());
-                Ok(())
-            } else {
-                help_message();
+            "r_name" => {
+                if let Ok(Lit::Str(litstr)) = value.parse() {
+                    self.r_name = Some(litstr.value());
+                    Ok(())
+                } else {
+                    Err(value.error("`r_name` must be a string literal"))
+                }
             }
-        } else if meta.path.is_ident("mod_name") {
-            if let Ok(Lit::Str(litstr)) = value.parse() {
-                self.mod_name = Some(litstr.value());
-                Ok(())
-            } else {
-                help_message();
+            "mod_name" => {
+                if let Ok(Lit::Str(litstr)) = value.parse() {
+                    self.mod_name = Some(litstr.value());
+                    Ok(())
+                } else {
+                    Err(value.error("`mod_name` must be a string literal"))
+                }
             }
-        } else if meta.path.is_ident("use_rng") {
-            if let Ok(LitBool { value, .. }) = value.parse() {
-                self.use_rng = value;
-                Ok(())
-            } else {
-                help_message();
+            "use_rng" => {
+                if let Ok(LitBool { value, .. }) = value.parse() {
+                    self.use_rng = value;
+                    Ok(())
+                } else {
+                    Err(value.error("`use_rng` must be `true` or `false`"))
+                }
             }
-        } else {
-            help_message();
+            _ => Err(syn::Error::new_spanned(meta.path, "Unexpected key")),
         }
     }
 }
diff --git a/extendr-macros/src/extendr_impl.rs b/extendr-macros/src/extendr_impl.rs
index 16f4a2f7c..92cf509e4 100644
--- a/extendr-macros/src/extendr_impl.rs
+++ b/extendr-macros/src/extendr_impl.rs
@@ -36,22 +36,34 @@ use crate::wrappers;
 ///     fn aux_func;
 /// }
 /// ```
-pub fn extendr_impl(mut item_impl: ItemImpl) -> TokenStream {
+pub fn extendr_impl(mut item_impl: ItemImpl) -> syn::Result<TokenStream> {
     // Only `impl name { }` allowed
     if item_impl.defaultness.is_some() {
-        return quote! { compile_error!("default not allowed in #[extendr] impl"); }.into();
+        return Err(syn::Error::new_spanned(
+            item_impl,
+            "default not allowed in #[extendr] impl",
+        ));
     }
 
     if item_impl.unsafety.is_some() {
-        return quote! { compile_error!("unsafe not allowed in #[extendr] impl"); }.into();
+        return Err(syn::Error::new_spanned(
+            item_impl,
+            "unsafe not allowed in #[extendr] impl",
+        ));
     }
 
     if item_impl.generics.const_params().count() != 0 {
-        return quote! { compile_error!("const params not allowed in #[extendr] impl"); }.into();
+        return Err(syn::Error::new_spanned(
+            item_impl,
+            "const params not allowed in #[extendr] impl",
+        ));
     }
 
     if item_impl.generics.type_params().count() != 0 {
-        return quote! { compile_error!("type params not allowed in #[extendr] impl"); }.into();
+        return Err(syn::Error::new_spanned(
+            item_impl,
+            "type params not allowed in #[extendr] impl",
+        ));
     }
 
     // if item_impl.generics.lifetimes().count() != 0 {
@@ -59,7 +71,10 @@ pub fn extendr_impl(mut item_impl: ItemImpl) -> TokenStream {
     // }
 
     if item_impl.generics.where_clause.is_some() {
-        return quote! { compile_error!("where clause not allowed in #[extendr] impl"); }.into();
+        return Err(syn::Error::new_spanned(
+            item_impl,
+            "where clause not allowed in #[extendr] impl",
+        ));
     }
 
     let opts = wrappers::ExtendrOptions::default();
@@ -97,7 +112,7 @@ pub fn extendr_impl(mut item_impl: ItemImpl) -> TokenStream {
                 &method.attrs,
                 &mut method.sig,
                 Some(self_ty),
-            );
+            )?;
         }
     }
 
@@ -187,7 +202,7 @@ pub fn extendr_impl(mut item_impl: ItemImpl) -> TokenStream {
     });
 
     //eprintln!("{}", expanded);
-    expanded
+    Ok(expanded)
 }
 
 // This structure contains parameters parsed from the #[extendr_module] definition.
diff --git a/extendr-macros/src/lib.rs b/extendr-macros/src/lib.rs
index 6f3296bd8..4336efea1 100644
--- a/extendr-macros/src/lib.rs
+++ b/extendr-macros/src/lib.rs
@@ -80,7 +80,10 @@ pub fn extendr(attr: TokenStream, item: TokenStream) -> TokenStream {
 
     match parse_macro_input!(item as Item) {
         Item::Fn(func) => extendr_function::extendr_function(func, &opts),
-        Item::Impl(item_impl) => extendr_impl::extendr_impl(item_impl),
+        Item::Impl(item_impl) => match extendr_impl::extendr_impl(item_impl) {
+            Ok(result) => result,
+            Err(e) => e.into_compile_error().into(),
+        },
         other_item => TokenStream::from(quote! {#other_item}),
     }
 }
@@ -203,7 +206,10 @@ pub fn Rraw(item: TokenStream) -> TokenStream {
 /// ```
 #[proc_macro_derive(TryFromRobj)]
 pub fn derive_try_from_robj(item: TokenStream) -> TokenStream {
-    list_struct::derive_try_from_robj(item)
+    match list_struct::derive_try_from_robj(item) {
+        Ok(result) => result,
+        Err(e) => e.into_compile_error().into(),
+    }
 }
 
 /// Derives an implementation of `From<Struct> for Robj` and `From<&Struct> for Robj` on this struct.
@@ -241,7 +247,10 @@ pub fn derive_try_from_robj(item: TokenStream) -> TokenStream {
 /// and converting it back to Rust will produce a copy of the original struct.
 #[proc_macro_derive(IntoRobj)]
 pub fn derive_into_robj(item: TokenStream) -> TokenStream {
-    list_struct::derive_into_robj(item)
+    match list_struct::derive_into_robj(item) {
+        Ok(result) => result,
+        Err(e) => e.into_compile_error().into(),
+    }
 }
 
 /// Enable the construction of dataframes from arrays of structures.
diff --git a/extendr-macros/src/list_struct.rs b/extendr-macros/src/list_struct.rs
index eb64aade7..4369c1a5f 100644
--- a/extendr-macros/src/list_struct.rs
+++ b/extendr-macros/src/list_struct.rs
@@ -1,19 +1,18 @@
 use proc_macro::TokenStream;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::quote;
-use syn::{parse_macro_input, Data, DeriveInput};
+use syn::{Data, DeriveInput};
 
 /// Implementation of the TryFromRobj macro. Refer to the documentation there
-pub fn derive_try_from_robj(item: TokenStream) -> TokenStream {
+pub fn derive_try_from_robj(item: TokenStream) -> syn::parse::Result<TokenStream> {
     // Parse the tokens into a Struct
-    let ast: DeriveInput = parse_macro_input!(item as DeriveInput);
-    let struct_name = ast.ident;
-    let inside;
-    if let Data::Struct(inner) = ast.data {
-        inside = inner;
+    let ast = syn::parse::<DeriveInput>(item)?;
+    let inside = if let Data::Struct(inner) = ast.data {
+        inner
     } else {
-        panic!("This is a derive macro, only use it on a struct")
+        return Err(syn::Error::new_spanned(ast, "Only struct is supported"));
     };
+    let struct_name = ast.ident;
 
     // Iterate each struct field and capture a conversion from Robj for each field
     let mut tokens = Vec::<TokenStream2>::with_capacity(inside.fields.len());
@@ -27,7 +26,7 @@ pub fn derive_try_from_robj(item: TokenStream) -> TokenStream {
     }
 
     // Emit the conversion trait impl
-    TokenStream::from(quote!(
+    Ok(TokenStream::from(quote!(
         impl std::convert::TryFrom<&Robj> for #struct_name {
             type Error = extendr_api::Error;
 
@@ -47,20 +46,19 @@ pub fn derive_try_from_robj(item: TokenStream) -> TokenStream {
                 })
             }
         }
-    ))
+    )))
 }
 
 /// Implementation of the IntoRobj macro. Refer to the documentation there
-pub fn derive_into_robj(item: TokenStream) -> TokenStream {
+pub fn derive_into_robj(item: TokenStream) -> syn::parse::Result<TokenStream> {
     // Parse the tokens into a Struct
-    let ast: DeriveInput = parse_macro_input!(item as DeriveInput);
-    let struct_name = ast.ident;
-    let inside;
-    if let Data::Struct(inner) = ast.data {
-        inside = inner;
+    let ast = syn::parse::<DeriveInput>(item)?;
+    let inside = if let Data::Struct(inner) = ast.data {
+        inner
     } else {
-        panic!("This is a derive macro, only use it on a struct")
+        return Err(syn::Error::new_spanned(ast, "Only struct is supported"));
     };
+    let struct_name = ast.ident;
 
     // Iterate each struct field and capture a token that creates a KeyValue pair (tuple) for
     // each field
@@ -75,7 +73,7 @@ pub fn derive_into_robj(item: TokenStream) -> TokenStream {
     }
 
     // The only thing we emit from this macro is the conversion trait impl
-    TokenStream::from(quote!(
+    Ok(TokenStream::from(quote!(
         impl std::convert::From<&#struct_name> for Robj {
             fn from(value: &#struct_name) -> Self {
                 extendr_api::List::from_pairs([#(#tokens),*]).into()
@@ -86,5 +84,5 @@ pub fn derive_into_robj(item: TokenStream) -> TokenStream {
                 extendr_api::List::from_pairs([#(#tokens),*]).into()
             }
         }
-    ))
+    )))
 }
diff --git a/extendr-macros/src/wrappers.rs b/extendr-macros/src/wrappers.rs
index e78578726..020e3a1a5 100644
--- a/extendr-macros/src/wrappers.rs
+++ b/extendr-macros/src/wrappers.rs
@@ -21,7 +21,7 @@ pub fn make_function_wrappers(
     attrs: &[syn::Attribute],
     sig: &mut syn::Signature,
     self_ty: Option<&syn::Type>,
-) {
+) -> syn::Result<()> {
     let rust_name = sig.ident.clone();
 
     let r_name_str = if let Some(r_name) = opts.r_name.as_ref() {
@@ -72,12 +72,15 @@ pub fn make_function_wrappers(
         quote! { #rust_name }
     };
 
-    let formal_args: Punctuated<FnArg, Token![,]> = inputs
+    let formal_args = inputs
         .iter()
         .map(|input| translate_formal(input, self_ty))
-        .collect();
+        .collect::<syn::Result<Punctuated<FnArg, Token![,]>>>()?;
 
-    let convert_args: Vec<syn::Stmt> = inputs.iter().map(translate_to_robj).collect();
+    let convert_args: Vec<syn::Stmt> = inputs
+        .iter()
+        .map(translate_to_robj)
+        .collect::<syn::Result<Vec<syn::Stmt>>>()?;
 
     let actual_args: Punctuated<Expr, Token![,]> = inputs
         .iter()
@@ -87,7 +90,7 @@ pub fn make_function_wrappers(
     let meta_args: Vec<Expr> = inputs
         .iter_mut()
         .map(|input| translate_meta_arg(input, self_ty))
-        .collect();
+        .collect::<syn::Result<Vec<Expr>>>()?;
 
     // Generate wrappers for rust functions to be called from R.
     // Example:
@@ -187,6 +190,8 @@ pub fn make_function_wrappers(
             })
         }
     ));
+
+    Ok(())
 }
 
 // Extract doc strings from attributes.
@@ -265,28 +270,33 @@ pub fn type_name(type_: &Type) -> String {
 }
 
 // Generate a list of arguments for the wrapper. All arguments are SEXP for .Call in R.
-pub fn translate_formal(input: &FnArg, self_ty: Option<&syn::Type>) -> FnArg {
+pub fn translate_formal(input: &FnArg, self_ty: Option<&syn::Type>) -> syn::Result<FnArg> {
     match input {
         // function argument.
         FnArg::Typed(ref pattype) => {
             let pat = &pattype.pat.as_ref();
-            parse_quote! { #pat : extendr_api::SEXP }
+            Ok(parse_quote! { #pat : extendr_api::SEXP })
         }
         // &self
         FnArg::Receiver(ref reciever) => {
             if !reciever.attrs.is_empty() || reciever.reference.is_none() {
-                panic!("expected &self or &mut self");
+                return Err(syn::Error::new_spanned(
+                    input,
+                    "expected &self or &mut self",
+                ));
             }
             if self_ty.is_none() {
-                panic!("found &self in non-impl function - have you missed the #[extendr] before the impl?");
+                return Err(syn::Error::new_spanned(
+                    input,"found &self in non-impl function - have you missed the #[extendr] before the impl?"
+                ));
             }
-            parse_quote! { _self : extendr_api::SEXP }
+            Ok(parse_quote! { _self : extendr_api::SEXP })
         }
     }
 }
 
 // Generate code to make a metadata::Arg.
-fn translate_meta_arg(input: &mut FnArg, self_ty: Option<&syn::Type>) -> Expr {
+fn translate_meta_arg(input: &mut FnArg, self_ty: Option<&syn::Type>) -> syn::Result<Expr> {
     match input {
         // function argument.
         FnArg::Typed(ref mut pattype) => {
@@ -299,49 +309,59 @@ fn translate_meta_arg(input: &mut FnArg, self_ty: Option<&syn::Type>) -> Expr {
             } else {
                 quote!(None)
             };
-            parse_quote! {
+            Ok(parse_quote! {
                 extendr_api::metadata::Arg {
                     name: #name_string,
                     arg_type: #type_string,
                     default: #default
                 }
-            }
+            })
         }
         // &self
         FnArg::Receiver(ref reciever) => {
             if !reciever.attrs.is_empty() || reciever.reference.is_none() {
-                panic!("expected &self or &mut self");
+                return Err(syn::Error::new_spanned(
+                    input,
+                    "expected &self or &mut self",
+                ));
             }
             if self_ty.is_none() {
-                panic!("found &self in non-impl function - have you missed the #[extendr] before the impl?");
+                return Err(syn::Error::new_spanned(
+                    input,
+            "found &self in non-impl function - have you missed the #[extendr] before the impl?"
+        )
+    );
             }
             let type_string = type_name(self_ty.unwrap());
-            parse_quote! {
+            Ok(parse_quote! {
                 extendr_api::metadata::Arg {
                     name: "self",
                     arg_type: #type_string,
                     default: None
                 }
-            }
+            })
         }
     }
 }
 
 /// Convert `SEXP` arguments into `Robj`.
 /// This maintains the lifetime of references.
-fn translate_to_robj(input: &FnArg) -> syn::Stmt {
+fn translate_to_robj(input: &FnArg) -> syn::Result<syn::Stmt> {
     match input {
         FnArg::Typed(ref pattype) => {
             let pat = &pattype.pat.as_ref();
             if let syn::Pat::Ident(ref ident) = pat {
                 let varname = format_ident!("_{}_robj", ident.ident);
-                parse_quote! { let #varname = extendr_api::robj::Robj::from_sexp(#pat); }
+                Ok(parse_quote! { let #varname = extendr_api::robj::Robj::from_sexp(#pat); })
             } else {
-                panic!("expect identifier as arg name")
+                Err(syn::Error::new_spanned(
+                    input,
+                    "expect identifier as arg name",
+                ))
             }
         }
         FnArg::Receiver(_) => {
-            parse_quote! { let mut _self_robj = extendr_api::robj::Robj::from_sexp(_self); }
+            Ok(parse_quote! { let mut _self_robj = extendr_api::robj::Robj::from_sexp(_self); })
         }
     }
 }


Test patch:
diff --git a/extendr-macros/tests/cases/case-extendr-macro.rs b/extendr-macros/tests/cases/case-extendr-macro.rs
new file mode 100644
index 000000000..86117d78e
--- /dev/null
+++ b/extendr-macros/tests/cases/case-extendr-macro.rs
@@ -0,0 +1,68 @@
+use extendr_macros::extendr;
+use extendr_macros::{IntoRobj, TryFromRobj};
+
+#[extendr(foo = true)]
+fn foo() {}
+
+#[extendr(use_try_from = 1)]
+fn foo() {}
+
+#[extendr(r_name = 1)]
+fn foo() {}
+
+#[extendr(mod_name = 1)]
+fn foo() {}
+
+#[extendr(use_rng = 1)]
+fn foo() {}
+
+// impl -----------------------------------------
+
+struct FooStruct {}
+
+#[extendr]
+impl FooStruct {
+    fn nonref_self(self) {}
+}
+
+impl FooStruct {
+    #[extendr]
+    fn misplaced_macro(&self) {}
+}
+
+#[extendr]
+default impl FooStruct {}
+
+#[extendr]
+unsafe impl FooStruct {}
+
+#[extendr]
+impl<const N: usize> FooStruct {}
+
+struct FooStructWithParam<A> {
+    a: A,
+}
+
+#[extendr]
+impl<A> FooStructWithParam<A> {}
+
+#[extendr]
+impl FooStructWithParam<A> where A: usize {}
+
+// derive ---------------------------------------
+
+#[derive(TryFromRobj)]
+enum FooEnum1 {
+    A,
+    B,
+    C,
+}
+
+#[derive(IntoRobj)]
+enum FooEnum2 {
+    A,
+    B,
+    C,
+}
+
+fn main() {}
diff --git a/extendr-macros/tests/cases/case-extendr-macro.stderr b/extendr-macros/tests/cases/case-extendr-macro.stderr
new file mode 100644
index 000000000..c63849b02
--- /dev/null
+++ b/extendr-macros/tests/cases/case-extendr-macro.stderr
@@ -0,0 +1,99 @@
+error: Unexpected key
+ --> tests/cases/case-extendr-macro.rs:4:11
+  |
+4 | #[extendr(foo = true)]
+  |           ^^^
+
+error: unexpected end of input, `use_try_from` must be `true` or `false`
+ --> tests/cases/case-extendr-macro.rs:7:1
+  |
+7 | #[extendr(use_try_from = 1)]
+  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  |
+  = note: this error originates in the attribute macro `extendr` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unexpected end of input, `r_name` must be a string literal
+  --> tests/cases/case-extendr-macro.rs:10:1
+   |
+10 | #[extendr(r_name = 1)]
+   | ^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: this error originates in the attribute macro `extendr` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unexpected end of input, `mod_name` must be a string literal
+  --> tests/cases/case-extendr-macro.rs:13:1
+   |
+13 | #[extendr(mod_name = 1)]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: this error originates in the attribute macro `extendr` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unexpected end of input, `use_rng` must be `true` or `false`
+  --> tests/cases/case-extendr-macro.rs:16:1
+   |
+16 | #[extendr(use_rng = 1)]
+   | ^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: this error originates in the attribute macro `extendr` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: expected &self or &mut self
+  --> tests/cases/case-extendr-macro.rs:25:20
+   |
+25 |     fn nonref_self(self) {}
+   |                    ^^^^
+
+error: found &self in non-impl function - have you missed the #[extendr] before the impl?
+  --> tests/cases/case-extendr-macro.rs:30:24
+   |
+30 |     fn misplaced_macro(&self) {}
+   |                        ^^^^^
+
+error: default not allowed in #[extendr] impl
+  --> tests/cases/case-extendr-macro.rs:34:1
+   |
+34 | default impl FooStruct {}
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: unsafe not allowed in #[extendr] impl
+  --> tests/cases/case-extendr-macro.rs:37:1
+   |
+37 | unsafe impl FooStruct {}
+   | ^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: const params not allowed in #[extendr] impl
+  --> tests/cases/case-extendr-macro.rs:40:1
+   |
+40 | impl<const N: usize> FooStruct {}
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: type params not allowed in #[extendr] impl
+  --> tests/cases/case-extendr-macro.rs:47:1
+   |
+47 | impl<A> FooStructWithParam<A> {}
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: where clause not allowed in #[extendr] impl
+  --> tests/cases/case-extendr-macro.rs:50:1
+   |
+50 | impl FooStructWithParam<A> where A: usize {}
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: Only struct is supported
+  --> tests/cases/case-extendr-macro.rs:55:1
+   |
+55 | / enum FooEnum1 {
+56 | |     A,
+57 | |     B,
+58 | |     C,
+59 | | }
+   | |_^
+
+error: Only struct is supported
+  --> tests/cases/case-extendr-macro.rs:62:1
+   |
+62 | / enum FooEnum2 {
+63 | |     A,
+64 | |     B,
+65 | |     C,
+66 | | }
+   | |_^
diff --git a/extendr-macros/tests/trybuild.rs b/extendr-macros/tests/trybuild.rs
new file mode 100644
index 000000000..bd18a8f66
--- /dev/null
+++ b/extendr-macros/tests/trybuild.rs
@@ -0,0 +1,5 @@
+#[test]
+fn test_macro_failures() {
+    let t = trybuild::TestCases::new();
+    t.compile_fail("tests/cases/*.rs");
+}