Skip to content

Commit

Permalink
Reject extra data passed to unit enum variants
Browse files Browse the repository at this point in the history
- Remove ToTokens impl from Variant to force caller to choose right usage context (e.g. as_data_match_arm)
- Update DataMatchArm to allow unit variants if the value is just a path
- Add test written by @Sufflope to avoid regressions

Fixes #306
  • Loading branch information
TedDriggs committed Jan 21, 2025
1 parent b7a248f commit af24bdf
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 13 deletions.
4 changes: 3 additions & 1 deletion core/src/codegen/from_meta_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ impl ToTokens for FromMetaImpl<'_> {
))
});

let data_variants = variants.iter().map(Variant::as_data_match_arm);

quote!(
fn from_list(__outer: &[::darling::export::NestedMeta]) -> ::darling::Result<Self> {
// An enum must have exactly one value inside the parentheses if it's not a unit
Expand All @@ -115,7 +117,7 @@ impl ToTokens for FromMetaImpl<'_> {
1 => {
if let ::darling::export::NestedMeta::Meta(ref __nested) = __outer[0] {
match ::darling::util::path_to_string(__nested.path()).as_ref() {
#(#variants)*
#(#data_variants)*
__other => ::darling::export::Err(::darling::Error::#unknown_variant_err.with_span(__nested))
}
} else {
Expand Down
20 changes: 9 additions & 11 deletions core/src/codegen/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,6 @@ impl UsesTypeParams for Variant<'_> {
}
}

impl ToTokens for Variant<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.data.is_unit() {
self.as_unit_match_arm().to_tokens(tokens);
} else {
self.as_data_match_arm().to_tokens(tokens)
}
}
}

/// Code generator for an enum variant in a unit match position.
/// This is placed in generated `from_string` calls for the parent enum.
/// Value-carrying variants wrapped in this type will emit code to produce an "unsupported format" error.
Expand Down Expand Up @@ -141,8 +131,16 @@ impl ToTokens for DataMatchArm<'_> {
let ty_ident = val.ty_ident;

if val.data.is_unit() {
// Allow unit variants to match a list item if it's just a path with no associated
// value, e.g. `volume(shout)` is allowed.
tokens.append_all(quote!(
#name_in_attr => ::darling::export::Err(::darling::Error::unsupported_format("list")),
#name_in_attr => {
if let ::darling::export::syn::Meta::Path(_) = *__nested {
::darling::export::Ok(#ty_ident::#variant_ident)
} else {
::darling::export::Err(::darling::Error::unsupported_format("non-path"))
}
},
));

return;
Expand Down
111 changes: 110 additions & 1 deletion tests/enums_unit.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Test expansion of enum variants which have no associated data.
use darling::FromMeta;
use darling::{ast::NestedMeta, FromMeta};
use syn::{parse_quote, spanned::Spanned, Meta};

#[derive(Debug, FromMeta)]
#[darling(rename_all = "snake_case")]
Expand All @@ -12,3 +13,111 @@ enum Pattern {

#[test]
fn expansion() {}

#[test]
fn rejected_in_unit_enum_variants() {
#[derive(Debug, FromMeta, PartialEq)]
struct Opts {
choices: Choices,
}

#[derive(Debug, PartialEq)]
struct Choices {
values: Vec<Choice>,
}

impl FromMeta for Choices {
fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
let values = items
.iter()
.map(|item| match item {
NestedMeta::Meta(meta) => match meta {
Meta::Path(path) => Choice::from_string(
&path
.get_ident()
.ok_or(
darling::Error::custom("choice must be an ident (no colon)")
.with_span(path),
)?
.to_string(),
)
.map_err(|e| e.with_span(path)),
Meta::List(list) => Choice::from_list(&[item.clone()])
.map_err(|e| e.with_span(&list.span())),
Meta::NameValue(n) => Err(darling::Error::custom(
"choice options are not set as name-value, use parentheses",
)
.with_span(&n.eq_token)),
},
_ => {
Err(darling::Error::custom("literal is not a valid choice").with_span(item))
}
})
.collect::<Result<_, _>>()?;
Ok(Self { values })
}
}

#[derive(Debug, FromMeta, PartialEq)]
enum Choice {
One(One),
Other,
}

#[derive(Debug, FromMeta, PartialEq)]
struct One {
foo: String,
}

for (tokens, expected) in [
(
parse_quote! {
choices(one(foo = "bar"))
},
Ok(Opts {
choices: Choices {
values: vec![Choice::One(One {
foo: "bar".to_string(),
})],
},
}),
),
(
parse_quote! {
choices(other)
},
Ok(Opts {
choices: Choices {
values: vec![Choice::Other],
},
}),
),
(
parse_quote! {
choices(other, one(foo = "bar"))
},
Ok(Opts {
choices: Choices {
values: vec![
Choice::Other,
Choice::One(One {
foo: "bar".to_string(),
}),
],
},
}),
),
(
parse_quote! {
choices(other(foo = "bar"))
},
Err("Unexpected meta-item format `non-path` at choices".to_string()),
),
] {
assert_eq!(
Opts::from_list(&NestedMeta::parse_meta_list(tokens).unwrap())
.map_err(|e| e.to_string()),
expected
)
}
}

0 comments on commit af24bdf

Please sign in to comment.