Repository: google/argh
Pull request #152 (2022-11-08 15:59:31)

reduce dependencies
heck encodes the knowledge of what constitutes a unicode word, but we don't actually need that here, we just want to repaces underscores with dashes

matklad: And https://matklad.github.io/2022/10/24/actions-permissions.html

Patch:
diff --git a/argh_derive/Cargo.toml b/argh_derive/Cargo.toml
index f6614e9..901f7e7 100644
--- a/argh_derive/Cargo.toml
+++ b/argh_derive/Cargo.toml
@@ -12,7 +12,6 @@ readme = "README.md"
 proc-macro = true
 
 [dependencies]
-heck = "0.4"
 proc-macro2 = "1.0"
 quote = "1.0"
 syn = "1.0"
diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs
index d790b06..94cc437 100644
--- a/argh_derive/src/lib.rs
+++ b/argh_derive/src/lib.rs
@@ -11,9 +11,8 @@ extern crate proc_macro;
 use {
     crate::{
         errors::Errors,
-        parse_attrs::{FieldAttrs, FieldKind, TypeAttrs},
+        parse_attrs::{check_long_name, FieldAttrs, FieldKind, TypeAttrs},
     },
-    heck::ToKebabCase,
     proc_macro2::{Span, TokenStream},
     quote::{quote, quote_spanned, ToTokens},
     std::{collections::HashMap, str::FromStr},
@@ -179,11 +178,11 @@ impl<'a> StructField<'a> {
         // Defaults to the kebab-case'd field name if `#[argh(long = "...")]` is omitted.
         let long_name = match kind {
             FieldKind::Switch | FieldKind::Option => {
-                let long_name = attrs
-                    .long
-                    .as_ref()
-                    .map(syn::LitStr::value)
-                    .unwrap_or_else(|| name.to_string().to_kebab_case());
+                let long_name = attrs.long.as_ref().map(syn::LitStr::value).unwrap_or_else(|| {
+                    let kebab_name = to_kebab_case(&name.to_string());
+                    check_long_name(errors, name, &kebab_name);
+                    kebab_name
+                });
                 if long_name == "help" {
                     errors.err(field, "Custom `--help` flags are not supported.");
                 }
@@ -205,6 +204,34 @@ impl<'a> StructField<'a> {
     }
 }
 
+fn to_kebab_case(s: &str) -> String {
+    let words = s.split('_').filter(|word| !word.is_empty());
+    let mut res = String::with_capacity(s.len());
+    for word in words {
+        if !res.is_empty() {
+            res.push('-')
+        }
+        res.push_str(word)
+    }
+    res
+}
+
+#[test]
+fn test_kebabs() {
+    #[track_caller]
+    fn check(s: &str, want: &str) {
+        let got = to_kebab_case(s);
+        assert_eq!(got.as_str(), want)
+    }
+    check("", "");
+    check("_", "");
+    check("foo", "foo");
+    check("__foo_", "foo");
+    check("foo_bar", "foo-bar");
+    check("foo__Bar", "foo-Bar");
+    check("foo_bar__baz_", "foo-bar-baz");
+}
+
 /// Implements `FromArgs` and `TopLevelCommand` or `SubCommand` for a `#[derive(FromArgs)]` struct.
 fn impl_from_args_struct(
     errors: &Errors,
diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs
index 0d1cca3..04dcbdd 100644
--- a/argh_derive/src/parse_attrs.rs
+++ b/argh_derive/src/parse_attrs.rs
@@ -183,12 +183,7 @@ impl FieldAttrs {
         parse_attr_single_string(errors, m, "long", &mut self.long);
         let long = self.long.as_ref().unwrap();
         let value = long.value();
-        if !value.is_ascii() {
-            errors.err(long, "Long names must be ASCII");
-        }
-        if !value.chars().all(|c| c.is_lowercase() || c == '-' || c.is_ascii_digit()) {
-            errors.err(long, "Long names must be lowercase");
-        }
+        check_long_name(errors, long, &value);
     }
 
     fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
@@ -203,6 +198,15 @@ impl FieldAttrs {
     }
 }
 
+pub(crate) fn check_long_name(errors: &Errors, spanned: &impl syn::spanned::Spanned, value: &str) {
+    if !value.is_ascii() {
+        errors.err(spanned, "Long names must be ASCII");
+    }
+    if !value.chars().all(|c| c.is_lowercase() || c == '-' || c.is_ascii_digit()) {
+        errors.err(spanned, "Long names must be lowercase");
+    }
+}
+
 fn parse_attr_fn_name(
     errors: &Errors,
     m: &syn::MetaList,


Test patch:
diff --git a/argh/tests/ui/bad-long-names/bad-long-names.rs b/argh/tests/ui/bad-long-names/bad-long-names.rs
new file mode 100644
index 0000000..61a9696
--- /dev/null
+++ b/argh/tests/ui/bad-long-names/bad-long-names.rs
@@ -0,0 +1,15 @@
+/// Command
+#[derive(argh::FromArgs)]
+struct Cmd {
+    #[argh(switch)]
+    /// non-ascii
+    привет: bool,
+    #[argh(switch)]
+    /// uppercase
+    XMLHTTPRequest: bool,
+    #[argh(switch, long = "not really")]
+    /// bad attr
+    ok: bool,
+}
+
+fn main() {}
diff --git a/argh/tests/ui/bad-long-names/bad-long-names.stderr b/argh/tests/ui/bad-long-names/bad-long-names.stderr
new file mode 100644
index 0000000..3396a5a
--- /dev/null
+++ b/argh/tests/ui/bad-long-names/bad-long-names.stderr
@@ -0,0 +1,17 @@
+error: Long names must be ASCII
+ --> tests/ui/bad-long-names/bad-long-names.rs:6:5
+  |
+6 |     привет: bool,
+  |     ^^^^^^
+
+error: Long names must be lowercase
+ --> tests/ui/bad-long-names/bad-long-names.rs:9:5
+  |
+9 |     XMLHTTPRequest: bool,
+  |     ^^^^^^^^^^^^^^
+
+error: Long names must be lowercase
+  --> tests/ui/bad-long-names/bad-long-names.rs:10:27
+   |
+10 |     #[argh(switch, long = "not really")]
+   |                           ^^^^^^^^^^^^