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

Issue:
Cannot use non-literal expressions for `name` attribute
### 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

rustc 1.76.0 (07dca489a 2024-02-04)

### Clap Version

4.5.3

### Minimal reproducible code

```rust
use clap::Parser;

pub trait HasName {
    const NAME: &'static str;
}

pub struct Foo;

impl HasName for Foo {
    const NAME: &'static str = "foo";
}

/// Simple program to greet a person
#[derive(Parser, Debug)]
struct Args<T: HasName> {
    /// Name of the person to greet
    #[clap(name = T::NAME, short, long)]
    name: String,

    #[clap(skip)]
    phantom: std::marker::PhantomData<T>,
}

fn main() {
    Args::<Foo>::parse();
}
```

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

`cargo build`

### Actual Behaviour

Build error:
```
error: expected a literal
  --> src\main.rs:17:19
   |
17 |     #[clap(name = T::NAME, short, long)]
   |                   ^^^^^^^
   |
   = note: only literals (like `"foo"`, `-42` and `3.14`) can be passed to `concat!()`
```

### Expected Behaviour

This code worked in clap `3.2.25` and was not documented as a change, so I think it still should work.

Also, [the documentation](https://docs.rs/clap/4.5.2/clap/_derive/index.html#command-attributes) says that the `name` attribute argument is an expression, so I would expect it to work. Especially since there is no restriction on the [Command::name](https://docs.rs/clap/4.5.2/clap/struct.Command.html#method.name) API. I don't think there are similar restrictions on other attributes.

### Additional Context

This blocks me from migrating to clap v4. I relied on this pattern to reuse the same structure definition, while avoiding the name collisions.

### Debug Output

_No response_


Discussion / hints:

Fix patch:
diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs
index 279f6ca0d7d..b1b16de5003 100644
--- a/clap_derive/src/derives/args.rs
+++ b/clap_derive/src/derives/args.rs
@@ -717,9 +717,21 @@ fn gen_parsers(
         },
 
         Ty::Other => {
-            quote_spanned! { ty.span()=>
-                #arg_matches.#get_one(#id)
-                    .ok_or_else(|| clap::Error::raw(clap::error::ErrorKind::MissingRequiredArgument, concat!("The following required argument was not provided: ", #id)))?
+            // Prefer `concat` where possible for reduced code size but fallback to `format!` to
+            // allow non-literal `id`s
+            match id {
+                Name::Assigned(_) => {
+                    quote_spanned! { ty.span()=>
+                        #arg_matches.#get_one(#id)
+                            .ok_or_else(|| clap::Error::raw(clap::error::ErrorKind::MissingRequiredArgument, format!("The following required argument was not provided: {}", #id)))?
+                    }
+                }
+                Name::Derived(_) => {
+                    quote_spanned! { ty.span()=>
+                        #arg_matches.#get_one(#id)
+                            .ok_or_else(|| clap::Error::raw(clap::error::ErrorKind::MissingRequiredArgument, concat!("The following required argument was not provided: ", #id)))?
+                    }
+                }
             }
         }
     };
diff --git a/clap_derive/src/item.rs b/clap_derive/src/item.rs
index 114849f6950..e66af319a85 100644
--- a/clap_derive/src/item.rs
+++ b/clap_derive/src/item.rs
@@ -974,8 +974,8 @@ impl Item {
         quote!( #(#doc_comment)* #(#methods)* )
     }
 
-    pub fn group_id(&self) -> TokenStream {
-        self.group_id.clone().raw()
+    pub fn group_id(&self) -> &Name {
+        &self.group_id
     }
 
     pub fn group_methods(&self) -> TokenStream {
@@ -998,8 +998,8 @@ impl Item {
         quote!( #(#next_help_heading)* )
     }
 
-    pub fn id(&self) -> TokenStream {
-        self.name.clone().raw()
+    pub fn id(&self) -> &Name {
+        &self.name
     }
 
     pub fn cased_name(&self) -> TokenStream {
@@ -1410,16 +1410,6 @@ pub enum Name {
 }
 
 impl Name {
-    pub fn raw(self) -> TokenStream {
-        match self {
-            Name::Assigned(tokens) => tokens,
-            Name::Derived(ident) => {
-                let s = ident.unraw().to_string();
-                quote_spanned!(ident.span()=> #s)
-            }
-        }
-    }
-
     pub fn translate(self, style: CasingStyle) -> TokenStream {
         use CasingStyle::*;
 
@@ -1466,3 +1456,15 @@ impl Name {
         }
     }
 }
+
+impl ToTokens for Name {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        match self {
+            Name::Assigned(t) => t.to_tokens(tokens),
+            Name::Derived(ident) => {
+                let s = ident.unraw().to_string();
+                quote_spanned!(ident.span()=> #s).to_tokens(tokens)
+            }
+        }
+    }
+}


Test patch:
diff --git a/tests/derive/non_literal_attributes.rs b/tests/derive/non_literal_attributes.rs
index 63a1d0254c4..eba4124f567 100644
--- a/tests/derive/non_literal_attributes.rs
+++ b/tests/derive/non_literal_attributes.rs
@@ -125,19 +125,19 @@ fn test_bool() {
     assert_eq!(result.unwrap_err().kind(), ErrorKind::NoEquals);
 }
 
-fn parse_hex(input: &str) -> Result<u64, ParseIntError> {
-    u64::from_str_radix(input, 16)
-}
-
-#[derive(Parser, PartialEq, Debug)]
-struct HexOpt {
-    #[arg(short, value_parser = parse_hex)]
-    number: u64,
-}
-
 #[test]
 #[cfg(feature = "error-context")]
 fn test_parse_hex_function_path() {
+    #[derive(Parser, PartialEq, Debug)]
+    struct HexOpt {
+        #[arg(short, value_parser = parse_hex)]
+        number: u64,
+    }
+
+    fn parse_hex(input: &str) -> Result<u64, ParseIntError> {
+        u64::from_str_radix(input, 16)
+    }
+
     assert_eq!(
         HexOpt { number: 5 },
         HexOpt::try_parse_from(["test", "-n", "5"]).unwrap()
@@ -156,3 +156,20 @@ fn test_parse_hex_function_path() {
         err
     );
 }
+
+#[test]
+#[cfg(feature = "error-context")]
+fn test_const_name() {
+    #[derive(Parser, PartialEq, Debug)]
+    struct Opt {
+        #[arg(id = NAME, short, long)]
+        number: u64,
+    }
+
+    const NAME: &str = "fun";
+
+    assert_eq!(
+        Opt { number: 5 },
+        Opt::try_parse_from(["test", "-f", "5"]).unwrap()
+    );
+}