diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 04:53:17 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 04:53:17 +0000 |
commit | 4486b528fdbd202ae8353499cd7e9a86a3e4a2b2 (patch) | |
tree | 4914214646d35e71a252c9b11f33de84e7479aca | |
parent | eb2289bc6b562538ab3a266d87bdc098d87de44c (diff) | |
parent | 611b7f5d2daf4a2974c8f34adfa89df292244042 (diff) | |
download | argh_derive-4486b528fdbd202ae8353499cd7e9a86a3e4a2b2.tar.gz |
Snap for 10453563 from 611b7f5d2daf4a2974c8f34adfa89df292244042 to mainline-ipsec-releaseaml_ips_341611000aml_ips_341510000aml_ips_340914280aml_ips_340914200aml_ips_340914000
Change-Id: I3afa56928c012b4ca334694c3ab477167805eb95
-rw-r--r-- | .cargo_vcs_info.json | 2 | ||||
-rw-r--r-- | Android.bp | 9 | ||||
-rw-r--r-- | Cargo.toml | 14 | ||||
-rw-r--r-- | Cargo.toml.orig | 5 | ||||
-rw-r--r-- | METADATA | 14 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | TEST_MAPPING | 3 | ||||
-rw-r--r-- | cargo2android.json | 4 | ||||
-rw-r--r-- | src/errors.rs | 5 | ||||
-rw-r--r-- | src/help.rs | 45 | ||||
-rw-r--r-- | src/lib.rs | 288 | ||||
-rw-r--r-- | src/parse_attrs.rs | 148 |
12 files changed, 404 insertions, 146 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index 1b6d0b1..9595563 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "f1f85d2d89cbe09314dc1b59e581b8a43531cf3e" + "sha1": "3f3c29726a21c4b541bb2b9aa2c592461897ded0" }, "path_in_vcs": "argh_derive" }
\ No newline at end of file @@ -1,8 +1,6 @@ -// This file is generated by cargo2android.py --run --device --tests. +// This file is generated by cargo2android.py --config cargo2android.json. // Do not modify this file as changes will be overridden on upgrade. - - package { default_applicable_licenses: ["external_rust_crates_argh_derive_license"], } @@ -24,14 +22,15 @@ rust_proc_macro { name: "libargh_derive", crate_name: "argh_derive", cargo_env_compat: true, - cargo_pkg_version: "0.1.7", + cargo_pkg_version: "0.1.10", srcs: ["src/lib.rs"], edition: "2018", rustlibs: [ "libargh_shared", - "libheck", "libproc_macro2", "libquote", "libsyn", ], + product_available: true, + vendor_available: true, } @@ -12,8 +12,12 @@ [package] edition = "2018" name = "argh_derive" -version = "0.1.7" -authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"] +version = "0.1.10" +authors = [ + "Taylor Cramer <cramertj@google.com>", + "Benjamin Brittain <bwb@google.com>", + "Erick Tryzelaar <etryzelaar@google.com>", +] description = "Derive-based argument parsing optimized for code size" readme = "README.md" license = "BSD-3-Clause" @@ -21,11 +25,9 @@ repository = "https://github.com/google/argh" [lib] proc-macro = true -[dependencies.argh_shared] -version = "0.1.7" -[dependencies.heck] -version = "0.3.1" +[dependencies.argh_shared] +version = "0.1.10" [dependencies.proc-macro2] version = "1.0" diff --git a/Cargo.toml.orig b/Cargo.toml.orig index b74329d..750b494 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "argh_derive" -version = "0.1.7" +version = "0.1.10" authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"] edition = "2018" license = "BSD-3-Clause" @@ -12,8 +12,7 @@ readme = "README.md" proc-macro = true [dependencies] -heck = "0.3.1" proc-macro2 = "1.0" quote = "1.0" syn = "1.0" -argh_shared = { version = "0.1.7", path = "../argh_shared" } +argh_shared = { version = "0.1.10", path = "../argh_shared" } @@ -1,3 +1,7 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update rust/crates/argh_derive +# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md + name: "argh_derive" description: "Derive-based argument parsing optimized for code size" third_party { @@ -7,13 +11,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/argh_derive/argh_derive-0.1.7.crate" + value: "https://static.crates.io/crates/argh_derive/argh_derive-0.1.10.crate" } - version: "0.1.7" + version: "0.1.10" license_type: NOTICE last_upgrade_date { - year: 2022 - month: 1 - day: 13 + year: 2023 + month: 2 + day: 1 } } @@ -175,3 +175,16 @@ struct SubCommandTwo { ``` NOTE: This is not an officially supported Google product. + + +## How to debug the expanded derive macro for `argh` + +The `argh::FromArgs` derive macro can be debugged with the [cargo-expand](https://crates.io/crates/cargo-expand) crate. + +### Expand the derive macro in `examples/simple_example.rs` + +See [argh/examples/simple_example.rs](./argh/examples/simple_example.rs) for the example struct we wish to expand. + +First, install `cargo-expand` by running `cargo install cargo-expand`. Note this requires the nightly build of Rust. + +Once installed, run `cargo expand` with in the `argh` package and you can see the expanded code. diff --git a/TEST_MAPPING b/TEST_MAPPING index 5cbb502..9f55024 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -3,6 +3,9 @@ "imports": [ { "path": "external/rust/crates/argh" + }, + { + "path": "packages/modules/Virtualization/virtualizationmanager" } ] } diff --git a/cargo2android.json b/cargo2android.json new file mode 100644 index 0000000..fe43c3f --- /dev/null +++ b/cargo2android.json @@ -0,0 +1,4 @@ +{ + "run": true, + "device": true +} diff --git a/src/errors.rs b/src/errors.rs index a5b69e6..cfad383 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -146,6 +146,11 @@ impl Errors { self.push(syn::Error::new(span, msg)); } + /// Issue an error spanning over the given syntax tree node. + pub fn err_span_tokens<T: ToTokens>(&self, tokens: T, msg: &str) { + self.push(syn::Error::new_spanned(tokens, msg)); + } + /// Push a `syn::Error` onto the list of errors to issue. pub fn push(&self, err: syn::Error) { self.errors.borrow_mut().push(err); diff --git a/src/help.rs b/src/help.rs index 5bf02b1..bd20622 100644 --- a/src/help.rs +++ b/src/help.rs @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +use std::fmt::Write; use { crate::{ errors::Errors, @@ -28,7 +29,9 @@ pub(crate) fn help( ) -> TokenStream { let mut format_lit = "Usage: {command_name}".to_string(); - let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + let positional = fields.iter().filter(|f| { + f.kind == FieldKind::Positional && f.attrs.greedy.is_none() && !f.attrs.hidden_help + }); let mut has_positional = false; for arg in positional.clone() { has_positional = true; @@ -36,12 +39,20 @@ pub(crate) fn help( positional_usage(&mut format_lit, arg); } - let options = fields.iter().filter(|f| f.long_name.is_some()); + let options = fields.iter().filter(|f| f.long_name.is_some() && !f.attrs.hidden_help); for option in options.clone() { format_lit.push(' '); option_usage(&mut format_lit, option); } + let remain = fields.iter().filter(|f| { + f.kind == FieldKind::Positional && f.attrs.greedy.is_some() && !f.attrs.hidden_help + }); + for arg in remain { + format_lit.push(' '); + positional_usage(&mut format_lit, arg); + } + if let Some(subcommand) = subcommand { format_lit.push(' '); if !subcommand.optionality.is_required() { @@ -85,6 +96,12 @@ pub(crate) fn help( subcommand_calculation = quote! { let subcommands = argh::print_subcommands( <#subcommand_ty as argh::SubCommands>::COMMANDS + .iter() + .copied() + .chain( + <#subcommand_ty as argh::SubCommands>::dynamic_commands() + .iter() + .copied()) ); }; } else { @@ -96,17 +113,17 @@ pub(crate) fn help( lits_section(&mut format_lit, "Notes:", &ty_attrs.notes); - if ty_attrs.error_codes.len() != 0 { + if !ty_attrs.error_codes.is_empty() { format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Error codes:"); for (code, text) in &ty_attrs.error_codes { format_lit.push('\n'); format_lit.push_str(INDENT); - format_lit.push_str(&format!("{} {}", code, text.value())); + write!(format_lit, "{} {}", code, text.value()).unwrap(); } } - format_lit.push_str("\n"); + format_lit.push('\n'); quote! { { #subcommand_calculation @@ -116,7 +133,7 @@ pub(crate) fn help( /// A section composed of exactly just the literals provided to the program. fn lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr]) { - if lits.len() != 0 { + if !lits.is_empty() { out.push_str(SECTION_SEPARATOR); out.push_str(heading); for lit in lits { @@ -135,13 +152,17 @@ fn positional_usage(out: &mut String, field: &StructField<'_>) { if !field.optionality.is_required() { out.push('['); } - out.push('<'); - let name = field.arg_name(); + if field.attrs.greedy.is_none() { + out.push('<'); + } + let name = field.positional_arg_name(); out.push_str(&name); if field.optionality == Optionality::Repeating { out.push_str("..."); } - out.push('>'); + if field.attrs.greedy.is_none() { + out.push('>'); + } if !field.optionality.is_required() { out.push(']'); } @@ -209,7 +230,7 @@ Add a doc comment or an `#[argh(description = \"...\")]` attribute.", /// Describes a positional argument like this: /// hello positional argument description fn positional_description(out: &mut String, field: &StructField<'_>) { - let field_name = field.arg_name(); + let field_name = field.positional_arg_name(); let mut description = String::from(""); if let Some(desc) = &field.attrs.description { @@ -219,7 +240,7 @@ fn positional_description(out: &mut String, field: &StructField<'_>) { } fn positional_description_format(out: &mut String, name: &str, description: &str) { - let info = argh_shared::CommandInfo { name: &*name, description }; + let info = argh_shared::CommandInfo { name, description }; argh_shared::write_description(out, &info); } @@ -249,6 +270,6 @@ fn option_description_format( } name.push_str(long_with_leading_dashes); - let info = argh_shared::CommandInfo { name: &*name, description }; + let info = argh_shared::CommandInfo { name: &name, description }; argh_shared::write_description(out, &info); } @@ -11,12 +11,12 @@ extern crate proc_macro; use { crate::{ errors::Errors, - parse_attrs::{FieldAttrs, FieldKind, TypeAttrs}, + parse_attrs::{check_long_name, FieldAttrs, FieldKind, TypeAttrs}, }, proc_macro2::{Span, TokenStream}, quote::{quote, quote_spanned, ToTokens}, - std::str::FromStr, - syn::{spanned::Spanned, LitStr}, + std::{collections::HashMap, str::FromStr}, + syn::{spanned::Spanned, GenericArgument, LitStr, PathArguments, Type}, }; mod errors; @@ -35,16 +35,14 @@ pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// as well as all errors that occurred. fn impl_from_args(input: &syn::DeriveInput) -> TokenStream { let errors = &Errors::default(); - if input.generics.params.len() != 0 { - errors.err( - &input.generics, - "`#![derive(FromArgs)]` cannot be applied to types with generic parameters", - ); - } let type_attrs = &TypeAttrs::parse(errors, input); let mut output_tokens = match &input.data { - syn::Data::Struct(ds) => impl_from_args_struct(errors, &input.ident, type_attrs, ds), - syn::Data::Enum(de) => impl_from_args_enum(errors, &input.ident, type_attrs, de), + syn::Data::Struct(ds) => { + impl_from_args_struct(errors, &input.ident, type_attrs, &input.generics, ds) + } + syn::Data::Enum(de) => { + impl_from_args_enum(errors, &input.ident, type_attrs, &input.generics, de) + } syn::Data::Union(_) => { errors.err(input, "`#[derive(FromArgs)]` cannot be applied to unions"); TokenStream::new() @@ -65,22 +63,15 @@ enum Optionality { impl PartialEq<Optionality> for Optionality { fn eq(&self, other: &Optionality) -> bool { use Optionality::*; - match (self, other) { - (None, None) | (Optional, Optional) | (Repeating, Repeating) => true, - // NB: (Defaulted, Defaulted) can't contain the same token streams - _ => false, - } + // NB: (Defaulted, Defaulted) can't contain the same token streams + matches!((self, other), (Optional, Optional) | (Repeating, Repeating)) } } impl Optionality { /// Whether or not this is `Optionality::None` fn is_required(&self) -> bool { - if let Optionality::None = self { - true - } else { - false - } + matches!(self, Optionality::None) } } @@ -125,7 +116,7 @@ impl<'a> StructField<'a> { field, concat!( "Missing `argh` field kind attribute.\n", - "Expected one of: `switch`, `option`, `subcommand`, `positional`", + "Expected one of: `switch`, `option`, `remaining`, `subcommand`, `positional`", ), ); return None; @@ -187,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(|| heck::KebabCase::to_kebab_case(&*name.to_string())); + 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."); } @@ -204,9 +195,41 @@ impl<'a> StructField<'a> { Some(StructField { field, attrs, kind, optionality, ty_without_wrapper, name, long_name }) } - pub(crate) fn arg_name(&self) -> String { - self.attrs.arg_name.as_ref().map(LitStr::value).unwrap_or_else(|| self.name.to_string()) + pub(crate) fn positional_arg_name(&self) -> String { + self.attrs + .arg_name + .as_ref() + .map(LitStr::value) + .unwrap_or_else(|| self.name.to_string().trim_matches('_').to_owned()) + } +} + +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. @@ -214,6 +237,7 @@ fn impl_from_args_struct( errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs, + generic_args: &syn::Generics, ds: &syn::DataStruct, ) -> TokenStream { let fields = match &ds.fields { @@ -240,6 +264,7 @@ fn impl_from_args_struct( }) .collect(); + ensure_unique_names(errors, &fields); ensure_only_last_positional_is_optional(errors, &fields); let impl_span = Span::call_site(); @@ -249,10 +274,12 @@ fn impl_from_args_struct( let redact_arg_values_method = impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields); - let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs); + let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs, generic_args); + let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); let trait_impl = quote_spanned! { impl_span => - impl argh::FromArgs for #name { + #[automatically_derived] + impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause { #from_args_method #redact_arg_values_method @@ -269,8 +296,8 @@ fn impl_from_args_struct_from_args<'a>( type_attrs: &TypeAttrs, fields: &'a [StructField<'a>], ) -> TokenStream { - let init_fields = declare_local_storage_for_from_args_fields(&fields); - let unwrap_fields = unwrap_from_args_fields(&fields); + let init_fields = declare_local_storage_for_from_args_fields(fields); + let unwrap_fields = unwrap_from_args_fields(fields); let positional_fields: Vec<&StructField<'_>> = fields.iter().filter(|field| field.kind == FieldKind::Positional).collect(); let positional_field_idents = positional_fields.iter().map(|field| &field.field.ident); @@ -279,6 +306,10 @@ fn impl_from_args_struct_from_args<'a>( .last() .map(|field| field.optionality == Optionality::Repeating) .unwrap_or(false); + let last_positional_is_greedy = positional_fields + .last() + .map(|field| field.kind == FieldKind::Positional && field.attrs.greedy.is_some()) + .unwrap_or(false); let flag_output_table = fields.iter().filter_map(|field| { let field_name = &field.field.ident; @@ -289,13 +320,13 @@ fn impl_from_args_struct_from_args<'a>( } }); - let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields); + let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(fields); let mut subcommands_iter = fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); - while let Some(dup_subcommand) = subcommands_iter.next() { + for dup_subcommand in subcommands_iter { errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); } @@ -304,7 +335,7 @@ fn impl_from_args_struct_from_args<'a>( let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span); let append_missing_requirements = - append_missing_requirements(&missing_requirements_ident, &fields); + append_missing_requirements(&missing_requirements_ident, fields); let parse_subcommands = if let Some(subcommand) = subcommand { let name = subcommand.name; @@ -312,6 +343,7 @@ fn impl_from_args_struct_from_args<'a>( quote_spanned! { impl_span => Some(argh::ParseStructSubCommand { subcommands: <#ty as argh::SubCommands>::COMMANDS, + dynamic_subcommands: &<#ty as argh::SubCommands>::dynamic_commands(), parse_func: &mut |__command, __remaining_args| { #name = Some(<#ty as argh::FromArgs>::from_args(__command, __remaining_args)?); Ok(()) @@ -324,12 +356,14 @@ fn impl_from_args_struct_from_args<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result<Self, argh::EarlyExit> { + #![allow(clippy::unwrap_in_result)] + #( #init_fields )* argh::parse_struct_args( @@ -349,6 +383,7 @@ fn impl_from_args_struct_from_args<'a>( )* ], last_is_repeating: #last_positional_is_repeating, + last_is_greedy: #last_positional_is_greedy, }, #parse_subcommands, &|| #help, @@ -374,8 +409,8 @@ fn impl_from_args_struct_redact_arg_values<'a>( type_attrs: &TypeAttrs, fields: &'a [StructField<'a>], ) -> TokenStream { - let init_fields = declare_local_storage_for_redacted_fields(&fields); - let unwrap_fields = unwrap_redacted_fields(&fields); + let init_fields = declare_local_storage_for_redacted_fields(fields); + let unwrap_fields = unwrap_redacted_fields(fields); let positional_fields: Vec<&StructField<'_>> = fields.iter().filter(|field| field.kind == FieldKind::Positional).collect(); @@ -385,6 +420,10 @@ fn impl_from_args_struct_redact_arg_values<'a>( .last() .map(|field| field.optionality == Optionality::Repeating) .unwrap_or(false); + let last_positional_is_greedy = positional_fields + .last() + .map(|field| field.kind == FieldKind::Positional && field.attrs.greedy.is_some()) + .unwrap_or(false); let flag_output_table = fields.iter().filter_map(|field| { let field_name = &field.field.ident; @@ -395,13 +434,13 @@ fn impl_from_args_struct_redact_arg_values<'a>( } }); - let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields); + let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(fields); let mut subcommands_iter = fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); - while let Some(dup_subcommand) = subcommands_iter.next() { + for dup_subcommand in subcommands_iter { errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); } @@ -410,7 +449,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span); let append_missing_requirements = - append_missing_requirements(&missing_requirements_ident, &fields); + append_missing_requirements(&missing_requirements_ident, fields); let redact_subcommands = if let Some(subcommand) = subcommand { let name = subcommand.name; @@ -418,6 +457,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( quote_spanned! { impl_span => Some(argh::ParseStructSubCommand { subcommands: <#ty as argh::SubCommands>::COMMANDS, + dynamic_subcommands: &<#ty as argh::SubCommands>::dynamic_commands(), parse_func: &mut |__command, __remaining_args| { #name = Some(<#ty as argh::FromArgs>::redact_arg_values(__command, __remaining_args)?); Ok(()) @@ -428,15 +468,15 @@ fn impl_from_args_struct_redact_arg_values<'a>( quote_spanned! { impl_span => None } }; - let cmd_name = if type_attrs.is_subcommand.is_none() { - quote! { __cmd_name.last().expect("no command name").to_string() } + let unwrap_cmd_name_err_string = if type_attrs.is_subcommand.is_none() { + quote! { "no command name" } } else { - quote! { __cmd_name.last().expect("no subcommand name").to_string() } + quote! { "no subcommand name" } }; // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> { @@ -459,6 +499,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( )* ], last_is_repeating: #last_positional_is_repeating, + last_is_greedy: #last_positional_is_greedy, }, #redact_subcommands, &|| #help, @@ -471,7 +512,11 @@ fn impl_from_args_struct_redact_arg_values<'a>( #missing_requirements_ident.err_on_any()?; let mut __redacted = vec![ - #cmd_name, + if let Some(cmd_name) = __cmd_name.last() { + (*cmd_name).to_owned() + } else { + return Err(argh::EarlyExit::from(#unwrap_cmd_name_err_string.to_owned())); + } ]; #( #unwrap_fields )* @@ -503,14 +548,54 @@ fn ensure_only_last_positional_is_optional(errors: &Errors, fields: &[StructFiel } } +/// Ensures that only one short or long name is used. +fn ensure_unique_names(errors: &Errors, fields: &[StructField<'_>]) { + let mut seen_short_names = HashMap::new(); + let mut seen_long_names = HashMap::new(); + + for field in fields { + if let Some(short_name) = &field.attrs.short { + let short_name = short_name.value(); + if let Some(first_use_field) = seen_short_names.get(&short_name) { + errors.err_span_tokens( + first_use_field, + &format!("The short name of \"-{}\" was already used here.", short_name), + ); + errors.err_span_tokens(field.field, "Later usage here."); + } + + seen_short_names.insert(short_name, &field.field); + } + + if let Some(long_name) = &field.long_name { + if let Some(first_use_field) = seen_long_names.get(&long_name) { + errors.err_span_tokens( + *first_use_field, + &format!("The long name of \"{}\" was already used here.", long_name), + ); + errors.err_span_tokens(field.field, "Later usage here."); + } + + seen_long_names.insert(long_name, field.field); + } + } +} + /// Implement `argh::TopLevelCommand` or `argh::SubCommand` as appropriate. -fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs) -> TokenStream { +fn top_or_sub_cmd_impl( + errors: &Errors, + name: &syn::Ident, + type_attrs: &TypeAttrs, + generic_args: &syn::Generics, +) -> TokenStream { let description = help::require_description(errors, name.span(), &type_attrs.description, "type"); + let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); if type_attrs.is_subcommand.is_none() { // Not a subcommand quote! { - impl argh::TopLevelCommand for #name {} + #[automatically_derived] + impl #impl_generics argh::TopLevelCommand for #name #ty_generics #where_clause {} } } else { let empty_str = syn::LitStr::new("", Span::call_site()); @@ -519,7 +604,8 @@ fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttr &empty_str }); quote! { - impl argh::SubCommand for #name { + #[automatically_derived] + impl #impl_generics argh::SubCommand for #name #ty_generics #where_clause { const COMMAND: &'static argh::CommandInfo = &argh::CommandInfo { name: #subcommand_name, description: #description, @@ -586,7 +672,9 @@ fn unwrap_from_args_fields<'a>( let field_name = field.name; match field.kind { FieldKind::Option | FieldKind::Positional => match &field.optionality { - Optionality::None => quote! { #field_name: #field_name.slot.unwrap() }, + Optionality::None => quote! { + #field_name: #field_name.slot.unwrap() + }, Optionality::Optional | Optionality::Repeating => { quote! { #field_name: #field_name.slot } } @@ -639,7 +727,7 @@ fn declare_local_storage_for_redacted_fields<'a>( let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> = argh::ParseValueSlotTy { slot: std::default::Default::default(), - parse_func: |arg, _| { Ok(arg.to_string()) }, + parse_func: |arg, _| { Ok(arg.to_owned()) }, }; } } @@ -653,12 +741,12 @@ fn declare_local_storage_for_redacted_fields<'a>( } }; - let arg_name = field.arg_name(); + let arg_name = field.positional_arg_name(); quote! { let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> = argh::ParseValueSlotTy { slot: std::default::Default::default(), - parse_func: |_, _| { Ok(#arg_name.to_string()) }, + parse_func: |_, _| { Ok(#arg_name.to_owned()) }, }; } } @@ -745,7 +833,7 @@ fn append_missing_requirements<'a>( match field.kind { FieldKind::Switch => unreachable!("switches are always optional"), FieldKind::Positional => { - let name = field.arg_name(); + let name = field.positional_arg_name(); quote! { if #field_name.slot.is_none() { #mri.missing_positional_arg(#name) @@ -765,7 +853,14 @@ fn append_missing_requirements<'a>( quote! { if #field_name.is_none() { #mri.missing_subcommands( - <#ty as argh::SubCommands>::COMMANDS, + <#ty as argh::SubCommands>::COMMANDS + .iter() + .cloned() + .chain( + <#ty as argh::SubCommands>::dynamic_commands() + .iter() + .copied() + ), ) } } @@ -786,6 +881,16 @@ fn ty_expect_switch(errors: &Errors, ty: &syn::Type) -> bool { return false; } let ident = &path.path.segments[0].ident; + // `Option<bool>` can be used as a `switch`. + if ident == "Option" { + if let PathArguments::AngleBracketed(args) = &path.path.segments[0].arguments { + if let GenericArgument::Type(Type::Path(p)) = &args.args[0] { + if p.path.segments[0].ident == "bool" { + return true; + } + } + } + } ["bool", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"] .iter() .any(|path| ident == path) @@ -796,7 +901,7 @@ fn ty_expect_switch(errors: &Errors, ty: &syn::Type) -> bool { let res = ty_can_be_switch(ty); if !res { - errors.err(ty, "switches must be of type `bool` or integer type"); + errors.err(ty, "switches must be of type `bool`, `Option<bool>`, or integer type"); } res } @@ -831,6 +936,7 @@ fn impl_from_args_enum( errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs, + generic_args: &syn::Generics, de: &syn::DataEnum, ) -> TokenStream { parse_attrs::check_enum_type_attrs(errors, type_attrs, &de.enum_token.span); @@ -841,27 +947,66 @@ fn impl_from_args_enum( ty: &'a syn::Type, } + let mut dynamic_type_and_variant = None; + let variants: Vec<SubCommandVariant<'_>> = de .variants .iter() .filter_map(|variant| { - parse_attrs::check_enum_variant_attrs(errors, variant); let name = &variant.ident; let ty = enum_only_single_field_unnamed_variants(errors, &variant.fields)?; - Some(SubCommandVariant { name, ty }) + if parse_attrs::VariantAttrs::parse(errors, variant).is_dynamic.is_some() { + if dynamic_type_and_variant.is_some() { + errors.err(variant, "Only one variant can have the `dynamic` attribute"); + } + dynamic_type_and_variant = Some((ty, name)); + None + } else { + Some(SubCommandVariant { name, ty }) + } }) .collect(); let name_repeating = std::iter::repeat(name.clone()); let variant_ty = variants.iter().map(|x| x.ty).collect::<Vec<_>>(); let variant_names = variants.iter().map(|x| x.name).collect::<Vec<_>>(); + let dynamic_from_args = + dynamic_type_and_variant.as_ref().map(|(dynamic_type, dynamic_variant)| { + quote! { + if let Some(result) = <#dynamic_type as argh::DynamicSubCommand>::try_from_args( + command_name, args) { + return result.map(#name::#dynamic_variant); + } + } + }); + let dynamic_redact_arg_values = dynamic_type_and_variant.as_ref().map(|(dynamic_type, _)| { + quote! { + if let Some(result) = <#dynamic_type as argh::DynamicSubCommand>::try_redact_arg_values( + command_name, args) { + return result; + } + } + }); + let dynamic_commands = dynamic_type_and_variant.as_ref().map(|(dynamic_type, _)| { + quote! { + fn dynamic_commands() -> &'static [&'static argh::CommandInfo] { + <#dynamic_type as argh::DynamicSubCommand>::commands() + } + } + }); + let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); quote! { - impl argh::FromArgs for #name { + impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause { fn from_args(command_name: &[&str], args: &[&str]) -> std::result::Result<Self, argh::EarlyExit> { - let subcommand_name = *command_name.last().expect("no subcommand name"); + let subcommand_name = if let Some(subcommand_name) = command_name.last() { + *subcommand_name + } else { + return Err(argh::EarlyExit::from("no subcommand name".to_owned())); + }; + #( if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name { return Ok(#name_repeating::#variant_names( @@ -869,24 +1014,37 @@ fn impl_from_args_enum( )); } )* - unreachable!("no subcommand matched") + + #dynamic_from_args + + Err(argh::EarlyExit::from("no subcommand matched".to_owned())) } fn redact_arg_values(command_name: &[&str], args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> { - let subcommand_name = *command_name.last().expect("no subcommand name"); + let subcommand_name = if let Some(subcommand_name) = command_name.last() { + *subcommand_name + } else { + return Err(argh::EarlyExit::from("no subcommand name".to_owned())); + }; + #( if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name { return <#variant_ty as argh::FromArgs>::redact_arg_values(command_name, args); } )* - unreachable!("no subcommand matched") + + #dynamic_redact_arg_values + + Err(argh::EarlyExit::from("no subcommand matched".to_owned())) } } - impl argh::SubCommands for #name { + impl #impl_generics argh::SubCommands for #name #ty_generics #where_clause { const COMMANDS: &'static [&'static argh::CommandInfo] = &[#( <#variant_ty as argh::SubCommand>::COMMAND, )*]; + + #dynamic_commands } } } diff --git a/src/parse_attrs.rs b/src/parse_attrs.rs index 83807ed..04dcbdd 100644 --- a/src/parse_attrs.rs +++ b/src/parse_attrs.rs @@ -18,6 +18,8 @@ pub struct FieldAttrs { pub long: Option<syn::LitStr>, pub short: Option<syn::LitChar>, pub arg_name: Option<syn::LitStr>, + pub greedy: Option<syn::Path>, + pub hidden_help: bool, } /// The purpose of a particular field on a `#![derive(FromArgs)]` struct. @@ -79,29 +81,29 @@ impl FieldAttrs { let name = meta.path(); if name.is_ident("arg_name") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_arg_name(errors, m); } } else if name.is_ident("default") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_default(errors, m); } } else if name.is_ident("description") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { parse_attr_description(errors, m, &mut this.description); } } else if name.is_ident("from_str_fn") { - if let Some(m) = errors.expect_meta_list(&meta) { + if let Some(m) = errors.expect_meta_list(meta) { this.parse_attr_from_str_fn(errors, m); } } else if name.is_ident("long") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_long(errors, m); } } else if name.is_ident("option") { parse_attr_field_type(errors, meta, FieldKind::Option, &mut this.field_type); } else if name.is_ident("short") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_short(errors, m); } } else if name.is_ident("subcommand") { @@ -120,13 +122,17 @@ impl FieldAttrs { FieldKind::Positional, &mut this.field_type, ); + } else if name.is_ident("greedy") { + this.greedy = Some(name.clone()); + } else if name.is_ident("hidden_help") { + this.hidden_help = true; } else { errors.err( &meta, concat!( "Invalid field-level `argh` attribute\n", - "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `long`, ", - "`option`, `short`, `subcommand`, `switch`", + "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `greedy`, ", + "`long`, `option`, `short`, `subcommand`, `switch`, `hidden_help`", ), ); } @@ -144,6 +150,16 @@ impl FieldAttrs { } } + match (&this.greedy, this.field_type.as_ref().map(|f| f.kind)) { + (Some(_), Some(FieldKind::Positional)) => {} + (Some(greedy), Some(_)) => errors.err( + &greedy, + "`greedy` may only be specified on `#[argh(positional)]` \ + fields", + ), + _ => {} + } + if let Some(d) = &this.description { check_option_description(errors, d.content.value().trim(), d.content.span()); } @@ -167,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 == '-') { - 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) { @@ -187,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, @@ -218,10 +238,8 @@ fn parse_attr_field_type( if let Some(path) = errors.expect_meta_word(meta) { if let Some(first) = slot { errors.duplicate_attrs("field kind", &first.ident, path); - } else { - if let Some(word) = path.get_ident() { - *slot = Some(FieldType { kind, ident: word.clone() }); - } + } else if let Some(word) = path.get_ident() { + *slot = Some(FieldType { kind, ident: word.clone() }); } } } @@ -304,28 +322,27 @@ impl TypeAttrs { let name = meta.path(); if name.is_ident("description") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { parse_attr_description(errors, m, &mut this.description); } } else if name.is_ident("error_code") { - if let Some(m) = errors.expect_meta_list(&meta) { + if let Some(m) = errors.expect_meta_list(meta) { this.parse_attr_error_code(errors, m); } } else if name.is_ident("example") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_example(errors, m); } } else if name.is_ident("name") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_name(errors, m); } } else if name.is_ident("note") { - if let Some(m) = errors.expect_meta_name_value(&meta) { + if let Some(m) = errors.expect_meta_name_value(meta) { this.parse_attr_note(errors, m); } } else if name.is_ident("subcommand") { - if let Some(ident) = errors.expect_meta_word(&meta).and_then(|p| p.get_ident()) - { + if let Some(ident) = errors.expect_meta_word(meta).and_then(|p| p.get_ident()) { this.parse_attr_subcommand(errors, ident); } } else { @@ -421,6 +438,62 @@ impl TypeAttrs { } } +/// Represents an enum variant's attributes. +#[derive(Default)] +pub struct VariantAttrs { + pub is_dynamic: Option<syn::Path>, +} + +impl VariantAttrs { + /// Parse enum variant `#[argh(...)]` attributes + pub fn parse(errors: &Errors, variant: &syn::Variant) -> Self { + let mut this = VariantAttrs::default(); + + let fields = match &variant.fields { + syn::Fields::Named(fields) => Some(&fields.named), + syn::Fields::Unnamed(fields) => Some(&fields.unnamed), + syn::Fields::Unit => None, + }; + + for field in fields.into_iter().flatten() { + for attr in &field.attrs { + if is_argh_attr(attr) { + err_unused_enum_attr(errors, attr); + } + } + } + + for attr in &variant.attrs { + let ml = if let Some(ml) = argh_attr_to_meta_list(errors, attr) { + ml + } else { + continue; + }; + + for meta in &ml.nested { + let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue }; + + let name = meta.path(); + if name.is_ident("dynamic") { + if let Some(prev) = this.is_dynamic.as_ref() { + errors.duplicate_attrs("dynamic", prev, meta); + } else { + this.is_dynamic = errors.expect_meta_word(meta).cloned(); + } + } else { + errors.err( + &meta, + "Invalid variant-level `argh` attribute\n\ + Variants can only have the #[argh(dynamic)] attribute.", + ); + } + } + } + + this + } +} + fn check_option_description(errors: &Errors, desc: &str, span: Span) { let chars = &mut desc.trim().chars(); match (chars.next(), chars.next()) { @@ -498,7 +571,7 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: // Ensure that `#[argh(subcommand)]` is present. if is_subcommand.is_none() { errors.err_span( - type_span.clone(), + *type_span, concat!( "`#![derive(FromArgs)]` on `enum`s can only be used to enumerate subcommands.\n", "Consider adding `#[argh(subcommand)]` to the `enum` declaration.", @@ -526,29 +599,6 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: } } -/// Checks that an enum variant and its fields have no `#[argh(...)]` attributes. -pub fn check_enum_variant_attrs(errors: &Errors, variant: &syn::Variant) { - for attr in &variant.attrs { - if is_argh_attr(attr) { - err_unused_enum_attr(errors, attr); - } - } - - let fields = match &variant.fields { - syn::Fields::Named(fields) => &fields.named, - syn::Fields::Unnamed(fields) => &fields.unnamed, - syn::Fields::Unit => return, - }; - - for field in fields { - for attr in &field.attrs { - if is_argh_attr(attr) { - err_unused_enum_attr(errors, attr); - } - } - } -} - fn err_unused_enum_attr(errors: &Errors, location: &impl syn::spanned::Spanned) { errors.err( location, |