Repository: clap-rs/clap
Pull request #4209 (2022-09-13 12:18:44)

feat(derive): Reserve behavior needed for implicit ArgGroups
These are the building blocks for us to automatically create an `ArgGroup` when deriving `Args` on a struct.  This will make it easier to compose (write validation rules) particularly when the struct comes from a third party.

The next step will then be to support deriving `Args` on enums, see #2621


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0a21b77..b792fc13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -198,6 +198,7 @@ Subtle changes (i.e. compiler won't catch):
 - *(env)* Parse `--help` and `--version` like any `ArgAction::SetTrue` flag (#3776)
 - *(derive)* Leave `Arg::id` as `verbatim` casing, requiring updating of string references to other args like in `conflicts_with` or `requires` (#3282)
 - *(derive)* Doc comments for `ValueEnum` variants will now show up in `--help` (#3312)
+- *(derive)* When deriving `Args`, and `ArgGroup` is created using the type's name, reserving it for future use (#2621, #4209)
 
 Easier to catch changes:
 
diff --git a/clap_derive/src/attr.rs b/clap_derive/src/attr.rs
index 4a670af9..df2ab568 100644
--- a/clap_derive/src/attr.rs
+++ b/clap_derive/src/attr.rs
@@ -34,6 +34,11 @@ impl ClapAttr {
                     Some(Sp::new(AttrKind::StructOpt, attr.path.span()))
                 } else if attr.path.is_ident("command") {
                     Some(Sp::new(AttrKind::Command, attr.path.span()))
+                } else if attr.path.is_ident("group") {
+                    abort!(
+                        attr.path.span(),
+                        "`#[group()]` attributes are not supported yet"
+                    )
                 } else if attr.path.is_ident("arg") {
                     Some(Sp::new(AttrKind::Arg, attr.path.span()))
                 } else if attr.path.is_ident("value") {
diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs
index f14ac09d..02cb0523 100644
--- a/clap_derive/src/derives/args.rs
+++ b/clap_derive/src/derives/args.rs
@@ -15,6 +15,7 @@
 use proc_macro2::{Ident, Span, TokenStream};
 use proc_macro_error::{abort, abort_call_site};
 use quote::{format_ident, quote, quote_spanned};
+use syn::ext::IdentExt;
 use syn::{
     punctuated::Punctuated, spanned::Spanned, token::Comma, Data, DataStruct, DeriveInput, Field,
     Fields, Generics,
@@ -317,10 +318,13 @@ pub fn gen_augment(
         quote!()
     };
     let initial_app_methods = parent_item.initial_top_level_methods();
+    let group_id = parent_item.ident().unraw().to_string();
     let final_app_methods = parent_item.final_top_level_methods();
     quote! {{
         #deprecations
-        let #app_var = #app_var #initial_app_methods;
+        let #app_var = #app_var
+            #initial_app_methods
+            .group(clap::ArgGroup::new(#group_id).multiple(true));
         #( #args )*
         #app_var #final_app_methods
     }}
diff --git a/clap_derive/src/item.rs b/clap_derive/src/item.rs
index 108f2894..3808364b 100644
--- a/clap_derive/src/item.rs
+++ b/clap_derive/src/item.rs
@@ -36,6 +36,7 @@ pub const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake;
 #[derive(Clone)]
 pub struct Item {
     name: Name,
+    ident: Ident,
     casing: Sp<CasingStyle>,
     env_casing: Sp<CasingStyle>,
     ty: Option<Type>,
@@ -54,13 +55,14 @@ pub struct Item {
 
 impl Item {
     pub fn from_args_struct(input: &DeriveInput, name: Name) -> Self {
+        let ident = input.ident.clone();
         let span = input.ident.span();
         let attrs = &input.attrs;
         let argument_casing = Sp::new(DEFAULT_CASING, span);
         let env_casing = Sp::new(DEFAULT_ENV_CASING, span);
         let kind = Sp::new(Kind::Command(Sp::new(Ty::Other, span)), span);
 
-        let mut res = Self::new(name, None, argument_casing, env_casing, kind);
+        let mut res = Self::new(name, ident, None, argument_casing, env_casing, kind);
         let parsed_attrs = ClapAttr::parse_all(attrs);
         res.infer_kind(&parsed_attrs);
         res.push_attrs(&parsed_attrs);
@@ -70,13 +72,14 @@ impl Item {
     }
 
     pub fn from_subcommand_enum(input: &DeriveInput, name: Name) -> Self {
+        let ident = input.ident.clone();
         let span = input.ident.span();
         let attrs = &input.attrs;
         let argument_casing = Sp::new(DEFAULT_CASING, span);
         let env_casing = Sp::new(DEFAULT_ENV_CASING, span);
         let kind = Sp::new(Kind::Command(Sp::new(Ty::Other, span)), span);
 
-        let mut res = Self::new(name, None, argument_casing, env_casing, kind);
+        let mut res = Self::new(name, ident, None, argument_casing, env_casing, kind);
         let parsed_attrs = ClapAttr::parse_all(attrs);
         res.infer_kind(&parsed_attrs);
         res.push_attrs(&parsed_attrs);
@@ -86,13 +89,14 @@ impl Item {
     }
 
     pub fn from_value_enum(input: &DeriveInput, name: Name) -> Self {
+        let ident = input.ident.clone();
         let span = input.ident.span();
         let attrs = &input.attrs;
         let argument_casing = Sp::new(DEFAULT_CASING, span);
         let env_casing = Sp::new(DEFAULT_ENV_CASING, span);
         let kind = Sp::new(Kind::Value, span);
 
-        let mut res = Self::new(name, None, argument_casing, env_casing, kind);
+        let mut res = Self::new(name, ident, None, argument_casing, env_casing, kind);
         let parsed_attrs = ClapAttr::parse_all(attrs);
         res.infer_kind(&parsed_attrs);
         res.push_attrs(&parsed_attrs);
@@ -116,6 +120,7 @@ impl Item {
         env_casing: Sp<CasingStyle>,
     ) -> Self {
         let name = variant.ident.clone();
+        let ident = variant.ident.clone();
         let span = variant.span();
         let ty = match variant.fields {
             syn::Fields::Unnamed(syn::FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => {
@@ -126,7 +131,14 @@ impl Item {
             }
         };
         let kind = Sp::new(Kind::Command(ty), span);
-        let mut res = Self::new(Name::Derived(name), None, struct_casing, env_casing, kind);
+        let mut res = Self::new(
+            Name::Derived(name),
+            ident,
+            None,
+            struct_casing,
+            env_casing,
+            kind,
+        );
         let parsed_attrs = ClapAttr::parse_all(&variant.attrs);
         res.infer_kind(&parsed_attrs);
         res.push_attrs(&parsed_attrs);
@@ -161,10 +173,12 @@ impl Item {
         argument_casing: Sp<CasingStyle>,
         env_casing: Sp<CasingStyle>,
     ) -> Self {
+        let ident = variant.ident.clone();
         let span = variant.span();
         let kind = Sp::new(Kind::Value, span);
         let mut res = Self::new(
             Name::Derived(variant.ident.clone()),
+            ident,
             None,
             argument_casing,
             env_casing,
@@ -186,11 +200,13 @@ impl Item {
         env_casing: Sp<CasingStyle>,
     ) -> Self {
         let name = field.ident.clone().unwrap();
+        let ident = field.ident.clone().unwrap();
         let span = field.span();
         let ty = Ty::from_syn_ty(&field.ty);
         let kind = Sp::new(Kind::Arg(ty), span);
         let mut res = Self::new(
             Name::Derived(name),
+            ident,
             Some(field.ty.clone()),
             struct_casing,
             env_casing,
@@ -234,6 +250,7 @@ impl Item {
 
     fn new(
         name: Name,
+        ident: Ident,
         ty: Option<Type>,
         casing: Sp<CasingStyle>,
         env_casing: Sp<CasingStyle>,
@@ -241,6 +258,7 @@ impl Item {
     ) -> Self {
         Self {
             name,
+            ident,
             ty,
             casing,
             env_casing,
@@ -894,6 +912,10 @@ impl Item {
         quote!( #(#next_help_heading)* )
     }
 
+    pub fn ident(&self) -> &Ident {
+        &self.ident
+    }
+
     pub fn id(&self) -> TokenStream {
         self.name.clone().raw()
     }
diff --git a/clap_derive/src/lib.rs b/clap_derive/src/lib.rs
index 30adb5bb..948543c4 100644
--- a/clap_derive/src/lib.rs
+++ b/clap_derive/src/lib.rs
@@ -50,7 +50,7 @@ pub fn parser(input: TokenStream) -> TokenStream {
 }
 
 /// Generates the `Subcommand` impl.
-#[proc_macro_derive(Subcommand, attributes(clap, command, arg))]
+#[proc_macro_derive(Subcommand, attributes(clap, command, arg, group))]
 #[proc_macro_error]
 pub fn subcommand(input: TokenStream) -> TokenStream {
     let input: DeriveInput = parse_macro_input!(input);
@@ -58,7 +58,7 @@ pub fn subcommand(input: TokenStream) -> TokenStream {
 }
 
 /// Generates the `Args` impl.
-#[proc_macro_derive(Args, attributes(clap, command, arg))]
+#[proc_macro_derive(Args, attributes(clap, command, arg, group))]
 #[proc_macro_error]
 pub fn args(input: TokenStream) -> TokenStream {
     let input: DeriveInput = parse_macro_input!(input);
diff --git a/src/derive.rs b/src/derive.rs
index 29dbbb05..9c62c4f1 100644
--- a/src/derive.rs
+++ b/src/derive.rs
@@ -282,6 +282,10 @@ pub trait FromArgMatches: Sized {
 /// }
 /// ```
 pub trait Args: FromArgMatches + Sized {
+    /// Report the [`ArgGroup::id`][crate::ArgGroup::id] for this set of arguments
+    fn group_id() -> Option<crate::Id> {
+        None
+    }
     /// Append to [`Command`] so it can instantiate `Self`.
     ///
     /// See also [`CommandFactory`].


Test patch:
diff --git a/tests/derive/groups.rs b/tests/derive/groups.rs
new file mode 100644
index 00000000..edc2d93c
--- /dev/null
+++ b/tests/derive/groups.rs
@@ -0,0 +1,23 @@
+use clap::Parser;
+
+#[test]
+fn test_safely_nest_parser() {
+    #[derive(Parser, Debug, PartialEq)]
+    struct Opt {
+        #[command(flatten)]
+        foo: Foo,
+    }
+
+    #[derive(Parser, Debug, PartialEq)]
+    struct Foo {
+        #[arg(long)]
+        foo: bool,
+    }
+
+    assert_eq!(
+        Opt {
+            foo: Foo { foo: true }
+        },
+        Opt::try_parse_from(&["test", "--foo"]).unwrap()
+    );
+}
diff --git a/tests/derive/main.rs b/tests/derive/main.rs
index 02e40d3e..33b59cdf 100644
--- a/tests/derive/main.rs
+++ b/tests/derive/main.rs
@@ -13,6 +13,7 @@ mod explicit_name_no_renaming;
 mod flags;
 mod flatten;
 mod generic;
+mod groups;
 mod help;
 mod issues;
 mod macros;
diff --git a/tests/derive/naming.rs b/tests/derive/naming.rs
index 693856ff..9afad2ab 100644
--- a/tests/derive/naming.rs
+++ b/tests/derive/naming.rs
@@ -1,3 +1,4 @@
+use clap::Args;
 use clap::Parser;
 
 #[test]
@@ -251,7 +252,7 @@ fn test_rename_all_is_not_propagated_from_struct_into_flattened() {
         foo: Foo,
     }
 
-    #[derive(Parser, Debug, PartialEq)]
+    #[derive(Args, Debug, PartialEq)]
     struct Foo {
         #[arg(long)]
         foo: bool,