From 78905f46151e070519982b3ab4f6e52495c32456 Mon Sep 17 00:00:00 2001 From: Ted Driggs Date: Fri, 23 Feb 2024 07:50:04 -0800 Subject: [PATCH] Support `with` on attrs magic field This allows the `attrs` magic field to have any type, rather than being limited to `Vec`. Fixes #273 --- CHANGELOG.md | 4 ++ README.md | 1 + core/src/codegen/attr_extractor.rs | 30 +++++--- core/src/codegen/attrs_field.rs | 38 ++++++++++ core/src/codegen/from_attributes_impl.rs | 4 ++ core/src/codegen/from_derive_impl.rs | 12 +++- core/src/codegen/from_field.rs | 12 +++- core/src/codegen/from_type_param.rs | 12 +++- core/src/codegen/from_variant_impl.rs | 12 +++- core/src/codegen/mod.rs | 1 + core/src/options/attrs_field.rs | 44 ++++++++++++ core/src/options/mod.rs | 2 + core/src/options/outer_from.rs | 10 +-- tests/attrs_with.rs | 80 +++++++++++++++++++++ tests/compile-fail/attrs_with_bad_fn.rs | 15 ++++ tests/compile-fail/attrs_with_bad_fn.stderr | 20 ++++++ 16 files changed, 273 insertions(+), 24 deletions(-) create mode 100644 core/src/codegen/attrs_field.rs create mode 100644 core/src/options/attrs_field.rs create mode 100644 tests/attrs_with.rs create mode 100644 tests/compile-fail/attrs_with_bad_fn.rs create mode 100644 tests/compile-fail/attrs_with_bad_fn.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ba98c6..35ea4187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add `#[darling(with = ...)]` support to `attrs` magic field to allow using custom receiver types for `attrs` [#273](https://github.com/TedDriggs/darling/issues/273) + ## v0.20.7 (February 22, 2024) - Add `#[darling(flatten)]` to allow forwarding unknown fields to another struct [#146](https://github.com/TedDriggs/darling/issues/146) diff --git a/README.md b/README.md index b0866d46..023e8adc 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Darling's features are built to work well for real-world projects. Additionally, `Option` and `darling::util::Flag` fields are innately optional; you don't need to declare `#[darling(default)]` for those. - **Field Renaming**: Fields can have different names in usage vs. the backing code. - **Auto-populated fields**: Structs deriving `FromDeriveInput` and `FromField` can declare properties named `ident`, `vis`, `ty`, `attrs`, and `generics` to automatically get copies of the matching values from the input AST. `FromDeriveInput` additionally exposes `data` to get access to the body of the deriving type, and `FromVariant` exposes `fields`. + - **Transformation of forwarded attributes**: You can add `#[darling(with=path)]` to the `attrs` field to use a custom function to transform the forwarded attributes before they're provided to your struct. The function signature is `fn(Vec) -> darling::Result`, where `T` is the type you declared for the `attrs` field. Returning an error from this function will propagate with all other parsing errors. - **Mapping function**: Use `#[darling(map="path")]` or `#[darling(and_then="path")]` to specify a function that runs on the result of parsing a meta-item field. This can change the return type, which enables you to parse to an intermediate form and convert that to the type you need in your struct. - **Skip fields**: Use `#[darling(skip)]` to mark a field that shouldn't be read from attribute meta-items. - **Multiple-occurrence fields**: Use `#[darling(multiple)]` on a `Vec` field to allow that field to appear multiple times in the meta-item. Each occurrence will be pushed into the `Vec`. diff --git a/core/src/codegen/attr_extractor.rs b/core/src/codegen/attr_extractor.rs index 4afb174b..64c8f271 100644 --- a/core/src/codegen/attr_extractor.rs +++ b/core/src/codegen/attr_extractor.rs @@ -1,7 +1,8 @@ use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, ToTokens}; -use crate::options::ForwardAttrs; +use crate::codegen::attrs_field; +use crate::options::{AttrsField, ForwardAttrs}; use crate::util::PathList; /// Infrastructure for generating an attribute extractor. @@ -14,6 +15,9 @@ pub trait ExtractAttribute { fn forwarded_attrs(&self) -> Option<&ForwardAttrs>; + /// Gets the field that will receive forwarded attributes. + fn attrs_field(&self) -> Option<&AttrsField>; + /// Gets the name used by the generated impl to return to the `syn` item passed as input. fn param_name(&self) -> TokenStream; @@ -30,13 +34,20 @@ pub trait ExtractAttribute { /// Generates the main extraction loop. fn extractor(&self) -> TokenStream { - let declarations = self.local_declarations(); + let mut declarations = self.local_declarations(); + self.attrs_field() + .map(attrs_field::Declaration) + .to_tokens(&mut declarations); let will_parse_any = !self.attr_names().is_empty(); - let will_fwd_any = self - .forwarded_attrs() - .map(|fa| !fa.is_empty()) - .unwrap_or_default(); + + // Forwarding requires both that there be some items we would forward, + // and a place that will keep the forwarded items. + let will_fwd_any = self.attrs_field().is_some() + && self + .forwarded_attrs() + .map(|fa| !fa.is_empty()) + .unwrap_or_default(); if !(will_parse_any || will_fwd_any) { return quote! { @@ -82,6 +93,8 @@ pub trait ExtractAttribute { quote!() }; + let fwd_population = self.attrs_field().map(attrs_field::ValuePopulator); + // Specifies the behavior for unhandled attributes. They will either be silently ignored or // forwarded to the inner struct for later analysis. let forward_unhandled = if will_fwd_any { @@ -93,7 +106,6 @@ pub trait ExtractAttribute { quote!( #declarations use ::darling::ToTokens; - let mut __fwd_attrs: ::darling::export::Vec<::darling::export::syn::Attribute> = vec![]; for __attr in #attrs_accessor { // Filter attributes based on name @@ -102,6 +114,8 @@ pub trait ExtractAttribute { #forward_unhandled } } + + #fwd_population ) } } diff --git a/core/src/codegen/attrs_field.rs b/core/src/codegen/attrs_field.rs new file mode 100644 index 00000000..b75bd6a5 --- /dev/null +++ b/core/src/codegen/attrs_field.rs @@ -0,0 +1,38 @@ +use quote::{quote, quote_spanned, ToTokens, TokenStreamExt}; +use syn::spanned::Spanned; + +use crate::options::AttrsField; + +pub struct Declaration<'a>(pub &'a AttrsField); + +impl ToTokens for Declaration<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ident = &self.0.ident; + tokens.append_all(quote! { + let mut __fwd_attrs: ::darling::export::Vec<::darling::export::syn::Attribute> = vec![]; + let mut #ident: ::darling::export::Option<_> = None; + }); + } +} + +pub struct ValuePopulator<'a>(pub &'a AttrsField); + +impl ToTokens for ValuePopulator<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let AttrsField { ident, with } = self.0; + let initializer_expr = match with { + Some(with) => quote_spanned!(with.span()=> __errors.handle(#with(__fwd_attrs))), + None => quote!(::darling::export::Some(__fwd_attrs)), + }; + tokens.append_all(quote!(#ident = #initializer_expr;)); + } +} + +pub struct Initializer<'a>(pub &'a AttrsField); + +impl ToTokens for Initializer<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ident = &self.0.ident; + tokens.append_all(quote!(#ident: #ident.expect("Errors were already checked"),)); + } +} diff --git a/core/src/codegen/from_attributes_impl.rs b/core/src/codegen/from_attributes_impl.rs index 49bbf02c..a2cf193d 100644 --- a/core/src/codegen/from_attributes_impl.rs +++ b/core/src/codegen/from_attributes_impl.rs @@ -81,6 +81,10 @@ impl<'a> ExtractAttribute for FromAttributesImpl<'a> { None } + fn attrs_field(&self) -> Option<&crate::options::AttrsField> { + None + } + fn param_name(&self) -> TokenStream { quote!(__di) } diff --git a/core/src/codegen/from_derive_impl.rs b/core/src/codegen/from_derive_impl.rs index 1c1df63d..ede385d7 100644 --- a/core/src/codegen/from_derive_impl.rs +++ b/core/src/codegen/from_derive_impl.rs @@ -5,15 +5,17 @@ use syn::Ident; use crate::{ ast::Data, codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}, - options::{DeriveInputShapeSet, ForwardAttrs}, + options::{AttrsField, DeriveInputShapeSet, ForwardAttrs}, util::PathList, }; +use super::attrs_field; + pub struct FromDeriveInputImpl<'a> { pub ident: Option<&'a Ident>, pub generics: Option<&'a Ident>, pub vis: Option<&'a Ident>, - pub attrs: Option<&'a Ident>, + pub attrs: Option<&'a AttrsField>, pub data: Option<&'a Ident>, pub base: TraitImpl<'a>, pub attr_names: &'a PathList, @@ -54,7 +56,7 @@ impl<'a> ToTokens for FromDeriveInputImpl<'a> { .generics .as_ref() .map(|i| quote!(#i: ::darling::FromGenerics::from_generics(&#input.generics)?,)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.attrs.clone().map(attrs_field::Initializer); let passed_body = self .data .as_ref() @@ -119,6 +121,10 @@ impl<'a> ExtractAttribute for FromDeriveInputImpl<'a> { self.forward_attrs } + fn attrs_field(&self) -> Option<&AttrsField> { + self.attrs + } + fn param_name(&self) -> TokenStream { quote!(__di) } diff --git a/core/src/codegen/from_field.rs b/core/src/codegen/from_field.rs index 74f34060..fd6d0411 100644 --- a/core/src/codegen/from_field.rs +++ b/core/src/codegen/from_field.rs @@ -4,17 +4,19 @@ use syn::Ident; use crate::{ codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}, - options::ForwardAttrs, + options::{AttrsField, ForwardAttrs}, util::PathList, }; +use super::attrs_field; + /// `impl FromField` generator. This is used for parsing an individual /// field and its attributes. pub struct FromFieldImpl<'a> { pub ident: Option<&'a Ident>, pub vis: Option<&'a Ident>, pub ty: Option<&'a Ident>, - pub attrs: Option<&'a Ident>, + pub attrs: Option<&'a AttrsField>, pub base: TraitImpl<'a>, pub attr_names: &'a PathList, pub forward_attrs: Option<&'a ForwardAttrs>, @@ -43,7 +45,7 @@ impl<'a> ToTokens for FromFieldImpl<'a> { .map(|i| quote!(#i: #input.ident.clone(),)); let passed_vis = self.vis.as_ref().map(|i| quote!(#i: #input.vis.clone(),)); let passed_ty = self.ty.as_ref().map(|i| quote!(#i: #input.ty.clone(),)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.attrs.clone().map(attrs_field::Initializer); // Determine which attributes to forward (if any). let grab_attrs = self.extractor(); @@ -86,6 +88,10 @@ impl<'a> ExtractAttribute for FromFieldImpl<'a> { self.forward_attrs } + fn attrs_field(&self) -> Option<&AttrsField> { + self.attrs + } + fn param_name(&self) -> TokenStream { quote!(__field) } diff --git a/core/src/codegen/from_type_param.rs b/core/src/codegen/from_type_param.rs index 1cce0cad..df6103c0 100644 --- a/core/src/codegen/from_type_param.rs +++ b/core/src/codegen/from_type_param.rs @@ -3,13 +3,15 @@ use quote::{quote, ToTokens}; use syn::Ident; use crate::codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}; -use crate::options::ForwardAttrs; +use crate::options::{AttrsField, ForwardAttrs}; use crate::util::PathList; +use super::attrs_field; + pub struct FromTypeParamImpl<'a> { pub base: TraitImpl<'a>, pub ident: Option<&'a Ident>, - pub attrs: Option<&'a Ident>, + pub attrs: Option<&'a AttrsField>, pub bounds: Option<&'a Ident>, pub default: Option<&'a Ident>, pub attr_names: &'a PathList, @@ -36,7 +38,7 @@ impl<'a> ToTokens for FromTypeParamImpl<'a> { .ident .as_ref() .map(|i| quote!(#i: #input.ident.clone(),)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.attrs.clone().map(attrs_field::Initializer); let passed_bounds = self .bounds .as_ref() @@ -85,6 +87,10 @@ impl<'a> ExtractAttribute for FromTypeParamImpl<'a> { self.forward_attrs } + fn attrs_field(&self) -> Option<&AttrsField> { + self.attrs + } + fn param_name(&self) -> TokenStream { quote!(__type_param) } diff --git a/core/src/codegen/from_variant_impl.rs b/core/src/codegen/from_variant_impl.rs index 1598e6f2..911f3706 100644 --- a/core/src/codegen/from_variant_impl.rs +++ b/core/src/codegen/from_variant_impl.rs @@ -3,9 +3,11 @@ use quote::{quote, ToTokens}; use syn::Ident; use crate::codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}; -use crate::options::{DataShape, ForwardAttrs}; +use crate::options::{AttrsField, DataShape, ForwardAttrs}; use crate::util::PathList; +use super::attrs_field; + pub struct FromVariantImpl<'a> { pub base: TraitImpl<'a>, /// If set, the ident of the field into which the variant ident should be placed. @@ -23,7 +25,7 @@ pub struct FromVariantImpl<'a> { /// variant should be placed. /// /// This is one of `darling`'s "magic fields". - pub attrs: Option<&'a Ident>, + pub attrs: Option<&'a AttrsField>, /// If set, the ident of the field into which the discriminant of the input variant /// should be placed. The receiving field must be an `Option` as not all enums have /// discriminants. @@ -48,7 +50,7 @@ impl<'a> ToTokens for FromVariantImpl<'a> { .discriminant .as_ref() .map(|i| quote!(#i: #input.discriminant.as_ref().map(|(_, expr)| expr.clone()),)); - let passed_attrs = self.attrs.as_ref().map(|i| quote!(#i: __fwd_attrs,)); + let passed_attrs = self.attrs.clone().map(attrs_field::Initializer); let passed_fields = self .fields .as_ref() @@ -115,6 +117,10 @@ impl<'a> ExtractAttribute for FromVariantImpl<'a> { self.forward_attrs } + fn attrs_field(&self) -> Option<&AttrsField> { + self.attrs + } + fn param_name(&self) -> TokenStream { quote!(__variant) } diff --git a/core/src/codegen/mod.rs b/core/src/codegen/mod.rs index d2a1546e..12d8058b 100644 --- a/core/src/codegen/mod.rs +++ b/core/src/codegen/mod.rs @@ -1,4 +1,5 @@ mod attr_extractor; +pub(in crate::codegen) mod attrs_field; mod default_expr; mod error; mod field; diff --git a/core/src/options/attrs_field.rs b/core/src/options/attrs_field.rs new file mode 100644 index 00000000..e16f9962 --- /dev/null +++ b/core/src/options/attrs_field.rs @@ -0,0 +1,44 @@ +use proc_macro2::Ident; +use syn::Path; + +use crate::{Error, FromField, FromMeta}; + +use super::ParseAttribute; + +/// The `attrs` magic field and attributes that influence its behavior. +#[derive(Debug, Clone)] +pub struct AttrsField { + /// The ident of the field that will receive the forwarded attributes. + pub ident: Ident, + /// Path of the function that will be called to convert the `Vec` of + /// forwarded attributes into the type expected by the field in `ident`. + pub with: Option, +} + +impl FromField for AttrsField { + fn from_field(field: &syn::Field) -> crate::Result { + let result = Self { + ident: field.ident.clone().ok_or_else(|| { + Error::custom("attributes receiver must be named field").with_span(field) + })?, + with: None, + }; + + result.parse_attributes(&field.attrs) + } +} + +impl ParseAttribute for AttrsField { + fn parse_nested(&mut self, mi: &syn::Meta) -> crate::Result<()> { + if mi.path().is_ident("with") { + if self.with.is_some() { + return Err(Error::duplicate_field_path(mi.path()).with_span(mi)); + } + + self.with = FromMeta::from_meta(mi)?; + Ok(()) + } else { + Err(Error::unknown_field_path_with_alts(mi.path(), &["with"]).with_span(mi)) + } + } +} diff --git a/core/src/options/mod.rs b/core/src/options/mod.rs index cb43ee76..1360e4d3 100644 --- a/core/src/options/mod.rs +++ b/core/src/options/mod.rs @@ -5,6 +5,7 @@ use crate::ast::NestedMeta; use crate::error::Accumulator; use crate::{Error, FromMeta, Result}; +mod attrs_field; mod core; mod forward_attrs; mod from_attributes; @@ -18,6 +19,7 @@ mod input_variant; mod outer_from; mod shape; +pub use self::attrs_field::AttrsField; pub use self::core::Core; pub use self::forward_attrs::ForwardAttrs; pub use self::from_attributes::FromAttributesOptions; diff --git a/core/src/options/outer_from.rs b/core/src/options/outer_from.rs index e3a4b471..1094491d 100644 --- a/core/src/options/outer_from.rs +++ b/core/src/options/outer_from.rs @@ -1,9 +1,11 @@ use syn::spanned::Spanned; use syn::{Field, Ident, Meta}; -use crate::options::{Core, DefaultExpression, ForwardAttrs, ParseAttribute, ParseData}; +use crate::options::{ + AttrsField, Core, DefaultExpression, ForwardAttrs, ParseAttribute, ParseData, +}; use crate::util::PathList; -use crate::{FromMeta, Result}; +use crate::{FromField, FromMeta, Result}; /// Reusable base for `FromDeriveInput`, `FromVariant`, `FromField`, and other top-level /// `From*` traits. @@ -13,7 +15,7 @@ pub struct OuterFrom { pub ident: Option, /// The field on the target struct which should receive the type attributes, if any. - pub attrs: Option, + pub attrs: Option, pub container: Core, @@ -72,7 +74,7 @@ impl ParseData for OuterFrom { Ok(()) } Some("attrs") => { - self.attrs = field.ident.clone(); + self.attrs = AttrsField::from_field(field).map(Some)?; Ok(()) } _ => self.container.parse_field(field), diff --git a/tests/attrs_with.rs b/tests/attrs_with.rs new file mode 100644 index 00000000..198322dc --- /dev/null +++ b/tests/attrs_with.rs @@ -0,0 +1,80 @@ +use std::collections::BTreeSet; + +use darling::{util, Error, FromDeriveInput, Result}; +use syn::{parse_quote, Attribute}; + +fn unique_idents(attrs: Vec) -> Result> { + let mut errors = Error::accumulator(); + let idents = attrs + .into_iter() + .filter_map(|attr| { + let path = attr.path(); + errors.handle( + path.get_ident() + .map(std::string::ToString::to_string) + .ok_or_else(|| { + Error::custom(format!("`{}` is not an ident", util::path_to_string(path))) + .with_span(path) + }), + ) + }) + .collect(); + + errors.finish_with(idents) +} + +#[derive(FromDeriveInput)] +#[darling(attributes(a), forward_attrs)] +struct Receiver { + #[darling(with = unique_idents)] + attrs: BTreeSet, + other: Option, +} + +#[test] +fn succeeds_on_no_attrs() { + let di = Receiver::from_derive_input(&parse_quote! { + struct Demo; + }) + .unwrap(); + + assert!(di.attrs.is_empty()); +} + +#[test] +fn succeeds_on_valid_input() { + let di = Receiver::from_derive_input(&parse_quote! { + #[allow(dead_code)] + /// testing + #[another] + struct Demo; + }) + .unwrap(); + + assert_eq!(di.attrs.len(), 3); + assert!(di.attrs.contains("allow")); + assert!(di.attrs.contains("another")); + assert!(di.attrs.contains("doc")); + assert_eq!(di.other, None); +} + +#[test] +fn errors_combined_with_others() { + let e = Receiver::from_derive_input(&parse_quote! { + #[path::to::attr(dead_code)] + #[a(other = 5)] + struct Demo; + }) + .map(|_| "Should have failed") + .unwrap_err(); + + let error = e.to_string(); + + assert_eq!(e.len(), 2); + + // Look for the error on the field `other` + assert!(error.contains("at other")); + + // Look for the invalid path from attrs conversion + assert!(error.contains("`path::to::attr`")); +} diff --git a/tests/compile-fail/attrs_with_bad_fn.rs b/tests/compile-fail/attrs_with_bad_fn.rs new file mode 100644 index 00000000..6063f647 --- /dev/null +++ b/tests/compile-fail/attrs_with_bad_fn.rs @@ -0,0 +1,15 @@ +use darling::FromDeriveInput; +use syn::Attribute; + +fn bad_converter(attrs: Vec) -> Vec { + attrs +} + +#[derive(FromDeriveInput)] +#[darling(forward_attrs)] +struct Receiver { + #[darling(with = bad_converter)] + attrs: Vec, +} + +fn main() {} diff --git a/tests/compile-fail/attrs_with_bad_fn.stderr b/tests/compile-fail/attrs_with_bad_fn.stderr new file mode 100644 index 00000000..efdc201c --- /dev/null +++ b/tests/compile-fail/attrs_with_bad_fn.stderr @@ -0,0 +1,20 @@ +error[E0308]: mismatched types + --> tests/compile-fail/attrs_with_bad_fn.rs:11:22 + | +11 | #[darling(with = bad_converter)] + | ^^^^^^^^^^^^^ + | | + | expected enum `Result`, found struct `Vec` + | arguments to this function are incorrect + | + = note: expected enum `Result<_, darling::Error>` + found struct `Vec` +note: associated function defined here + --> core/src/error/mod.rs + | + | pub fn handle(&mut self, result: Result) -> Option { + | ^^^^^^ +help: try wrapping the expression in `Ok` + | +11 | #[darling(with = Ok(bad_converter))] + | +++ +