diff --git a/CHANGELOG.md b/CHANGELOG.md index 6666686..5add02c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Support `#[darling(with = ...)]` on the `data` field when deriving `FromDeriveInput`. This allows the use of simpler receiver types, such as a `Vec` of enum variants. - Bump version of `proc-macro2` to 1.0.86. +- Accept closures for `#[darling(with = ...)]` on fields in `FromDeriveInput`, `FromMeta`, `FromField`, etc. [#309](https://github.com/TedDriggs/darling/issues/309) ## v0.20.10 (July 9, 2024) diff --git a/Cargo.toml b/Cargo.toml index 5ac3de0..d474d43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ syn = "2.0.15" [target.'cfg(compiletests)'.dev-dependencies] rustversion = "1.0.9" -trybuild = "1.0.38" +trybuild = "1.0.89" [features] default = ["suggestions"] diff --git a/compiletests.sh b/compiletests.sh index a8ce042..52dbc0e 100755 --- a/compiletests.sh +++ b/compiletests.sh @@ -1 +1 @@ -RUSTFLAGS="--cfg=compiletests" cargo +1.65.0 test --test compiletests \ No newline at end of file +RUSTFLAGS="--cfg=compiletests" cargo +1.77.0 test --test compiletests \ No newline at end of file diff --git a/core/src/codegen/field.rs b/core/src/codegen/field.rs index d4f9369..a666422 100644 --- a/core/src/codegen/field.rs +++ b/core/src/codegen/field.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens, TokenStreamExt}; -use syn::{spanned::Spanned, Ident, Path, Type}; +use syn::{spanned::Spanned, Ident, Type}; use crate::codegen::{DefaultExpression, PostfixTransform}; use crate::usage::{self, IdentRefSet, IdentSet, UsesTypeParams}; @@ -22,7 +22,10 @@ pub struct Field<'a> { /// The type of the field in the input. pub ty: &'a Type, pub default_expression: Option>, - pub with_path: Cow<'a, Path>, + /// An expression that will be wrapped in a call to [`core::convert::identity`] and + /// then used for converting a provided value into the field value _before_ postfix + /// transforms are called. + pub with_callable: Cow<'a, syn::Expr>, pub post_transform: Option<&'a PostfixTransform>, pub skip: bool, pub multiple: bool, @@ -156,7 +159,7 @@ impl<'a> ToTokens for MatchArm<'a> { let name_str = &field.name_in_attr; let ident = field.ident; - let with_path = &field.with_path; + let with_callable = &field.with_callable; let post_transform = field.post_transform.as_ref(); // Errors include the location of the bad input, so we compute that here. @@ -171,7 +174,7 @@ impl<'a> ToTokens for MatchArm<'a> { quote!(#name_str) }; - // Give darling's generated code the span of the `with_path` so that if the target + // Give darling's generated code the span of the `with_callable` so that if the target // type doesn't impl FromMeta, darling's immediate user gets a properly-spanned error. // // Within the generated code, add the span immediately on extraction failure, so that it's @@ -179,7 +182,11 @@ impl<'a> ToTokens for MatchArm<'a> { // The behavior of `with_span` makes this safe to do; if the child applied an // even-more-specific span, our attempt here will not overwrite that and will only cost // us one `if` check. - let extractor = quote_spanned!(with_path.span()=>#with_path(__inner)#post_transform.map_err(|e| e.with_span(&__inner).at(#location))); + let extractor = quote_spanned!(with_callable.span()=> + ::darling::export::identity:: ::darling::Result<_>>(#with_callable)(__inner) + #post_transform + .map_err(|e| e.with_span(&__inner).at(#location)) + ); tokens.append_all(if field.multiple { quote!( diff --git a/core/src/options/input_field.rs b/core/src/options/input_field.rs index 9b688d8..6569b83 100644 --- a/core/src/options/input_field.rs +++ b/core/src/options/input_field.rs @@ -13,7 +13,7 @@ pub struct InputField { pub attr_name: Option, pub ty: syn::Type, pub default: Option, - pub with: Option, + pub with: Option, /// If `true`, generated code will not look for this field in the input meta item, /// instead always falling back to either `InputField::default` or `Default::default`. @@ -34,7 +34,7 @@ impl InputField { .map_or_else(|| Cow::Owned(self.ident.to_string()), Cow::Borrowed), ty: &self.ty, default_expression: self.as_codegen_default(), - with_path: self.with.as_ref().map_or_else( + with_callable: self.with.as_ref().map(|w| &w.call).map_or_else( || { Cow::Owned( parse_quote_spanned!(self.ty.span()=> ::darling::FromMeta::from_meta), @@ -149,7 +149,7 @@ impl ParseAttribute for InputField { return Err(Error::duplicate_field_path(path).with_span(mi)); } - self.with = Some(FromMeta::from_meta(mi)?); + self.with = Some(With::from_meta(mi)?); if self.flatten.is_present() { return Err( @@ -239,3 +239,24 @@ impl ParseAttribute for InputField { Ok(()) } } + +#[derive(Debug, Clone)] +pub struct With { + /// The callable + call: syn::Expr, +} + +impl With { + pub fn from_meta(meta: &syn::Meta) -> Result { + if let syn::Meta::NameValue(nv) = meta { + match &nv.value { + syn::Expr::Path(_) | syn::Expr::Closure(_) => Ok(Self { + call: nv.value.clone(), + }), + _ => Err(Error::unexpected_expr_type(&nv.value)), + } + } else { + Err(Error::unsupported_format("non-value")) + } + } +} diff --git a/examples/expr_with.rs b/examples/expr_with.rs index 037ce25..ed20e4f 100644 --- a/examples/expr_with.rs +++ b/examples/expr_with.rs @@ -1,4 +1,4 @@ -use darling::{util::parse_expr, FromDeriveInput}; +use darling::{util::parse_expr, FromDeriveInput, FromMeta}; use syn::{parse_quote, Expr}; #[derive(FromDeriveInput)] @@ -6,14 +6,21 @@ use syn::{parse_quote, Expr}; pub struct Receiver { #[darling(with = parse_expr::preserve_str_literal, map = Some)] example1: Option, + #[darling( + // A closure can be used in lieu of a path. + with = |m| Ok(String::from_meta(m)?.to_uppercase()), + default + )] + example2: String, } fn main() { let input = Receiver::from_derive_input(&parse_quote! { - #[demo(example1 = test::path)] + #[demo(example1 = test::path, example2 = "hello")] struct Example; }) .unwrap(); assert_eq!(input.example1, Some(parse_quote!(test::path))); + assert_eq!(input.example2, "HELLO".to_string()); } diff --git a/src/lib.rs b/src/lib.rs index 42004f3..f9ba0ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,7 +94,7 @@ pub use darling_core::ToTokens; /// of the referenced types. #[doc(hidden)] pub mod export { - pub use core::convert::From; + pub use core::convert::{identity, From}; pub use core::default::Default; pub use core::option::Option::{self, None, Some}; pub use core::result::Result::{self, Err, Ok}; diff --git a/tests/compile-fail/attrs_with_bad_fn.stderr b/tests/compile-fail/attrs_with_bad_fn.stderr index a1b2672..c3d7906 100644 --- a/tests/compile-fail/attrs_with_bad_fn.stderr +++ b/tests/compile-fail/attrs_with_bad_fn.stderr @@ -15,6 +15,6 @@ note: method defined here | pub fn handle(&mut self, result: Result) -> Option { | ^^^^^^ help: try wrapping the expression in `Ok` - | -11 | #[darling(with = Ok(bad_converter))] - | +++ + + | +11 | #[darling(with = Ok(bad_converter))] + | +++ + diff --git a/tests/compile-fail/with_closure_capture.rs b/tests/compile-fail/with_closure_capture.rs new file mode 100644 index 0000000..8fada99 --- /dev/null +++ b/tests/compile-fail/with_closure_capture.rs @@ -0,0 +1,21 @@ +use darling::{FromDeriveInput, FromMeta}; + +#[derive(FromDeriveInput)] +#[darling(attributes(demo))] +pub struct Receiver { + example1: String, + #[darling( + // This should fail because `example1` is a local that's been captured + // from the `FromDeriveInput` impl. That's disallowed because exposing + // those internals would make any change to the derived method body a + // potentially-breaking change. + with = |m| Ok( + String::from_meta(m)?.to_uppercase() + + example1.1.as_ref().map(|s| s.as_str()).unwrap_or("") + ), + default + )] + example2: String, +} + +fn main() {} diff --git a/tests/compile-fail/with_closure_capture.stderr b/tests/compile-fail/with_closure_capture.stderr new file mode 100644 index 0000000..fc36752 --- /dev/null +++ b/tests/compile-fail/with_closure_capture.stderr @@ -0,0 +1,30 @@ +error[E0308]: mismatched types + --> tests/compile-fail/with_closure_capture.rs:12:16 + | +12 | with = |m| Ok( + | ^ arguments to this function are incorrect + | ________________| + | | +13 | | String::from_meta(m)?.to_uppercase() +14 | | + example1.1.as_ref().map(|s| s.as_str()).unwrap_or("") +15 | | ), + | |_________^ expected fn pointer, found closure + | + = note: expected fn pointer `for<'a> fn(&'a syn::Meta) -> Result` + found closure `{closure@$DIR/tests/compile-fail/with_closure_capture.rs:12:16: 12:19}` +note: closures can only be coerced to `fn` types if they do not capture any variables + --> tests/compile-fail/with_closure_capture.rs:14:15 + | +14 | + example1.1.as_ref().map(|s| s.as_str()).unwrap_or("") + | ^^^^^^^^ `example1` captured here +help: the return type of this call is `{closure@$DIR/tests/compile-fail/with_closure_capture.rs:12:16: 12:19}` due to the type of the argument passed + --> tests/compile-fail/with_closure_capture.rs:12:16 + | +12 | with = |m| Ok( + | ________________^ +13 | | String::from_meta(m)?.to_uppercase() +14 | | + example1.1.as_ref().map(|s| s.as_str()).unwrap_or("") +15 | | ), + | |_________- this argument influences the return type of `{{root}}` +note: function defined here + --> $RUST/core/src/convert/mod.rs diff --git a/tests/meta_with.rs b/tests/meta_with.rs new file mode 100644 index 0000000..d4f2821 --- /dev/null +++ b/tests/meta_with.rs @@ -0,0 +1,33 @@ +use darling::{util::parse_expr, FromDeriveInput, FromMeta}; +use syn::{parse_quote, Expr}; + +#[derive(FromDeriveInput)] +#[darling(attributes(demo))] +pub struct Receiver { + #[darling(with = parse_expr::preserve_str_literal, map = Some)] + example1: Option, + #[darling( + with = |m| Ok(String::from_meta(m)?.to_uppercase()), + map = Some + )] + example2: Option, + // This is deliberately strange - it keeps the field name, and ignores + // the rest of the attribute. In normal operation, this is strongly discouraged. + // It's used here to verify that the parameter type is known even if it can't be + // inferred from usage within the closure. + #[darling(with = |m| Ok(m.path().clone()))] + example3: syn::Path, +} + +#[test] +fn handles_all_cases() { + let input = Receiver::from_derive_input(&parse_quote! { + #[demo(example1 = test::path, example2 = "hello", example3)] + struct Example; + }) + .unwrap(); + + assert_eq!(input.example1, Some(parse_quote!(test::path))); + assert_eq!(input.example2, Some("HELLO".to_string())); + assert_eq!(input.example3, parse_quote!(example3)); +}