diff --git a/README.md b/README.md index 8fc0ac5..8c7188d 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,13 @@ Originally based off of [deep-clone-derive](https://github.com/asajeffrey/deep-c * normal [structs](./tests/struct.rs) * enums with tuple variants [tuple enums](./tests/simple_enum.rs) * `IntoOwned` alike fields (actually assumes all fields with types with lifetimes are `IntoOwned` alike) - * [options of Cow or Cow-like types](./tests/opt_field.rs) `Option>` and `Option>` - * [vectors of Cow or Cow-like types](./tests/vec.rs) + * `Cow<'a, B>` + * [`Option`](./tests/opt_field.rs) + * [`Vec`](./tests/vec.rs) + * `Box` + * [arrays](./tests/array.rs) + * [tuples](./tests/tuple.rs) + * arbitrarily nested types built from the above ([Example 1](./tests/nested_types.rs), [Example 2](./tests/triple_cow.rs)) But wait there is even more! `[derive(Borrowed)]` generates a currently perhaps a bit limited version of a method like: @@ -59,9 +64,7 @@ Note, there's no trait implementation expected because I didn't find one at the Currently deriving will fail miserably for at least but not limited to: * `IntoOwned`: borrowed fields like `&'a str` - * `Borrowed`: struct/enum has more than one lifetime - * both: arrays not supported - * both: into_owned/borrowed types inside tuples inside vectors + * `Borrowed`: struct/enum has multiple lifetimes that depend on each other (explicitly or implicitly) Using with incompatible types results in not so understandable error messages. For example, given a struct: diff --git a/src/field_kind.rs b/src/field_kind.rs index 7894945..15b1e9a 100644 --- a/src/field_kind.rs +++ b/src/field_kind.rs @@ -1,60 +1,126 @@ use quote::{format_ident, quote}; -use crate::helpers::{collect_segments, is_cow, is_cow_alike, is_iter_field, is_opt_cow}; +use crate::helpers::{collect_segments, cow_field, generic_field, is_cow_alike}; #[derive(Debug)] pub enum FieldKind { - PlainCow, + PlainCow(Box), AssumedCow, - /// Option fields with either PlainCow or AssumedCow - OptField(usize, Box), + OptField(Box), IterableField(Box), + Box(Box), + Array(Box), + Tuple(Vec), JustMoved, } impl FieldKind { pub fn resolve(ty: &syn::Type) -> Self { - if let syn::Type::Path(syn::TypePath { ref path, .. }) = ty { - if is_cow(&collect_segments(path)) { - FieldKind::PlainCow - } else if is_cow_alike(&collect_segments(path)) { - FieldKind::AssumedCow - } else if let Some(kind) = is_opt_cow(collect_segments(path)) { - kind - } else if let Some(kind) = is_iter_field(collect_segments(path)) { - kind - } else { - FieldKind::JustMoved + let field_kind = match ty { + syn::Type::Path(syn::TypePath { path, .. }) => { + let segments = collect_segments(path); + + if let Some(kind) = cow_field(&segments) { + FieldKind::PlainCow(Box::new(kind)) + } else if is_cow_alike(&segments) { + FieldKind::AssumedCow + } else if let Some(kind) = generic_field(&segments, "std::option::Option") { + FieldKind::OptField(Box::new(kind)) + } else if let Some(kind) = generic_field(&segments, "std::vec::Vec") { + FieldKind::IterableField(Box::new(kind)) + } else if let Some(kind) = generic_field(&segments, "std::boxed::Box") { + FieldKind::Box(Box::new(kind)) + } else { + FieldKind::JustMoved + } + } + syn::Type::Array(syn::TypeArray { elem, .. }) => { + FieldKind::Array(Box::new(FieldKind::resolve(elem))) + } + syn::Type::Tuple(syn::TypeTuple { elems, .. }) => { + if elems.is_empty() { + // Unit + FieldKind::JustMoved + } else { + FieldKind::Tuple(elems.iter().map(FieldKind::resolve).collect()) + } } - } else { - FieldKind::JustMoved + _ => FieldKind::JustMoved, + }; + + // Optimization to shortcut to JustMoved for containers that contain no + // references (thus generating a single move / clone instead of handling + // all inner fields) + if field_kind.is_static() { + return FieldKind::JustMoved; + } + + field_kind + } + + fn is_static(&self) -> bool { + match self { + FieldKind::PlainCow(_) => false, + FieldKind::AssumedCow => false, + FieldKind::OptField(inner) => inner.is_static(), + FieldKind::IterableField(inner) => inner.is_static(), + FieldKind::Box(inner) => inner.is_static(), + FieldKind::Array(inner) => inner.is_static(), + FieldKind::Tuple(elems) => elems.iter().all(FieldKind::is_static), + FieldKind::JustMoved => true, } } pub fn move_or_clone_field(&self, var: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { use self::FieldKind::*; - match *self { - PlainCow => quote! { ::std::borrow::Cow::Owned(#var.into_owned()) }, - AssumedCow => quote! { #var.into_owned() }, - OptField(levels, ref inner) => { + match self { + PlainCow(inner) => { let next = format_ident!("val"); - let next = quote! { #next }; - - let mut tokens = inner.move_or_clone_field(&next); + let tokens = inner.move_or_clone_field("e! { #next }); - for _ in 0..(levels - 1) { - tokens = quote! { #next.map(|#next| #tokens) }; + quote! { + { + let #next = ::std::borrow::Cow::into_owned(#var); + ::std::borrow::Cow::Owned(#tokens) + } } + } + AssumedCow => quote! { #var.into_owned() }, + OptField(inner) => { + let next = format_ident!("val"); + let tokens = inner.move_or_clone_field("e! { #next }); quote! { #var.map(|#next| #tokens) } } - IterableField(ref inner) => { + IterableField(inner) => { let next = format_ident!("x"); - let next = quote! { #next }; + let tokens = inner.move_or_clone_field("e! { #next }); - let tokens = inner.move_or_clone_field(&next); + quote! { #var.into_iter().map(|#next| #tokens).collect() } + } + Box(inner) => { + let tokens = inner.move_or_clone_field("e! { (*#var) }); - quote! { #var.into_iter().map(|x| #tokens).collect() } + quote! { ::std::boxed::Box::new(#tokens) } + } + Array(inner) => { + let next = format_ident!("x"); + let tokens = inner.move_or_clone_field("e! { #next }); + + quote! { #var.map(|#next| #tokens) } + } + Tuple(fields) => { + let next = format_ident!("val"); + let fields = fields.iter().enumerate().map(|(index, field)| { + let index = syn::Index::from(index); + field.move_or_clone_field("e! { #next.#index }) + }); + quote! { + { + let #next = #var; + ( #(#fields),* , ) + } + } } JustMoved => quote! { #var }, } @@ -63,28 +129,44 @@ impl FieldKind { pub fn borrow_or_clone(&self, var: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { use self::FieldKind::*; - match *self { - PlainCow => quote! { ::std::borrow::Cow::Borrowed(#var.as_ref()) }, + match self { + PlainCow(_) => quote! { ::std::borrow::Cow::Borrowed(#var.as_ref()) }, AssumedCow => quote! { #var.borrowed() }, - OptField(levels, ref inner) => { + OptField(inner) => { let next = format_ident!("val"); - let next = quote! { #next }; - - let mut tokens = inner.borrow_or_clone(&next); - - for _ in 0..(levels - 1) { - tokens = quote! { #next.as_ref().map(|#next| #tokens) }; - } + let tokens = inner.borrow_or_clone("e! { #next }); quote! { #var.as_ref().map(|#next| #tokens) } } - IterableField(ref inner) => { + IterableField(inner) => { let next = format_ident!("x"); - let next = quote! { #next }; + let tokens = inner.borrow_or_clone("e! { #next }); - let tokens = inner.borrow_or_clone(&next); + quote! { #var.iter().map(|#next| #tokens).collect() } + } + Box(inner) => { + let tokens = inner.borrow_or_clone("e! { #var.as_ref() }); + + quote! { ::std::boxed::Box::new(#tokens) } + } + Array(inner) => { + let next = format_ident!("x"); + let tokens = inner.borrow_or_clone("e! { #next }); - quote! { #var.iter().map(|x| #tokens).collect() } + quote! { #var.each_ref().map(|#next| #tokens) } + } + Tuple(fields) => { + let next = format_ident!("val"); + let fields = fields.iter().enumerate().map(|(index, field)| { + let index = syn::Index::from(index); + field.borrow_or_clone("e! { (&#next.#index) }) + }); + quote! { + { + let #next = #var; + ( #(#fields),* , ) + } + } } JustMoved => quote! { #var.clone() }, } diff --git a/src/helpers.rs b/src/helpers.rs index 9843691..2965a44 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,6 +1,6 @@ use crate::field_kind::FieldKind; -pub fn has_lifetime_arguments(segments: &[syn::PathSegment]) -> bool { +pub fn has_lifetime_arguments(segments: &[&syn::PathSegment]) -> bool { if let Some(syn::PathArguments::AngleBracketed(generics)) = segments.last().map(|x| &x.arguments) { @@ -13,36 +13,7 @@ pub fn has_lifetime_arguments(segments: &[syn::PathSegment]) -> bool { } } -pub fn number_of_type_arguments(segments: &[syn::PathSegment]) -> usize { - if let Some(syn::PathArguments::AngleBracketed(generics)) = - segments.last().map(|x| &x.arguments) - { - generics - .args - .iter() - .filter(|f| matches!(f, syn::GenericArgument::Type(_))) - .count() - } else { - 0 - } -} - -pub fn has_binding_arguments(segments: &[syn::PathSegment]) -> bool { - if let Some(syn::PathArguments::AngleBracketed(generics)) = - segments.last().map(|x| &x.arguments) - { - generics.args.iter().any(|f| { - matches!( - f, - syn::GenericArgument::AssocConst(_) | syn::GenericArgument::AssocType(_) - ) - }) - } else { - false - } -} - -fn type_hopefully_is(segments: &[syn::PathSegment], expected: &str) -> bool { +fn type_hopefully_is(segments: &[&syn::PathSegment], expected: &str) -> bool { let expected = expected .split("::") .map(|x| quote::format_ident!("{}", x)) @@ -63,106 +34,65 @@ fn type_hopefully_is(segments: &[syn::PathSegment], expected: &str) -> bool { false } -pub fn is_cow(segments: &[syn::PathSegment]) -> bool { - type_hopefully_is(segments, "std::borrow::Cow") -} - -pub fn is_cow_alike(segments: &[syn::PathSegment]) -> bool { - if let Some(syn::PathArguments::AngleBracketed(_data)) = segments.last().map(|x| &x.arguments) { - has_lifetime_arguments(segments) - } else { - false +pub fn cow_field(segments: &[&syn::PathSegment]) -> Option { + if !type_hopefully_is(segments, "std::borrow::Cow") { + return None; } -} - -pub fn collect_segments(path: &syn::Path) -> Vec { - path.segments.iter().cloned().collect::>() -} - -pub fn is_opt_cow(mut segments: Vec) -> Option { - let mut levels = 0; - loop { - if type_hopefully_is(&segments, "std::option::Option") { - if let syn::PathSegment { - arguments: syn::PathArguments::AngleBracketed(ref data), - .. - } = *segments.last().expect("last segment") - { - if has_lifetime_arguments(&segments) || has_binding_arguments(&segments) { - // Option<&'a ?> cannot be moved but let the compiler complain - // don't know about data bindings - break; - } - if number_of_type_arguments(&segments) != 1 { - // Option probably means some other, movable option - break; - } - - match *data.args.first().expect("first arg") { - syn::GenericArgument::Type(syn::Type::Path(syn::TypePath { - // segments: ref next_segments, - ref path, - .. - })) => { - levels += 1; - segments = collect_segments(path); - continue; - } - _ => break, - } - } - } else if is_cow(&segments) { - return Some(FieldKind::OptField(levels, Box::new(FieldKind::PlainCow))); - } else if is_cow_alike(&segments) { - return Some(FieldKind::OptField(levels, Box::new(FieldKind::AssumedCow))); - } + let syn::PathSegment { + arguments: syn::PathArguments::AngleBracketed(data), + .. + } = segments.last().expect("last segment") + else { + return None; + }; - break; + if data.args.len() != 2 { + return None; } + let syn::GenericArgument::Lifetime(_) = &data.args[0] else { + return None; + }; + let syn::GenericArgument::Type(arg_type) = &data.args[1] else { + return None; + }; + + Some(FieldKind::resolve(arg_type)) +} - None +pub fn is_cow_alike(segments: &[&syn::PathSegment]) -> bool { + matches!( + segments.last().map(|x| &x.arguments), + Some(syn::PathArguments::AngleBracketed(_)) + ) && has_lifetime_arguments(segments) } -pub fn is_iter_field(mut segments: Vec) -> Option { - loop { - // this should be easy to do for arrays as well.. - if type_hopefully_is(&segments, "std::vec::Vec") { - if let syn::PathSegment { - arguments: syn::PathArguments::AngleBracketed(ref data), - .. - } = *segments.last().expect("last segment") - { - if has_lifetime_arguments(&segments) || has_binding_arguments(&segments) { - break; - } +pub fn collect_segments(path: &syn::Path) -> Vec<&syn::PathSegment> { + path.segments.iter().collect::>() +} - // if data.types.len() != 1 { - if number_of_type_arguments(&segments) != 1 { - // TODO: this could be something like Vec<(u32, Bar<'a>)>? - break; - } +/// Checks for a given type with a single generic type argument +/// +/// Examples for such types are [Option] and [Vec]. +pub fn generic_field(segments: &[&syn::PathSegment], type_name: &str) -> Option { + if !type_hopefully_is(segments, type_name) { + return None; + } - match *data.args.first().expect("first arg") { - syn::GenericArgument::Type(syn::Type::Path(syn::TypePath { - // segments: ref next_segments, - ref path, - .. - })) => { - segments = collect_segments(path); - continue; - } - _ => break, - } - } - } else if is_cow(&segments) { - return Some(FieldKind::IterableField(Box::new(FieldKind::PlainCow))); - } else if is_cow_alike(&segments) { - return Some(FieldKind::IterableField(Box::new(FieldKind::AssumedCow))); - } + let syn::PathSegment { + arguments: syn::PathArguments::AngleBracketed(data), + .. + } = segments.last().expect("last segment") + else { + return None; + }; - break; + if data.args.len() != 1 { + return None; } + let syn::GenericArgument::Type(arg_type) = &data.args[0] else { + return None; + }; - None + Some(FieldKind::resolve(arg_type)) } diff --git a/src/lib.rs b/src/lib.rs index a2de8c6..da8865a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -292,13 +292,44 @@ impl BodyGenerator for BorrowedGen { } fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream { - let fields = data.fields.iter().map(|field| { - let ident = field.ident.as_ref().expect("this fields has no ident (4)"); - let field_ref = quote! { self.#ident }; - let code = FieldKind::resolve(&field.ty).borrow_or_clone(&field_ref); - quote! { #ident: #code } - }); - quote! { { #(#fields),* } } + // Helper ternary to avoid Option + enum Fields { + Named, + Tuple, + Unit, + } + + use Fields::*; + + let fields_kind = data + .fields + .iter() + .next() + .map(|field| if field.ident.is_some() { Named } else { Tuple }) + .unwrap_or(Unit); + + match fields_kind { + Named => { + let fields = data.fields.iter().map(|field| { + let ident = field.ident.as_ref().expect("unexpected unnamed field"); + let field_ref = quote! { (&self.#ident) }; + let code = FieldKind::resolve(&field.ty).borrow_or_clone(&field_ref); + quote! { #ident: #code } + }); + quote! { { #(#fields),* } } + } + Tuple => { + let fields = data.fields.iter().enumerate().map(|(index, field)| { + let index = syn::Index::from(index); + let index = quote! { (&self.#index) }; + FieldKind::resolve(&field.ty).borrow_or_clone(&index) + }); + quote! { ( #(#fields),* ) } + } + Unit => { + quote! {} + } + } } fn visit_enum_data( diff --git a/tests/array.rs b/tests/array.rs new file mode 100644 index 0000000..a61b10e --- /dev/null +++ b/tests/array.rs @@ -0,0 +1,31 @@ +#[macro_use] +extern crate derive_into_owned; + +use std::borrow::Cow; + +#[derive(IntoOwned, Borrowed)] +struct Array<'a> { + a: [Cow<'a, str>; 2], +} + +#[test] +fn array() { + let val = Array { + a: [Cow::Owned("str".to_owned()), Cow::Borrowed("str")], + }; + let owned = val.into_owned(); + + test_static(&owned); + + let borrowed = owned.borrowed(); + // owned cannot be moved while borrowed exists + test_borrowed(&owned, borrowed); +} + +fn test_static(_s: &Array<'static>) {} + +fn test_borrowed<'b, 'a: 'b>(lives_longer: &Array<'a>, lives_less: Array<'b>) { + drop(lives_less); + #[allow(dropping_references)] + drop(lives_longer); +} diff --git a/tests/many_fields.rs b/tests/many_fields.rs new file mode 100644 index 0000000..f1c0731 --- /dev/null +++ b/tests/many_fields.rs @@ -0,0 +1,27 @@ +#[macro_use] +extern crate derive_into_owned; + +use std::borrow::Cow; + +#[allow(dead_code, clippy::redundant_allocation)] +#[derive(IntoOwned, Borrowed)] +struct TestTypes<'a> { + a: Box>, + b: Box, + c: Option, + d: Vec, + e: Option>, + f: Option>>, + g: Box>, + h: String, + i: (String, String), + j: (), + k: Option<(i32, String)>, + l: Option<(i32, Vec, Cow<'a, str>)>, + m: Box<(i32, String, Vec>)>, + n: Vec<(i32, Option, Option>)>, + o: ((), ()), + p: (String, (String, (String, String))), + #[allow(clippy::type_complexity)] + q: (String, (String, (String, Box>))), +} diff --git a/tests/nested_types.rs b/tests/nested_types.rs new file mode 100644 index 0000000..6dba84d --- /dev/null +++ b/tests/nested_types.rs @@ -0,0 +1,32 @@ +#[macro_use] +extern crate derive_into_owned; + +use std::borrow::Cow; + +#[derive(IntoOwned, Borrowed)] +struct NestedTypes<'a> { + #[allow(clippy::type_complexity)] + a: Vec>>>>>, +} + +#[test] +fn triple_cow() { + let val = NestedTypes { + a: vec![Some(Cow::Owned(Some(Box::new(Cow::Borrowed("str")))))], + }; + let owned = val.into_owned(); + + test_static(&owned); + + let borrowed = owned.borrowed(); + // owned cannot be moved while borrowed exists + test_borrowed(&owned, borrowed); +} + +fn test_static(_s: &NestedTypes<'static>) {} + +fn test_borrowed<'b, 'a: 'b>(lives_longer: &NestedTypes<'a>, lives_less: NestedTypes<'b>) { + drop(lives_less); + #[allow(dropping_references)] + drop(lives_longer); +} diff --git a/tests/recursive.rs b/tests/recursive.rs new file mode 100644 index 0000000..48935cc --- /dev/null +++ b/tests/recursive.rs @@ -0,0 +1,30 @@ +use std::borrow::Cow; + +#[macro_use] +extern crate derive_into_owned; + +#[derive(IntoOwned, Borrowed)] +enum Rec<'a> { + Rec(Option>>), + Cow(Cow<'a, str>), +} + +#[test] +fn rec() { + let val = Rec::Rec(Some(Box::new(Rec::Cow(Cow::Borrowed("str"))))); + let owned = val.into_owned(); + + test_static(&owned); + + let borrowed = owned.borrowed(); + // owned cannot be moved while borrowed exists + test_borrowed(&owned, borrowed); +} + +fn test_static(_s: &Rec<'static>) {} + +fn test_borrowed<'b, 'a: 'b>(lives_longer: &Rec<'a>, lives_less: Rec<'b>) { + drop(lives_less); + #[allow(dropping_references)] + drop(lives_longer); +} diff --git a/tests/triple_cow.rs b/tests/triple_cow.rs new file mode 100644 index 0000000..f261cf6 --- /dev/null +++ b/tests/triple_cow.rs @@ -0,0 +1,28 @@ +#[macro_use] +extern crate derive_into_owned; + +use std::borrow::Cow; + +// Note: Borrowed currently can't be derived for a type with multiple dependent +// lifetimes - there is no easy way to determine whether multiple lifetimes +// on an inner type are used for separate fields, or nested like here. At the +// moment, it is assumed that the lifetimes are indepentent. + +#[derive(IntoOwned)] +struct TripleCow<'a, 'b, 'c> { + a: Cow<'a, Cow<'b, Cow<'c, str>>>, +} + +#[test] +fn triple_cow() { + let inner1 = Cow::Owned("str".to_owned()); + let inner2 = Cow::Borrowed(&inner1); + let val = TripleCow { + a: Cow::Borrowed(&inner2), + }; + let owned = val.into_owned(); + + test_static(&owned); +} + +fn test_static(_s: &TripleCow<'static, 'static, 'static>) {} diff --git a/tests/tuple.rs b/tests/tuple.rs new file mode 100644 index 0000000..2046dc6 --- /dev/null +++ b/tests/tuple.rs @@ -0,0 +1,33 @@ +#[macro_use] +extern crate derive_into_owned; + +use std::borrow::Cow; + +#[derive(IntoOwned, Borrowed)] +struct Tuple<'a, 'b, 'c> { + a: (Cow<'a, str>, Option>), + b: (Cow<'c, str>,), +} + +#[test] +fn tuple() { + let val = Tuple { + a: (Cow::Owned("str".to_owned()), None), + b: (Cow::Borrowed("str"),), + }; + let owned = val.into_owned(); + + test_static(&owned); + + let borrowed = owned.borrowed(); + // owned cannot be moved while borrowed exists + test_borrowed(&owned, borrowed); +} + +fn test_static(_s: &Tuple<'static, 'static, 'static>) {} + +fn test_borrowed<'b, 'a: 'b>(lives_longer: &Tuple<'a, 'a, 'a>, lives_less: Tuple<'b, 'b, 'b>) { + drop(lives_less); + #[allow(dropping_references)] + drop(lives_longer); +} diff --git a/tests/tuple_struct.rs b/tests/tuple_struct.rs index 05e7467..a95b73b 100644 --- a/tests/tuple_struct.rs +++ b/tests/tuple_struct.rs @@ -3,22 +3,21 @@ #[macro_use] extern crate derive_into_owned; -use std::borrow; -use std::borrow::Cow; +use std::borrow::{self, Cow}; -#[derive(IntoOwned)] +#[derive(IntoOwned, Borrowed)] struct Foo<'a>(Cow<'a, str>); -#[derive(IntoOwned)] +#[derive(IntoOwned, Borrowed)] struct FooExtraFields<'a>(u32, Cow<'a, str>, bool, Vec); -#[derive(IntoOwned)] +#[derive(IntoOwned, Borrowed)] struct Bar<'a>(::std::borrow::Cow<'a, str>); -#[derive(IntoOwned)] +#[derive(IntoOwned, Borrowed)] struct Car<'a>(std::borrow::Cow<'a, str>); -#[derive(IntoOwned)] +#[derive(IntoOwned, Borrowed)] struct Dar<'a>(borrow::Cow<'a, str>); #[test] @@ -26,10 +25,19 @@ fn tuple_struct() { let non_static_string: String = "foobar".to_string(); let thing = Foo(Cow::Borrowed(&non_static_string)); + let owned = thing.into_owned(); - accepts_only_static(thing.into_owned()); + accepts_only_static(&owned); + + let borrowed = owned.borrowed(); + // owned cannot be moved while borrowed exists + test_borrowed(&owned, borrowed); } -fn accepts_only_static(static_foo: Foo<'static>) { - drop(static_foo); +fn accepts_only_static(_static_foo: &Foo<'static>) {} + +fn test_borrowed<'b, 'a: 'b>(lives_longer: &Foo<'a>, lives_less: Foo<'b>) { + drop(lives_less); + #[allow(dropping_references)] + drop(lives_longer); } diff --git a/tests/unit_struct.rs b/tests/unit_struct.rs index 320c1d9..10cdfb2 100644 --- a/tests/unit_struct.rs +++ b/tests/unit_struct.rs @@ -1,4 +1,7 @@ #![allow(unused)] -#[derive(derive_into_owned::IntoOwned)] +#[macro_use] +extern crate derive_into_owned; + +#[derive(IntoOwned, Borrowed)] struct Far;