Repository: JelteF/derive_more
Pull request #303 (2023-09-20 11:21:48)

Refactor and unify new attribute parsing
## Synopsis

In the recent PRs, reworking `AsRef`/`AsMut`, `Debug`, `Display`, `From` and `Into` macros, the new approach was introduced for attributes parsing: attributes are parsed as dedicated types, representing each attribute. However, due to lack of a single machinery to do so, it brought a lot of boilerplate and code duplication. Utterly, the `parse_attrs()` logic is fully repeated for each attribute type. This also lead, that the same attribute type (like `skip` of `forward`) is implemented twice in different macros, leading to some hardly noticeable behavior differences. This was partially fixed in those previous PRs by extracting some repeated attributes to `utils` module, but this is not enough.


## Solution

This PR introduces `utils::attr` module, which serves and provides the following:
- `ParseMultiple` trait machinery, which represents an extracted and abstracted `parse_attrs()` approach, and DRYes all the attributes' implementations by eliminating boilerplate. Now, implementing a custom attribute parsing only requires specifying behavior deviating from the default one, everything else is there already.
- Extracted commonly used attributes, like `skip`, `forward`, `(<types>)`, `repr`, etc, so they can be easily reused in different existing macros and in future implementations.
- `Either` implementation for `ParseMultiple`, which allows simply combine different attributes in `Either<OneAttribute, AnotherAttribute>` way without introducing new types and implementations for it.


## Additionally

Removes `parsing::Type`. The `parsing::Expr` was introduced to avoid using `syn/full` feature for `syn::Expr`, as it blows compilation times. Later, similar `parsing::Type` was introduced instead of `syn::Type`. However, `syn::Type` doesn't require a `syn/full` feature to work fully and properly, and so, we can just use `syn::Type` and don't bother.



## Checklist

- [x] Documentation is updated
- [x] Tests are added/updated
- [x] ~~[CHANGELOG entry][l:1] is added~~ (not required)




[l:1]: /CHANGELOG.md


tyranron: @JelteF ping
tyranron: @JelteF ping
tyranron: @JelteF yup, because generic code usually requires more lines when declared. However, the actual code duplication was largely reduced, and it should show even more reusage in subsequent PRs.

Patch:
diff --git a/impl/src/as/mod.rs b/impl/src/as/mod.rs
index 33e7c80..e2b1bf8 100644
--- a/impl/src/as/mod.rs
+++ b/impl/src/as/mod.rs
@@ -7,22 +7,19 @@ use std::{borrow::Cow, iter};
 
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
-use syn::{
-    parse::{discouraged::Speculative as _, Parse, ParseStream},
-    parse_quote,
-    punctuated::Punctuated,
-    spanned::Spanned,
-    Token,
-};
+use syn::{parse_quote, spanned::Spanned, Token};
 
