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

Improve FromMeta implementation for enums #260

Merged
merged 14 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
26 changes: 23 additions & 3 deletions core/src/codegen/from_meta_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ impl<'a> ToTokens for FromMetaImpl<'a> {
}
Data::Enum(ref variants) => {
let unit_arms = variants.iter().map(Variant::as_unit_match_arm);
let struct_arms = variants.iter().map(Variant::as_data_match_arm);

let unknown_variant_err = if !variants.is_empty() {
let names = variants.iter().map(Variant::as_name);
Expand All @@ -90,16 +89,33 @@ impl<'a> ToTokens for FromMetaImpl<'a> {
}
};

let word_or_err = variants
.iter()
.find_map(|variant| {
if variant.word {
let ty_ident = variant.ty_ident;
let variant_ident = variant.variant_ident;
Some(quote!(::darling::export::Ok(#ty_ident::#variant_ident)))
} else {
None
}
})
.unwrap_or_else(|| {
quote!(::darling::export::Err(
::darling::Error::unsupported_format("word")
))
});

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
// match arm
// match arm.
match __outer.len() {
0 => ::darling::export::Err(::darling::Error::too_few_items(1)),
1 => {
if let ::darling::export::NestedMeta::Meta(ref __nested) = __outer[0] {
match ::darling::util::path_to_string(__nested.path()).as_ref() {
#(#struct_arms)*
#(#variants)*
__other => ::darling::export::Err(::darling::Error::#unknown_variant_err.with_span(__nested))
}
} else {
Expand All @@ -116,6 +132,10 @@ impl<'a> ToTokens for FromMetaImpl<'a> {
__other => ::darling::export::Err(::darling::Error::unknown_value(__other))
}
}

fn from_word() -> ::darling::Result<Self> {
#word_or_err
}
)
}
};
Expand Down
14 changes: 14 additions & 0 deletions core/src/codegen/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub struct Variant<'a> {
/// Whether or not the variant should be skipped in the generated code.
pub skip: bool,

/// Whether or not the variant should be used to create an instance for
/// `FromMeta::from_word`.
pub word: bool,

pub allow_unknown_fields: bool,
}

Expand Down Expand Up @@ -53,6 +57,16 @@ impl<'a> UsesTypeParams for Variant<'a> {
}
}

impl<'a> ToTokens for Variant<'a> {
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
27 changes: 26 additions & 1 deletion core/src/options/from_meta.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use proc_macro2::TokenStream;
use quote::ToTokens;

use crate::ast::Data;
use crate::codegen::FromMetaImpl;
use crate::error::Accumulator;
use crate::options::{Core, ParseAttribute, ParseData};
use crate::Result;
use crate::{Error, Result};

pub struct FromMetaOptions {
base: Core,
Expand Down Expand Up @@ -33,6 +35,29 @@ impl ParseData for FromMetaOptions {
fn parse_field(&mut self, field: &syn::Field) -> Result<()> {
self.base.parse_field(field)
}

fn validate_body(&self, errors: &mut Accumulator) {
match self.base.data {
Data::Enum(ref data) => {
// Adds errors for duplicate `#[darling(word)]` annotations across all variants.
let word_variants: Vec<_> = data
.iter()
.filter(|variant| variant.word.is_some())
.collect();
if word_variants.len() > 1 {
for variant in word_variants {
if let Some(word) = variant.word {
errors.push(
Error::custom("`word` can only be applied to one variant")
.with_span(&word.span()),
);
}
}
}
}
Data::Struct(_) => (),
}
}
}

impl<'a> From<&'a FromMetaOptions> for FromMetaImpl<'a> {
Expand Down
20 changes: 20 additions & 0 deletions core/src/options/input_variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::borrow::Cow;
use crate::ast::Fields;
use crate::codegen;
use crate::options::{Core, InputField, ParseAttribute};
use crate::util::SpannedValue;
use crate::{Error, FromMeta, Result};

#[derive(Debug, Clone)]
Expand All @@ -11,6 +12,7 @@ pub struct InputVariant {
attr_name: Option<String>,
data: Fields<InputField>,
skip: Option<bool>,
pub word: Option<SpannedValue<bool>>,
/// Whether or not unknown fields are acceptable in this
allow_unknown_fields: Option<bool>,
}
Expand All @@ -26,6 +28,7 @@ impl InputVariant {
.map_or_else(|| Cow::Owned(self.ident.to_string()), Cow::Borrowed),
data: self.data.as_ref().map(InputField::as_codegen_field),
skip: self.skip.unwrap_or_default(),
word: *self.word.unwrap_or_default(),
allow_unknown_fields: self.allow_unknown_fields.unwrap_or_default(),
}
}
Expand All @@ -36,6 +39,7 @@ impl InputVariant {
attr_name: Default::default(),
data: Fields::empty_from(&v.fields),
skip: Default::default(),
word: Default::default(),
allow_unknown_fields: None,
})
.parse_attributes(&v.attrs)?;
Expand Down Expand Up @@ -95,6 +99,22 @@ impl ParseAttribute for InputVariant {
}

self.skip = FromMeta::from_meta(mi)?;
} else if path.is_ident("word") {
if self.word.is_some() {
return Err(Error::duplicate_field_path(path).with_span(mi));
}

if !self.data.is_unit() {
let note = "`#[darling(word)]` can only be applied to a unit variant";
#[cfg(feature = "diagnostics")]
let error = Error::unknown_field_path(path).note(note);
#[cfg(not(feature = "diagnostics"))]
let error = Error::custom(format!("Unexpected field: `word`. {}", note));

return Err(error.with_span(mi));
}

self.word = FromMeta::from_meta(mi)?;
} else {
return Err(Error::unknown_field_path(path).with_span(mi));
}
Expand Down
9 changes: 9 additions & 0 deletions core/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use proc_macro2::Span;
use syn::{parse_quote, spanned::Spanned};

use crate::ast::NestedMeta;
use crate::error::Accumulator;
use crate::{Error, FromMeta, Result};

mod core;
Expand Down Expand Up @@ -132,6 +133,8 @@ pub trait ParseData: Sized {
Data::Union(_) => unreachable!(),
};

self.validate_body(&mut errors);

errors.finish_with(self)
}

