Repository: TedDriggs/darling (rust)

Issue:
Support `with` on magic field `attrs`
In colin-kiegel/rust-derive-builder#310, @dtolnay pointed out some clunky code that handles “distribution and unnesting” of attributes. The `Options` struct is forced to have an `attrs` field that is only temporarily used.

This can be avoided by allowing the deriver of `FromDeriveInput` _et alia_ to provide a conversion function that takes attributes and returns a `darling::Result<?>`

This function must participate in the error accumulator behavior of the rest of the derived impl, so it must be invoked before the error check, and we’ll need a new local variable for the output.

`with` makes more sense than the postfix items, since this must allow failure and doesn’t itself take in a `Result`

Pull request description:
Support `with` on `attrs` magic field
Fixes #273 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3ba98c..35ea418 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 b0866d4..023e8ad 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<T>` 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<Attribute>) -> darling::Result<T>`, 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 4afb174..612c28f 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 0000000..bbd43f8
--- /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<Declaration> {
+        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<ValuePopulator> {
+        self.field.map(ValuePopulator)
+    }
+
+    /// Get the field initializer for use when building the deriving struct.
+    pub fn as_initializer(&self) -> Option<Initializer> {
+        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 49bbf02..72da349 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 1c1df63..d64d9e2 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 74f3406..5bd7350 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 1cce0ca..d44e8eb 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 1598e6f..a835b13 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 d2a1546..af999b9 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/error/mod.rs b/core/src/error/mod.rs
index 0ebe3a9..3c22d4c 100644
--- a/core/src/error/mod.rs
+++ b/core/src/error/mod.rs
@@ -125,6 +125,17 @@ impl Error {
         Error::new(ErrorUnknownField::with_alts(field, alternates).into())
     }
 
+    /// Creates a new error for a field name that appears in the input but does not correspond to
+    /// a known attribute. The second argument is the list of known attributes; if a similar name
+    /// is found that will be shown in the emitted error message.
+    pub fn unknown_field_path_with_alts<'a, T, I>(field: &Path, alternates: I) -> Self
+    where
+        T: AsRef<str> + 'a,
+        I: IntoIterator<Item = &'a T>,
+    {
+        Error::new(ErrorUnknownField::with_alts(&path_to_string(field), alternates).into())
+    }
+
     /// Creates a new error for a struct or variant that does not adhere to the supported shape.
     pub fn unsupported_shape(shape: &str) -> Self {
         Error::new(ErrorKind::UnsupportedShape {
diff --git a/core/src/options/forward_attrs.rs b/core/src/options/forward_attrs.rs
index ac9f4e1..abe2d85 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<Path>,
+}
+
+impl FromField for AttrsField {
+    fn from_field(field: &syn::Field) -> crate::Result<Self> {
+        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<Self> {
-        Ok(ForwardAttrs::All)
+        Ok(ForwardAttrsFilter::All)
     }
 
     fn from_list(nested: &[NestedMeta]) -> Result<Self> {
-        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 b99278d..dc11b29 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 6eec1a0..eb27641 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 485a0ad..6dcd0f6 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 d8f3b5b..e14de99 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 c260a3a..c3223c5 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 cb43ee7..c00e0f1 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 e3a4b47..da97b47 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<Ident>,
 
     /// The field on the target struct which should receive the type attributes, if any.
-    pub attrs: Option<Ident>,
+    pub attrs: Option<AttrsField>,
 
     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<ForwardAttrs>,
+    pub forward_attrs: Option<ForwardAttrsFilter>,
 
     /// 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),


Test patch:
diff --git a/tests/attrs_with.rs b/tests/attrs_with.rs
new file mode 100644
index 0000000..198322d
--- /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<Attribute>) -> Result<BTreeSet<String>> {
+    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<String>,
+    other: Option<bool>,
+}
+
+#[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 0000000..6063f64
--- /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<Attribute>) -> Vec<Attribute> {
+    attrs
+}
+
+#[derive(FromDeriveInput)]
+#[darling(forward_attrs)]
+struct Receiver {
+    #[darling(with = bad_converter)]
+    attrs: Vec<Attribute>,
+}
+
+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 0000000..efdc201
--- /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<Attribute>`
+note: associated function defined here
+  --> core/src/error/mod.rs
+   |
+   |     pub fn handle<T>(&mut self, result: Result<T>) -> Option<T> {
+   |            ^^^^^^
+help: try wrapping the expression in `Ok`
+    |
+11  |     #[darling(with = Ok(bad_converter))]
+    |                      +++             +