Add compile fail tests for FromPyObject derives + some fixes.

Fix some error messages and accidental passes.
This commit is contained in:
Sebastian Pütz 2020-08-30 12:54:13 +02:00
parent 7781bb78de
commit a8c5379eff
4 changed files with 401 additions and 51 deletions

View File

@ -1,6 +1,7 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::{quote, ToTokens};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse_quote, Attribute, DataEnum, DeriveInput, Fields, Ident, Meta, Result};
/// Describes derivation input of an enum.
@ -17,8 +18,8 @@ impl<'a> Enum<'a> {
/// `Identifier` of the enum.
fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result<Self> {
if data_enum.variants.is_empty() {
return Err(syn::Error::new_spanned(
&data_enum.variants,
return Err(spanned_err(
&ident,
"Cannot derive FromPyObject for empty enum.",
));
}
@ -121,6 +122,12 @@ impl<'a> Container<'a> {
attrs: Vec<ContainerAttribute>,
is_enum_variant: bool,
) -> Result<Self> {
if fields.is_empty() {
return Err(spanned_err(
fields,
"Cannot derive FromPyObject for empty structs and variants.",
));
}
let transparent = attrs.iter().any(ContainerAttribute::transparent);
if transparent {
Self::check_transparent_len(fields)?;
@ -154,10 +161,11 @@ impl<'a> Container<'a> {
ContainerType::Struct(fields)
}
(Fields::Unit, _) => {
return Err(syn::Error::new_spanned(
// covered by length check above
return Err(spanned_err(
&fields,
"Cannot derive FromPyObject for Unit structs and variants",
))
));
}
};
let err_name = attrs
@ -175,16 +183,30 @@ impl<'a> Container<'a> {
Ok(v)
}
fn verify_struct_container_attrs(attrs: &'a [ContainerAttribute]) -> Result<()> {
fn verify_struct_container_attrs(
attrs: &'a [ContainerAttribute],
original: &[Attribute],
) -> Result<()> {
for attr in attrs {
match attr {
ContainerAttribute::Transparent => continue,
ContainerAttribute::ErrorAnnotation(_) => {
let span = original
.iter()
.map(|a| a.span())
.fold(None, |mut acc: Option<Span>, span| {
if let Some(all) = acc.as_mut() {
all.join(span)
} else {
Some(span)
}
})
.unwrap_or_else(Span::call_site);
return Err(syn::Error::new(
Span::call_site(),
span,
"Annotating error messages for structs is \
not supported. Remove the annotation attribute.",
))
));
}
}
}
@ -264,7 +286,7 @@ impl<'a> Container<'a> {
fn check_transparent_len(fields: &Fields) -> Result<()> {
if fields.len() != 1 {
return Err(syn::Error::new_spanned(
return Err(spanned_err(
fields,
"Transparent structs and variants can only have 1 field",
));
@ -315,16 +337,18 @@ impl ContainerAttribute {
if let syn::Lit::Str(s) = &nv.lit {
attrs.push(ContainerAttribute::ErrorAnnotation(s.value()))
} else {
return Err(syn::Error::new_spanned(
&nv.lit,
"Expected string literal.",
));
return Err(spanned_err(&nv.lit, "Expected string literal."));
}
}
_ => (),
other => {
return Err(spanned_err(
other,
"Expected `transparent` or `annotation = \"name\"`",
))
}
}
} else {
return Err(syn::Error::new_spanned(
return Err(spanned_err(
meta,
"Unknown container attribute, expected `transparent` or \
`annotation(\"err_name\")`",
@ -352,8 +376,8 @@ impl FieldAttribute {
return Ok(None);
}
if list.nested.len() > 1 {
return Err(syn::Error::new_spanned(
list,
return Err(spanned_err(
list.nested,
"Only one of `item`, `attribute` can be provided, possibly with an \
additional argument: `item(\"key\")` or `attribute(\"name\").",
));
@ -362,7 +386,7 @@ impl FieldAttribute {
let meta = match metaitem {
syn::NestedMeta::Meta(meta) => meta,
syn::NestedMeta::Lit(lit) => {
return Err(syn::Error::new_spanned(
return Err(spanned_err(
lit,
"Expected `attribute` or `item`, not a literal.",
))
@ -374,10 +398,7 @@ impl FieldAttribute {
} else if path.is_ident("item") {
Ok(Some(FieldAttribute::GetItem(Self::item_arg(meta)?)))
} else {
Err(syn::Error::new_spanned(
meta,
"Expected `attribute` or `item`.",
))
Err(spanned_err(meta, "Expected `attribute` or `item`."))
}
}
@ -387,25 +408,25 @@ impl FieldAttribute {
syn::Meta::Path(_) => return Ok(None),
Meta::NameValue(nv) => {
let err_msg = "Expected a string literal or no argument: `pyo3(attribute(\"name\") or `pyo3(attribute)`";
return Err(syn::Error::new_spanned(nv, err_msg));
return Err(spanned_err(nv, err_msg));
}
};
if arg_list.nested.len() != 1 {
return Err(syn::Error::new_spanned(
arg_list,
"Expected a single string literal.",
));
let arg_msg = "Expected a single string literal argument.";
if arg_list.nested.is_empty() {
return Err(spanned_err(arg_list, arg_msg));
} else if arg_list.nested.len() > 1 {
return Err(spanned_err(arg_list.nested, arg_msg));
}
let first = arg_list.nested.first().unwrap();
if let syn::NestedMeta::Lit(lit) = first {
if let syn::Lit::Str(litstr) = lit {
if litstr.value().is_empty() {
return Err(spanned_err(litstr, "Attribute name cannot be empty."));
}
return Ok(Some(parse_quote!(#litstr)));
}
}
Err(syn::Error::new_spanned(
first,
"Expected a single string literal.",
))
Err(spanned_err(first, arg_msg))
}
fn item_arg(meta: syn::Meta) -> syn::Result<Option<syn::Lit>> {
@ -413,26 +434,30 @@ impl FieldAttribute {
syn::Meta::List(list) => list,
syn::Meta::Path(_) => return Ok(None),
Meta::NameValue(nv) => {
return Err(syn::Error::new_spanned(
return Err(spanned_err(
nv,
"Expected a literal or no argument: `pyo3(item(\"key\") or `pyo3(item)`",
))
}
};
if arg_list.nested.len() != 1 {
return Err(syn::Error::new_spanned(
arg_list,
"Expected a single literal.",
));
let arg_msg = "Expected a single literal argument.";
if arg_list.nested.is_empty() {
return Err(spanned_err(arg_list, arg_msg));
} else if arg_list.nested.len() > 1 {
return Err(spanned_err(arg_list.nested, arg_msg));
}
let first = arg_list.nested.first().unwrap();
if let syn::NestedMeta::Lit(lit) = first {
return Ok(Some(parse_quote!(#lit)));
}
Err(syn::Error::new_spanned(first, "Expected a literal."))
Err(spanned_err(first, arg_msg))
}
}
fn spanned_err<T: ToTokens>(tokens: T, msg: &str) -> syn::Error {
syn::Error::new_spanned(tokens, msg)
}
/// Extract pyo3 metalist, flattens multiple lists into a single one.
fn get_pyo3_meta_list(attrs: &[Attribute]) -> Result<syn::MetaList> {
let mut list: Punctuated<syn::NestedMeta, syn::Token![,]> = Punctuated::new();
@ -443,12 +468,7 @@ fn get_pyo3_meta_list(attrs: &[Attribute]) -> Result<syn::MetaList> {
list.push(meta);
}
}
_ => {
return Err(syn::Error::new_spanned(
value,
"Expected `pyo3()` attribute.",
))
}
_ => continue,
}
}
Ok(syn::MetaList {
@ -461,9 +481,9 @@ fn get_pyo3_meta_list(attrs: &[Attribute]) -> Result<syn::MetaList> {
fn verify_and_get_lifetime(generics: &syn::Generics) -> Result<Option<&syn::LifetimeDef>> {
let lifetimes = generics.lifetimes().collect::<Vec<_>>();
if lifetimes.len() > 1 {
return Err(syn::Error::new_spanned(
return Err(spanned_err(
&generics,
"FromPyObject can only be derived with at most one lifetime parameter.",
"FromPyObject can be derived with at most one lifetime parameter.",
));
}
Ok(lifetimes.into_iter().next())
@ -500,15 +520,15 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result<TokenStream> {
}
syn::Data::Struct(st) => {
let attrs = ContainerAttribute::parse_attrs(&tokens.attrs)?;
Container::verify_struct_container_attrs(&attrs)?;
Container::verify_struct_container_attrs(&attrs, &tokens.attrs)?;
let ident = &tokens.ident;
let st = Container::new(&st.fields, parse_quote!(#ident), attrs, false)?;
st.build()
}
_ => {
return Err(syn::Error::new_spanned(
syn::Data::Union(_) => {
return Err(spanned_err(
tokens,
"FromPyObject can only be derived for structs and enums.",
"FromPyObject can not be derived for unions.",
))
}
};

View File

@ -2,6 +2,7 @@
#[test]
fn test_compile_errors() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/invalid_frompy_derive.rs");
t.compile_fail("tests/ui/invalid_macro_args.rs");
t.compile_fail("tests/ui/invalid_property_args.rs");
t.compile_fail("tests/ui/invalid_pyclass_args.rs");

View File

@ -0,0 +1,156 @@
use pyo3::prelude::FromPyObject;
#[derive(FromPyObject)]
struct Foo();
#[derive(FromPyObject)]
struct Foo2 {}
#[derive(FromPyObject)]
enum EmptyEnum {}
#[derive(FromPyObject)]
enum EnumWithEmptyTupleVar {
EmptyTuple(),
Valid(String),
}
#[derive(FromPyObject)]
enum EnumWithEmptyStructVar {
EmptyStruct {},
Valid(String),
}
#[derive(FromPyObject)]
#[pyo3(transparent)]
struct EmptyTransparentTup();
#[derive(FromPyObject)]
#[pyo3(transparent)]
struct EmptyTransparentStruct {}
#[derive(FromPyObject)]
enum EnumWithTransparentEmptyTupleVar {
#[pyo3(transparent)]
EmptyTuple(),
Valid(String),
}
#[derive(FromPyObject)]
enum EnumWithTransparentEmptyStructVar {
#[pyo3(transparent)]
EmptyStruct {},
Valid(String),
}
#[derive(FromPyObject)]
#[pyo3(transparent)]
struct TransparentTupTooManyFields(String, String);
#[derive(FromPyObject)]
#[pyo3(transparent)]
struct TransparentStructTooManyFields {
foo: String,
bar: String,
}
#[derive(FromPyObject)]
enum EnumWithTransparentTupleTooMany {
#[pyo3(transparent)]
EmptyTuple(String, String),
Valid(String),
}
#[derive(FromPyObject)]
enum EnumWithTransparentStructTooMany {
#[pyo3(transparent)]
EmptyStruct {
foo: String,
bar: String,
},
Valid(String),
}
#[derive(FromPyObject)]
struct UnknownAttribute {
#[pyo3(attr)]
a: String,
}
#[derive(FromPyObject)]
struct InvalidAttributeArg {
#[pyo3(attribute(1))]
a: String,
}
#[derive(FromPyObject)]
struct TooManyAttributeArgs {
#[pyo3(attribute("a", "b"))]
a: String,
}
#[derive(FromPyObject)]
struct EmptyAttributeArg {
#[pyo3(attribute(""))]
a: String,
}
#[derive(FromPyObject)]
struct NoAttributeArg {
#[pyo3(attribute())]
a: String,
}
#[derive(FromPyObject)]
struct TooManyitemArgs {
#[pyo3(item("a", "b"))]
a: String,
}
#[derive(FromPyObject)]
struct NoItemArg {
#[pyo3(item())]
a: String,
}
#[derive(FromPyObject)]
struct ItemAndAttribute {
#[pyo3(item, attribute)]
a: String,
}
#[derive(FromPyObject)]
#[pyo3(unknown = "should not work")]
struct UnknownContainerAttr {
a: String,
}
#[derive(FromPyObject)]
#[pyo3(annotation = "should not work")]
struct AnnotationOnStruct {
a: String,
}
#[derive(FromPyObject)]
enum InvalidAnnotatedEnum {
#[pyo3(annotation = 1)]
Foo(String),
}
#[derive(FromPyObject)]
enum TooManyLifetimes<'a, 'b> {
Foo(&'a str),
Bar(&'b str),
}
#[derive(FromPyObject)]
union Union {
a: usize,
}
#[derive(FromPyObject)]
enum UnitEnum {
Unit,
}
fn main() {}

View File

@ -0,0 +1,173 @@
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:4:11
|
4 | struct Foo();
| ^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:7:13
|
7 | struct Foo2 {}
| ^^
error: Cannot derive FromPyObject for empty enum.
--> $DIR/invalid_frompy_derive.rs:10:6
|
10 | enum EmptyEnum {}
| ^^^^^^^^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:14:15
|
14 | EmptyTuple(),
| ^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:20:17
|
20 | EmptyStruct {},
| ^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:26:27
|
26 | struct EmptyTransparentTup();
| ^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:30:31
|
30 | struct EmptyTransparentStruct {}
| ^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:35:15
|
35 | EmptyTuple(),
| ^^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:42:17
|
42 | EmptyStruct {},
| ^^
error: Transparent structs and variants can only have 1 field
--> $DIR/invalid_frompy_derive.rs:48:35
|
48 | struct TransparentTupTooManyFields(String, String);
| ^^^^^^^^^^^^^^^^
error: Transparent structs and variants can only have 1 field
--> $DIR/invalid_frompy_derive.rs:52:39
|
52 | struct TransparentStructTooManyFields {
| _______________________________________^
53 | | foo: String,
54 | | bar: String,
55 | | }
| |_^
error: Transparent structs and variants can only have 1 field
--> $DIR/invalid_frompy_derive.rs:60:15
|
60 | EmptyTuple(String, String),
| ^^^^^^^^^^^^^^^^
error: Transparent structs and variants can only have 1 field
--> $DIR/invalid_frompy_derive.rs:67:17
|
67 | EmptyStruct {
| _________________^
68 | | foo: String,
69 | | bar: String,
70 | | },
| |_____^
error: Expected `attribute` or `item`.
--> $DIR/invalid_frompy_derive.rs:76:12
|
76 | #[pyo3(attr)]
| ^^^^
error: Expected a single string literal argument.
--> $DIR/invalid_frompy_derive.rs:82:22
|
82 | #[pyo3(attribute(1))]
| ^
error: Expected a single string literal argument.
--> $DIR/invalid_frompy_derive.rs:88:22
|
88 | #[pyo3(attribute("a", "b"))]
| ^^^^^^^^
error: Attribute name cannot be empty.
--> $DIR/invalid_frompy_derive.rs:94:22
|
94 | #[pyo3(attribute(""))]
| ^^
error: Expected a single string literal argument.
--> $DIR/invalid_frompy_derive.rs:100:12
|
100 | #[pyo3(attribute())]
| ^^^^^^^^^^^
error: Expected a single literal argument.
--> $DIR/invalid_frompy_derive.rs:106:17
|
106 | #[pyo3(item("a", "b"))]
| ^^^^^^^^
error: Expected a single literal argument.
--> $DIR/invalid_frompy_derive.rs:112:12
|
112 | #[pyo3(item())]
| ^^^^^^
error: Only one of `item`, `attribute` can be provided, possibly with an additional argument: `item("key")` or `attribute("name").
--> $DIR/invalid_frompy_derive.rs:118:12
|
118 | #[pyo3(item, attribute)]
| ^^^^^^^^^^^^^^^
error: Expected `transparent` or `annotation = "name"`
--> $DIR/invalid_frompy_derive.rs:123:8
|
123 | #[pyo3(unknown = "should not work")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Annotating error messages for structs is not supported. Remove the annotation attribute.
--> $DIR/invalid_frompy_derive.rs:129:1
|
129 | #[pyo3(annotation = "should not work")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Expected string literal.
--> $DIR/invalid_frompy_derive.rs:136:25
|
136 | #[pyo3(annotation = 1)]
| ^
error: FromPyObject can be derived with at most one lifetime parameter.
--> $DIR/invalid_frompy_derive.rs:141:22
|
141 | enum TooManyLifetimes<'a, 'b> {
| ^^^^^^^^
error: FromPyObject can not be derived for unions.
--> $DIR/invalid_frompy_derive.rs:147:1
|
147 | / union Union {
148 | | a: usize,
149 | | }
| |_^
error: Cannot derive FromPyObject for empty structs and variants.
--> $DIR/invalid_frompy_derive.rs:151:10
|
151 | #[derive(FromPyObject)]
| ^^^^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)