diff options
author | Jeff Vander Stoep <jeffv@google.com> | 2022-12-06 12:10:09 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-12-06 12:10:09 +0000 |
commit | 2c9ca048ab5a5b35dc4e7ce0244a567444e7aa61 (patch) | |
tree | 9c38bf5cadc25895c2512aebc854ed88f86e3953 | |
parent | 8dac7e38b2be2034571925cb1f363ad3fd619de6 (diff) | |
parent | 7fa4601eb8a30b3c570d9ef25bcfd4b2a52775bb (diff) | |
download | argh_derive-2c9ca048ab5a5b35dc4e7ce0244a567444e7aa61.tar.gz |
Upgrade argh_derive to 0.1.9 am: 695f3045d9 am: 7fa4601eb8
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/argh_derive/+/2327875
Change-Id: I5d06a62631ba65f76765b335ab49d45fae35bab0
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r-- | Android.bp | 4 | ||||
-rw-r--r-- | Cargo.toml | 13 | ||||
-rw-r--r-- | Cargo.toml.orig | 6 | ||||
-rw-r--r-- | METADATA | 8 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | cargo2android.json | 3 | ||||
-rw-r--r-- | patches/0001-Use-libheck-0.3.3.patch | 28 | ||||
-rw-r--r-- | src/errors.rs | 5 | ||||
-rw-r--r-- | src/help.rs | 38 | ||||
-rw-r--r-- | src/lib.rs | 225 | ||||
-rw-r--r-- | src/parse_attrs.rs | 131 |
11 files changed, 324 insertions, 150 deletions
@@ -22,12 +22,12 @@ 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.9", srcs: ["src/lib.rs"], edition: "2018", rustlibs: [ "libargh_shared", - "libheck_deprecated", + "libheck", "libproc_macro2", "libquote", "libsyn", @@ -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.9" +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,12 @@ repository = "https://github.com/google/argh" [lib] proc-macro = true + [dependencies.argh_shared] -version = "0.1.7" +version = "0.1.9" [dependencies.heck] -version = "0.3.1" +version = "0.4" [dependencies.proc-macro2] version = "1.0" diff --git a/Cargo.toml.orig b/Cargo.toml.orig index b74329d..f6614e9 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "argh_derive" -version = "0.1.7" +version = "0.1.9" 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,8 @@ readme = "README.md" proc-macro = true [dependencies] -heck = "0.3.1" +heck = "0.4" proc-macro2 = "1.0" quote = "1.0" syn = "1.0" -argh_shared = { version = "0.1.7", path = "../argh_shared" } +argh_shared = { version = "0.1.9", path = "../argh_shared" } @@ -7,13 +7,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.9.crate" } - version: "0.1.7" + version: "0.1.9" license_type: NOTICE last_upgrade_date { year: 2022 - month: 1 - day: 13 + month: 12 + day: 5 } } @@ -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/cargo2android.json b/cargo2android.json index 4456e1f..fe43c3f 100644 --- a/cargo2android.json +++ b/cargo2android.json @@ -1,5 +1,4 @@ { "run": true, - "device": true, - "patch": "patches/0001-Use-libheck-0.3.3.patch" + "device": true } diff --git a/patches/0001-Use-libheck-0.3.3.patch b/patches/0001-Use-libheck-0.3.3.patch deleted file mode 100644 index e43b4c0..0000000 --- a/patches/0001-Use-libheck-0.3.3.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 5dd441921a3868023533fa6818b425660232cdad Mon Sep 17 00:00:00 2001 -From: Matthew Maurer <mmaurer@google.com> -Date: Wed, 7 Sep 2022 16:10:32 -0700 -Subject: [PATCH] Use libheck 0.3.3 - -Upstream has not yet migrated to 0.4. - -Change-Id: I0b7ccbb4b33ae77c88da6697562d0b7e8582e728 ---- - Android.bp | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/Android.bp b/Android.bp -index 1d3fdfe..54e460b 100644 ---- a/Android.bp -+++ b/Android.bp -@@ -29,7 +29,7 @@ rust_proc_macro { - edition: "2018", - rustlibs: [ - "libargh_shared", -- "libheck", -+ "libheck_deprecated", - "libproc_macro2", - "libquote", - "libsyn", --- -2.37.2.789.g6183377224-goog - 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..c9daad5 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, @@ -26,9 +27,11 @@ pub(crate) fn help( fields: &[StructField<'_>], subcommand: Option<&StructField<'_>>, ) -> TokenStream { + #![allow(clippy::format_push_string)] 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()); let mut has_positional = false; for arg in positional.clone() { has_positional = true; @@ -42,6 +45,13 @@ pub(crate) fn help( option_usage(&mut format_lit, option); } + let remain = + fields.iter().filter(|f| f.kind == FieldKind::Positional && f.attrs.greedy.is_some()); + 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 +95,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 +112,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 +132,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 +151,17 @@ fn positional_usage(out: &mut String, field: &StructField<'_>) { if !field.optionality.is_required() { out.push('['); } - out.push('<'); + if field.attrs.greedy.is_none() { + out.push('<'); + } let name = field.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(']'); } @@ -219,7 +239,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 +269,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); } @@ -13,9 +13,10 @@ use { errors::Errors, parse_attrs::{FieldAttrs, FieldKind, TypeAttrs}, }, + heck::ToKebabCase, proc_macro2::{Span, TokenStream}, quote::{quote, quote_spanned, ToTokens}, - std::str::FromStr, + std::{collections::HashMap, str::FromStr}, syn::{spanned::Spanned, LitStr}, }; @@ -35,16 +36,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 +64,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 +117,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; @@ -191,7 +183,7 @@ impl<'a> StructField<'a> { .long .as_ref() .map(syn::LitStr::value) - .unwrap_or_else(|| heck::KebabCase::to_kebab_case(&*name.to_string())); + .unwrap_or_else(|| name.to_string().to_kebab_case()); if long_name == "help" { errors.err(field, "Custom `--help` flags are not supported."); } @@ -214,6 +206,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 +233,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 +243,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 +265,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 +275,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 +289,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 +304,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 +312,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 +325,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 +352,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 +378,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 +389,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 +403,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 +418,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 +426,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 +437,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 +468,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 +481,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 +517,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 +573,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 +641,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 +696,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()) }, }; } } @@ -658,7 +715,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: |_, _| { Ok(#arg_name.to_string()) }, + parse_func: |_, _| { Ok(#arg_name.to_owned()) }, }; } } @@ -765,7 +822,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() + ), ) } } @@ -831,6 +895,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 +906,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 +973,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..f5fa841 100644 --- a/src/parse_attrs.rs +++ b/src/parse_attrs.rs @@ -18,6 +18,7 @@ pub struct FieldAttrs { pub long: Option<syn::LitStr>, pub short: Option<syn::LitChar>, pub arg_name: Option<syn::LitStr>, + pub greedy: Option<syn::Path>, } /// The purpose of a particular field on a `#![derive(FromArgs)]` struct. @@ -79,29 +80,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 +121,15 @@ impl FieldAttrs { FieldKind::Positional, &mut this.field_type, ); + } else if name.is_ident("greedy") { + this.greedy = Some(name.clone()); } 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`", ), ); } @@ -144,6 +147,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()); } @@ -170,7 +183,7 @@ impl FieldAttrs { if !value.is_ascii() { errors.err(long, "Long names must be ASCII"); } - if !value.chars().all(|c| c.is_lowercase() || c == '-') { + if !value.chars().all(|c| c.is_lowercase() || c == '-' || c.is_ascii_digit()) { errors.err(long, "Long names must be lowercase"); } } @@ -218,10 +231,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 +315,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 +431,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 +564,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 +592,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, |