Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flatten #270

Merged
merged 6 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add `#[darling(flatten)]` to allow forwarding unknown fields to another struct [#146](https://github.com/TedDriggs/darling/issues/146)
- Don't suggest names of skipped fields in derived impls [#268](https://github.com/TedDriggs/darling/issues/268)

## v0.20.6 (February 14, 2024)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Darling's features are built to work well for real-world projects.
- **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`.
- **Span access**: Use `darling::util::SpannedValue` in a struct to get access to that meta item's source code span. This can be used to emit warnings that point at a specific field from your proc macro. In addition, you can use `darling::Error::write_errors` to automatically get precise error location details in most cases.
- **"Did you mean" suggestions**: Compile errors from derived darling trait impls include suggestions for misspelled fields.
- **Struct flattening**: Use `#[darling(flatten)]` to remove one level of structure when presenting your meta item to users. Fields that are not known to the parent struct will be forwarded to the `flatten` field.

## Shape Validation

Expand Down
121 changes: 90 additions & 31 deletions core/src/codegen/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ pub struct Field<'a> {
pub post_transform: Option<&'a PostfixTransform>,
pub skip: bool,
pub multiple: bool,
/// If set, this field will be given all unclaimed meta items and will
/// not be exposed as a standard named field.
pub flatten: bool,
}