-use crate::utils::{forward, skip, Either, GenericsSearch, Spanning};
+use crate::utils::{
+    attr::{self, ParseMultiple as _},
+    Either, GenericsSearch, Spanning,
+};
 
 /// Expands an [`AsRef`]/[`AsMut`] derive macro.
 pub fn expand(
     input: &syn::DeriveInput,
     trait_info: ExpansionCtx<'_>,
 ) -> syn::Result<TokenStream> {
-    let (trait_ident, attr_ident, _) = trait_info;
+    let (trait_ident, attr_name, _) = trait_info;
 
     let data = match &input.data {
         syn::Data::Struct(data) => Ok(data),
@@ -37,7 +34,7 @@ pub fn expand(
     }?;
 
     let expansions = if let Some(attr) =
-        StructAttribute::parse_attrs(&input.attrs, attr_ident)?
+        StructAttribute::parse_attrs(&input.attrs, attr_name)?
     {
         if data.fields.len() != 1 {
             return Err(syn::Error::new(
@@ -47,17 +44,17 @@ pub fn expand(
                     data.fields.span()
                 },
                 format!(
-                    "`#[{attr_ident}(...)]` attribute can only be placed on structs with exactly \
+                    "`#[{attr_name}(...)]` attribute can only be placed on structs with exactly \
                      one field",
                 ),
             ));
         }
 
         let field = data.fields.iter().next().unwrap();
-        if FieldAttribute::parse_attrs(&field.attrs, attr_ident)?.is_some() {
+        if FieldAttribute::parse_attrs(&field.attrs, attr_name)?.is_some() {
             return Err(syn::Error::new(
                 field.span(),
-                format!("`#[{attr_ident}(...)]` cannot be placed on both struct and its field"),
+                format!("`#[{attr_name}(...)]` cannot be placed on both struct and its field"),
             ));
         }
 
@@ -73,7 +70,7 @@ pub fn expand(
         let attrs = data
             .fields
             .iter()
-            .map(|field| FieldAttribute::parse_attrs(&field.attrs, attr_ident))
+            .map(|field| FieldAttribute::parse_attrs(&field.attrs, attr_name))
             .collect::<syn::Result<Vec<_>>>()?;
 
         let present_attrs = attrs.iter().filter_map(Option::as_ref).collect::<Vec<_>>();
@@ -93,8 +90,8 @@ pub fn expand(
                 return Err(syn::Error::new(
                     skip_attr.span(),
                     format!(
-                        "`#[{attr_ident}({})]` cannot be used in the same struct with other \
-                         `#[{attr_ident}(...)]` attributes",
+                        "`#[{attr_name}({})]` cannot be used in the same struct with other \
+                         `#[{attr_name}(...)]` attributes",
                         skip_attr.name(),
                     ),
                 ));
@@ -123,16 +120,18 @@ pub fn expand(
                 .enumerate()
                 .zip(attrs)
                 .filter_map(|((i, field), attr)| match attr.map(Spanning::into_inner) {
-                    Some(attr @ (FieldAttribute::Empty | FieldAttribute::Args(_))) => {
-                        Some(Expansion {
-                            trait_info,
-                            ident: &input.ident,
-                            generics: &input.generics,
-                            field,
-                            field_index: i,
-                            conversions: attr.into_conversion_attribute(),
-                        })
-                    }
+                    Some(
+                        attr @ (FieldAttribute::Empty(_)
+                        | FieldAttribute::Forward(_)
+                        | FieldAttribute::Types(_)),
+                    ) => Some(Expansion {
+                        trait_info,
+                        ident: &input.ident,
+                        generics: &input.generics,
+                        field,
+                        field_index: i,
+                        conversions: attr.into(),
+                    }),
                     Some(FieldAttribute::Skip(_)) => unreachable!(),
                     None => None,
                 })
@@ -170,7 +169,7 @@ struct Expansion<'a> {
     field_index: usize,
 
     /// Attribute specifying which conversions should be generated.
-    conversions: Option<ConversionAttribute>,
+    conversions: Option<attr::Conversion>,
 }
 
 impl<'a> ToTokens for Expansion<'a> {
@@ -198,14 +197,14 @@ impl<'a> ToTokens for Expansion<'a> {
         let field_contains_generics = generics_search.any_in(field_ty);
 
         let is_blanket =
-            matches!(&self.conversions, Some(ConversionAttribute::Forward(_)));
+            matches!(&self.conversions, Some(attr::Conversion::Forward(_)));
 
         let return_tys = match &self.conversions {
-            Some(ConversionAttribute::Forward(_)) => {
+            Some(attr::Conversion::Forward(_)) => {
                 Either::Left(iter::once(Cow::Owned(parse_quote! { __AsT })))
             }
-            Some(ConversionAttribute::Types(tys)) => {
-                Either::Right(tys.iter().map(Cow::Borrowed))
+            Some(attr::Conversion::Types(tys)) => {
+                Either::Right(tys.0.iter().map(Cow::Borrowed))
             }
             None => Either::Left(iter::once(Cow::Borrowed(field_ty))),
         };
@@ -294,159 +293,14 @@ impl<'a> ToTokens for Expansion<'a> {
 /// #[as_ref(forward)]
 /// #[as_ref(<types>)]
 /// ```
-type StructAttribute = ConversionAttribute;
-
-impl StructAttribute {
-    /// Parses a [`StructAttribute`] from the provided [`syn::Attribute`]s, preserving its [`Span`].
-    ///
-    /// [`Span`]: proc_macro2::Span
-    fn parse_attrs(
-        attrs: &[syn::Attribute],
-        attr_ident: &syn::Ident,
-    ) -> syn::Result<Option<Spanning<Self>>> {
-        attrs.iter().filter(|attr| attr.path().is_ident(attr_ident))
-            .try_fold(None, |attrs: Option<Spanning<Self>>, attr| {
-                let parsed: Spanning<Self> = Spanning::new(attr.parse_args()?, attr.span());
-
-                if let Some(prev) = attrs {
-                    let span = prev.span.join(parsed.span).unwrap_or(prev.span);
-                    if let Some(new) = prev.item.merge(parsed.item) {
-                        Ok(Some(Spanning::new(new, span)))
-                    } else {
-                        Err(syn::Error::new(
-                            parsed.span,
-                            format!("only single `#[{attr_ident}(...)]` attribute is allowed here"),
-                        ))
-                    }
-                } else {
-                    Ok(Some(parsed))
-                }
-            })
-    }
-}
+type StructAttribute = attr::Conversion;
 
 /// Representation of an [`AsRef`]/[`AsMut`] derive macro field attribute.
 ///
 /// ```rust,ignore
 /// #[as_ref]
-/// #[as_ref(forward)]
-/// #[as_ref(<types>)]
 /// #[as_ref(skip)] #[as_ref(ignore)]
-/// ```
-enum FieldAttribute {
-    Empty,
-    Args(ConversionAttribute),
-    Skip(skip::Attribute),
-}
-
-impl Parse for FieldAttribute {
-    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        if input.is_empty() {
-            return Ok(Self::Empty);
-        }
-
-        let ahead = input.fork();
-        if let Ok(attr) = ahead.parse::<skip::Attribute>() {
-            input.advance_to(&ahead);
-            return Ok(Self::Skip(attr));
-        }
-
-        input.parse::<ConversionAttribute>().map(Self::Args)
-    }
-}
-
-impl FieldAttribute {
-    /// Parses a [`FieldAttribute`] from the provided [`syn::Attribute`]s, preserving its [`Span`].
-    ///
-    /// [`Span`]: proc_macro2::Span
-    fn parse_attrs(
-        attrs: &[syn::Attribute],
-        attr_ident: &syn::Ident,
-    ) -> syn::Result<Option<Spanning<Self>>> {
-        attrs
-            .iter()
-            .filter(|attr| attr.path().is_ident(attr_ident))
-            .try_fold(None, |attrs: Option<Spanning<Self>>, attr| {
-                let parsed = Spanning::new(
-                    if matches!(attr.meta, syn::Meta::Path(_)) {
-                        Self::Empty
-                    } else {
-                        attr.parse_args()?
-                    },
-                    attr.span(),
-                );
-
-                if let Some(prev) = attrs {
-                    let span = prev.span.join(parsed.span).unwrap_or(prev.span);
-                    if let Some(new) = prev.item.merge(parsed.item) {
-                        Ok(Some(Spanning::new(new, span)))
-                    } else {
-                        Err(syn::Error::new(
-                            parsed.span,
-                            format!("only single `#[{attr_ident}(...)]` attribute is allowed here")
-                        ))
-                    }
-                } else {
-                    Ok(Some(parsed))
-                }
-            })
-    }
-
-    /// Extracts a [`ConversionAttribute`], if possible.
-    fn into_conversion_attribute(self) -> Option<ConversionAttribute> {
-        match self {
-            Self::Args(args) => Some(args),
-            Self::Empty | Self::Skip(_) => None,
-        }
-    }
-
-    /// Merges two [`FieldAttribute`]s, if possible
-    fn merge(self, other: Self) -> Option<Self> {
-        if let (Self::Args(args), Self::Args(more)) = (self, other) {
-            args.merge(more).map(Self::Args)
-        } else {
-            None
-        }
-    }
-}
-
-/// Representation of an attribute, specifying which conversions should be generated.
-///
-/// ```rust,ignore
 /// #[as_ref(forward)]
 /// #[as_ref(<types>)]
 /// ```
-enum ConversionAttribute {
-    /// Blanket impl, fully forwarding implementation to the one of the field type.
-    Forward(forward::Attribute),
-
-    /// Concrete specified types, which can include both the type of the field itself, and types for
-    /// which the field type implements [`AsRef`].
-    Types(Punctuated<syn::Type, Token![,]>),
-}
-
-impl Parse for ConversionAttribute {
-    fn parse(input: ParseStream) -> syn::Result<Self> {
-        let ahead = input.fork();
-        if let Ok(attr) = ahead.parse::<forward::Attribute>() {
-            input.advance_to(&ahead);
-            return Ok(Self::Forward(attr));
-        }
-
-        input
-            .parse_terminated(syn::Type::parse, Token![,])
-            .map(Self::Types)
-    }
-}
-
-impl ConversionAttribute {
-    /// Merges two [`ConversionAttribute`]s, if possible.
-    fn merge(self, other: Self) -> Option<Self> {
-        if let (Self::Types(mut tys), Self::Types(more)) = (self, other) {
-            tys.extend(more);
-            Some(Self::Types(tys))
-        } else {
-            None
-        }
-    }
-}
+type FieldAttribute = attr::FieldConversion;
diff --git a/impl/src/fmt/debug.rs b/impl/src/fmt/debug.rs
index b608191..5cd43b1 100644
--- a/impl/src/fmt/debug.rs
+++ b/impl/src/fmt/debug.rs
@@ -4,27 +4,29 @@
 
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
-use syn::{
-    parse::{Parse, ParseStream},
-    parse_quote,
-    spanned::Spanned as _,
-    Ident,
-};
+use syn::{parse_quote, spanned::Spanned as _, Ident};
 
-use crate::utils::skip;
+use crate::utils::{
+    attr::{self, ParseMultiple as _},
+    Either, Spanning,
+};
 
-use super::{ContainerAttributes, FmtAttribute};
+use super::{trait_name_to_attribute_name, ContainerAttributes, FmtAttribute};
 
 /// Expands a [`fmt::Debug`] derive macro.
 ///
 /// [`fmt::Debug`]: std::fmt::Debug
 pub fn expand(input: &syn::DeriveInput, _: &str) -> syn::Result<TokenStream> {
-    let attrs = ContainerAttributes::parse_attrs(&input.attrs, "Debug")?;
+    let attr_name = format_ident!("{}", trait_name_to_attribute_name("Debug"));
+
+    let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?
+        .map(Spanning::into_inner)
+        .unwrap_or_default();
     let ident = &input.ident;
 
     let (bounds, body) = match &input.data {
-        syn::Data::Struct(s) => expand_struct(attrs, ident, s),
-        syn::Data::Enum(e) => expand_enum(attrs, e),
+        syn::Data::Struct(s) => expand_struct(attrs, ident, s, &attr_name),
+        syn::Data::Enum(e) => expand_enum(attrs, e, &attr_name),
         syn::Data::Union(_) => {
             return Err(syn::Error::new(
                 input.span(),
@@ -63,11 +65,13 @@ fn expand_struct(
     attrs: ContainerAttributes,
     ident: &Ident,
     s: &syn::DataStruct,
+    attr_name: &syn::Ident,
 ) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
     let s = Expansion {
         attr: &attrs,
         fields: &s.fields,
         ident,
+        attr_name,
     };
     s.validate_attrs()?;
     let bounds = s.generate_bounds()?;
@@ -96,12 +100,15 @@ fn expand_struct(
 fn expand_enum(
     mut attrs: ContainerAttributes,
     e: &syn::DataEnum,
+    attr_name: &syn::Ident,
 ) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
     if let Some(enum_fmt) = attrs.fmt.as_ref() {
         return Err(syn::Error::new_spanned(
             enum_fmt,
-            "`#[debug(\"...\", ...)]` attribute is not allowed on enum, place it on its variants \
-             instead",
+            format!(
+                "`#[{attr_name}(\"...\", ...)]` attribute is not allowed on enum, place it on its \
+                 variants instead",
+            ),
         ));
     }
 
@@ -119,7 +126,9 @@ fn expand_enum(
                     attrs.replace(attr).map_or(Ok(()), |dup| {
                         Err(syn::Error::new(
                             dup.span(),
-                            "multiple `#[debug(\"...\", ...)]` attributes aren't allowed",
+                            format!(
+                                "multiple `#[{attr_name}(\"...\", ...)]` attributes aren't allowed",
+                            ),
                         ))
                     })?;
                     Ok::<_, syn::Error>(attrs)
@@ -129,6 +138,7 @@ fn expand_enum(
                 attr: &attrs,
                 fields: &variant.fields,
                 ident,
+                attr_name,
             };
             v.validate_attrs()?;
             let arm_body = v.generate_body()?;
@@ -165,54 +175,12 @@ fn expand_enum(
 /// Representation of a [`fmt::Debug`] derive macro field attribute.
 ///
 /// ```rust,ignore
-/// #[debug("<fmt_literal>", <fmt_args>)]
 /// #[debug(skip)]
+/// #[debug("<fmt-literal>", <fmt-args>)]
 /// ```
 ///
 /// [`fmt::Debug`]: std::fmt::Debug
-enum FieldAttribute {
-    /// [`fmt`] attribute.
-    ///
-    /// [`fmt`]: std::fmt
-    Fmt(FmtAttribute),
-
-    /// Attribute for skipping field.
-    Skip(skip::Attribute),
-}
-
-impl FieldAttribute {
-    /// Parses a [`FieldAttribute`] from the provided [`syn::Attribute`]s.
-    fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> syn::Result<Option<Self>> {
-        Ok(attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident("debug"))
-            .try_fold(None, |mut attrs, attr| {
-                let field_attr = attr.parse_args::<FieldAttribute>()?;
-                if let Some((path, _)) = attrs.replace((attr.path(), field_attr)) {
-                    Err(syn::Error::new(
-                        path.span(),
-                        "only single `#[debug(...)]` attribute is allowed here",
-                    ))
-                } else {
-                    Ok(attrs)
-                }
-            })?
-            .map(|(_, attr)| attr))
-    }
-}
-
-impl Parse for FieldAttribute {
-    fn parse(input: ParseStream) -> syn::Result<Self> {
-        FmtAttribute::check_legacy_fmt(input)?;
-
-        if input.peek(syn::LitStr) {
-            input.parse().map(Self::Fmt)
-        } else {
-            input.parse::<skip::Attribute>().map(Self::Skip)
-        }
-    }
-}
+type FieldAttribute = Either<attr::Skip, FmtAttribute>;
 
 /// Helper struct to generate [`Debug::fmt()`] implementation body and trait
 /// bounds for a struct or an enum variant.
@@ -227,6 +195,9 @@ struct Expansion<'a> {
 
     /// Struct or enum [`syn::Fields`].
     fields: &'a syn::Fields,
+
+    /// Name of the attributes, considered by this macro.
+    attr_name: &'a syn::Ident,
 }
 
 impl<'a> Expansion<'a> {
@@ -236,11 +207,13 @@ impl<'a> Expansion<'a> {
             for field_attr in self
                 .fields
                 .iter()
-                .map(|f| FieldAttribute::parse_attrs(&f.attrs))
+                .map(|f| FieldAttribute::parse_attrs(&f.attrs, self.attr_name))
             {
-                if let Some(FieldAttribute::Fmt(fmt)) = field_attr? {
+                if let Some(FieldAttribute::Right(fmt_attr)) =
+                    field_attr?.map(Spanning::into_inner)
+                {
                     return Err(syn::Error::new_spanned(
-                        fmt,
+                        fmt_attr,
                         "`#[debug(...)]` attributes are not allowed on fields when \
                          `#[debug(\"...\", ...)]` is specified on struct or variant",
                     ));
@@ -280,22 +253,26 @@ impl<'a> Expansion<'a> {
                 };
                 let out = unnamed.unnamed.iter().enumerate().try_fold(
                     out,
-                    |out, (i, field)| match FieldAttribute::parse_attrs(&field.attrs)? {
-                        Some(FieldAttribute::Skip(_)) => {
-                            exhaustive = false;
-                            Ok::<_, syn::Error>(out)
-                        }
-                        Some(FieldAttribute::Fmt(fmt)) => Ok(quote! {
-                            ::derive_more::__private::DebugTuple::field(
-                                #out,
-                                &::core::format_args!(#fmt),
-                            )
-                        }),
-                        None => {
-                            let ident = format_ident!("_{i}");
-                            Ok(quote! {
-                                ::derive_more::__private::DebugTuple::field(#out, #ident)
-                            })
+                    |out, (i, field)| {
+                        match FieldAttribute::parse_attrs(&field.attrs, self.attr_name)?
+                            .map(Spanning::into_inner)
+                        {
+                            Some(FieldAttribute::Left(_skip)) => {
+                                exhaustive = false;
+                                Ok::<_, syn::Error>(out)
+                            }
+                            Some(FieldAttribute::Right(fmt_attr)) => Ok(quote! {
+                                ::derive_more::__private::DebugTuple::field(
+                                    #out,
+                                    &::core::format_args!(#fmt_attr),
+                                )
+                            }),
+                            None => {
+                                let ident = format_ident!("_{i}");
+                                Ok(quote! {
+                                    ::derive_more::__private::DebugTuple::field(#out, #ident)
+                                })
+                            }
                         }
                     },
                 )?;
@@ -320,16 +297,18 @@ impl<'a> Expansion<'a> {
                             unreachable!("`syn::Fields::Named`");
                         });
                         let field_str = field_ident.to_string();
-                        match FieldAttribute::parse_attrs(&field.attrs)? {
-                            Some(FieldAttribute::Skip(_)) => {
+                        match FieldAttribute::parse_attrs(&field.attrs, self.attr_name)?
+                            .map(Spanning::into_inner)
+                        {
+                            Some(FieldAttribute::Left(_skip)) => {
                                 exhaustive = false;
                                 Ok::<_, syn::Error>(out)
                             }
-                            Some(FieldAttribute::Fmt(fmt)) => Ok(quote! {
+                            Some(FieldAttribute::Right(fmt_attr)) => Ok(quote! {
                                 ::core::fmt::DebugStruct::field(
                                     #out,
                                     #field_str,
-                                    &::core::format_args!(#fmt),
+                                    &::core::format_args!(#fmt_attr),
                                 )
                             }),
                             None => Ok(quote! {
@@ -359,16 +338,18 @@ impl<'a> Expansion<'a> {
         } else {
             self.fields.iter().try_fold(out, |mut out, field| {
                 let ty = &field.ty;
-                match FieldAttribute::parse_attrs(&field.attrs)? {
-                    Some(FieldAttribute::Fmt(attr)) => {
-                        out.extend(attr.bounded_types(self.fields).map(
+                match FieldAttribute::parse_attrs(&field.attrs, self.attr_name)?
+                    .map(Spanning::into_inner)
+                {
+                    Some(FieldAttribute::Right(fmt_attr)) => {
+                        out.extend(fmt_attr.bounded_types(self.fields).map(
                             |(ty, trait_name)| {
                                 let trait_name = format_ident!("{trait_name}");
                                 parse_quote! { #ty: ::core::fmt::#trait_name }
                             },
                         ));
                     }
-                    Some(FieldAttribute::Skip(_)) => {}
+                    Some(FieldAttribute::Left(_skip)) => {}
                     None => out.extend([parse_quote! { #ty: ::core::fmt::Debug }]),
                 }
                 Ok(out)
diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs
index 5d1079f..789f99b 100644
--- a/impl/src/fmt/display.rs
+++ b/impl/src/fmt/display.rs
@@ -3,10 +3,12 @@
 #[cfg(doc)]
 use std::fmt;
 
-use proc_macro2::{Ident, TokenStream};
+use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
 use syn::{parse_quote, spanned::Spanned as _};
 
+use crate::utils::{attr::ParseMultiple as _, Spanning};
+
 use super::{trait_name_to_attribute_name, ContainerAttributes};
 
 /// Expands a [`fmt::Display`]-like derive macro.
@@ -22,12 +24,15 @@ use super::{trait_name_to_attribute_name, ContainerAttributes};
 /// - [`UpperHex`](fmt::UpperHex)
 pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenStream> {
     let trait_name = normalize_trait_name(trait_name);
+    let attr_name = format_ident!("{}", trait_name_to_attribute_name(trait_name));
 
-    let attrs = ContainerAttributes::parse_attrs(&input.attrs, trait_name)?;
+    let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?
+        .map(Spanning::into_inner)
+        .unwrap_or_default();
     let trait_ident = format_ident!("{trait_name}");
     let ident = &input.ident;
 
-    let ctx = (&attrs, ident, &trait_ident, trait_name);
+    let ctx = (&attrs, ident, &trait_ident, &attr_name);
     let (bounds, body) = match &input.data {
         syn::Data::Struct(s) => expand_struct(s, ctx),
         syn::Data::Enum(e) => expand_enum(e, ctx),
@@ -59,10 +64,15 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenSt
 
 /// Type alias for an expansion context:
 /// - [`ContainerAttributes`].
-/// - Struct/enum/union [`Ident`].
-/// - Derived trait [`Ident`].
-/// - Derived trait `&`[`str`].
-type ExpansionCtx<'a> = (&'a ContainerAttributes, &'a Ident, &'a Ident, &'a str);
+/// - Struct/enum/union [`syn::Ident`].
+/// - Derived trait [`syn::Ident`].
+/// - Attribute name [`syn::Ident`].
+type ExpansionCtx<'a> = (
+    &'a ContainerAttributes,
+    &'a syn::Ident,
+    &'a syn::Ident,
+    &'a syn::Ident,
+);
 
 /// Expands a [`fmt::Display`]-like derive macro for the provided struct.
 fn expand_struct(
@@ -100,7 +110,7 @@ fn expand_struct(
 /// Expands a [`fmt`]-like derive macro for the provided enum.
 fn expand_enum(
     e: &syn::DataEnum,
-    (attrs, _, trait_ident, trait_name): ExpansionCtx<'_>,
+    (attrs, _, trait_ident, attr_name): ExpansionCtx<'_>,
 ) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
     if attrs.fmt.is_some() {
         todo!("https://github.com/JelteF/derive_more/issues/142");
@@ -109,20 +119,20 @@ fn expand_enum(
     let (bounds, match_arms) = e.variants.iter().try_fold(
         (Vec::new(), TokenStream::new()),
         |(mut bounds, mut arms), variant| {
-            let attrs = ContainerAttributes::parse_attrs(&variant.attrs, trait_name)?;
+            let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?
+                .map(Spanning::into_inner)
+                .unwrap_or_default();
             let ident = &variant.ident;
 
             if attrs.fmt.is_none()
                 && variant.fields.is_empty()
-                && trait_name != "Display"
+                && attr_name != "display"
             {
                 return Err(syn::Error::new(
                     e.variants.span(),
                     format!(
-                        "implicit formatting of unit enum variant is supported \
-                         only for `Display` macro, use `#[{}(\"...\")]` to \
-                         explicitly specify the formatting",
-                        trait_name_to_attribute_name(trait_name),
+                        "implicit formatting of unit enum variant is supported only for `Display` \
+                         macro, use `#[{attr_name}(\"...\")]` to explicitly specify the formatting",
                     ),
                 ));
             }
@@ -167,15 +177,12 @@ fn expand_enum(
 /// Expands a [`fmt::Display`]-like derive macro for the provided union.
 fn expand_union(
     u: &syn::DataUnion,
-    (attrs, _, _, trait_name): ExpansionCtx<'_>,
+    (attrs, _, _, attr_name): ExpansionCtx<'_>,
 ) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
     let fmt = &attrs.fmt.as_ref().ok_or_else(|| {
         syn::Error::new(
             u.fields.span(),
-            format!(
-                "unions must have `#[{}(\"...\", ...)]` attribute",
-                trait_name_to_attribute_name(trait_name),
-            ),
+            format!("unions must have `#[{attr_name}(\"...\", ...)]` attribute"),
         )
     })?;
 
@@ -194,14 +201,14 @@ struct Expansion<'a> {
     /// Derive macro [`ContainerAttributes`].
     attrs: &'a ContainerAttributes,
 
-    /// Struct or enum [`Ident`].
-    ident: &'a Ident,
+    /// Struct or enum [`syn::Ident`].
+    ident: &'a syn::Ident,
 
     /// Struct or enum [`syn::Fields`].
     fields: &'a syn::Fields,
 
-    /// [`fmt`] trait [`Ident`].
-    trait_ident: &'a Ident,
+    /// [`fmt`] trait [`syn::Ident`].
+    trait_ident: &'a syn::Ident,
 }
 
 impl<'a> Expansion<'a> {
diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs
index de4014d..baeaadb 100644
--- a/impl/src/fmt/mod.rs
+++ b/impl/src/fmt/mod.rs
@@ -8,6 +8,8 @@ pub(crate) mod debug;
 pub(crate) mod display;
 mod parsing;
 
+use std::mem;
+
 use proc_macro2::TokenStream;
 use quote::ToTokens;
 use syn::{
@@ -17,11 +19,47 @@ use syn::{
     token, Ident,
 };
 
-use crate::{parsing::Expr, utils::Either};
+use crate::{
+    parsing::Expr,
+    utils::{attr, Either, Spanning},
+};
 
-/// Representation of a macro attribute expressing additional trait bounds.
+/// Representation of a `bound` macro attribute, expressing additional trait bounds.
+///
+/// ```rust,ignore
+/// #[<attribute>(bound(<where-predicates>))]
+/// #[<attribute>(bounds(<where-predicates>))]
+/// #[<attribute>(where(<where-predicates>))]
+/// ```
 #[derive(Debug, Default)]
-struct BoundsAttribute(Punctuated<syn::WherePredicate, syn::token::Comma>);
+struct BoundsAttribute(Punctuated<syn::WherePredicate, token::Comma>);
+
+impl Parse for BoundsAttribute {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        Self::check_legacy_fmt(input)?;
+
+        let _ = input.parse::<syn::Path>().and_then(|p| {
+            if ["bound", "bounds", "where"]
+                .into_iter()
+                .any(|i| p.is_ident(i))
+            {
+                Ok(p)
+            } else {
+                Err(syn::Error::new(
+                    p.span(),
+                    "unknown attribute argument, expected `bound(...)`",
+                ))
+            }
+        })?;
+
+        let content;
+        syn::parenthesized!(content in input);
+
+        content
+            .parse_terminated(syn::WherePredicate::parse, token::Comma)
+            .map(Self)
+    }
+}
 
 impl BoundsAttribute {
     /// Errors in case legacy syntax is encountered: `bound = "..."`.
@@ -30,7 +68,7 @@ impl BoundsAttribute {
 
         let path = fork
             .parse::<syn::Path>()
-            .and_then(|path| fork.parse::<syn::token::Eq>().map(|_| path));
+            .and_then(|path| fork.parse::<token::Eq>().map(|_| path));
         match path {
             Ok(path) if path.is_ident("bound") => fork
                 .parse::<syn::Lit>()
@@ -50,33 +88,12 @@ impl BoundsAttribute {
     }
 }
 
-impl Parse for BoundsAttribute {
-    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        let _ = input.parse::<syn::Path>().and_then(|p| {
-            if ["bound", "bounds", "where"]
-                .into_iter()
-                .any(|i| p.is_ident(i))
-            {
-                Ok(p)
-            } else {
-                Err(syn::Error::new(
-                    p.span(),
-                    "unknown attribute, expected `bound(...)`",
-                ))
-            }
-        })?;
-
-        let content;
-        syn::parenthesized!(content in input);
-
-        content
-            .parse_terminated(syn::WherePredicate::parse, token::Comma)
-            .map(Self)
-    }
-}
-
 /// Representation of a [`fmt`]-like attribute.
 ///
+/// ```rust,ignore
+/// #[<attribute>("<fmt-literal>", <fmt-args>)]
+/// ```
+///
 /// [`fmt`]: std::fmt
 #[derive(Debug)]
 struct FmtAttribute {
@@ -92,6 +109,31 @@ struct FmtAttribute {
     args: Punctuated<FmtArgument, token::Comma>,
 }
 
+impl Parse for FmtAttribute {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        Self::check_legacy_fmt(input)?;
+
+        Ok(Self {
+            lit: input.parse()?,
+            comma: input
+                .peek(token::Comma)
+                .then(|| input.parse())
+                .transpose()?,
+            args: input.parse_terminated(FmtArgument::parse, token::Comma)?,
+        })
+    }
+}
+
+impl attr::ParseMultiple for FmtAttribute {}
+
+impl ToTokens for FmtAttribute {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        self.lit.to_tokens(tokens);
+        self.comma.to_tokens(tokens);
+        self.args.to_tokens(tokens);
+    }
+}
+
 impl FmtAttribute {
     /// Returns an [`Iterator`] over bounded [`syn::Type`]s and trait names.
     fn bounded_types<'a>(
@@ -137,7 +179,7 @@ impl FmtAttribute {
 
         let path = fork
             .parse::<syn::Path>()
-            .and_then(|path| fork.parse::<syn::token::Eq>().map(|_| path));
+            .and_then(|path| fork.parse::<token::Eq>().map(|_| path));
         match path {
             Ok(path) if path.is_ident("fmt") => (|| {
                 let args = fork
@@ -174,27 +216,6 @@ impl FmtAttribute {
     }
 }
 
-impl Parse for FmtAttribute {
-    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        Ok(Self {
-            lit: input.parse()?,
-            comma: input
-                .peek(syn::token::Comma)
-                .then(|| input.parse())
-                .transpose()?,
-            args: input.parse_terminated(FmtArgument::parse, token::Comma)?,
-        })
-    }
-}
-
-impl ToTokens for FmtAttribute {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        self.lit.to_tokens(tokens);
-        self.comma.to_tokens(tokens);
-        self.args.to_tokens(tokens);
-    }
-}
-
 /// Representation of a [named parameter][1] (`identifier '=' expression`) in
 /// in a [`FmtAttribute`].
 ///
@@ -325,12 +346,12 @@ impl Placeholder {
 /// or enum variant).
 ///
 /// ```rust,ignore
-/// #[<fmt_trait>("<fmt_literal>", <fmt_args>)]
-/// #[bound(<bounds>)]
+/// #[<attribute>("<fmt-literal>", <fmt-args>)]
+/// #[<attribute>(bound(<where-predicates>))]
 /// ```
 ///
-/// `#[<fmt_trait>(...)]` can be specified only once, while multiple
-/// `#[<fmt_trait>(bound(...))]` are allowed.
+/// `#[<attribute>(...)]` can be specified only once, while multiple `#[<attribute>(bound(...))]`
+/// are allowed.
 #[derive(Debug, Default)]
 struct ContainerAttributes {
     /// Interpolation [`FmtAttribute`].
@@ -340,56 +361,48 @@ struct ContainerAttributes {
     bounds: BoundsAttribute,
 }
 
-impl ContainerAttributes {
-    /// Parses [`ContainerAttributes`] from the provided [`syn::Attribute`]s.
-    pub(super) fn parse_attrs(
-        attrs: impl AsRef<[syn::Attribute]>,
-        trait_name: &str,
-    ) -> syn::Result<Self> {
-        attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident(trait_name_to_attribute_name(trait_name)))
-            .try_fold(Self::default(), |mut attrs, attr| {
-                match attr.parse_args::<ContainerAttribute>()? {
-                    ContainerAttribute::Bounds(more) => {
-                        attrs.bounds.0.extend(more.0);
-                    }
-                    ContainerAttribute::Fmt(fmt) => {
-                        attrs.fmt.replace(fmt).map_or(Ok(()), |dup| Err(syn::Error::new(
-                            dup.span(),
-                            format!(
-                                "multiple `#[{}(\"...\", ...)]` attributes aren't allowed",
-                                trait_name_to_attribute_name(trait_name),
-                            ))))?;
-                    }
-                };
-                Ok(attrs)
-            })
-    }
-}
-
-/// Representation of a single [`fmt::Display`]-like derive macro attribute, placed on a container
-/// (struct or enum variant).
-#[derive(Debug)]
-enum ContainerAttribute {
-    /// [`fmt`] attribute.
-    Fmt(FmtAttribute),
-
-    /// Addition trait bounds.
-    Bounds(BoundsAttribute),
-}
-
-impl Parse for ContainerAttribute {
+impl Parse for ContainerAttributes {
     fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        BoundsAttribute::check_legacy_fmt(input)?;
+        // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow
+        // any error of the `Either::Left` if the `Either::Right` succeeds.
         FmtAttribute::check_legacy_fmt(input)?;
+        <Either<FmtAttribute, BoundsAttribute>>::parse(input).map(|v| match v {
+            Either::Left(fmt) => Self {
+                bounds: BoundsAttribute::default(),
+                fmt: Some(fmt),
+            },
+            Either::Right(bounds) => Self { bounds, fmt: None },
+        })
+    }
+}
 
-        if input.peek(syn::LitStr) {
-            input.parse().map(Self::Fmt)
-        } else {
-            input.parse().map(Self::Bounds)
+impl attr::ParseMultiple for ContainerAttributes {
+    fn merge_attrs(
+        prev: Spanning<Self>,
+        new: Spanning<Self>,
+        name: &Ident,
+    ) -> syn::Result<Spanning<Self>> {
+        let Spanning {
+            span: prev_span,
+            item: mut prev,
+        } = prev;
+        let Spanning {
+            span: new_span,
+            item: new,
+        } = new;
+
+        if mem::replace(&mut prev.fmt, new.fmt).is_some() {
+            return Err(syn::Error::new(
+                new_span,
+                format!("multiple `#[{name}(\"...\", ...)]` attributes aren't allowed"),
+            ));
         }
+        prev.bounds.0.extend(new.bounds.0);
+
+        Ok(Spanning::new(
+            prev,
+            prev_span.join(new_span).unwrap_or(prev_span),
+        ))
     }
 }
 
diff --git a/impl/src/from.rs b/impl/src/from.rs
index 9c8bf25..a204c03 100644
--- a/impl/src/from.rs
+++ b/impl/src/from.rs
@@ -1,29 +1,39 @@
 //! Implementation of a [`From`] derive macro.
 
-use std::iter;
+use std::{
+    any::{Any, TypeId},
+    iter,
+};
 
 use proc_macro2::{Span, TokenStream};
 use quote::{format_ident, quote, ToTokens as _, TokenStreamExt as _};
 use syn::{
-    parse::{discouraged::Speculative as _, Parse, ParseStream},
+    parse::{Parse, ParseStream},
     parse_quote,
-    punctuated::Punctuated,
     spanned::Spanned as _,
-    token, Ident,
+    token,
 };
 
-use crate::{
-    parsing::Type,
-    utils::{forward, polyfill, skip, Either},
+use crate::utils::{
+    attr::{self, ParseMultiple as _},
+    polyfill, Either, Spanning,
 };
 
 /// Expands a [`From`] derive macro.
 pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
+    let attr_name = format_ident!("from");
+
     match &input.data {
         syn::Data::Struct(data) => Expansion {
-            attrs: StructAttribute::parse_attrs(&input.attrs, &data.fields)?
-                .map(Into::into)
-                .as_ref(),
+            attrs: StructAttribute::parse_attrs_with(
+                &input.attrs,
+                &attr_name,
+                &ConsiderLegacySyntax {
+                    fields: &data.fields,
+                },
+            )?
+            .map(|attr| attr.into_inner().into())
+            .as_ref(),
             ident: &input.ident,
             variant: None,
             fields: &data.fields,
@@ -37,19 +47,25 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
                 .variants
                 .iter()
                 .map(|variant| {
-                    let attrs =
-                        VariantAttribute::parse_attrs(&variant.attrs, &variant.fields)?;
+                    let attr = VariantAttribute::parse_attrs_with(
+                        &variant.attrs,
+                        &attr_name,
+                        &ConsiderLegacySyntax {
+                            fields: &variant.fields,
+                        },
+                    )?
+                    .map(Spanning::into_inner);
                     if matches!(
-                        attrs,
+                        attr,
                         Some(
-                            VariantAttribute::From
+                            VariantAttribute::Empty(_)
                                 | VariantAttribute::Types(_)
                                 | VariantAttribute::Forward(_)
                         ),
                     ) {
                         has_explicit_from = true;
                     }
-                    Ok(attrs)
+                    Ok(attr)
                 })
                 .collect::<syn::Result<Vec<_>>>()?;
 
@@ -79,153 +95,20 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
 /// Representation of a [`From`] derive macro struct container attribute.
 ///
 /// ```rust,ignore
-/// #[from(<types>)]
 /// #[from(forward)]
+/// #[from(<types>)]
 /// ```
-enum StructAttribute {
-    /// [`Type`]s to derive [`From`].
-    Types(Punctuated<Type, token::Comma>),
-
-    /// Forward [`From`] implementation.
-    Forward(forward::Attribute),
-}
-
-impl StructAttribute {
-    /// Parses a [`StructAttribute`] from the provided [`syn::Attribute`]s.
-    fn parse_attrs(
-        attrs: impl AsRef<[syn::Attribute]>,
-        fields: &syn::Fields,
-    ) -> syn::Result<Option<Self>> {
-        Ok(attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident("from"))
-            .try_fold(None, |attrs, attr| {
-                let field_attr = attr.parse_args_with(|stream: ParseStream<'_>| {
-                    Self::parse(stream, fields)
-                })?;
-                match (attrs, field_attr) {
-                    (
-                        Some((path, StructAttribute::Types(mut tys))),
-                        StructAttribute::Types(more),
-                    ) => {
-                        tys.extend(more);
-                        Ok(Some((path, StructAttribute::Types(tys))))
-                    }
-                    (None, field_attr) => Ok(Some((attr.path(), field_attr))),
-                    _ => Err(syn::Error::new(
-                        attr.path().span(),
-                        "only single `#[from(...)]` attribute is allowed here",
-                    )),
-                }
-            })?
-            .map(|(_, attr)| attr))
-    }
-
-    /// Parses single [`StructAttribute`].
-    fn parse(input: ParseStream<'_>, fields: &syn::Fields) -> syn::Result<Self> {
-        let ahead = input.fork();
-        if let Ok(attr) = ahead.parse::<forward::Attribute>() {
-            input.advance_to(&ahead);
-            return Ok(Self::Forward(attr));
-        }
-
-        let ahead = input.fork();
-        match ahead.parse::<syn::Path>() {
-            Ok(p) if p.is_ident("types") => legacy_error(&ahead, input.span(), fields),
-            _ => input
-                .parse_terminated(Type::parse, token::Comma)
-                .map(Self::Types),
-        }
-    }
-}
+type StructAttribute = attr::Conversion;
 
 /// Representation of a [`From`] derive macro enum variant attribute.
 ///
 /// ```rust,ignore
 /// #[from]
-/// #[from(<types>)]
-/// #[from(forward)]
 /// #[from(skip)] #[from(ignore)]
+/// #[from(forward)]
+/// #[from(<types>)]
 /// ```
-enum VariantAttribute {
-    /// Explicitly derive [`From`].
-    From,
-
-    /// [`Type`]s to derive [`From`].
-    Types(Punctuated<Type, token::Comma>),
-
-    /// Forward [`From`] implementation.
-    Forward(forward::Attribute),
-
-    /// Skip variant.
-    Skip(skip::Attribute),
-}
-
-impl VariantAttribute {
-    /// Parses a [`VariantAttribute`] from the provided [`syn::Attribute`]s.
-    fn parse_attrs(
-        attrs: impl AsRef<[syn::Attribute]>,
-        fields: &syn::Fields,
-    ) -> syn::Result<Option<Self>> {
-        Ok(attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident("from"))
-            .try_fold(None, |mut attrs, attr| {
-                let field_attr = Self::parse_attr(attr, fields)?;
-                if let Some((path, _)) = attrs.replace((attr.path(), field_attr)) {
-                    Err(syn::Error::new(
-                        path.span(),
-                        "only single `#[from(...)]` attribute is allowed here",
-                    ))
-                } else {
-                    Ok(attrs)
-                }
-            })?
-            .map(|(_, attr)| attr))
-    }
-
-    /// Parses a [`VariantAttribute`] from the single provided [`syn::Attribute`].
-    fn parse_attr(attr: &syn::Attribute, fields: &syn::Fields) -> syn::Result<Self> {
-        if matches!(attr.meta, syn::Meta::Path(_)) {
-            return Ok(Self::From);
-        }
-
-        attr.parse_args_with(|input: ParseStream<'_>| {
-            let ahead = input.fork();
-            if let Ok(attr) = ahead.parse::<forward::Attribute>() {
-                input.advance_to(&ahead);
-                return Ok(Self::Forward(attr));
-            }
-
-            let ahead = input.fork();
-            if let Ok(attr) = ahead.parse::<skip::Attribute>() {
-                input.advance_to(&ahead);
-                return Ok(Self::Skip(attr));
-            }
-
-            let ahead = input.fork();
-            match ahead.parse::<syn::Path>() {
-                Ok(p) if p.is_ident("types") => {
-                    legacy_error(&ahead, input.span(), fields)
-                }
-                _ => input
-                    .parse_terminated(Type::parse, token::Comma)
-                    .map(Self::Types),
-            }
-        })
-    }
-}
-
-impl From<StructAttribute> for VariantAttribute {
-    fn from(value: StructAttribute) -> Self {
-        match value {
-            StructAttribute::Types(tys) => Self::Types(tys),
-            StructAttribute::Forward(attr) => Self::Forward(attr),
-        }
-    }
-}
+type VariantAttribute = attr::FieldConversion;
 
 /// Expansion of a macro for generating [`From`] implementation of a struct or
 /// enum.
@@ -236,11 +119,11 @@ struct Expansion<'a> {
     /// it for both derives.
     attrs: Option<&'a VariantAttribute>,
 
-    /// Struct or enum [`Ident`].
-    ident: &'a Ident,
+    /// Struct or enum [`syn::Ident`].
+    ident: &'a syn::Ident,
 
-    /// Variant [`Ident`] in case of enum expansion.
-    variant: Option<&'a Ident>,
+    /// Variant [`syn::Ident`] in case of enum expansion.
+    variant: Option<&'a syn::Ident>,
 
     /// Struct or variant [`syn::Fields`].
     fields: &'a syn::Fields,
@@ -269,7 +152,7 @@ impl<'a> Expansion<'a> {
             || (self.variant.is_some() && self.fields.is_empty());
         match (self.attrs, skip_variant) {
             (Some(VariantAttribute::Types(tys)), _) => {
-                tys.iter().map(|ty| {
+                tys.0.iter().map(|ty| {
                     let variant = self.variant.iter();
 
                     let mut from_tys = self.fields.validate_type(ty)?;
@@ -298,7 +181,7 @@ impl<'a> Expansion<'a> {
                 })
                 .collect()
             }
-            (Some(VariantAttribute::From), _) | (None, false) => {
+            (Some(VariantAttribute::Empty(_)), _) | (None, false) => {
                 let variant = self.variant.iter();
                 let init = self.expand_fields(|ident, _, index| {
                     let ident = ident.into_iter();
@@ -373,7 +256,11 @@ impl<'a> Expansion<'a> {
     /// [`syn::FieldsUnnamed`].
     fn expand_fields(
         &self,
-        mut wrap: impl FnMut(Option<&Ident>, &syn::Type, Option<syn::Index>) -> TokenStream,
+        mut wrap: impl FnMut(
+            Option<&syn::Ident>,
+            &syn::Type,
+            Option<syn::Index>,
+        ) -> TokenStream,
     ) -> TokenStream {
         let surround = match self.fields {
             syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
@@ -421,6 +308,27 @@ impl<'a> Expansion<'a> {
     }
 }
 
+/// [`attr::Parser`] considering legacy syntax for [`attr::Types`] and emitting [`legacy_error`], if
+/// any occurs.
+struct ConsiderLegacySyntax<'a> {
+    /// [`syn::Fields`] of a struct or enum variant, the attribute is parsed for.
+    fields: &'a syn::Fields,
+}
+
+impl attr::Parser for ConsiderLegacySyntax<'_> {
+    fn parse<T: Parse + Any>(&self, input: ParseStream<'_>) -> syn::Result<T> {
+        if TypeId::of::<T>() == TypeId::of::<attr::Types>() {
+            let ahead = input.fork();
+            if let Ok(p) = ahead.parse::<syn::Path>() {
+                if p.is_ident("types") {
+                    return legacy_error(&ahead, input.span(), self.fields);
+                }
+            }
+        }
+        T::parse(input)
+    }
+}
+
 /// Constructs a [`syn::Error`] for legacy syntax: `#[from(types(i32, "&str"))]`.
 fn legacy_error<T>(
     tokens: ParseStream<'_>,
diff --git a/impl/src/into.rs b/impl/src/into.rs
index 69587bc..347c97a 100644
--- a/impl/src/into.rs
+++ b/impl/src/into.rs
@@ -1,6 +1,10 @@
 //! Implementation of an [`Into`] derive macro.
 
-use std::{borrow::Cow, iter};
+use std::{
+    any::{Any, TypeId},
+    borrow::Cow,
+    iter,
+};
 
 use proc_macro2::{Span, TokenStream};
 use quote::{format_ident, quote, ToTokens as _};
@@ -9,16 +13,18 @@ use syn::{
     parse::{discouraged::Speculative as _, Parse, ParseStream},
     punctuated::Punctuated,
     spanned::Spanned as _,
-    token, Ident,
+    token,
 };
 
-use crate::{
-    parsing::Type,
-    utils::{polyfill, skip, Either, FieldsExt as _},
+use crate::utils::{
+    attr::{self, ParseMultiple as _},
+    polyfill, Either, FieldsExt as _, Spanning,
 };
 
 /// Expands an [`Into`] derive macro.
 pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
+    let attr_name = format_ident!("into");
+
     let data = match &input.data {
         syn::Data::Struct(data) => Ok(data),
         syn::Data::Enum(e) => Err(syn::Error::new(
@@ -31,19 +37,26 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
         )),
     }?;
 
-    let attr = StructAttribute::parse_attrs(&input.attrs, &data.fields)?
-        .unwrap_or_else(|| StructAttribute {
-            owned: Some(Punctuated::new()),
-            r#ref: None,
-            ref_mut: None,
-        });
+    let attr = StructAttribute::parse_attrs_with(
+        &input.attrs,
+        &attr_name,
+        &ConsiderLegacySyntax {
+            fields: &data.fields,
+        },
+    )?
+    .map(Spanning::into_inner)
+    .unwrap_or_else(|| StructAttribute {
+        owned: Some(Punctuated::new()),
+        r#ref: None,
+        ref_mut: None,
+    });
     let ident = &input.ident;
     let fields = data
         .fields
         .iter()
         .enumerate()
-        .filter_map(|(i, f)| {
-            match SkipFieldAttribute::parse_attrs(&f.attrs, &format_ident!("into")) {
+        .filter_map(
+            |(i, f)| match attr::Skip::parse_attrs(&f.attrs, &attr_name) {
                 Ok(None) => Some(Ok((
                     &f.ty,
                     f.ident.as_ref().map_or_else(
@@ -53,8 +66,8 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
                 ))),
                 Ok(Some(_)) => None,
                 Err(e) => Some(Err(e)),
-            }
-        })
+            },
+        )
         .collect::<syn::Result<Vec<_>>>()?;
     let (fields_tys, fields_idents): (Vec<_>, Vec<_>) = fields.into_iter().unzip();
     let (fields_tys, fields_idents) = (&fields_tys, &fields_idents);
@@ -79,7 +92,10 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
 
         Either::Right(
             if tys.is_empty() {
-                Either::Left(iter::once(Type::tuple(fields_tys.clone())))
+                Either::Left(iter::once(syn::Type::Tuple(syn::TypeTuple {
+                    paren_token: token::Paren::default(),
+                    elems: fields_tys.iter().cloned().cloned().collect(),
+                })))
             } else {
                 Either::Right(tys.into_iter())
             }
@@ -126,74 +142,35 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
 #[derive(Debug, Default)]
 struct StructAttribute {
     /// [`Type`]s wrapped into `owned(...)` or simply `#[into(...)]`.
-    owned: Option<Punctuated<Type, token::Comma>>,
+    owned: Option<Punctuated<syn::Type, token::Comma>>,
 
     /// [`Type`]s wrapped into `ref(...)`.
-    r#ref: Option<Punctuated<Type, token::Comma>>,
+    r#ref: Option<Punctuated<syn::Type, token::Comma>>,
 
     /// [`Type`]s wrapped into `ref_mut(...)`.
-    ref_mut: Option<Punctuated<Type, token::Comma>>,
+    ref_mut: Option<Punctuated<syn::Type, token::Comma>>,
 }
 
-impl StructAttribute {
-    /// Parses a [`StructAttribute`] from the provided [`syn::Attribute`]s.
-    fn parse_attrs(
-        attrs: impl AsRef<[syn::Attribute]>,
-        fields: &syn::Fields,
-    ) -> syn::Result<Option<Self>> {
-        fn infer<T>(v: T) -> T
-        where
-            T: for<'a> FnOnce(ParseStream<'a>) -> syn::Result<StructAttribute>,
-        {
-            v
-        }
-
-        attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident("into"))
-            .try_fold(None, |mut attrs, attr| {
-                let merge = |out: &mut Option<_>, tys| match (out.as_mut(), tys) {
-                    (None, Some(tys)) => {
-                        *out = Some::<Punctuated<_, _>>(tys);
-                    }
-                    (Some(out), Some(tys)) => out.extend(tys),
-                    (Some(_), None) | (None, None) => {}
-                };
-
-                let field_attr =
-                    attr.parse_args_with(infer(|stream| Self::parse(stream, fields)))?;
-                let out = attrs.get_or_insert_with(Self::default);
-                merge(&mut out.owned, field_attr.owned);
-                merge(&mut out.r#ref, field_attr.r#ref);
-                merge(&mut out.ref_mut, field_attr.ref_mut);
-
-                Ok(attrs)
-            })
-    }
-
-    /// Parses a single [`StructAttribute`].
-    fn parse(content: ParseStream<'_>, fields: &syn::Fields) -> syn::Result<Self> {
-        check_legacy_syntax(content, fields)?;
-
+impl Parse for StructAttribute {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
         let mut out = Self::default();
 
         let parse_inner = |ahead, types: &mut Option<_>| {
-            content.advance_to(&ahead);
+            input.advance_to(&ahead);
 
             let types = types.get_or_insert_with(Punctuated::new);
-            if content.peek(token::Paren) {
+            if input.peek(token::Paren) {
                 let inner;
-                syn::parenthesized!(inner in content);
+                syn::parenthesized!(inner in input);
 
                 types.extend(
                     inner
-                        .parse_terminated(Type::parse, token::Comma)?
+                        .parse_terminated(syn::Type::parse, token::Comma)?
                         .into_pairs(),
                 );
             }
-            if content.peek(token::Comma) {
-                let comma = content.parse::<token::Comma>()?;
+            if input.peek(token::Comma) {
+                let comma = input.parse::<token::Comma>()?;
                 if !types.empty_or_trailing() {
                     types.push_punct(comma);
                 }
@@ -205,10 +182,10 @@ impl StructAttribute {
         let mut has_wrapped_type = false;
         let mut top_level_type = None;
 
-        while !content.is_empty() {
-            let ahead = content.fork();
-            let res = if ahead.peek(Ident::peek_any) {
-                ahead.call(Ident::parse_any).map(Into::into)
+        while !input.is_empty() {
+            let ahead = input.fork();
+            let res = if ahead.peek(syn::Ident::peek_any) {
+                ahead.call(syn::Ident::parse_any).map(Into::into)
             } else {
                 ahead.parse::<syn::Path>()
             };
@@ -226,14 +203,14 @@ impl StructAttribute {
                     parse_inner(ahead, &mut out.ref_mut)?;
                 }
                 _ => {
-                    let ty = content.parse::<Type>()?;
+                    let ty = input.parse::<syn::Type>()?;
                     let _ = top_level_type.get_or_insert_with(|| ty.clone());
                     out.owned.get_or_insert_with(Punctuated::new).push_value(ty);
 
-                    if content.peek(token::Comma) {
+                    if input.peek(token::Comma) {
                         out.owned
                             .get_or_insert_with(Punctuated::new)
-                            .push_punct(content.parse::<token::Comma>()?)
+                            .push_punct(input.parse::<token::Comma>()?)
                     }
                 }
             }
@@ -243,9 +220,8 @@ impl StructAttribute {
             Err(syn::Error::new(
                 ty.span(),
                 format!(
-                    "mixing regular types with wrapped into \
-                     `owned`/`ref`/`ref_mut` is not allowed, try wrapping \
-                     this type into `owned({ty}), ref({ty}), ref_mut({ty})`",
+                    "mixing regular types with wrapped into `owned`/`ref`/`ref_mut` is not \
+                     allowed, try wrapping this type into `owned({ty}), ref({ty}), ref_mut({ty})`",
                     ty = ty.into_token_stream(),
                 ),
             ))
@@ -255,8 +231,55 @@ impl StructAttribute {
     }
 }
 
-/// `#[into(skip)]`/`#[into(ignore)]` field attribute.
-type SkipFieldAttribute = skip::Attribute;
+impl attr::ParseMultiple for StructAttribute {
+    fn merge_attrs(
+        prev: Spanning<Self>,
+        new: Spanning<Self>,
+        _: &syn::Ident,
+    ) -> syn::Result<Spanning<Self>> {
+        let merge = |out: &mut Option<_>, tys| match (out.as_mut(), tys) {
+            (None, Some(tys)) => {
+                *out = Some::<Punctuated<_, _>>(tys);
+            }
+            (Some(out), Some(tys)) => out.extend(tys),
+            (Some(_), None) | (None, None) => {}
+        };
+
+        let Spanning {
+            span: prev_span,
+            item: mut prev,
+        } = prev;
+        let Spanning {
+            span: new_span,
+            item: new,
+        } = new;
+
+        merge(&mut prev.owned, new.owned);
+        merge(&mut prev.r#ref, new.r#ref);
+        merge(&mut prev.ref_mut, new.ref_mut);
+
+        Ok(Spanning::new(
+            prev,
+            prev_span.join(new_span).unwrap_or(prev_span),
+        ))
+    }
+}
+
+/// [`attr::Parser`] considering legacy syntax and performing [`check_legacy_syntax()`] for a
+/// [`StructAttribute`].
+struct ConsiderLegacySyntax<'a> {
+    /// [`syn::Fields`] of a struct, the [`StructAttribute`] is parsed for.
+    fields: &'a syn::Fields,
+}
+
+impl attr::Parser for ConsiderLegacySyntax<'_> {
+    fn parse<T: Parse + Any>(&self, input: ParseStream<'_>) -> syn::Result<T> {
+        if TypeId::of::<T>() == TypeId::of::<StructAttribute>() {
+            check_legacy_syntax(input, self.fields)?;
+        }
+        T::parse(input)
+    }
+}
 
 /// [`Error`]ors for legacy syntax: `#[into(types(i32, "&str"))]`.
 fn check_legacy_syntax(
diff --git a/impl/src/lib.rs b/impl/src/lib.rs
index a229c84..a4ecebc 100644
--- a/impl/src/lib.rs
+++ b/impl/src/lib.rs
@@ -55,12 +55,7 @@ mod mul_helpers;
 mod mul_like;
 #[cfg(feature = "not")]
 mod not_like;
-#[cfg(any(
-    feature = "debug",
-    feature = "display",
-    feature = "from",
-    feature = "into",
-))]
+#[cfg(any(feature = "debug", feature = "display"))]
 pub(crate) mod parsing;
 #[cfg(feature = "sum")]
 mod sum_like;
diff --git a/impl/src/parsing.rs b/impl/src/parsing.rs
index 6e038cf..98028f7 100644
--- a/impl/src/parsing.rs
+++ b/impl/src/parsing.rs
@@ -1,115 +1,21 @@
 //! Common parsing utilities for derive macros.
 //!
-//! Fair parsing of [`syn::Type`] and [`syn::Expr`] requires [`syn`]'s `full`
-//! feature to be enabled, which unnecessary increases compile times. As we
-//! don't have complex AST manipulation, usually requiring only understanding
-//! where syntax item begins and ends, simpler manual parsing is implemented.
+//! Fair parsing of [`syn::Expr`] requires [`syn`]'s `full` feature to be enabled, which unnecessary
+//! increases compile times. As we don't have complex AST manipulation, usually requiring only
+//! understanding where syntax item begins and ends, simpler manual parsing is implemented.
 
-use proc_macro2::{Delimiter, Spacing, TokenStream};
+use proc_macro2::{Spacing, TokenStream};
 use quote::ToTokens;
 use syn::{
     buffer::Cursor,
     parse::{Parse, ParseStream},
-    punctuated::Punctuated,
-    spanned::Spanned as _,
-    token, Error, Ident, Result,
 };
 
-/// [`syn::Type`] [`Parse`]ing polyfill.
-#[derive(Clone, Debug)]
-pub(crate) enum Type {
-    /// [`syn::Type::Tuple`] [`Parse`]ing polyfill.
-    Tuple {
-        paren: token::Paren,
-        items: Punctuated<TokenStream, token::Comma>,
-    },
-
-    /// Every other [`syn::Type`] variant.
-    Other(TokenStream),
-}
-
-impl Type {
-    /// Creates a [`Type::Tuple`] from the provided [`Iterator`] of [`TokenStream`]s.
-    pub(crate) fn tuple<T: ToTokens>(items: impl IntoIterator<Item = T>) -> Self {
-        Self::Tuple {
-            paren: token::Paren::default(),
-            items: items.into_iter().map(ToTokens::into_token_stream).collect(),
-        }
-    }
-}
-
-impl Parse for Type {
-    fn parse(input: ParseStream) -> Result<Self> {
-        input.step(|c| {
-            let outer = *c;
-
-            if let Some((mut cursor, paren_span, next_item)) =
-                outer.group(Delimiter::Parenthesis)
-            {
-                let mut items = Punctuated::new();
-                while !cursor.eof() {
-                    let (stream, c) = Self::parse_other(cursor).ok_or_else(|| {
-                        Error::new(cursor.span(), "failed to parse type")
-                    })?;
-                    items.push_value(stream);
-                    cursor = c;
-                    if let Some((p, c)) = punct(',')(cursor) {
-                        items.push_punct(token::Comma(p.span()));
-                        cursor = c;
-                    }
-                }
-                // `(Type)` is equivalent to `Type`, so isn't top-level tuple.
-                if items.len() == 1 && !items.trailing_punct() {
-                    let stream = outer
-                        .token_tree()
-                        .unwrap_or_else(|| unreachable!())
-                        .0
-                        .into_token_stream();
-                    Ok((Type::Other(stream), next_item))
-                } else {
-                    Ok((
-                        Type::Tuple {
-                            paren: token::Paren(paren_span),
-                            items,
-                        },
-                        next_item,
-                    ))
-                }
-            } else {
-                Self::parse_other(outer)
-                    .map(|(s, c)| (Self::Other(s), c))
-                    .ok_or_else(|| Error::new(outer.span(), "failed to parse type"))
-            }
-        })
-    }
-}
-
-impl ToTokens for Type {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        match self {
-            Type::Tuple { paren, items } => {
-                paren.surround(tokens, |tokens| items.to_tokens(tokens))
-            }
-            Type::Other(other) => other.to_tokens(tokens),
-        }
-    }
-}
-
-impl Type {
-    /// Parses a single [`Type::Other`].
-    pub fn parse_other(c: Cursor<'_>) -> Option<(TokenStream, Cursor<'_>)> {
-        take_until1(
-            alt([&mut balanced_pair(punct('<'), punct('>')), &mut token_tree]),
-            punct(','),
-        )(c)
-    }
-}
-
 /// [`syn::Expr`] [`Parse`]ing polyfill.
 #[derive(Debug)]
 pub(crate) enum Expr {
     /// [`syn::Expr::Path`] of length 1 [`Parse`]ing polyfill.
-    Ident(Ident),
+    Ident(syn::Ident),
 
     /// Every other [`syn::Expr`] variant.
     Other(TokenStream),
@@ -117,7 +23,7 @@ pub(crate) enum Expr {
 
 impl Expr {
     /// Returns an [`Ident`] in case this [`Expr`] is represented only by it.
-    pub(crate) fn ident(&self) -> Option<&Ident> {
+    pub(crate) fn ident(&self) -> Option<&syn::Ident> {
         match self {
             Self::Ident(ident) => Some(ident),
             Self::Other(_) => None,
@@ -126,11 +32,11 @@ impl Expr {
 }
 
 impl Parse for Expr {
-    fn parse(input: ParseStream) -> Result<Self> {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
         if let Ok(ident) = input.step(|c| {
             c.ident()
                 .filter(|(_, c)| c.eof() || punct(',')(*c).is_some())
-                .ok_or_else(|| Error::new(c.span(), "expected `ident(,|eof)`"))
+                .ok_or_else(|| syn::Error::new(c.span(), "expected `ident(,|eof)`"))
         }) {
             Ok(Self::Ident(ident))
         } else {
@@ -151,7 +57,7 @@ impl Parse for Expr {
                     punct(','),
                 )(*c)
                 .map(|(stream, cursor)| (Self::Other(stream), cursor))
-                .ok_or_else(|| Error::new(c.span(), "failed to parse expression"))
+                .ok_or_else(|| syn::Error::new(c.span(), "failed to parse expression"))
             })
         }
     }
@@ -307,7 +213,7 @@ mod spec {
         token::Comma,
     };
 
-    use super::{Expr, Type};
+    use super::Expr;
 
     fn assert<'a, T: Debug + Parse + ToTokens>(
         input: &'a str,
@@ -341,83 +247,6 @@ mod spec {
             });
     }
 
-    mod tuple {
-        use super::*;
-
-        #[test]
-        fn zst_is_tuple() {
-            let zst = "()";
-            match syn::parse_str::<Type>(zst).unwrap() {
-                Type::Tuple { items, .. } => {
-                    assert!(items.is_empty(), "Expected empty tuple, found: {items:?}");
-                }
-                other => panic!("Expected `Type::Tuple {{ .. }}`, found: {other:?}"),
-            }
-        }
-
-        #[test]
-        fn group_not_tuple() {
-            let group = "(Type)";
-            match syn::parse_str::<Type>(group).unwrap() {
-                Type::Other(tokens) => {
-                    assert_eq!(tokens.to_string(), group);
-                }
-                tuple => panic!("Expected `Type::Other(_)`, found: {tuple:?}"),
-            }
-        }
-
-        #[test]
-        fn single_element_tuple() {
-            let tuple = "(Type,)";
-            match syn::parse_str::<Type>(tuple).unwrap() {
-                Type::Tuple { items, .. } => {
-                    assert_eq!(
-                        items.len(),
-                        1,
-                        "Expected empty tuple, found: {items:?}",
-                    );
-                    assert_eq!(items.first().unwrap().to_string(), "Type");
-                }
-                other => panic!("Expected `Type::Tuple {{ .. }}`, found: {other:?}"),
-            }
-        }
-
-        #[test]
-        fn cases() {
-            let cases = [
-                "[Type ; 3]",
-                "fn (usize) -> bool",
-                "for <'a > fn (&'a usize) -> bool",
-                "(Type)",
-                "path :: to :: Type",
-                "path :: to :: Generic < Type >",
-                "< Type as Trait >:: Assoc",
-                "< Type as Trait >:: Assoc < GAT >",
-                "* const ()",
-                "* mut ()",
-                "& i32",
-                "&'static str",
-                "& [str]",
-                "dyn Trait",
-                "dyn Trait + Send",
-                "()",
-                "(Type ,)",
-                "(Type , Type)",
-                "(Type , Type ,)",
-            ];
-
-            assert::<Type>("", []);
-            for i in 1..4 {
-                for permutations in cases.into_iter().permutations(i) {
-                    let mut input = permutations.join(",");
-                    assert::<Type>(&input, &permutations);
-                    input.push(',');
-                    assert::<Type>(&input, &permutations);
-                }
-            }
-        }
-    }
-
     mod expr {
         use super::*;
 
diff --git a/impl/src/try_from.rs b/impl/src/try_from.rs
index 8a84c68..9dbd926 100644
--- a/impl/src/try_from.rs
+++ b/impl/src/try_from.rs
@@ -1,11 +1,14 @@
 //! Implementation of a [`TryFrom`] derive macro.
 
-use std::mem;
-
-use proc_macro2::{Literal, Span, TokenStream};
+use proc_macro2::{Literal, TokenStream};
 use quote::{format_ident, quote, ToTokens};
 use syn::spanned::Spanned as _;
 
+use crate::utils::{
+    attr::{self, ParseMultiple as _},
+    Spanning,
+};
+
 /// Expands a [`TryFrom`] derive macro.
 pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
     match &input.data {
@@ -14,8 +17,21 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
             "`TryFrom` cannot be derived for structs",
         )),
         syn::Data::Enum(data) => Ok(Expansion {
-            repr: ReprAttribute::parse_attrs(&input.attrs)?,
-            attr: ItemAttribute::parse_attrs(&input.attrs)?,
+            repr: attr::ReprInt::parse_attrs(&input.attrs, &format_ident!("repr"))?
+                .map(Spanning::into_inner)
+                .unwrap_or_default(),
+            attr: ItemAttribute::parse_attrs(&input.attrs, &format_ident!("try_from"))?
+                .map(|attr| {
+                    if matches!(attr.item, ItemAttribute::Types(_)) {
+                        Err(syn::Error::new(
+                            attr.span,
+                            "`#[try_from(repr(...))]` attribute is not supported yet",
+                        ))
+                    } else {
+                        Ok(attr.item)
+                    }
+                })
+                .transpose()?,
             ident: input.ident.clone(),
             generics: input.generics.clone(),
             variants: data.variants.clone().into_iter().collect(),
@@ -32,100 +48,14 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
 ///
 /// ```rust,ignore
 /// #[try_from(repr)]
+/// #[try_from(repr(<types>))]
 /// ```
-struct ItemAttribute;
-
-impl ItemAttribute {
-    /// Parses am [`ItemAttribute`] from the provided [`syn::Attribute`]s.
-    fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> syn::Result<Option<Self>> {
-        attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident("try_from"))
-            .try_fold(None, |mut attrs, attr| {
-                let mut parsed = None;
-                attr.parse_nested_meta(|meta| {
-                    if meta.path.is_ident("repr") {
-                        parsed = Some(ItemAttribute);
-                        Ok(())
-                    } else {
-                        Err(meta.error("only `repr` is allowed here"))
-                    }
-                })?;
-                if mem::replace(&mut attrs, parsed).is_some() {
-                    Err(syn::Error::new(
-                        attr.span(),
-                        "only single `#[try_from(repr)]` attribute is allowed here",
-                    ))
-                } else {
-                    Ok(attrs)
-                }
-            })
-    }
-}
-
-/// Representation of a [`#[repr(u/i*)]` Rust attribute][0].
-///
-/// **NOTE**: Disregards any non-integer representation `#[repr]`s.
-///
-/// ```rust,ignore
-/// #[repr(<type>)]
-/// ```
-///
-/// [0]: https://doc.rust-lang.org/reference/type-layout.html#primitive-representations
-struct ReprAttribute(syn::Ident);
-
-impl ReprAttribute {
-    /// Parses a [`ReprAttribute`] from the provided [`syn::Attribute`]s.
-    ///
-    /// If there is no [`ReprAttribute`], then parses a [default `isize` discriminant][0].
-    ///
-    /// [0]: https://doc.rust-lang.org/reference/items/enumerations.html#discriminants
-    fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> syn::Result<Self> {
-        attrs
-            .as_ref()
-            .iter()
-            .filter(|attr| attr.path().is_ident("repr"))
-            .try_fold(None, |mut repr, attr| {
-                attr.parse_nested_meta(|meta| {
-                    if let Some(ident) = meta.path.get_ident() {
-                        if matches!(
-                            ident.to_string().as_str(),
-                            "u8" | "u16"
-                                | "u32"
-                                | "u64"
-                                | "u128"
-                                | "usize"
-                                | "i8"
-                                | "i16"
-                                | "i32"
-                                | "i64"
-                                | "i128"
-                                | "isize"
-                        ) {
-                            repr = Some(ident.clone());
-                            return Ok(());
-                        }
-                    }
-                    // Ignore all other attributes that could have a body, e.g. `align`.
-                    _ = meta.input.parse::<proc_macro2::Group>();
-                    Ok(())
-                })
-                .map(|_| repr)
-            })
-            .map(|repr| {
-                // Default discriminant is interpreted as `isize`:
-                // https://doc.rust-lang.org/reference/items/enumerations.html#discriminants
-                repr.unwrap_or_else(|| syn::Ident::new("isize", Span::call_site()))
-            })
-            .map(Self)
-    }
-}
+type ItemAttribute = attr::ReprConversion;
 
 /// Expansion of a macro for generating [`TryFrom`] implementation of an enum.
 struct Expansion {
     /// `#[repr(u/i*)]` of the enum.
-    repr: ReprAttribute,
+    repr: attr::ReprInt,
 
     /// [`ItemAttribute`] of the enum.
     attr: Option<ItemAttribute>,
@@ -146,12 +76,13 @@ impl ToTokens for Expansion {
         if self.attr.is_none() {
             return;
         }
+
         let ident = &self.ident;
         let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
 
-        let repr = &self.repr.0;
+        let repr_ty = &self.repr.ty();
 
-        let mut last_discriminant = quote! {0};
+        let mut last_discriminant = quote! { 0 };
         let mut inc = 0usize;
         let (consts, (discriminants, variants)): (
             Vec<syn::Ident>,
@@ -188,15 +119,15 @@ impl ToTokens for Expansion {
 
         quote! {
             #[automatically_derived]
-            impl #impl_generics ::core::convert::TryFrom<#repr #ty_generics> for #ident
+            impl #impl_generics ::core::convert::TryFrom<#repr_ty #ty_generics> for #ident
                  #where_clause
             {
-                type Error = ::derive_more::TryFromReprError<#repr>;
+                type Error = ::derive_more::TryFromReprError<#repr_ty>;
 
                 #[allow(non_upper_case_globals)]
                 #[inline]
-                fn try_from(val: #repr) -> ::core::result::Result<Self, Self::Error> {
-                    #( const #consts: #repr = #discriminants; )*
+                fn try_from(val: #repr_ty) -> ::core::result::Result<Self, Self::Error> {
+                    #( const #consts: #repr_ty = #discriminants; )*
                     match val {
                         #(#consts => ::core::result::Result::Ok(#ident::#variants),)*
                         _ => ::core::result::Result::Err(::derive_more::TryFromReprError::new(val)),
diff --git a/impl/src/utils.rs b/impl/src/utils.rs
index ff23256..a1829c0 100644
--- a/impl/src/utils.rs
+++ b/impl/src/utils.rs
@@ -19,6 +19,7 @@ use syn::{
     feature = "display",
     feature = "from",
     feature = "into",
+    feature = "try_from",
 ))]
 pub(crate) use self::either::Either;
 #[cfg(any(feature = "from", feature = "into"))]
@@ -28,8 +29,10 @@ pub(crate) use self::generics_search::GenericsSearch;
 #[cfg(any(
     feature = "as_ref",
     feature = "debug",
+    feature = "display",
     feature = "from",
     feature = "into",
+    feature = "try_from",
 ))]
 pub(crate) use self::spanning::Spanning;
 
@@ -1323,111 +1326,18 @@ pub fn is_type_parameter_used_in_type(
     }
 }
 
-#[cfg(any(feature = "as_ref", feature = "from"))]
-pub(crate) mod forward {
-    use syn::{
-        parse::{Parse, ParseStream},
-        spanned::Spanned as _,
-    };
-
-    /// Representation of a `forward` attribute.
-    ///
-    /// ```rust,ignore
-    /// #[<attribute>(forward)]
-    /// ```
-    pub(crate) struct Attribute;
-
-    impl Parse for Attribute {
-        fn parse(content: ParseStream<'_>) -> syn::Result<Self> {
-            match content.parse::<syn::Path>()? {
-                p if p.is_ident("forward") => Ok(Self),
-                p => Err(syn::Error::new(p.span(), "only `forward` allowed here")),
-            }
-        }
-    }
-}
-
-#[cfg(any(
-    feature = "as_ref",
-    feature = "debug",
-    feature = "from",
-    feature = "into",
-))]
-pub(crate) mod skip {
-    use syn::{
-        parse::{Parse, ParseStream},
-        spanned::Spanned as _,
-    };
-
-    use super::Spanning;
-
-    /// Representation of a `skip`/`ignore` attribute.
-    ///
-    /// ```rust,ignore
-    /// #[<attribute>(skip)]
-    /// #[<attribute>(ignore)]
-    /// ```
-    pub(crate) struct Attribute(&'static str);
-
-    impl Parse for Attribute {
-        fn parse(content: ParseStream<'_>) -> syn::Result<Self> {
-            match content.parse::<syn::Path>()? {
-                p if p.is_ident("skip") => Ok(Self("skip")),
-                p if p.is_ident("ignore") => Ok(Self("ignore")),
-                p => Err(syn::Error::new(
-                    p.span(),
-                    "only `skip`/`ignore` allowed here",
-                )),
-            }
-        }
-    }
-
-    impl Attribute {
-        /// Returns the concrete name of this attribute (`skip` or `ignore`).
-        pub(crate) const fn name(&self) -> &'static str {
-            self.0
-        }
-
-        /// Parses an [`Attribute`] from the provided [`syn::Attribute`]s, preserving its [`Span`].
-        ///
-        /// [`Span`]: proc_macro2::Span
-        pub(crate) fn parse_attrs(
-            attrs: impl AsRef<[syn::Attribute]>,
-            attr_ident: &syn::Ident,
-        ) -> syn::Result<Option<Spanning<Self>>> {
-            attrs
-                .as_ref()
-                .iter()
-                .filter(|attr| attr.path().is_ident(attr_ident))
-                .try_fold(None, |mut attrs, attr| {
-                    let parsed = Spanning::new(attr.parse_args()?, attr.span());
-                    if attrs.replace(parsed).is_some() {
-                        Err(syn::Error::new(
-                            attr.span(),
-                            format!(
-                                "only single `#[{attr_ident}(skip)]`/`#[{attr_ident}(ignore)]` \
-                                 attribute is allowed here",
-                            ),
-                        ))
-                    } else {
-                        Ok(attrs)
-                    }
-                })
-        }
-    }
-}
-
 #[cfg(any(
     feature = "as_ref",
     feature = "debug",
     feature = "display",
     feature = "from",
     feature = "into",
+    feature = "try_from",
 ))]
 mod either {
     use proc_macro2::TokenStream;
     use quote::ToTokens;
-    use syn::parse::{Parse, ParseStream};
+    use syn::parse::{discouraged::Speculative as _, Parse, ParseStream};
 
     /// Either [`Left`] or [`Right`].
     ///
@@ -1448,10 +1358,12 @@ mod either {
         R: Parse,
     {
         fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-            if L::parse(&input.fork()).is_ok() {
-                L::parse(input).map(Self::Left)
+            let ahead = input.fork();
+            if let Ok(left) = ahead.parse::<L>() {
+                input.advance_to(&ahead);
+                Ok(Self::Left(left))
             } else {
-                R::parse(input).map(Self::Right)
+                input.parse::<R>().map(Self::Right)
             }
         }
     }
@@ -1465,8 +1377,8 @@ mod either {
 
         fn next(&mut self) -> Option<Self::Item> {
             match self {
-                Either::Left(left) => left.next(),
-                Either::Right(right) => right.next(),
+                Self::Left(left) => left.next(),
+                Self::Right(right) => right.next(),
             }
         }
     }
@@ -1478,8 +1390,8 @@ mod either {
     {
         fn to_tokens(&self, tokens: &mut TokenStream) {
             match self {
-                Either::Left(l) => l.to_tokens(tokens),
-                Either::Right(r) => r.to_tokens(tokens),
+                Self::Left(l) => l.to_tokens(tokens),
+                Self::Right(r) => r.to_tokens(tokens),
             }
         }
     }
@@ -1488,8 +1400,10 @@ mod either {
 #[cfg(any(
     feature = "as_ref",
     feature = "debug",
+    feature = "display",
     feature = "from",
     feature = "into",
+    feature = "try_from",
 ))]
 mod spanning {
     use std::ops::{Deref, DerefMut};
@@ -1565,15 +1479,677 @@ mod spanning {
     }
 }
 
+#[cfg(any(
+    feature = "as_ref",
+    feature = "debug",
+    feature = "display",
+    feature = "from",
+    feature = "into",
+    feature = "try_from",
+))]
+pub(crate) mod attr {
+    use std::any::Any;
+
+    use syn::{
+        parse::{Parse, ParseStream},
+        spanned::Spanned as _,
+    };
+
+    use super::{Either, Spanning};
+
+    #[cfg(any(
+        feature = "as_ref",
+        feature = "debug",
+        feature = "display",
+        feature = "from",
+        feature = "into",
+    ))]
+    pub(crate) use self::skip::Skip;
+    #[cfg(any(feature = "as_ref", feature = "from"))]
+    pub(crate) use self::{
+        conversion::Conversion, field_conversion::FieldConversion, forward::Forward,
+    };
+    #[cfg(any(feature = "as_ref", feature = "from", feature = "try_from"))]
+    pub(crate) use self::{empty::Empty, types::Types};
+    #[cfg(feature = "try_from")]
+    pub(crate) use self::{repr_conversion::ReprConversion, repr_int::ReprInt};
+
+    /// [`Parse`]ing with additional state or metadata.
+    pub(crate) trait Parser {
+        /// [`Parse`]s an item, using additional state or metadata.
+        ///
+        /// Default implementation just calls [`Parse::parse()`] directly.
+        fn parse<T: Parse + Any>(&self, input: ParseStream<'_>) -> syn::Result<T> {
+            T::parse(input)
+        }
+    }
+
+    impl Parser for () {}
+
+    /// Parsing of a typed attribute from multiple [`syn::Attribute`]s.
+    pub(crate) trait ParseMultiple: Parse + Sized + 'static {
+        /// Parses this attribute from the provided single [`syn::Attribute`] with the provided
+        /// [`Parser`].
+        ///
+        /// Required, because with [`Parse`] we only able to parse inner attribute tokens, which
+        /// doesn't work for attributes with empty arguments, like `#[attr]`.
+        ///
+        /// Override this method if the default [`syn::Attribute::parse_args_with()`] is not enough.
+        fn parse_attr_with<P: Parser>(
+            attr: &syn::Attribute,
+            parser: &P,
+        ) -> syn::Result<Self> {
+            attr.parse_args_with(|ps: ParseStream<'_>| parser.parse(ps))
+        }
+
+        /// Merges multiple values of this attribute into a single one.
+        ///
+        /// Default implementation only errors, disallowing multiple values of the same attribute.
+        fn merge_attrs(
+            _prev: Spanning<Self>,
+            new: Spanning<Self>,
+            name: &syn::Ident,
+        ) -> syn::Result<Spanning<Self>> {
+            Err(syn::Error::new(
+                new.span,
+                format!("only single `#[{name}(...)]` attribute is allowed here"),
+            ))
+        }
+
+        /// Parses this attribute from the provided multiple [`syn::Attribute`]s with the provided
+        /// [`Parser`], merging them, and preserving their [`Span`].
+        ///
+        /// [`Span`]: proc_macro2::Span
+        fn parse_attrs_with<P: Parser>(
+            attrs: impl AsRef<[syn::Attribute]>,
+            name: &syn::Ident,
+            parser: &P,
+        ) -> syn::Result<Option<Spanning<Self>>> {
+            attrs
+                .as_ref()
+                .iter()
+                .filter(|attr| attr.path().is_ident(name))
+                .try_fold(None, |merged, attr| {
+                    let parsed = Spanning::new(
+                        Self::parse_attr_with(attr, parser)?,
+                        attr.span(),
+                    );
+                    if let Some(prev) = merged {
+                        Self::merge_attrs(prev, parsed, name).map(Some)
+                    } else {
+                        Ok(Some(parsed))
+                    }
+                })
+        }
+
+        /// Parses this attribute from the provided multiple [`syn::Attribute`]s with the default
+        /// [`Parse`], merging them, and preserving their [`Span`].
+        ///
+        /// [`Span`]: proc_macro2::Span
+        fn parse_attrs(
+            attrs: impl AsRef<[syn::Attribute]>,
+            name: &syn::Ident,
+        ) -> syn::Result<Option<Spanning<Self>>> {
+            Self::parse_attrs_with(attrs, name, &())
+        }
+    }
+
+    impl<L: ParseMultiple, R: ParseMultiple> ParseMultiple for Either<L, R> {
+        fn parse_attr_with<P: Parser>(
+            attr: &syn::Attribute,
+            parser: &P,
+        ) -> syn::Result<Self> {
+            L::parse_attr_with(attr, parser)
+                .map(Self::Left)
+                .or_else(|_| R::parse_attr_with(attr, parser).map(Self::Right))
+        }
+
+        fn merge_attrs(
+            prev: Spanning<Self>,
+            new: Spanning<Self>,
+            name: &syn::Ident,
+        ) -> syn::Result<Spanning<Self>> {
+            Ok(match (prev.item, new.item) {
+                (Self::Left(p), Self::Left(n)) => {
+                    L::merge_attrs(Spanning::new(p, prev.span), Spanning::new(n, new.span), name)?
+                        .map(Self::Left)
+                },
+                (Self::Right(p), Self::Right(n)) => {
+                    R::merge_attrs(Spanning::new(p, prev.span), Spanning::new(n, new.span), name)?
+                        .map(Self::Right)
+                },
+                _ => return Err(syn::Error::new(
+                    new.span,
+                    format!("only single kind of `#[{name}(...)]` attribute is allowed here"),
+                ))
+            })
+        }
+    }
+
+    #[cfg(any(feature = "as_ref", feature = "from", feature = "try_from"))]
+    mod empty {
+        use syn::{
+            parse::{Parse, ParseStream},
+            spanned::Spanned as _,
+        };
+
+        use super::{ParseMultiple, Parser, Spanning};
+
+        /// Representation of an empty attribute, containing no arguments.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>]
+        /// ```
+        pub(crate) struct Empty;
+
+        impl Parse for Empty {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                if input.is_empty() {
+                    Ok(Self)
+                } else {
+                    Err(syn::Error::new(
+                        input.span(),
+                        "no attribute arguments allowed here",
+                    ))
+                }
+            }
+        }
+
+        impl ParseMultiple for Empty {
+            fn parse_attr_with<P: Parser>(
+                attr: &syn::Attribute,
+                _: &P,
+            ) -> syn::Result<Self> {
+                if matches!(attr.meta, syn::Meta::Path(_)) {
+                    Ok(Self)
+                } else {
+                    Err(syn::Error::new(
+                        attr.span(),
+                        "no attribute arguments allowed here",
+                    ))
+                }
+            }
+
+            fn merge_attrs(
+                _prev: Spanning<Self>,
+                new: Spanning<Self>,
+                name: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                Err(syn::Error::new(
+                    new.span,
+                    format!("only single `#[{name}]` attribute is allowed here"),
+                ))
+            }
+        }
+    }
+
+    #[cfg(any(feature = "as_ref", feature = "from"))]
+    mod forward {
+        use syn::{
+            parse::{Parse, ParseStream},
+            spanned::Spanned as _,
+        };
+
+        use super::ParseMultiple;
+
+        /// Representation of a `forward` attribute.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>(forward)]
+        /// ```
+        pub(crate) struct Forward;
+
+        impl Parse for Forward {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                match input.parse::<syn::Path>()? {
+                    p if p.is_ident("forward") => Ok(Self),
+                    p => Err(syn::Error::new(p.span(), "only `forward` allowed here")),
+                }
+            }
+        }
+
+        impl ParseMultiple for Forward {}
+    }
+
+    #[cfg(feature = "try_from")]
+    mod repr_int {
+        use proc_macro2::Span;
+        use syn::parse::{Parse, ParseStream};
+
+        use super::{ParseMultiple, Parser, Spanning};
+
+        /// Representation of a [`#[repr(u/i*)]` Rust attribute][0].
+        ///
+        /// **NOTE**: Disregards any non-integer representation `#[repr]`s.
+        ///
+        /// ```rust,ignore
+        /// #[repr(<type>)]
+        /// ```
+        ///
+        /// [0]: https://doc.rust-lang.org/reference/type-layout.html#primitive-representations
+        #[derive(Default)]
+        pub(crate) struct ReprInt(Option<syn::Ident>);
+
+        impl ReprInt {
+            /// Returns [`syn::Ident`] of the primitive integer type behind this [`ReprInt`]
+            /// attribute.
+            ///
+            /// If there is no explicitly specified  primitive integer type, then returns a
+            /// [default `isize` discriminant][0].
+            ///
+            /// [0]: https://doc.rust-lang.org/reference/items/enumerations.html#discriminants
+            pub(crate) fn ty(&self) -> syn::Ident {
+                self.0
+                    .as_ref()
+                    .cloned()
+                    .unwrap_or_else(|| syn::Ident::new("isize", Span::call_site()))
+            }
+        }
+
+        impl Parse for ReprInt {
+            fn parse(_: ParseStream<'_>) -> syn::Result<Self> {
+                unreachable!("call `attr::ParseMultiple::parse_attr_with()` instead")
+            }
+        }
+
+        impl ParseMultiple for ReprInt {
+            fn parse_attr_with<P: Parser>(
+                attr: &syn::Attribute,
+                _: &P,
+            ) -> syn::Result<Self> {
+                let mut repr = None;
+                attr.parse_nested_meta(|meta| {
+                    if let Some(ident) = meta.path.get_ident() {
+                        if matches!(
+                            ident.to_string().as_str(),
+                            "u8" | "u16"
+                                | "u32"
+                                | "u64"
+                                | "u128"
+                                | "usize"
+                                | "i8"
+                                | "i16"
+                                | "i32"
+                                | "i64"
+                                | "i128"
+                                | "isize"
+                        ) {
+                            repr = Some(ident.clone());
+                            return Ok(());
+                        }
+                    }
+                    // Ignore all other attributes that could have a body, e.g. `align`.
+                    _ = meta.input.parse::<proc_macro2::Group>();
+                    Ok(())
+                })?;
+                Ok(Self(repr))
+            }
+
+            fn merge_attrs(
+                prev: Spanning<Self>,
+                new: Spanning<Self>,
+                name: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                match (&prev.item.0, &new.item.0) {
+                    (Some(_), None) | (None, None) => Ok(prev),
+                    (None, Some(_)) => Ok(new),
+                    (Some(_), Some(_)) => Err(syn::Error::new(
+                        new.span,
+                        format!(
+                            "only single `#[{name}(u/i*)]` attribute is expected here",
+                        ),
+                    )),
+                }
+            }
+        }
+    }
+
+    #[cfg(any(
+        feature = "as_ref",
+        feature = "debug",
+        feature = "display",
+        feature = "from",
+        feature = "into",
+    ))]
+    mod skip {
+        use syn::{
+            parse::{Parse, ParseStream},
+            spanned::Spanned as _,
+        };
+
+        use super::{ParseMultiple, Spanning};
+
+        /// Representation of a `skip`/`ignore` attribute.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>(skip)]
+        /// #[<attribute>(ignore)]
+        /// ```
+        pub(crate) struct Skip(&'static str);
+
+        impl Parse for Skip {
+            fn parse(content: ParseStream<'_>) -> syn::Result<Self> {
+                match content.parse::<syn::Path>()? {
+                    p if p.is_ident("skip") => Ok(Self("skip")),
+                    p if p.is_ident("ignore") => Ok(Self("ignore")),
+                    p => Err(syn::Error::new(
+                        p.span(),
+                        "only `skip`/`ignore` allowed here",
+                    )),
+                }
+            }
+        }
+
+        impl Skip {
+            /// Returns the concrete name of this attribute (`skip` or `ignore`).
+            pub(crate) const fn name(&self) -> &'static str {
+                self.0
+            }
+        }
+
+        impl ParseMultiple for Skip {
+            fn merge_attrs(
+                _: Spanning<Self>,
+                new: Spanning<Self>,
+                name: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                Err(syn::Error::new(
+                    new.span,
+                    format!(
+                        "only single `#[{name}(skip)]`/`#[{name}(ignore)]` attribute is allowed \
+                         here",
+                    ),
+                ))
+            }
+        }
+    }
+
+    #[cfg(any(feature = "as_ref", feature = "from", feature = "try_from"))]
+    mod types {
+        use syn::{
+            parse::{Parse, ParseStream},
+            punctuated::Punctuated,
+            Token,
+        };
+
+        use super::{ParseMultiple, Spanning};
+
+        /// Representation of an attribute, containing a comma-separated list of types.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>(<types>)]
+        /// ```
+        pub(crate) struct Types(pub(crate) Punctuated<syn::Type, Token![,]>);
+
+        impl Parse for Types {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                input
+                    .parse_terminated(syn::Type::parse, Token![,])
+                    .map(Self)
+            }
+        }
+
+        impl ParseMultiple for Types {
+            fn merge_attrs(
+                mut prev: Spanning<Self>,
+                new: Spanning<Self>,
+                _: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                prev.item.0.extend(new.item.0);
+                Ok(Spanning::new(
+                    prev.item,
+                    prev.span.join(new.span).unwrap_or(prev.span),
+                ))
+            }
+        }
+    }
+
+    #[cfg(any(feature = "as_ref", feature = "from"))]
+    mod conversion {
+        use syn::parse::{Parse, ParseStream};
+
+        use crate::utils::attr;
+
+        use super::{Either, ParseMultiple, Spanning};
+
+        /// Untyped analogue of a [`Conversion`], recreating its type structure via [`Either`].
+        ///
+        /// Used to piggyback [`Parse`] and [`ParseMultiple`] impls to [`Either`].
+        type Untyped = Either<attr::Forward, attr::Types>;
+
+        /// Representation of an attribute, specifying which conversions should be generated:
+        /// either forwarded via a blanket impl, or direct for concrete specified types.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>(forward)]
+        /// #[<attribute>(<types>)]
+        /// ```
+        pub(crate) enum Conversion {
+            Forward(attr::Forward),
+            Types(attr::Types),
+        }
+
+        impl From<Untyped> for Conversion {
+            fn from(v: Untyped) -> Self {
+                match v {
+                    Untyped::Left(f) => Self::Forward(f),
+                    Untyped::Right(t) => Self::Types(t),
+                }
+            }
+        }
+        impl From<Conversion> for Untyped {
+            fn from(v: Conversion) -> Self {
+                match v {
+                    Conversion::Forward(f) => Self::Left(f),
+                    Conversion::Types(t) => Self::Right(t),
+                }
+            }
+        }
+
+        impl Parse for Conversion {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                Untyped::parse(input).map(Self::from)
+            }
+        }
+
+        impl ParseMultiple for Conversion {
+            fn parse_attr_with<P: attr::Parser>(
+                attr: &syn::Attribute,
+                parser: &P,
+            ) -> syn::Result<Self> {
+                Untyped::parse_attr_with(attr, parser).map(Self::from)
+            }
+
+            fn merge_attrs(
+                prev: Spanning<Self>,
+                new: Spanning<Self>,
+                name: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                Untyped::merge_attrs(prev.map(Into::into), new.map(Into::into), name)
+                    .map(|v| v.map(Self::from))
+            }
+        }
+    }
+
+    #[cfg(any(feature = "as_ref", feature = "from"))]
+    mod field_conversion {
+        use syn::parse::{Parse, ParseStream};
+
+        use crate::utils::attr;
+
+        use super::{Either, ParseMultiple, Spanning};
+
+        /// Untyped analogue of a [`FieldConversion`], recreating its type structure via [`Either`].
+        ///
+        /// Used to piggyback [`Parse`] and [`ParseMultiple`] impls to [`Either`].
+        type Untyped =
+            Either<attr::Empty, Either<attr::Skip, Either<attr::Forward, attr::Types>>>;
+
+        /// Representation of an attribute, specifying which conversions should be generated:
+        /// either forwarded via a blanket impl, or direct for concrete specified types.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>]
+        /// #[<attribute>(skip)] #[<attribute>(ignore)]
+        /// #[<attribute>(forward)]
+        /// #[<attribute>(<types>)]
+        /// ```
+        pub(crate) enum FieldConversion {
+            Empty(attr::Empty),
+            Skip(attr::Skip),
+            Forward(attr::Forward),
+            Types(attr::Types),
+        }
+
+        impl From<Untyped> for FieldConversion {
+            fn from(v: Untyped) -> Self {
+                match v {
+                    Untyped::Left(e) => Self::Empty(e),
+                    Untyped::Right(Either::Left(s)) => Self::Skip(s),
+                    Untyped::Right(Either::Right(Either::Left(f))) => Self::Forward(f),
+                    Untyped::Right(Either::Right(Either::Right(t))) => Self::Types(t),
+                }
+            }
+        }
+
+        impl From<FieldConversion> for Untyped {
+            fn from(v: FieldConversion) -> Self {
+                match v {
+                    FieldConversion::Empty(e) => Self::Left(e),
+                    FieldConversion::Skip(s) => Self::Right(Either::Left(s)),
+                    FieldConversion::Forward(f) => {
+                        Self::Right(Either::Right(Either::Left(f)))
+                    }
+                    FieldConversion::Types(t) => {
+                        Self::Right(Either::Right(Either::Right(t)))
+                    }
+                }
+            }
+        }
+
+        impl From<attr::Conversion> for FieldConversion {
+            fn from(v: attr::Conversion) -> Self {
+                match v {
+                    attr::Conversion::Forward(f) => Self::Forward(f),
+                    attr::Conversion::Types(t) => Self::Types(t),
+                }
+            }
+        }
+
+        impl From<FieldConversion> for Option<attr::Conversion> {
+            fn from(v: FieldConversion) -> Self {
+                match v {
+                    FieldConversion::Forward(f) => Some(attr::Conversion::Forward(f)),
+                    FieldConversion::Types(t) => Some(attr::Conversion::Types(t)),
+                    FieldConversion::Empty(_) | FieldConversion::Skip(_) => None,
+                }
+            }
+        }
+
+        impl Parse for FieldConversion {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                Untyped::parse(input).map(Self::from)
+            }
+        }
+
+        impl ParseMultiple for FieldConversion {
+            fn parse_attr_with<P: attr::Parser>(
+                attr: &syn::Attribute,
+                parser: &P,
+            ) -> syn::Result<Self> {
+                Untyped::parse_attr_with(attr, parser).map(Self::from)
+            }
+
+            fn merge_attrs(
+                prev: Spanning<Self>,
+                new: Spanning<Self>,
+                name: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                Untyped::merge_attrs(prev.map(Into::into), new.map(Into::into), name)
+                    .map(|v| v.map(Self::from))
+            }
+        }
+    }
+
+    #[cfg(feature = "try_from")]
+    mod repr_conversion {
+        use syn::parse::{Parse, ParseStream};
+
+        use crate::utils::attr;
+
+        use super::{ParseMultiple, Spanning};
+
+        /// Representation of an attribute, specifying which `repr`-conversions should be generated:
+        /// either direct into a discriminant, or for concrete specified types forwarding from a
+        /// discriminant.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>(repr)]
+        /// #[<attribute>(repr(<types>))]
+        /// ```
+        pub(crate) enum ReprConversion {
+            Discriminant(attr::Empty),
+            Types(attr::Types),
+        }
+
+        impl Parse for ReprConversion {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                let prefix = syn::Ident::parse(input)?;
+                if prefix != "repr" {
+                    return Err(syn::Error::new(
+                        prefix.span(),
+                        "expected `repr` argument here",
+                    ));
+                }
+                if input.is_empty() {
+                    Ok(Self::Discriminant(attr::Empty))
+                } else {
+                    let inner;
+                    syn::parenthesized!(inner in input);
+                    Ok(Self::Types(attr::Types::parse(&inner)?))
+                }
+            }
+        }
+
+        impl ParseMultiple for ReprConversion {
+            fn merge_attrs(
+                prev: Spanning<Self>,
+                new: Spanning<Self>,
+                name: &syn::Ident,
+            ) -> syn::Result<Spanning<Self>> {
+                Ok(match (prev.item, new.item) {
+                    (Self::Discriminant(_), Self::Discriminant(_)) => {
+                        return Err(syn::Error::new(
+                            new.span,
+                            format!("only single `#[{name}(repr)]` attribute is allowed here"),
+                        ))
+                    },
+                    (Self::Types(p), Self::Types(n)) => {
+                        attr::Types::merge_attrs(
+                            Spanning::new(p, prev.span),
+                            Spanning::new(n, new.span),
+                            name,
+                        )?.map(Self::Types)
+                    },
+                    _ => return Err(syn::Error::new(
+                        new.span,
+                        format!(
+                            "only single kind of `#[{name}(repr(...))]` attribute is allowed here",
+                        ),
+                    ))
+                })
+            }
+        }
+    }
+}
+
 #[cfg(any(feature = "from", feature = "into"))]
 mod fields_ext {
     use std::{cmp, iter};
 
-    use proc_macro2::TokenStream;
+    use quote::ToTokens as _;
     use syn::{punctuated, spanned::Spanned as _};
 
-    use crate::parsing;
-
     use super::Either;
 
     /// Abstraction over `.len()` method to use it on type parameters.
@@ -1599,13 +2175,13 @@ mod fields_ext {
         /// Validates the provided [`parsing::Type`] against these [`syn::Fields`].
         fn validate_type<'t>(
             &self,
-            ty: &'t parsing::Type,
+            ty: &'t syn::Type,
         ) -> syn::Result<
-            Either<punctuated::Iter<'t, TokenStream>, iter::Once<&'t TokenStream>>,
+            Either<punctuated::Iter<'t, syn::Type>, iter::Once<&'t syn::Type>>,
         > {
             match ty {
-                parsing::Type::Tuple { items, .. } if self.len() > 1 => {
-                    match self.len().cmp(&items.len()) {
+                syn::Type::Tuple(syn::TypeTuple { elems, .. }) if self.len() > 1 => {
+                    match self.len().cmp(&elems.len()) {
                         cmp::Ordering::Greater => {
                             return Err(syn::Error::new(
                                 ty.span(),
@@ -1613,18 +2189,18 @@ mod fields_ext {
                                     "wrong tuple length: expected {}, found {}. \
                                      Consider adding {} more type{}: `({})`",
                                     self.len(),
-                                    items.len(),
-                                    self.len() - items.len(),
-                                    if self.len() - items.len() > 1 {
+                                    elems.len(),
+                                    self.len() - elems.len(),
+                                    if self.len() - elems.len() > 1 {
                                         "s"
                                     } else {
                                         ""
                                     },
-                                    items
+                                    elems
                                         .iter()
-                                        .map(|item| item.to_string())
+                                        .map(|ty| ty.into_token_stream().to_string())
                                         .chain(
-                                            (0..(self.len() - items.len()))
+                                            (0..(self.len() - elems.len()))
                                                 .map(|_| "_".to_string())
                                         )
                                         .collect::<Vec<_>>()
@@ -1639,17 +2215,17 @@ mod fields_ext {
                                     "wrong tuple length: expected {}, found {}. \
                                      Consider removing last {} type{}: `({})`",
                                     self.len(),
-                                    items.len(),
-                                    items.len() - self.len(),
-                                    if items.len() - self.len() > 1 {
+                                    elems.len(),
+                                    elems.len() - self.len(),
+                                    if elems.len() - self.len() > 1 {
                                         "s"
                                     } else {
                                         ""
                                     },
-                                    items
+                                    elems
                                         .iter()
                                         .take(self.len())
-                                        .map(ToString::to_string)
+                                        .map(|ty| ty.into_token_stream().to_string())
                                         .collect::<Vec<_>>()
                                         .join(", "),
                                 ),
@@ -1658,25 +2234,26 @@ mod fields_ext {
                         cmp::Ordering::Equal => {}
                     }
                 }
-                parsing::Type::Other(other) if self.len() > 1 => {
-                    if self.len() > 1 {
-                        return Err(syn::Error::new(
-                            other.span(),
-                            format!(
-                                "expected tuple: `({other}, {})`",
-                                (0..(self.len() - 1))
-                                    .map(|_| "_")
-                                    .collect::<Vec<_>>()
-                                    .join(", "),
-                            ),
-                        ));
-                    }
+                other if self.len() > 1 => {
+                    return Err(syn::Error::new(
+                        other.span(),
+                        format!(
+                            "expected tuple: `({}, {})`",
+                            other.into_token_stream(),
+                            (0..(self.len() - 1))
+                                .map(|_| "_")
+                                .collect::<Vec<_>>()
+                                .join(", "),
+                        ),
+                    ));
                 }
-                parsing::Type::Tuple { .. } | parsing::Type::Other(_) => {}
+                _ => {}
             }
             Ok(match ty {
-                parsing::Type::Tuple { items, .. } => Either::Left(items.iter()),
-                parsing::Type::Other(other) => Either::Right(iter::once(other)),
+                syn::Type::Tuple(syn::TypeTuple { elems, .. }) => {
+                    Either::Left(elems.iter())
+                }
+                other => Either::Right(iter::once(other)),
             })
         }
     }


Test patch:
diff --git a/tests/compile_fail/as_mut/multiple_field_attrs.stderr b/tests/compile_fail/as_mut/multiple_field_attrs.stderr
index 6e07576..55dd5d1 100644
--- a/tests/compile_fail/as_mut/multiple_field_attrs.stderr
+++ b/tests/compile_fail/as_mut/multiple_field_attrs.stderr
@@ -1,4 +1,4 @@
-error: only single `#[as_mut(...)]` attribute is allowed here
+error: only single kind of `#[as_mut(...)]` attribute is allowed here
  --> tests/compile_fail/as_mut/multiple_field_attrs.rs:4:5
   |
 4 |     #[as_mut(forward)]
diff --git a/tests/compile_fail/as_ref/multiple_field_attrs.stderr b/tests/compile_fail/as_ref/multiple_field_attrs.stderr
index e019ef3..8e78e48 100644
--- a/tests/compile_fail/as_ref/multiple_field_attrs.stderr
+++ b/tests/compile_fail/as_ref/multiple_field_attrs.stderr
@@ -1,4 +1,4 @@
-error: only single `#[as_ref(...)]` attribute is allowed here
+error: only single kind of `#[as_ref(...)]` attribute is allowed here
  --> tests/compile_fail/as_ref/multiple_field_attrs.rs:4:5
   |
 4 |     #[as_ref(forward)]
diff --git a/tests/compile_fail/debug/both_fmt_and_skip_on_field.stderr b/tests/compile_fail/debug/both_fmt_and_skip_on_field.stderr
index 31167d3..a777e3c 100644
--- a/tests/compile_fail/debug/both_fmt_and_skip_on_field.stderr
+++ b/tests/compile_fail/debug/both_fmt_and_skip_on_field.stderr
@@ -1,5 +1,5 @@
-error: only single `#[debug(...)]` attribute is allowed here
- --> tests/compile_fail/debug/both_fmt_and_skip_on_field.rs:3:7
+error: only single kind of `#[debug(...)]` attribute is allowed here
+ --> tests/compile_fail/debug/both_fmt_and_skip_on_field.rs:4:5
   |
-3 |     #[debug("Stuff({}): {}", bar)]
-  |       ^^^^^
+4 |     #[debug(skip)]
+  |     ^
diff --git a/tests/compile_fail/debug/duplicate_fmt_on_struct.stderr b/tests/compile_fail/debug/duplicate_fmt_on_struct.stderr
index ad98a72..ffc7a0a 100644
--- a/tests/compile_fail/debug/duplicate_fmt_on_struct.stderr
+++ b/tests/compile_fail/debug/duplicate_fmt_on_struct.stderr
@@ -1,5 +1,5 @@
 error: multiple `#[debug("...", ...)]` attributes aren't allowed
- --> tests/compile_fail/debug/duplicate_fmt_on_struct.rs:2:9
+ --> tests/compile_fail/debug/duplicate_fmt_on_struct.rs:3:1
   |
-2 | #[debug("Test")]
-  |         ^^^^^^
+3 | #[debug("Second")]
+  | ^
diff --git a/tests/compile_fail/debug/unknown_attribute.stderr b/tests/compile_fail/debug/unknown_attribute.stderr
index 63b1896..1820df7 100644
--- a/tests/compile_fail/debug/unknown_attribute.stderr
+++ b/tests/compile_fail/debug/unknown_attribute.stderr
@@ -1,4 +1,4 @@
-error: unknown attribute, expected `bound(...)`
+error: unknown attribute argument, expected `bound(...)`
  --> tests/compile_fail/debug/unknown_attribute.rs:2:9
   |
 2 | #[debug(unknown = "unknown")]
diff --git a/tests/compile_fail/display/unknown_attribute.stderr b/tests/compile_fail/display/unknown_attribute.stderr
index 5750586..be551fc 100644
--- a/tests/compile_fail/display/unknown_attribute.stderr
+++ b/tests/compile_fail/display/unknown_attribute.stderr
@@ -1,4 +1,4 @@
-error: unknown attribute, expected `bound(...)`
+error: unknown attribute argument, expected `bound(...)`
  --> tests/compile_fail/display/unknown_attribute.rs:3:11
   |
 3 | #[display(unknown = "unknown")]
diff --git a/tests/compile_fail/from/multiple_enum_attributes.stderr b/tests/compile_fail/from/multiple_enum_attributes.stderr
index 966e989..400aff0 100644
--- a/tests/compile_fail/from/multiple_enum_attributes.stderr
+++ b/tests/compile_fail/from/multiple_enum_attributes.stderr
@@ -1,5 +1,5 @@
-error: only single `#[from(...)]` attribute is allowed here
- --> tests/compile_fail/from/multiple_enum_attributes.rs:3:7
+error: only single kind of `#[from(...)]` attribute is allowed here
+ --> tests/compile_fail/from/multiple_enum_attributes.rs:4:5
   |
-3 |     #[from(i32)]
-  |       ^^^^
+4 |     #[from(forward)]
+  |     ^
diff --git a/tests/compile_fail/from/multiple_struct_attributes.stderr b/tests/compile_fail/from/multiple_struct_attributes.stderr
index 6c3c8ca..afdb6ac 100644
--- a/tests/compile_fail/from/multiple_struct_attributes.stderr
+++ b/tests/compile_fail/from/multiple_struct_attributes.stderr
@@ -1,5 +1,5 @@
-error: only single `#[from(...)]` attribute is allowed here
- --> tests/compile_fail/from/multiple_struct_attributes.rs:3:3
+error: only single kind of `#[from(...)]` attribute is allowed here
+ --> tests/compile_fail/from/multiple_struct_attributes.rs:3:1
   |
 3 | #[from(forward)]
-  |   ^^^^
+  | ^