Expand All @@ -146,4 +149,10 @@ pub trait ParseData: Sized {
fn parse_field(&mut self, field: &syn::Field) -> Result<()> {
Err(Error::unsupported_format("struct field").with_span(field))
}

/// Perform validation checks that require data from more than one field or variant.
/// This default implementation is essentially a noop.
/// Implementors can override this method as appropriate for their use-case.
#[allow(unused_variables)]
fn validate_body(&self, errors: &mut Accumulator) {}
}
12 changes: 12 additions & 0 deletions tests/compile-fail/duplicate_word_across_variants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use darling::FromMeta;

#[derive(FromMeta)]
enum Choice {
#[darling(word)]
A,
#[darling(word)]
B,
C,
}

fn main() {}
11 changes: 11 additions & 0 deletions tests/compile-fail/duplicate_word_across_variants.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: `word` can only be applied to one variant
--> tests/compile-fail/duplicate_word_across_variants.rs:5:15
|
5 | #[darling(word)]
| ^^^^

error: `word` can only be applied to one variant
--> tests/compile-fail/duplicate_word_across_variants.rs:7:15
|
7 | #[darling(word)]
| ^^^^
10 changes: 10 additions & 0 deletions tests/compile-fail/duplicate_word_on_variant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use darling::FromMeta;

#[derive(FromMeta)]
enum Choice {
#[darling(word, word)]
A,
B,
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile-fail/duplicate_word_on_variant.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: Duplicate field `word`
--> tests/compile-fail/duplicate_word_on_variant.rs:5:21
|
5 | #[darling(word, word)]
| ^^^^
10 changes: 10 additions & 0 deletions tests/compile-fail/word_on_wrong_variant_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use darling::FromMeta;

#[derive(FromMeta)]
enum Meta {
Unit,
#[darling(word)]
NotUnit(String)
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile-fail/word_on_wrong_variant_type.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: Unexpected field: `word`. `#[darling(word)]` can only be applied to a unit variant
--> tests/compile-fail/word_on_wrong_variant_type.rs:6:15
|
6 | #[darling(word)]
| ^^^^
44 changes: 44 additions & 0 deletions tests/enums_default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use darling::{FromDeriveInput, FromMeta};
use syn::parse_quote;

#[derive(Debug, FromMeta, PartialEq, Eq)]
enum Dolor {
Sit,
#[darling(word)]
Amet,
}

impl Default for Dolor {
fn default() -> Self {
Dolor::Sit
}
}

#[derive(FromDeriveInput)]
#[darling(attributes(hello))]
struct Receiver {
#[darling(default)]
example: Dolor,
}

#[test]
fn missing_meta() {
let di = Receiver::from_derive_input(&parse_quote! {
#[hello]
struct Example;
})
.unwrap();

assert_eq!(Dolor::Sit, di.example);
}

#[test]
fn empty_meta() {
let di = Receiver::from_derive_input(&parse_quote! {
#[hello(example)]
struct Example;
})
.unwrap();

assert_eq!(Dolor::Amet, di.example);
}
43 changes: 43 additions & 0 deletions tests/from_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,46 @@ fn nested_meta_lit_bool_errors() {
Error::unsupported_format("literal").to_string()
);
}

/// Tests behavior of FromMeta implementation for enums.
mod enum_impl {
use darling::{Error, FromMeta};
use syn::parse_quote;

/// A playback volume.
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromMeta)]
enum Volume {
Normal,
Low,
High,
#[darling(rename = "dB")]
Decibels(u8),
}

#[test]
fn string_for_unit_variant() {
let volume = Volume::from_string("low").unwrap();
assert_eq!(volume, Volume::Low);
}

#[test]
fn single_value_list() {
let unit_variant = Volume::from_list(&[parse_quote!(high)]).unwrap();
assert_eq!(unit_variant, Volume::High);

let newtype_variant = Volume::from_list(&[parse_quote!(dB = 100)]).unwrap();
assert_eq!(newtype_variant, Volume::Decibels(100));
}

#[test]
fn empty_list_errors() {
let err = Volume::from_list(&[]).unwrap_err();
assert_eq!(err.to_string(), Error::too_few_items(1).to_string());
}

#[test]
fn multiple_values_list_errors() {
let err = Volume::from_list(&[parse_quote!(low), parse_quote!(dB = 20)]).unwrap_err();
assert_eq!(err.to_string(), Error::too_many_items(1).to_string());
}
}