Repository: clap-rs/clap (language: rust)

Issue:
Possible bug with enum fields
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the [open](https://github.com/clap-rs/clap/issues) and [rejected](https://github.com/clap-rs/clap/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### Rust Version

1.6.9

### Clap Version

4.2.1

### Minimal reproducible code

```rust
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0ba3a366598032fb8252f66cc8b11851
// This could probably be more concise, but I pulled it out of a real-world example and kept the fields in
// because I think it has something to do with dependencies between fields. Demo on the rust playground link above.

use clap::{Parser, ValueEnum}; // 4.2.3

fn main() -> Result<(), anyhow::Error> {
    // This prints out the help as expected because not enough args.
    // let ok1 = ["--dry-run", "--save-config"];
    // let a1 = EtlConfig::parse_from(ok1.iter());
    // dbg!("a1 = {?:}", a1);
    
    // This prints out the help as expected because not enough args.
    // let ok2 = ["--dry-run", "--save-config", "--mysql-host", "localhost"];
    // let a2 = EtlConfig::parse_from(ok2.iter());
    // dbg!("a2 = {?:}", a2);
    
    // This panics! --target is required, and is missing here, 
    // but rather than print the help, it panics.
    let panicking_args = ["--dry-run", "--save-config", "--mysql-host", "127.0.0.1", "--mongo-host", "127.0.0.1"];
    EtlConfig::parse_from(panicking_args.iter());
    
    Ok(())
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum TargetDatabase {
    /// MongoDB
    MongoDB,

    /// SurrealDB
    SurrealDB,
}

/// Configuration.
#[derive(Debug, Parser, Clone)]
struct EtlConfig {
    /// Save the configuration.
    ///
    /// If the --config-file option is not set, it will output to STDOUT.
    #[arg(long)]
    save_config: bool,

    /// Load configuration from file.
    ///
    /// If the --config-file option is not set, it will read from STDIN.
    #[arg(long)]
    load_config: bool,

    /// Configuration file to save to or load from.
    #[arg(long)]
    config_file: Option<String>,

    /// Start by resetting the new database and starting from scratch.
    #[arg(long)]
    reset: bool,

    /// Do a dry-run: Don't write to the new database, but do everything else (as much as possible).
    #[arg(long)]
    dry_run: bool,

    /// Target database system.
    #[arg(long, value_enum)]
    target: TargetDatabase,

    /// MySQL (Chili) connection configuration.
    #[command(flatten)]
    mysql: MySqlConfig,

    /// MongoDB connection configuration.
    #[command(flatten)]
    mongo: Option<MongoDbConfig>,

    /// SurrealDB connection configuration.
    #[command(flatten)]
    surreal: Option<SurrealDbConfig>,
}


/// MySql (Chili) Configuration.
#[derive(Debug, Parser,  Clone)]
#[group(multiple = false)]
struct MySqlConfig {
    /// MySQL username.
    #[arg(long, default_value_t = String::from("chili"))]
    mysql_username: String,

    /// MySQL password.
    #[arg(long, default_value_t = String::from("chili"))]
    mysql_password: String,

    /// MySQL server host name.
    #[arg(long, default_value_t = String::from("localhost"))]
    mysql_host: String,

    /// MySQL port.
    #[arg(long, default_value_t = 3306, value_parser = clap::value_parser!(u16).range(1..))]
    mysql_port: u16,

    /// MySQL database.
    #[arg(long, default_value_t = String::from("chili"))]
    mysql_database: String,
}

/// MongoDB Configuration.
#[derive(Debug, Parser,  Clone)]
#[group(multiple = false, requires("mongo-db"))]
struct MongoDbConfig {
    /// MongoDB username.
    #[arg(long, default_value_t = String::from("root"))]
    mongo_username: String,

    /// MongoDB password.
    #[arg(long, default_value_t = String::from("root"))]
    #[arg(requires_if("mongo-db", "target"))]
    mongo_password: String,

    /// MongoDB server host name.
    #[arg(long, default_value_t = String::from("localhost"))]
    #[arg(requires_if("mongo-db", "target"))]
    mongo_host: String,

    /// MongoDB port.
    #[arg(long, default_value_t = 27017, value_parser = clap::value_parser!(u16).range(1..))]
    #[arg(requires_if("mongo-db", "target"))]
    mongo_port: u16,

    /// MongoDB database.
    #[arg(long, default_value_t = String::from("reaper"))]
    #[arg(requires_if("mongo-db", "target"))]

    mongo_database: String,
}

/// SurrealDB Configuration.
#[derive(Debug, Parser,  Clone)]
#[group(multiple = false, requires("surreal-db"))]
struct SurrealDbConfig {
    /// SurrealDB username.
    #[arg(long, default_value_t = String::from("root"))]

    surreal_username: String,

    /// SurrealDB password.
    #[arg(long, default_value_t = String::from("root"))]
    #[arg(requires_if("surreal-db", "target"))]

    surreal_password: String,

    /// SurrealDB server host name.
    #[arg(long, default_value_t = String::from("localhost"))]
    #[arg(requires_if("surreal-db", "target"))]

    surreal_host: String,

    /// SurrealDB port.
    #[arg(long, default_value_t = 8000, value_parser = clap::value_parser!(u16).range(1..))]
    #[arg(requires_if("surreal-db", "target"))]

    surreal_port: u16,

    /// SurrealDB namespace.
    #[arg(long, default_value_t = String::from("reaper"))]
    #[arg(requires_if("surreal-db", "target"))]

    surreal_namespace: String,

    /// SurrealDB database.
    #[arg(long, default_value_t = String::from("reaper"))]
    #[arg(requires_if("surreal-db", "target"))]

    surreal_database: String,
}
```


### Steps to reproduce the bug with the above code

Run the code in the playground.

There are some commented out sections of code, with descriptions to illustrate other behavior. This ticket documents the code as it currently stands, though.

### Actual Behaviour

Code panics and crashes during parsing.

### Expected Behaviour

I expected to see the help screen saying that --target is a required field.

### Additional Context

_No response_

### Debug Output

_No response_


Discussion / hints:

Fix patch:
diff --git a/clap_builder/src/builder/debug_asserts.rs b/clap_builder/src/builder/debug_asserts.rs
index 7a7fd6ae183..b7bb1ff8329 100644
--- a/clap_builder/src/builder/debug_asserts.rs
+++ b/clap_builder/src/builder/debug_asserts.rs
@@ -300,6 +300,28 @@ pub(crate) fn assert_app(cmd: &Command) {
                 arg
             );
         }
+
+        for arg in &group.requires {
+            // Args listed inside groups should exist
+            assert!(
+                cmd.id_exists(arg),
+                "Command {}: Argument group '{}' requires non-existent '{}' id",
+                cmd.get_name(),
+                group.get_id(),
+                arg
+            );
+        }
+
+        for arg in &group.conflicts {
+            // Args listed inside groups should exist
+            assert!(
+                cmd.id_exists(arg),
+                "Command {}: Argument group '{}' conflicts with non-existent '{}' id",
+                cmd.get_name(),
+                group.get_id(),
+                arg
+            );
+        }
     }
 
     // Conflicts between flags and subcommands
diff --git a/clap_builder/src/output/usage.rs b/clap_builder/src/output/usage.rs
index dd99c63c7b7..87bb945283d 100644
--- a/clap_builder/src/output/usage.rs
+++ b/clap_builder/src/output/usage.rs
@@ -439,7 +439,7 @@ impl<'cmd> Usage<'cmd> {
                 required_groups.insert(elem);
                 required_groups_members.extend(group_members);
             } else {
-                debug_assert!(self.cmd.find(req).is_some());
+                debug_assert!(self.cmd.find(req).is_some(), "`{req}` must exist");
             }
         }
 


Test patch: