From 4a19407275e05c0dec0560c72093f9a6716994c5 Mon Sep 17 00:00:00 2001 From: Ted Driggs Date: Fri, 23 Feb 2024 07:50:04 -0800 Subject: [PATCH] Support `with` on attrs magic field This allows the `attrs` magic field to have any type, rather than being limited to `Vec`. Fixes #273 --- CHANGELOG.md | 4 + README.md | 1 + core/src/codegen/attr_extractor.rs | 45 ++++---- core/src/codegen/attrs_field.rs | 107 ++++++++++++++++++++ core/src/codegen/from_attributes_impl.rs | 8 +- core/src/codegen/from_derive_impl.rs | 13 +-- core/src/codegen/from_field.rs | 12 +-- core/src/codegen/from_type_param.rs | 12 +-- core/src/codegen/from_variant_impl.rs | 17 ++-- core/src/codegen/mod.rs | 2 + core/src/options/forward_attrs.rs | 59 +++++++++-- core/src/options/from_attributes.rs | 1 + core/src/options/from_derive.rs | 3 +- core/src/options/from_field.rs | 3 +- core/src/options/from_type_param.rs | 3 +- core/src/options/from_variant.rs | 3 +- core/src/options/mod.rs | 2 +- core/src/options/outer_from.rs | 20 +++- tests/attrs_with.rs | 80 +++++++++++++++ tests/compile-fail/attrs_with_bad_fn.rs | 15 +++ tests/compile-fail/attrs_with_bad_fn.stderr | 20 ++++ 21 files changed, 347 insertions(+), 83 deletions(-) create mode 100644 core/src/codegen/attrs_field.rs create mode 100644 tests/attrs_with.rs create mode 100644 tests/compile-fail/attrs_with_bad_fn.rs create mode 100644 tests/compile-fail/attrs_with_bad_fn.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ba98c6..35ea4187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add `#[darling(with = ...)]` support to `attrs` magic field to allow using custom receiver types for `attrs` [#273](https://github.com/TedDriggs/darling/issues/273) + ## v0.20.7 (February 22, 2024) - Add `#[darling(flatten)]` to allow forwarding unknown fields to another struct [#146](https://github.com/TedDriggs/darling/issues/146) diff --git a/README.md b/README.md index b0866d46..023e8adc 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Darling's features are built to work well for real-world projects. Additionally, `Option` and `darling::util::Flag` fields are innately optional; you don't need to declare `#[darling(default)]` for those. - **Field Renaming**: Fields can have different names in usage vs. the backing code. - **Auto-populated fields**: Structs deriving `FromDeriveInput` and `FromField` can declare properties named `ident`, `vis`, `ty`, `attrs`, and `generics` to automatically get copies of the matching values from the input AST. `FromDeriveInput` additionally exposes `data` to get access to the body of the deriving type, and `FromVariant` exposes `fields`. + - **Transformation of forwarded attributes**: You can add `#[darling(with=path)]` to the `attrs` field to use a custom function to transform the forwarded attributes before they're provided to your struct. The function signature is `fn(Vec) -> darling::Result`, where `T` is the type you declared for the `attrs` field. Returning an error from this function will propagate with all other parsing errors. - **Mapping function**: Use `#[darling(map="path")]` or `#[darling(and_then="path")]` to specify a function that runs on the result of parsing a meta-item field. This can change the return type, which enables you to parse to an intermediate form and convert that to the type you need in your struct. - **Skip fields**: Use `#[darling(skip)]` to mark a field that shouldn't be read from attribute meta-items. - **Multiple-occurrence fields**: Use `#[darling(multiple)]` on a `Vec` field to allow that field to appear multiple times in the meta-item. Each occurrence will be pushed into the `Vec`. diff --git a/core/src/codegen/attr_extractor.rs b/core/src/codegen/attr_extractor.rs index 4afb174b..612c28f1 100644 --- a/core/src/codegen/attr_extractor.rs +++ b/core/src/codegen/attr_extractor.rs @@ -1,9 +1,10 @@ use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, ToTokens}; -use crate::options::ForwardAttrs; use crate::util::PathList; +use super::ForwardAttrs; + /// Infrastructure for generating an attribute extractor. pub trait ExtractAttribute { /// A set of mutable declarations for all members of the implementing type. @@ -12,7 +13,7 @@ pub trait ExtractAttribute { /// Gets the list of attribute names that should be parsed by the extractor. fn attr_names(&self) -> &PathList; - fn forwarded_attrs(&self) -> Option<&ForwardAttrs>; + fn forward_attrs(&self) -> &ForwardAttrs; /// Gets the name used by the generated impl to return to the `syn` item passed as input. fn param_name(&self) -> TokenStream; @@ -30,13 +31,16 @@ pub trait ExtractAttribute { /// Generates the main extraction loop. fn extractor(&self) -> TokenStream { - let declarations = self.local_declarations(); + let mut declarations = self.local_declarations(); + self.forward_attrs() + .as_declaration() + .to_tokens(&mut declarations); let will_parse_any = !self.attr_names().is_empty(); - let will_fwd_any = self - .forwarded_attrs() - .map(|fa| !fa.is_empty()) - .unwrap_or_default(); + + // Forwarding requires both that there be some items we would forward, + // and a place that will keep the forwarded items. + let will_fwd_any = self.forward_attrs().will_forward_any(); if !(will_parse_any || will_fwd_any) { return quote! { @@ -82,18 +86,15 @@ pub trait ExtractAttribute { quote!() }; + let fwd_population = self.forward_attrs().as_value_populator(); + // Specifies the behavior for unhandled attributes. They will either be silently ignored or // forwarded to the inner struct for later analysis. - let forward_unhandled = if will_fwd_any { - forwards_to_local(self.forwarded_attrs().unwrap()) - } else { - quote!(_ => continue) - }; + let forward_unhandled = self.forward_attrs().as_match_arms(); quote!( #declarations use ::darling::ToTokens; - let mut __fwd_attrs: ::darling::export::Vec<::darling::export::syn::Attribute> = vec![]; for __attr in #attrs_accessor { // Filter attributes based on name @@ -102,20 +103,8 @@ pub trait ExtractAttribute { #forward_unhandled } } - ) - } -} -fn forwards_to_local(behavior: &ForwardAttrs) -> TokenStream { - let push_command = quote!(__fwd_attrs.push(__attr.clone())); - match *behavior { - ForwardAttrs::All => quote!(_ => #push_command), - ForwardAttrs::Only(ref idents) => { - let names = idents.to_strings(); - quote!( - #(#names)|* => #push_command, - _ => continue, - ) - } + #fwd_population + ) } } diff --git a/core/src/codegen/attrs_field.rs b/core/src/codegen/attrs_field.rs new file mode 100644 index 00000000..bbd43f84 --- /dev/null +++ b/core/src/codegen/attrs_field.rs @@ -0,0 +1,107 @@ +use quote::{quote, quote_spanned, ToTokens, TokenStreamExt}; +use syn::spanned::Spanned; + +use crate::options::{AttrsField, ForwardAttrsFilter}; + +#[derive(Default)] +pub struct ForwardAttrs<'a> { + pub filter: Option<&'a ForwardAttrsFilter>, + pub field: Option<&'a AttrsField>, +} + +impl ForwardAttrs<'_> { + /// Check if this will forward any attributes; this requires both that + /// there be a filter which can match some attributes and a field to receive them. + pub fn will_forward_any(&self) -> bool { + if let Some(filter) = self.filter { + !filter.is_empty() && self.field.is_some() + } else { + false + } + } + + /// Get the field declarations to support attribute forwarding + pub fn as_declaration(&self) -> Option { + self.field.map(Declaration) + } + + /// Get the match arms for attribute matching + pub fn as_match_arms(&self) -> MatchArms { + MatchArms(self) + } + + /// Get the statement that will try to transform forwarded attributes into + /// the result expected by the receiver field. + pub fn as_value_populator(&self) -> Option { + self.field.map(ValuePopulator) + } + + /// Get the field initializer for use when building the deriving struct. + pub fn as_initializer(&self) -> Option { + self.field.map(Initializer) + } +} + +pub struct Declaration<'a>(pub &'a AttrsField); + +impl ToTokens for Declaration<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ident = &self.0.ident; + tokens.append_all(quote! { + let mut __fwd_attrs: ::darling::export::Vec<::darling::export::syn::Attribute> = vec![]; + let mut #ident: ::darling::export::Option<_> = None; + }); + } +} + +pub struct ValuePopulator<'a>(pub &'a AttrsField); + +impl ToTokens for ValuePopulator<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let AttrsField { ident, with } = self.0; + let initializer_expr = match with { + Some(with) => quote_spanned!(with.span()=> __errors.handle(#with(__fwd_attrs))), + None => quote!(::darling::export::Some(__fwd_attrs)), + }; + tokens.append_all(quote!(#ident = #initializer_expr;)); + } +} + +pub struct Initializer<'a>(pub &'a AttrsField); + +impl ToTokens for Initializer<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ident = &self.0.ident; + tokens.append_all(quote!(#ident: #ident.expect("Errors were already checked"),)); + } +} + +pub struct MatchArms<'a>(&'a ForwardAttrs<'a>); + +impl ToTokens for MatchArms<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + if !self.0.will_forward_any() { + tokens.append_all(quote!(_ => continue)); + return; + } + + let push_command = quote!(__fwd_attrs.push(__attr.clone())); + + tokens.append_all( + match self + .0 + .filter + .expect("Can only forward attributes if filter is defined") + { + ForwardAttrsFilter::All => quote!(_ => #push_command), + ForwardAttrsFilter::Only(idents) => { + let names = idents.to_strings(); + quote! { + #(#names)|* => #push_command, + _ => continue, + } + } + }, + ); + } +} diff --git a/core/src/codegen/from_attributes_impl.rs b/core/src/codegen/from_attributes_impl.rs index 49bbf02c..72da3497 100644 --- a/core/src/codegen/from_attributes_impl.rs +++ b/core/src/codegen/from_attributes_impl.rs @@ -4,13 +4,15 @@ use quote::{quote, ToTokens}; use crate::{ ast::Data, codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}, - options::ForwardAttrs, util::PathList, }; +use super::ForwardAttrs; + pub struct FromAttributesImpl<'a> { pub base: TraitImpl<'a>, pub attr_names: &'a PathList, + pub forward_attrs: ForwardAttrs<'a>, } impl ToTokens for FromAttributesImpl<'_> { @@ -77,8 +79,8 @@ impl<'a> ExtractAttribute for FromAttributesImpl<'a> { self.attr_names } - fn forwarded_attrs(&self) -> Option<&ForwardAttrs> { - None + fn forward_attrs(&self) -> &super::ForwardAttrs { + &self.forward_attrs } fn param_name(&self) -> TokenStream { diff --git a/core/src/codegen/from_derive_impl.rs b/core/src/codegen/from_derive_impl.rs index 1c1df63d..d64d9e27 100644 --- a/core/src/codegen/from_derive_impl.rs +++ b/core/src/codegen/from_derive_impl.rs @@ -5,19 +5,20 @@ use syn::Ident; use crate::{ ast::Data, codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}, - options::{DeriveInputShapeSet, ForwardAttrs}, + options::DeriveInputShapeSet, util::PathList, }; +use super::ForwardAttrs; + pub struct FromDeriveInputImpl<'a> { pub ident: Option<&'a Ident>, pub generics: Option<&'a Ident>, pub vis: Option<&'a Ident>, - pub attrs: Option<&'a Ident>, pub data: Option<&'a Ident>, pub base: TraitImpl<'a>, pub attr_names: &'a PathList, - pub forward_attrs: Option<&'a ForwardAttrs>, + pub forward_attrs: ForwardAttrs<'a>, pub from_ident: bool, pub supports: Option<&'a DeriveInputShapeSet>, } @@ -54,7 +55,7 @@ impl<'a> ToTokens for FromDeriveInputImpl<'a> { .generics .as_ref() .map(|i| quote!(#i: ::darling::FromGenerics::from_generics(&#input.generics)?,)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.forward_attrs.as_initializer(); let passed_body = self .data .as_ref() @@ -115,8 +116,8 @@ impl<'a> ExtractAttribute for FromDeriveInputImpl<'a> { self.attr_names } - fn forwarded_attrs(&self) -> Option<&ForwardAttrs> { - self.forward_attrs + fn forward_attrs(&self) -> &ForwardAttrs { + &self.forward_attrs } fn param_name(&self) -> TokenStream { diff --git a/core/src/codegen/from_field.rs b/core/src/codegen/from_field.rs index 74f34060..5bd73506 100644 --- a/core/src/codegen/from_field.rs +++ b/core/src/codegen/from_field.rs @@ -4,20 +4,20 @@ use syn::Ident; use crate::{ codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}, - options::ForwardAttrs, util::PathList, }; +use super::ForwardAttrs; + /// `impl FromField` generator. This is used for parsing an individual /// field and its attributes. pub struct FromFieldImpl<'a> { pub ident: Option<&'a Ident>, pub vis: Option<&'a Ident>, pub ty: Option<&'a Ident>, - pub attrs: Option<&'a Ident>, pub base: TraitImpl<'a>, pub attr_names: &'a PathList, - pub forward_attrs: Option<&'a ForwardAttrs>, + pub forward_attrs: ForwardAttrs<'a>, pub from_ident: bool, } @@ -43,7 +43,7 @@ impl<'a> ToTokens for FromFieldImpl<'a> { .map(|i| quote!(#i: #input.ident.clone(),)); let passed_vis = self.vis.as_ref().map(|i| quote!(#i: #input.vis.clone(),)); let passed_ty = self.ty.as_ref().map(|i| quote!(#i: #input.ty.clone(),)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.forward_attrs.as_initializer(); // Determine which attributes to forward (if any). let grab_attrs = self.extractor(); @@ -82,8 +82,8 @@ impl<'a> ExtractAttribute for FromFieldImpl<'a> { self.attr_names } - fn forwarded_attrs(&self) -> Option<&ForwardAttrs> { - self.forward_attrs + fn forward_attrs(&self) -> &super::ForwardAttrs { + &self.forward_attrs } fn param_name(&self) -> TokenStream { diff --git a/core/src/codegen/from_type_param.rs b/core/src/codegen/from_type_param.rs index 1cce0cad..d44e8eb5 100644 --- a/core/src/codegen/from_type_param.rs +++ b/core/src/codegen/from_type_param.rs @@ -2,18 +2,16 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::Ident; -use crate::codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}; -use crate::options::ForwardAttrs; +use crate::codegen::{ExtractAttribute, ForwardAttrs, OuterFromImpl, TraitImpl}; use crate::util::PathList; pub struct FromTypeParamImpl<'a> { pub base: TraitImpl<'a>, pub ident: Option<&'a Ident>, - pub attrs: Option<&'a Ident>, pub bounds: Option<&'a Ident>, pub default: Option<&'a Ident>, pub attr_names: &'a PathList, - pub forward_attrs: Option<&'a ForwardAttrs>, + pub forward_attrs: ForwardAttrs<'a>, pub from_ident: bool, } @@ -36,7 +34,7 @@ impl<'a> ToTokens for FromTypeParamImpl<'a> { .ident .as_ref() .map(|i| quote!(#i: #input.ident.clone(),)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.forward_attrs.as_initializer(); let passed_bounds = self .bounds .as_ref() @@ -81,8 +79,8 @@ impl<'a> ExtractAttribute for FromTypeParamImpl<'a> { self.attr_names } - fn forwarded_attrs(&self) -> Option<&ForwardAttrs> { - self.forward_attrs + fn forward_attrs(&self) -> &ForwardAttrs { + &self.forward_attrs } fn param_name(&self) -> TokenStream { diff --git a/core/src/codegen/from_variant_impl.rs b/core/src/codegen/from_variant_impl.rs index 1598e6f2..a835b135 100644 --- a/core/src/codegen/from_variant_impl.rs +++ b/core/src/codegen/from_variant_impl.rs @@ -2,8 +2,8 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::Ident; -use crate::codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}; -use crate::options::{DataShape, ForwardAttrs}; +use crate::codegen::{ExtractAttribute, ForwardAttrs, OuterFromImpl, TraitImpl}; +use crate::options::DataShape; use crate::util::PathList; pub struct FromVariantImpl<'a> { @@ -19,11 +19,6 @@ pub struct FromVariantImpl<'a> { /// /// This is one of `darling`'s "magic fields". pub fields: Option<&'a Ident>, - /// If set, the ident of the field into which the forwarded attributes of the input - /// variant should be placed. - /// - /// This is one of `darling`'s "magic fields". - pub attrs: Option<&'a Ident>, /// If set, the ident of the field into which the discriminant of the input variant /// should be placed. The receiving field must be an `Option` as not all enums have /// discriminants. @@ -31,7 +26,7 @@ pub struct FromVariantImpl<'a> { /// This is one of `darling`'s "magic fields". pub discriminant: Option<&'a Ident>, pub attr_names: &'a PathList, - pub forward_attrs: Option<&'a ForwardAttrs>, + pub forward_attrs: ForwardAttrs<'a>, pub from_ident: bool, pub supports: Option<&'a DataShape>, } @@ -48,7 +43,7 @@ impl<'a> ToTokens for FromVariantImpl<'a> { .discriminant .as_ref() .map(|i| quote!(#i: #input.discriminant.as_ref().map(|(_, expr)| expr.clone()),)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.forward_attrs.as_initializer(); let passed_fields = self .fields .as_ref() @@ -111,8 +106,8 @@ impl<'a> ExtractAttribute for FromVariantImpl<'a> { self.attr_names } - fn forwarded_attrs(&self) -> Option<&ForwardAttrs> { - self.forward_attrs + fn forward_attrs(&self) -> &ForwardAttrs { + &self.forward_attrs } fn param_name(&self) -> TokenStream { diff --git a/core/src/codegen/mod.rs b/core/src/codegen/mod.rs index d2a1546e..af999b9e 100644 --- a/core/src/codegen/mod.rs +++ b/core/src/codegen/mod.rs @@ -1,4 +1,5 @@ mod attr_extractor; +mod attrs_field; mod default_expr; mod error; mod field; @@ -15,6 +16,7 @@ mod variant; mod variant_data; pub(in crate::codegen) use self::attr_extractor::ExtractAttribute; +pub use self::attrs_field::ForwardAttrs; pub use self::default_expr::DefaultExpression; pub use self::field::Field; pub use self::from_attributes_impl::FromAttributesImpl; diff --git a/core/src/options/forward_attrs.rs b/core/src/options/forward_attrs.rs index ac9f4e1a..abe2d852 100644 --- a/core/src/options/forward_attrs.rs +++ b/core/src/options/forward_attrs.rs @@ -1,30 +1,73 @@ +use proc_macro2::Ident; +use syn::Path; + use crate::ast::NestedMeta; use crate::util::PathList; -use crate::{FromMeta, Result}; +use crate::{Error, FromField, FromMeta, Result}; + +use super::ParseAttribute; + +/// The `attrs` magic field and attributes that influence its behavior. +#[derive(Debug, Clone)] +pub struct AttrsField { + /// The ident of the field that will receive the forwarded attributes. + pub ident: Ident, + /// Path of the function that will be called to convert the `Vec` of + /// forwarded attributes into the type expected by the field in `ident`. + pub with: Option, +} + +impl FromField for AttrsField { + fn from_field(field: &syn::Field) -> crate::Result { + let result = Self { + ident: field.ident.clone().ok_or_else(|| { + Error::custom("attributes receiver must be named field").with_span(field) + })?, + with: None, + }; + + result.parse_attributes(&field.attrs) + } +} + +impl ParseAttribute for AttrsField { + fn parse_nested(&mut self, mi: &syn::Meta) -> crate::Result<()> { + if mi.path().is_ident("with") { + if self.with.is_some() { + return Err(Error::duplicate_field_path(mi.path()).with_span(mi)); + } + + self.with = FromMeta::from_meta(mi)?; + Ok(()) + } else { + Err(Error::unknown_field_path_with_alts(mi.path(), &["with"]).with_span(mi)) + } + } +} /// A rule about which attributes to forward to the generated struct. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum ForwardAttrs { +pub enum ForwardAttrsFilter { All, Only(PathList), } -impl ForwardAttrs { +impl ForwardAttrsFilter { /// Returns `true` if this will not forward any attributes. pub fn is_empty(&self) -> bool { match *self { - ForwardAttrs::All => false, - ForwardAttrs::Only(ref list) => list.is_empty(), + ForwardAttrsFilter::All => false, + ForwardAttrsFilter::Only(ref list) => list.is_empty(), } } } -impl FromMeta for ForwardAttrs { +impl FromMeta for ForwardAttrsFilter { fn from_word() -> Result { - Ok(ForwardAttrs::All) + Ok(ForwardAttrsFilter::All) } fn from_list(nested: &[NestedMeta]) -> Result { - Ok(ForwardAttrs::Only(PathList::from_list(nested)?)) + Ok(ForwardAttrsFilter::Only(PathList::from_list(nested)?)) } } diff --git a/core/src/options/from_attributes.rs b/core/src/options/from_attributes.rs index b99278d6..dc11b29f 100644 --- a/core/src/options/from_attributes.rs +++ b/core/src/options/from_attributes.rs @@ -62,6 +62,7 @@ impl<'a> From<&'a FromAttributesOptions> for FromAttributesImpl<'a> { FromAttributesImpl { base: (&v.base.container).into(), attr_names: &v.base.attr_names, + forward_attrs: Default::default(), } } } diff --git a/core/src/options/from_derive.rs b/core/src/options/from_derive.rs index 6eec1a08..eb276411 100644 --- a/core/src/options/from_derive.rs +++ b/core/src/options/from_derive.rs @@ -84,8 +84,7 @@ impl<'a> From<&'a FdiOptions> for FromDeriveInputImpl<'a> { vis: v.vis.as_ref(), data: v.data.as_ref(), generics: v.generics.as_ref(), - attrs: v.base.attrs.as_ref(), - forward_attrs: v.base.forward_attrs.as_ref(), + forward_attrs: v.base.as_forward_attrs(), supports: v.supports.as_ref(), } } diff --git a/core/src/options/from_field.rs b/core/src/options/from_field.rs index 485a0ad5..6dcd0f60 100644 --- a/core/src/options/from_field.rs +++ b/core/src/options/from_field.rs @@ -61,10 +61,9 @@ impl<'a> From<&'a FromFieldOptions> for FromFieldImpl<'a> { ident: v.base.ident.as_ref(), vis: v.vis.as_ref(), ty: v.ty.as_ref(), - attrs: v.base.attrs.as_ref(), base: (&v.base.container).into(), attr_names: &v.base.attr_names, - forward_attrs: v.base.forward_attrs.as_ref(), + forward_attrs: v.base.as_forward_attrs(), from_ident: v.base.from_ident, } } diff --git a/core/src/options/from_type_param.rs b/core/src/options/from_type_param.rs index d8f3b5bb..e14de998 100644 --- a/core/src/options/from_type_param.rs +++ b/core/src/options/from_type_param.rs @@ -60,11 +60,10 @@ impl<'a> From<&'a FromTypeParamOptions> for FromTypeParamImpl<'a> { FromTypeParamImpl { base: (&v.base.container).into(), ident: v.base.ident.as_ref(), - attrs: v.base.attrs.as_ref(), bounds: v.bounds.as_ref(), default: v.default.as_ref(), attr_names: &v.base.attr_names, - forward_attrs: v.base.forward_attrs.as_ref(), + forward_attrs: v.base.as_forward_attrs(), from_ident: v.base.from_ident, } } diff --git a/core/src/options/from_variant.rs b/core/src/options/from_variant.rs index c260a3ad..c3223c50 100644 --- a/core/src/options/from_variant.rs +++ b/core/src/options/from_variant.rs @@ -36,9 +36,8 @@ impl<'a> From<&'a FromVariantOptions> for FromVariantImpl<'a> { ident: v.base.ident.as_ref(), discriminant: v.discriminant.as_ref(), fields: v.fields.as_ref(), - attrs: v.base.attrs.as_ref(), attr_names: &v.base.attr_names, - forward_attrs: v.base.forward_attrs.as_ref(), + forward_attrs: v.base.as_forward_attrs(), from_ident: v.base.from_ident, supports: v.supports.as_ref(), } diff --git a/core/src/options/mod.rs b/core/src/options/mod.rs index cb43ee76..c00e0f15 100644 --- a/core/src/options/mod.rs +++ b/core/src/options/mod.rs @@ -19,7 +19,7 @@ mod outer_from; mod shape; pub use self::core::Core; -pub use self::forward_attrs::ForwardAttrs; +pub use self::forward_attrs::{AttrsField, ForwardAttrsFilter}; pub use self::from_attributes::FromAttributesOptions; pub use self::from_derive::FdiOptions; pub use self::from_field::FromFieldOptions; diff --git a/core/src/options/outer_from.rs b/core/src/options/outer_from.rs index e3a4b471..da97b47e 100644 --- a/core/src/options/outer_from.rs +++ b/core/src/options/outer_from.rs @@ -1,9 +1,12 @@ use syn::spanned::Spanned; use syn::{Field, Ident, Meta}; -use crate::options::{Core, DefaultExpression, ForwardAttrs, ParseAttribute, ParseData}; +use crate::codegen::ForwardAttrs; +use crate::options::{ + AttrsField, Core, DefaultExpression, ForwardAttrsFilter, ParseAttribute, ParseData, +}; use crate::util::PathList; -use crate::{FromMeta, Result}; +use crate::{FromField, FromMeta, Result}; /// Reusable base for `FromDeriveInput`, `FromVariant`, `FromField`, and other top-level /// `From*` traits. @@ -13,7 +16,7 @@ pub struct OuterFrom { pub ident: Option, /// The field on the target struct which should receive the type attributes, if any. - pub attrs: Option, + pub attrs: Option, pub container: Core, @@ -22,7 +25,7 @@ pub struct OuterFrom { /// The attribute names that should be forwarded. The presence of the word with no additional /// filtering will cause _all_ attributes to be cloned and exposed to the struct after parsing. - pub forward_attrs: Option, + pub forward_attrs: Option, /// Whether or not the container can be made through conversion from the type `Ident`. pub from_ident: bool, @@ -39,6 +42,13 @@ impl OuterFrom { from_ident: Default::default(), }) } + + pub fn as_forward_attrs(&self) -> ForwardAttrs { + ForwardAttrs { + field: self.attrs.as_ref(), + filter: self.forward_attrs.as_ref(), + } + } } impl ParseAttribute for OuterFrom { @@ -72,7 +82,7 @@ impl ParseData for OuterFrom { Ok(()) } Some("attrs") => { - self.attrs = field.ident.clone(); + self.attrs = AttrsField::from_field(field).map(Some)?; Ok(()) } _ => self.container.parse_field(field), diff --git a/tests/attrs_with.rs b/tests/attrs_with.rs new file mode 100644 index 00000000..198322dc --- /dev/null +++ b/tests/attrs_with.rs @@ -0,0 +1,80 @@ +use std::collections::BTreeSet; + +use darling::{util, Error, FromDeriveInput, Result}; +use syn::{parse_quote, Attribute}; + +fn unique_idents(attrs: Vec) -> Result> { + let mut errors = Error::accumulator(); + let idents = attrs + .into_iter() + .filter_map(|attr| { + let path = attr.path(); + errors.handle( + path.get_ident() + .map(std::string::ToString::to_string) + .ok_or_else(|| { + Error::custom(format!("`{}` is not an ident", util::path_to_string(path))) + .with_span(path) + }), + ) + }) + .collect(); + + errors.finish_with(idents) +} + +#[derive(FromDeriveInput)] +#[darling(attributes(a), forward_attrs)] +struct Receiver { + #[darling(with = unique_idents)] + attrs: BTreeSet, + other: Option, +} + +#[test] +fn succeeds_on_no_attrs() { + let di = Receiver::from_derive_input(&parse_quote! { + struct Demo; + }) + .unwrap(); + + assert!(di.attrs.is_empty()); +} + +#[test] +fn succeeds_on_valid_input() { + let di = Receiver::from_derive_input(&parse_quote! { + #[allow(dead_code)] + /// testing + #[another] + struct Demo; + }) + .unwrap(); + + assert_eq!(di.attrs.len(), 3); + assert!(di.attrs.contains("allow")); + assert!(di.attrs.contains("another")); + assert!(di.attrs.contains("doc")); + assert_eq!(di.other, None); +} + +#[test] +fn errors_combined_with_others() { + let e = Receiver::from_derive_input(&parse_quote! { + #[path::to::attr(dead_code)] + #[a(other = 5)] + struct Demo; + }) + .map(|_| "Should have failed") + .unwrap_err(); + + let error = e.to_string(); + + assert_eq!(e.len(), 2); + + // Look for the error on the field `other` + assert!(error.contains("at other")); + + // Look for the invalid path from attrs conversion + assert!(error.contains("`path::to::attr`")); +} diff --git a/tests/compile-fail/attrs_with_bad_fn.rs b/tests/compile-fail/attrs_with_bad_fn.rs new file mode 100644 index 00000000..6063f647 --- /dev/null +++ b/tests/compile-fail/attrs_with_bad_fn.rs @@ -0,0 +1,15 @@ +use darling::FromDeriveInput; +use syn::Attribute; + +fn bad_converter(attrs: Vec) -> Vec { + attrs +} + +#[derive(FromDeriveInput)] +#[darling(forward_attrs)] +struct Receiver { + #[darling(with = bad_converter)] + attrs: Vec, +} + +fn main() {} diff --git a/tests/compile-fail/attrs_with_bad_fn.stderr b/tests/compile-fail/attrs_with_bad_fn.stderr new file mode 100644 index 00000000..efdc201c --- /dev/null +++ b/tests/compile-fail/attrs_with_bad_fn.stderr @@ -0,0 +1,20 @@ +error[E0308]: mismatched types + --> tests/compile-fail/attrs_with_bad_fn.rs:11:22 + | +11 | #[darling(with = bad_converter)] + | ^^^^^^^^^^^^^ + | | + | expected enum `Result`, found struct `Vec` + | arguments to this function are incorrect + | + = note: expected enum `Result<_, darling::Error>` + found struct `Vec` +note: associated function defined here + --> core/src/error/mod.rs + | + | pub fn handle(&mut self, result: Result) -> Option { + | ^^^^^^ +help: try wrapping the expression in `Ok` + | +11 | #[darling(with = Ok(bad_converter))] + | +++ +