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

ValueEnum fallback #5886

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ default = [
"usage",
"error-context",
"suggestions",
"unstable-derive-ui-tests"
]
debug = ["clap_builder/debug", "clap_derive?/debug"] # Enables debug messages
unstable-doc = ["clap_builder/unstable-doc", "derive"] # for docs.rs
Expand Down
2 changes: 1 addition & 1 deletion clap_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ proc-macro = true
bench = false

[dependencies]
syn = { version = "2.0.8", features = ["full"] }
syn = { version = "2.0.73", features = ["full"] }
quote = "1.0.9"
proc-macro2 = "1.0.69"
heck = "0.5.0"
Expand Down
2 changes: 2 additions & 0 deletions clap_derive/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl Parse for ClapAttr {
"long_help" => Some(MagicAttrName::LongHelp),
"author" => Some(MagicAttrName::Author),
"version" => Some(MagicAttrName::Version),
"fallback" => Some(MagicAttrName::Fallback),
_ => None,
};

Expand Down Expand Up @@ -168,6 +169,7 @@ pub(crate) enum MagicAttrName {
DefaultValuesOsT,
NextDisplayOrder,
NextHelpHeading,
Fallback,
}

#[derive(Clone)]
Expand Down
64 changes: 64 additions & 0 deletions clap_derive/src/derives/value_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub(crate) fn gen_for_enum(
let lits = lits(variants)?;
let value_variants = gen_value_variants(&lits);
let to_possible_value = gen_to_possible_value(item, &lits);
let from_str_for_fallback = gen_from_str_for_fallback(variants)?;

Ok(quote! {
#[allow(
Expand All @@ -75,6 +76,7 @@ pub(crate) fn gen_for_enum(
impl clap::ValueEnum for #item_name {
#value_variants
#to_possible_value
#from_str_for_fallback
}
})
}
Expand All @@ -85,6 +87,9 @@ fn lits(variants: &[(&Variant, Item)]) -> Result<Vec<(TokenStream, Ident)>, syn:
if let Kind::Skip(_, _) = &*item.kind() {
continue;
}
if item.is_fallback() {
continue;
}
if !matches!(variant.fields, Fields::Unit) {
abort!(variant.span(), "`#[derive(ValueEnum)]` only supports unit variants. Non-unit variants must be skipped");
}
Expand Down Expand Up @@ -128,3 +133,62 @@ fn gen_to_possible_value(item: &Item, lits: &[(TokenStream, Ident)]) -> TokenStr
}
}
}