impl<'a> Field<'a> {
/// Get the name of the meta item that should be matched against input and should be used in diagnostics.
///
/// This will be `None` if the field is `skip` or `flatten`, as neither kind of field is addressable
/// by name from the input meta.
pub fn as_name(&'a self) -> Option<&'a str> {
if self.skip {
if self.skip || self.flatten {
None
} else {
Some(&self.name_in_attr)
Expand All @@ -41,6 +48,16 @@ impl<'a> Field<'a> {
Declaration(self)
}

pub fn as_flatten_initializer(
&'a self,
parent_field_names: Vec<&'a str>,
) -> FlattenInitializer<'a> {
FlattenInitializer {
field: self,
parent_field_names,
}
}

pub fn as_match(&'a self) -> MatchArm<'a> {
MatchArm(self)
}
Expand Down Expand Up @@ -82,41 +99,84 @@ impl<'a> ToTokens for Declaration<'a> {
}
}

pub struct FlattenInitializer<'a> {
field: &'a Field<'a>,
parent_field_names: Vec<&'a str>,
}

impl<'a> ToTokens for FlattenInitializer<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
field,
parent_field_names,
} = self;
let ident = field.ident;

let add_parent_fields = if parent_field_names.is_empty() {
None
} else {
Some(quote! {
.map_err(|e| e.add_sibling_alts_for_unknown_field(&[#(#parent_field_names),*]))
})
};

tokens.append_all(quote! {
#ident = (true,
__errors.handle(
::darling::FromMeta::from_list(
__flatten
.into_iter()
.cloned()
.map(::darling::ast::NestedMeta::Meta)
.collect::<Vec<_>>().as_slice()
) #add_parent_fields
)
);
});
}
}

/// Represents an individual field in the match.
pub struct MatchArm<'a>(&'a Field<'a>);

impl<'a> ToTokens for MatchArm<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let field: &Field = self.0;
if !field.skip {
let name_str = &field.name_in_attr;
let ident = field.ident;
let with_path = &field.with_path;
let post_transform = field.post_transform.as_ref();

// Errors include the location of the bad input, so we compute that here.
// Fields that take multiple values add the index of the error for convenience,
// while single-value fields only expose the name in the input attribute.
let location = if field.multiple {
// we use the local variable `len` here because location is accessed via
// a closure, and the borrow checker gets very unhappy if we try to immutably
// borrow `#ident` in that closure when it was declared `mut` outside.
quote!(&format!("{}[{}]", #name_str, __len))
} else {
quote!(#name_str)
};

// Give darling's generated code the span of the `with_path` so that if the target
// type doesn't impl FromMeta, darling's immediate user gets a properly-spanned error.
//
// Within the generated code, add the span immediately on extraction failure, so that it's
// as specific as possible.
// The behavior of `with_span` makes this safe to do; if the child applied an
// even-more-specific span, our attempt here will not overwrite that and will only cost
// us one `if` check.
let extractor = quote_spanned!(with_path.span()=>#with_path(__inner)#post_transform.map_err(|e| e.with_span(&__inner).at(#location)));

tokens.append_all(if field.multiple {

// Skipped and flattened fields cannot be populated by a meta
// with their name, so they do not have a match arm.
if field.skip || field.flatten {
return;
}

let name_str = &field.name_in_attr;
let ident = field.ident;
let with_path = &field.with_path;
let post_transform = field.post_transform.as_ref();

// Errors include the location of the bad input, so we compute that here.
// Fields that take multiple values add the index of the error for convenience,
// while single-value fields only expose the name in the input attribute.
let location = if field.multiple {
// we use the local variable `len` here because location is accessed via
// a closure, and the borrow checker gets very unhappy if we try to immutably
// borrow `#ident` in that closure when it was declared `mut` outside.
quote!(&format!("{}[{}]", #name_str, __len))
} else {
quote!(#name_str)
};

// Give darling's generated code the span of the `with_path` so that if the target
// type doesn't impl FromMeta, darling's immediate user gets a properly-spanned error.
//
// Within the generated code, add the span immediately on extraction failure, so that it's
// as specific as possible.
// The behavior of `with_span` makes this safe to do; if the child applied an
// even-more-specific span, our attempt here will not overwrite that and will only cost
// us one `if` check.
let extractor = quote_spanned!(with_path.span()=>#with_path(__inner)#post_transform.map_err(|e| e.with_span(&__inner).at(#location)));

tokens.append_all(if field.multiple {
quote!(
#name_str => {
// Store the index of the name we're assessing in case we need
Expand All @@ -138,7 +198,6 @@ impl<'a> ToTokens for MatchArm<'a> {
}
)
});
}
}
}

Expand Down
1 change: 1 addition & 0 deletions core/src/codegen/outer_from_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub trait OuterFromImpl<'a> {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

tokens.append_all(quote!(
#[automatically_derived]
impl #impl_generics #trayt for #ty_ident #ty_generics
#where_clause
{
Expand Down
34 changes: 31 additions & 3 deletions core/src/codegen/variant_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ use crate::codegen::Field;
pub struct FieldsGen<'a> {
fields: &'a Fields<Field<'a>>,
allow_unknown_fields: bool,
flatten_field: Option<&'a Field<'a>>,
}

impl<'a> FieldsGen<'a> {
pub fn new(fields: &'a Fields<Field<'a>>, allow_unknown_fields: bool) -> Self {
Self {
fields,
flatten_field: fields.fields.iter().find(|f| f.flatten),
allow_unknown_fields,
}
}
Expand All @@ -36,11 +38,29 @@ impl<'a> FieldsGen<'a> {
pub(in crate::codegen) fn core_loop(&self) -> TokenStream {
let arms = self.fields.as_ref().map(Field::as_match);

let (flatten_buffer, flatten_declaration) = if let Some(flatten_field) = self.flatten_field
{
(
quote! { let mut __flatten = vec![]; },
Some(flatten_field.as_flatten_initializer(self.field_names().collect())),
)
} else {
(quote!(), None)
};

// If there is a flatten field, buffer the unknown field so it can be passed
// to the flatten function with all other unknown fields.
let handle_unknown = if self.flatten_field.is_some() {
quote! {
__flatten.push(__inner);
}
}
// If we're allowing unknown fields, then handling one is a no-op.
// Otherwise, we're going to push a new spanned error pointing at the field.
let handle_unknown = if self.allow_unknown_fields {
else if self.allow_unknown_fields {
quote!()
} else {
}
// Otherwise, we're going to push a new spanned error pointing at the field.
else {
let mut names = self.fields.iter().filter_map(Field::as_name).peekable();
// We can't call `unknown_field_with_alts` with an empty slice, or else it fails to
// infer the type of the slice item.
Expand All @@ -57,6 +77,8 @@ impl<'a> FieldsGen<'a> {
let arms = arms.iter();

quote!(
#flatten_buffer

for __item in __items {
match *__item {
::darling::export::NestedMeta::Meta(ref __inner) => {
Expand All @@ -72,6 +94,8 @@ impl<'a> FieldsGen<'a> {
}
}
}

#flatten_declaration
)
}

Expand All @@ -95,4 +119,8 @@ impl<'a> FieldsGen<'a> {

quote!(#(#inits),*)
}

fn field_names(&self) -> impl Iterator<Item = &str> {
self.fields.iter().filter_map(Field::as_name)
}
}
36 changes: 27 additions & 9 deletions core/src/error/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type MetaFormat = String;
#[derive(Debug, Clone)]
// Don't want to publicly commit to ErrorKind supporting equality yet, but
// not having it makes testing very difficult.
#[cfg_attr(test, derive(PartialEq, Eq))]
#[cfg_attr(test, derive(PartialEq))]
pub(in crate::error) enum ErrorKind {
/// An arbitrary error message.
Custom(String),
Expand Down Expand Up @@ -120,14 +120,14 @@ impl From<ErrorUnknownField> for ErrorKind {
#[derive(Clone, Debug)]
// Don't want to publicly commit to ErrorKind supporting equality yet, but
// not having it makes testing very difficult.
#[cfg_attr(test, derive(PartialEq, Eq))]
#[cfg_attr(test, derive(PartialEq))]
pub(in crate::error) struct ErrorUnknownField {
name: String,
did_you_mean: Option<String>,
did_you_mean: Option<(f64, String)>,
}

impl ErrorUnknownField {
pub fn new<I: Into<String>>(name: I, did_you_mean: Option<String>) -> Self {
pub fn new<I: Into<String>>(name: I, did_you_mean: Option<(f64, String)>) -> Self {
ErrorUnknownField {
name: name.into(),
did_you_mean,
Expand All @@ -142,14 +142,32 @@ impl ErrorUnknownField {
ErrorUnknownField::new(field, did_you_mean(field, alternates))
}

/// Add more alternate field names to the error, updating the `did_you_mean` suggestion
/// if a closer match to the unknown field's name is found.
pub fn add_alts<'a, T, I>(&mut self, alternates: I)
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>,
{
if let Some(bna) = did_you_mean(&self.name, alternates) {
if let Some(current) = &self.did_you_mean {
if bna.0 > current.0 {
self.did_you_mean = Some(bna);
}
} else {
self.did_you_mean = Some(bna);
}
}
}

#[cfg(feature = "diagnostics")]
pub fn into_diagnostic(self, span: Option<::proc_macro2::Span>) -> ::proc_macro::Diagnostic {
let base = span
.unwrap_or_else(::proc_macro2::Span::call_site)
.unwrap()
.error(self.top_line());
match self.did_you_mean {
Some(alt_name) => base.help(format!("did you mean `{}`?", alt_name)),
Some((_, alt_name)) => base.help(format!("did you mean `{}`?", alt_name)),
None => base,
}
}
Expand All @@ -176,7 +194,7 @@ impl fmt::Display for ErrorUnknownField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Unknown field: `{}`", self.name)?;

if let Some(ref did_you_mean) = self.did_you_mean {
if let Some((_, ref did_you_mean)) = self.did_you_mean {
write!(f, ". Did you mean `{}`?", did_you_mean)?;
}

Expand All @@ -185,7 +203,7 @@ impl fmt::Display for ErrorUnknownField {
}

#[cfg(feature = "suggestions")]
fn did_you_mean<'a, T, I>(field: &str, alternates: I) -> Option<String>
fn did_you_mean<'a, T, I>(field: &str, alternates: I) -> Option<(f64, String)>
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>,
Expand All @@ -198,11 +216,11 @@ where
candidate = Some((confidence, pv.as_ref()));
}
}
candidate.map(|(_, candidate)| candidate.into())
candidate.map(|(score, candidate)| (score, candidate.into()))
}

#[cfg(not(feature = "suggestions"))]
fn did_you_mean<'a, T, I>(_field: &str, _alternates: I) -> Option<String>
fn did_you_mean<'a, T, I>(_field: &str, _alternates: I) -> Option<(f64, String)>
where
T: AsRef<str> + 'a,
I: IntoIterator<Item = &'a T>,
Expand Down
Loading