Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions sqlx-macros-core/src/derives/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub struct SqlxContainerAttributes {
pub type_name: Option<TypeName>,
pub rename_all: Option<RenameAll>,
pub repr: Option<Ident>,
/// Set when `#[repr(C)]` is present; enums reject this (see `check_enum_attributes`).
pub repr_c: Option<Ident>,
pub no_pg_array: bool,
pub default: bool,
}
Expand All @@ -78,6 +80,7 @@ pub struct SqlxChildAttributes {
pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContainerAttributes> {
let mut transparent = None;
let mut repr = None;
let mut repr_c = None;
let mut type_name = None;
let mut rename_all = None;
let mut no_pg_array = None;
Expand Down Expand Up @@ -127,15 +130,25 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContai
let list: Punctuated<Meta, Token![,]> =
attr.parse_args_with(<Punctuated<Meta, Token![,]>>::parse_terminated)?;

if let Some(path) = list.iter().find_map(|f| f.require_path_only().ok()) {
try_set!(repr, path.get_ident().unwrap().clone(), list);
for ident in list
.iter()
.filter_map(|f| f.require_path_only().ok()?.get_ident())
{
if is_integer_repr(ident) {
try_set!(repr, ident.clone(), list);
} else if ident == "C" {
repr_c = Some(ident.clone());
}
// Other reprs (`Rust`, `transparent`, `align(..)`, `packed`) only affect
// layout and say nothing about how the enum is encoded, so they're ignored.
}
}
}

Ok(SqlxContainerAttributes {
transparent: transparent.unwrap_or(false),
repr,
repr_c,
type_name,
rename_all,
no_pg_array: no_pg_array.unwrap_or(false),
Expand Down Expand Up @@ -223,6 +236,23 @@ pub fn check_transparent_attributes(
Ok(attributes)
}

fn is_integer_repr(ident: &Ident) -> bool {
matches!(
ident.to_string().as_str(),
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
)
}

pub fn check_enum_attributes(input: &DeriveInput) -> syn::Result<SqlxContainerAttributes> {
let attributes = parse_container_attributes(&input.attrs)?;

Expand All @@ -232,6 +262,16 @@ pub fn check_enum_attributes(input: &DeriveInput) -> syn::Result<SqlxContainerAt
input
);

if let Some(ident) = &attributes.repr_c {
if attributes.repr.is_none() {
fail!(
ident,
"#[repr(C)] is ambiguous for #[derive(sqlx::Type)]: use an explicit integer repr \
like #[repr(i32)] to encode as an integer, or remove #[repr(C)] to encode as a string"
);
}
}

Ok(attributes)
}

Expand Down Expand Up @@ -305,3 +345,41 @@ pub fn check_struct_attributes(

Ok(attributes)
}

#[cfg(test)]
mod tests {
use super::*;

fn enum_input(attrs: TokenStream) -> DeriveInput {
syn::parse2(quote::quote! {
#attrs
enum Foo { A, B }
})
.unwrap()
}

#[test]
fn enum_repr_c_is_rejected() {
let input = enum_input(quote::quote!(#[repr(C)]));
let err = check_enum_attributes(&input)
.err()
.expect("repr(C) should be rejected");
assert!(err.to_string().contains("#[repr(i32)]"), "{err}");
}

#[test]
fn enum_repr_c_with_integer_is_accepted() {
let input = enum_input(quote::quote!(#[repr(C, u8)]));
let attrs = check_enum_attributes(&input).unwrap();
assert_eq!(attrs.repr.unwrap(), "u8");
}

#[test]
fn enum_layout_only_reprs_are_ignored() {
let input = enum_input(quote::quote!(#[repr(align(8))]));
assert!(check_enum_attributes(&input).unwrap().repr.is_none());

let input = enum_input(quote::quote!(#[repr(i16, align(8))]));
assert_eq!(check_enum_attributes(&input).unwrap().repr.unwrap(), "i16");
}
}
Loading