fn gen_from_str_for_fallback(variants: &[(&Variant, Item)]) -> syn::Result<TokenStream> {
let fallbacks: Vec<_> = variants
.iter()
.filter(|(_, item)| item.is_fallback())
.collect();

match fallbacks.as_slice() {
[] => Ok(quote!()),
[(variant, _)] => {
let ident = &variant.ident;
let variant_initialization = match variant.fields.len() {
_ if matches!(variant.fields, Fields::Unit) => quote! {#ident},
0 => quote! {#ident{}},
1 => {
let member = variant
.fields
.members()
.next()
.expect("there should be exactly one field");
quote! {#ident{
#member: {
use std::convert::Into;
__input.into()
},
}}
}
_ => abort!(
variant,
"`fallback` only supports Unit variants, or variants with a single field"
),
};
Ok(quote! {
fn from_str(__input: &::std::primitive::str, __ignore_case: ::std::primitive::bool) -> ::std::result::Result<Self, ::std::string::String> {
Ok(Self::value_variants()
.iter()
.find(|v| {
v.to_possible_value()
.expect("ValueEnum::value_variants contains only values with a corresponding ValueEnum::to_possible_value")
.matches(__input, __ignore_case)
})
.cloned()
.unwrap_or_else(|| Self::#variant_initialization))
}
})
}
[first, second, ..] => {
let mut error = syn::Error::new_spanned(
first.0,
"`#[derive(ValueEnum)]` only supports one `fallback`.",
);
error.combine(syn::Error::new_spanned(
second.0,
"second fallback defined here",
));
Err(error)
}
}
}
13 changes: 13 additions & 0 deletions clap_derive/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub(crate) struct Item {
skip_group: bool,
group_id: Name,
group_methods: Vec<Method>,
/// Used as fallback value `ValueEnum`.
is_fallback: bool,
kind: Sp<Kind>,
}

Expand Down Expand Up @@ -279,6 +281,7 @@ impl Item {
group_id,
group_methods: vec![],
kind,
is_fallback: false,
}
}

Expand Down Expand Up @@ -835,6 +838,12 @@ impl Item {
self.skip_group = true;
}

Some(MagicAttrName::Fallback) => {
assert_attr_kind(attr, &[AttrKind::Value])?;

self.is_fallback = true;
}

None
// Magic only for the default, otherwise just forward to the builder
| Some(MagicAttrName::Short)
Expand Down Expand Up @@ -1077,6 +1086,10 @@ impl Item {
pub(crate) fn skip_group(&self) -> bool {
self.skip_group
}

pub(crate) fn is_fallback(&self) -> bool {
self.is_fallback
}
}

#[derive(Clone)]
Expand Down
149 changes: 149 additions & 0 deletions tests/derive/value_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,152 @@ fn vec_type_default_value() {
Opt::try_parse_from(["", "-a", "foo,baz"]).unwrap()
);
}

#[test]
fn unit_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback,
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn empty_tuple_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback(),
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback()
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn empty_struct_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback {},
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback {}
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn non_empty_struct_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback {
value: String,
},
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback {
value: String::from("not-foo")
}
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn non_empty_tuple_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback(String),
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback(String::from("not-foo"))
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}
10 changes: 10 additions & 0 deletions tests/derive_ui/value_enum_fallback_two_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use clap::ValueEnum;

#[derive(ValueEnum, Clone, Debug)]
enum Opt {
#[clap(fallback)]
First(String, String),
}

fn main() {}

6 changes: 6 additions & 0 deletions tests/derive_ui/value_enum_fallback_two_fields.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: `fallback` only supports Unit variants, or variants with a single field
--> tests/derive_ui/value_enum_fallback_two_fields.rs:5:5
|
5 | / #[clap(fallback)]
6 | | First(String, String),
| |_________________________^
11 changes: 11 additions & 0 deletions tests/derive_ui/value_enum_two_fallback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use clap::ValueEnum;

#[derive(ValueEnum, Clone, Debug)]
enum Opt {
#[clap(fallback)]
First(String),
#[clap(fallback)]
Second(String),
}

fn main() {}
13 changes: 13 additions & 0 deletions tests/derive_ui/value_enum_two_fallback.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
error: `#[derive(ValueEnum)]` only supports one `fallback`.
--> tests/derive_ui/value_enum_two_fallback.rs:5:5
|
5 | / #[clap(fallback)]
6 | | First(String),
| |_________________^

error: second fallback defined here
--> tests/derive_ui/value_enum_two_fallback.rs:7:5
|
7 | / #[clap(fallback)]
8 | | Second(String),
| |__________________^
12 changes: 10 additions & 2 deletions tests/derive_ui/value_parser_unsupported.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ note: the traits `From`, `FromStr`, `ValueEnum`, and `ValueParserFactory` must
|
| pub trait ValueEnum: Sized + Clone {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--> $RUST/core/src/convert/mod.rs
--> $RUST/core/src/str/traits.rs
|
::: $RUST/core/src/convert/mod.rs
|
| pub trait From<T>: Sized {
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
::: $RUST/core/src/str/traits.rs
|
| pub trait FromStr: Sized {
| ^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the macro `clap::value_parser` (in Nightly builds, run with -Z macro-backtrace for more info)
Loading