aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Walbran <qwandor@google.com>2022-01-13 12:20:07 +0000
committerAndrew Walbran <qwandor@google.com>2022-01-19 11:49:35 +0000
commitb8b69372db53bd46c321696ddaa47537d8ec3cc3 (patch)
treec1a86a0518e3c7c16813c4f4b738520b1dd1f403
downloadargh_derive-backup-master.tar.gz
Initial import.backup-master
Bug: 215313373 Bug: 210727578 Bug: 209794844 Test: m crosvm Change-Id: I870386d55368207c594cbe4f0799bf33f4c194ee
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--Android.bp20
-rw-r--r--Cargo.toml37
-rw-r--r--Cargo.toml.orig19
-rw-r--r--LICENSE27
-rw-r--r--METADATA19
-rw-r--r--MODULE_LICENSE_BSD_LIKE0
-rw-r--r--OWNERS1
-rw-r--r--README.md177
-rw-r--r--src/errors.rs161
-rw-r--r--src/help.rs254
-rw-r--r--src/lib.rs948
-rw-r--r--src/parse_attrs.rs561
13 files changed, 2230 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..1b6d0b1
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "f1f85d2d89cbe09314dc1b59e581b8a43531cf3e"
+ },
+ "path_in_vcs": "argh_derive"
+} \ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..9fa8754
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,20 @@
+// This file is generated by cargo2android.py --run --device --tests.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_proc_macro {
+ name: "libargh_derive",
+ crate_name: "argh_derive",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.1.7",
+ srcs: ["src/lib.rs"],
+ edition: "2018",
+ rustlibs: [
+ "libargh_shared",
+ "libheck",
+ "libproc_macro2",
+ "libquote",
+ "libsyn",
+ ],
+}
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..82f7d44
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,37 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[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>"]
+description = "Derive-based argument parsing optimized for code size"
+readme = "README.md"
+license = "BSD-3-Clause"
+repository = "https://github.com/google/argh"
+
+[lib]
+proc-macro = true
+[dependencies.argh_shared]
+version = "0.1.7"
+
+[dependencies.heck]
+version = "0.3.1"
+
+[dependencies.proc-macro2]
+version = "1.0"
+
+[dependencies.quote]
+version = "1.0"
+
+[dependencies.syn]
+version = "1.0"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..b74329d
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,19 @@
+[package]
+name = "argh_derive"
+version = "0.1.7"
+authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"]
+edition = "2018"
+license = "BSD-3-Clause"
+description = "Derive-based argument parsing optimized for code size"
+repository = "https://github.com/google/argh"
+readme = "README.md"
+
+[lib]
+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" }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..87f152c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Fuchsia Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..fb0a64d
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,19 @@
+name: "argh_derive"
+description: "Derive-based argument parsing optimized for code size"
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://crates.io/crates/argh_derive"
+ }
+ url {
+ type: ARCHIVE
+ value: "https://static.crates.io/crates/argh_derive/argh_derive-0.1.7.crate"
+ }
+ version: "0.1.7"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2022
+ month: 1
+ day: 13
+ }
+}
diff --git a/MODULE_LICENSE_BSD_LIKE b/MODULE_LICENSE_BSD_LIKE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_BSD_LIKE
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4e949e4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,177 @@
+# Argh
+**Argh is an opinionated Derive-based argument parser optimized for code size**
+
+[![crates.io](https://img.shields.io/crates/v/argh.svg)](https://crates.io/crates/argh)
+[![license](https://img.shields.io/badge/license-BSD3.0-blue.svg)](https://github.com/google/argh/LICENSE)
+[![docs.rs](https://docs.rs/argh/badge.svg)](https://docs.rs/crate/argh/)
+![Argh](https://github.com/google/argh/workflows/Argh/badge.svg)
+
+Derive-based argument parsing optimized for code size and conformance
+to the Fuchsia commandline tools specification
+
+The public API of this library consists primarily of the `FromArgs`
+derive and the `from_env` function, which can be used to produce
+a top-level `FromArgs` type from the current program's commandline
+arguments.
+
+## Basic Example
+
+```rust,no_run
+use argh::FromArgs;
+
+#[derive(FromArgs)]
+/// Reach new heights.
+struct GoUp {
+ /// whether or not to jump
+ #[argh(switch, short = 'j')]
+ jump: bool,
+
+ /// how high to go
+ #[argh(option)]
+ height: usize,
+
+ /// an optional nickname for the pilot
+ #[argh(option)]
+ pilot_nickname: Option<String>,
+}
+
+fn main() {
+ let up: GoUp = argh::from_env();
+}
+```
+
+`./some_bin --help` will then output the following:
+
+```
+Usage: cmdname [-j] --height <height> [--pilot-nickname <pilot-nickname>]
+
+Reach new heights.
+
+Options:
+ -j, --jump whether or not to jump
+ --height how high to go
+ --pilot-nickname an optional nickname for the pilot
+ --help display usage information
+```
+
+The resulting program can then be used in any of these ways:
+- `./some_bin --height 5`
+- `./some_bin -j --height 5`
+- `./some_bin --jump --height 5 --pilot-nickname Wes`
+
+Switches, like `jump`, are optional and will be set to true if provided.
+
+Options, like `height` and `pilot_nickname`, can be either required,
+optional, or repeating, depending on whether they are contained in an
+`Option` or a `Vec`. Default values can be provided using the
+`#[argh(default = "<your_code_here>")]` attribute, and in this case an
+option is treated as optional.
+
+```rust
+use argh::FromArgs;
+
+fn default_height() -> usize {
+ 5
+}
+
+#[derive(FromArgs)]
+/// Reach new heights.
+struct GoUp {
+ /// an optional nickname for the pilot
+ #[argh(option)]
+ pilot_nickname: Option<String>,
+
+ /// an optional height
+ #[argh(option, default = "default_height()")]
+ height: usize,
+
+ /// an optional direction which is "up" by default
+ #[argh(option, default = "String::from(\"only up\")")]
+ direction: String,
+}
+
+fn main() {
+ let up: GoUp = argh::from_env();
+}
+```
+
+Custom option types can be deserialized so long as they implement the
+`FromArgValue` trait (automatically implemented for all `FromStr` types).
+If more customized parsing is required, you can supply a custom
+`fn(&str) -> Result<T, String>` using the `from_str_fn` attribute:
+
+```rust
+use argh::FromArgs;
+
+#[derive(FromArgs)]
+/// Goofy thing.
+struct FiveStruct {
+ /// always five
+ #[argh(option, from_str_fn(always_five))]
+ five: usize,
+}
+
+fn always_five(_value: &str) -> Result<usize, String> {
+ Ok(5)
+}
+```
+
+Positional arguments can be declared using `#[argh(positional)]`.
+These arguments will be parsed in order of their declaration in
+the structure:
+
+```rust
+use argh::FromArgs;
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// A command with positional arguments.
+struct WithPositional {
+ #[argh(positional)]
+ first: String,
+}
+```
+
+The last positional argument may include a default, or be wrapped in
+`Option` or `Vec` to indicate an optional or repeating positional argument.
+
+Subcommands are also supported. To use a subcommand, declare a separate
+`FromArgs` type for each subcommand as well as an enum that cases
+over each command:
+
+```rust
+use argh::FromArgs;
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Top-level command.
+struct TopLevel {
+ #[argh(subcommand)]
+ nested: MySubCommandEnum,
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+#[argh(subcommand)]
+enum MySubCommandEnum {
+ One(SubCommandOne),
+ Two(SubCommandTwo),
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// First subcommand.
+#[argh(subcommand, name = "one")]
+struct SubCommandOne {
+ #[argh(option)]
+ /// how many x
+ x: usize,
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Second subcommand.
+#[argh(subcommand, name = "two")]
+struct SubCommandTwo {
+ #[argh(switch)]
+ /// whether to fooey
+ fooey: bool,
+}
+```
+
+NOTE: This is not an officially supported Google product.
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..a5b69e6
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,161 @@
+// Copyright (c) 2020 Google LLC All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+use {
+ proc_macro2::{Span, TokenStream},
+ quote::ToTokens,
+ std::cell::RefCell,
+};
+
+/// A type for collecting procedural macro errors.
+#[derive(Default)]
+pub struct Errors {
+ errors: RefCell<Vec<syn::Error>>,
+}
+
+/// Produce functions to expect particular variants of `syn::Lit`
+macro_rules! expect_lit_fn {
+ ($(($fn_name:ident, $syn_type:ident, $variant:ident, $lit_name:literal),)*) => {
+ $(
+ pub fn $fn_name<'a>(&self, lit: &'a syn::Lit) -> Option<&'a syn::$syn_type> {
+ if let syn::Lit::$variant(inner) = lit {
+ Some(inner)
+ } else {
+ self.unexpected_lit($lit_name, lit);
+ None
+ }
+ }
+ )*
+ }
+}
+
+/// Produce functions to expect particular variants of `syn::Meta`
+macro_rules! expect_meta_fn {
+ ($(($fn_name:ident, $syn_type:ident, $variant:ident, $meta_name:literal),)*) => {
+ $(
+ pub fn $fn_name<'a>(&self, meta: &'a syn::Meta) -> Option<&'a syn::$syn_type> {
+ if let syn::Meta::$variant(inner) = meta {
+ Some(inner)
+ } else {
+ self.unexpected_meta($meta_name, meta);
+ None
+ }
+ }
+ )*
+ }
+}
+
+impl Errors {
+ /// Issue an error like:
+ ///
+ /// Duplicate foo attribute
+ /// First foo attribute here
+ pub fn duplicate_attrs(
+ &self,
+ attr_kind: &str,
+ first: &impl syn::spanned::Spanned,
+ second: &impl syn::spanned::Spanned,
+ ) {
+ self.duplicate_attrs_inner(attr_kind, first.span(), second.span())
+ }
+
+ fn duplicate_attrs_inner(&self, attr_kind: &str, first: Span, second: Span) {
+ self.err_span(second, &["Duplicate ", attr_kind, " attribute"].concat());
+ self.err_span(first, &["First ", attr_kind, " attribute here"].concat());
+ }
+
+ /// Error on literals, expecting attribute syntax.
+ pub fn expect_nested_meta<'a>(&self, nm: &'a syn::NestedMeta) -> Option<&'a syn::Meta> {
+ match nm {
+ syn::NestedMeta::Lit(l) => {
+ self.err(l, "Unexpected literal");
+ None
+ }
+ syn::NestedMeta::Meta(m) => Some(m),
+ }
+ }
+
+ /// Error on attribute syntax, expecting literals
+ pub fn expect_nested_lit<'a>(&self, nm: &'a syn::NestedMeta) -> Option<&'a syn::Lit> {
+ match nm {
+ syn::NestedMeta::Meta(m) => {
+ self.err(m, "Expected literal");
+ None
+ }
+ syn::NestedMeta::Lit(l) => Some(l),
+ }
+ }
+
+ expect_lit_fn![
+ (expect_lit_str, LitStr, Str, "string"),
+ (expect_lit_char, LitChar, Char, "character"),
+ (expect_lit_int, LitInt, Int, "integer"),
+ ];
+
+ expect_meta_fn![
+ (expect_meta_word, Path, Path, "path"),
+ (expect_meta_list, MetaList, List, "list"),
+ (expect_meta_name_value, MetaNameValue, NameValue, "name-value pair"),
+ ];
+
+ fn unexpected_lit(&self, expected: &str, found: &syn::Lit) {
+ fn lit_kind(lit: &syn::Lit) -> &'static str {
+ use syn::Lit::{Bool, Byte, ByteStr, Char, Float, Int, Str, Verbatim};
+ match lit {
+ Str(_) => "string",
+ ByteStr(_) => "bytestring",
+ Byte(_) => "byte",
+ Char(_) => "character",
+ Int(_) => "integer",
+ Float(_) => "float",
+ Bool(_) => "boolean",
+ Verbatim(_) => "unknown (possibly extra-large integer)",
+ }
+ }
+
+ self.err(
+ found,
+ &["Expected ", expected, " literal, found ", lit_kind(found), " literal"].concat(),
+ )
+ }
+
+ fn unexpected_meta(&self, expected: &str, found: &syn::Meta) {
+ fn meta_kind(meta: &syn::Meta) -> &'static str {
+ use syn::Meta::{List, NameValue, Path};
+ match meta {
+ Path(_) => "path",
+ List(_) => "list",
+ NameValue(_) => "name-value pair",
+ }
+ }
+
+ self.err(
+ found,
+ &["Expected ", expected, " attribute, found ", meta_kind(found), " attribute"].concat(),
+ )
+ }
+
+ /// Issue an error relating to a particular `Spanned` structure.
+ pub fn err(&self, spanned: &impl syn::spanned::Spanned, msg: &str) {
+ self.err_span(spanned.span(), msg);
+ }
+
+ /// Issue an error relating to a particular `Span`.
+ pub fn err_span(&self, span: Span, msg: &str) {
+ self.push(syn::Error::new(span, 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);
+ }
+}
+
+impl ToTokens for Errors {
+ /// Convert the errors into tokens that, when emit, will cause
+ /// the user of the macro to receive compiler errors.
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ tokens.extend(self.errors.borrow().iter().map(|e| e.to_compile_error()));
+ }
+}
diff --git a/src/help.rs b/src/help.rs
new file mode 100644
index 0000000..5bf02b1
--- /dev/null
+++ b/src/help.rs
@@ -0,0 +1,254 @@
+// Copyright (c) 2020 Google LLC All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+use {
+ crate::{
+ errors::Errors,
+ parse_attrs::{Description, FieldKind, TypeAttrs},
+ Optionality, StructField,
+ },
+ argh_shared::INDENT,
+ proc_macro2::{Span, TokenStream},
+ quote::quote,
+};
+
+const SECTION_SEPARATOR: &str = "\n\n";
+
+/// Returns a `TokenStream` generating a `String` help message.
+///
+/// Note: `fields` entries with `is_subcommand.is_some()` will be ignored
+/// in favor of the `subcommand` argument.
+pub(crate) fn help(
+ errors: &Errors,
+ cmd_name_str_array_ident: syn::Ident,
+ ty_attrs: &TypeAttrs,
+ fields: &[StructField<'_>],
+ subcommand: Option<&StructField<'_>>,
+) -> TokenStream {
+ let mut format_lit = "Usage: {command_name}".to_string();
+
+ let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional);
+ let mut has_positional = false;
+ for arg in positional.clone() {
+ has_positional = true;
+ format_lit.push(' ');
+ positional_usage(&mut format_lit, arg);
+ }
+
+ let options = fields.iter().filter(|f| f.long_name.is_some());
+ for option in options.clone() {
+ format_lit.push(' ');
+ option_usage(&mut format_lit, option);
+ }
+
+ if let Some(subcommand) = subcommand {
+ format_lit.push(' ');
+ if !subcommand.optionality.is_required() {
+ format_lit.push('[');
+ }
+ format_lit.push_str("<command>");
+ if !subcommand.optionality.is_required() {
+ format_lit.push(']');
+ }
+ format_lit.push_str(" [<args>]");
+ }
+
+ format_lit.push_str(SECTION_SEPARATOR);
+
+ let description = require_description(errors, Span::call_site(), &ty_attrs.description, "type");
+ format_lit.push_str(&description);
+
+ if has_positional {
+ format_lit.push_str(SECTION_SEPARATOR);
+ format_lit.push_str("Positional Arguments:");
+ for arg in positional {
+ positional_description(&mut format_lit, arg);
+ }
+ }
+
+ format_lit.push_str(SECTION_SEPARATOR);
+ format_lit.push_str("Options:");
+ for option in options {
+ option_description(errors, &mut format_lit, option);
+ }
+ // Also include "help"
+ option_description_format(&mut format_lit, None, "--help", "display usage information");
+
+ let subcommand_calculation;
+ let subcommand_format_arg;
+ if let Some(subcommand) = subcommand {
+ format_lit.push_str(SECTION_SEPARATOR);
+ format_lit.push_str("Commands:{subcommands}");
+ let subcommand_ty = subcommand.ty_without_wrapper;
+ subcommand_format_arg = quote! { subcommands = subcommands };
+ subcommand_calculation = quote! {
+ let subcommands = argh::print_subcommands(
+ <#subcommand_ty as argh::SubCommands>::COMMANDS
+ );
+ };
+ } else {
+ subcommand_calculation = TokenStream::new();
+ subcommand_format_arg = TokenStream::new()
+ }
+
+ lits_section(&mut format_lit, "Examples:", &ty_attrs.examples);
+
+ lits_section(&mut format_lit, "Notes:", &ty_attrs.notes);
+
+ if ty_attrs.error_codes.len() != 0 {
+ 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()));
+ }
+ }
+
+ format_lit.push_str("\n");
+
+ quote! { {
+ #subcommand_calculation
+ format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg)
+ } }
+}
+
+/// 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 {
+ out.push_str(SECTION_SEPARATOR);
+ out.push_str(heading);
+ for lit in lits {
+ let value = lit.value();
+ for line in value.split('\n') {
+ out.push('\n');
+ out.push_str(INDENT);
+ out.push_str(line);
+ }
+ }
+ }
+}
+
+/// Add positional arguments like `[<foo>...]` to a help format string.
+fn positional_usage(out: &mut String, field: &StructField<'_>) {
+ if !field.optionality.is_required() {
+ out.push('[');
+ }
+ out.push('<');
+ let name = field.arg_name();
+ out.push_str(&name);
+ if field.optionality == Optionality::Repeating {
+ out.push_str("...");
+ }
+ out.push('>');
+ if !field.optionality.is_required() {
+ out.push(']');
+ }
+}
+
+/// Add options like `[-f <foo>]` to a help format string.
+/// This function must only be called on options (things with `long_name.is_some()`)
+fn option_usage(out: &mut String, field: &StructField<'_>) {
+ // bookend with `[` and `]` if optional
+ if !field.optionality.is_required() {
+ out.push('[');
+ }
+
+ let long_name = field.long_name.as_ref().expect("missing long name for option");
+ if let Some(short) = field.attrs.short.as_ref() {
+ out.push('-');
+ out.push(short.value());
+ } else {
+ out.push_str(long_name);
+ }
+
+ match field.kind {
+ FieldKind::SubCommand | FieldKind::Positional => unreachable!(), // don't have long_name
+ FieldKind::Switch => {}
+ FieldKind::Option => {
+ out.push_str(" <");
+ if let Some(arg_name) = &field.attrs.arg_name {
+ out.push_str(&arg_name.value());
+ } else {
+ out.push_str(long_name.trim_start_matches("--"));
+ }
+ if field.optionality == Optionality::Repeating {
+ out.push_str("...");
+ }
+ out.push('>');
+ }
+ }
+
+ if !field.optionality.is_required() {
+ out.push(']');
+ }
+}
+
+// TODO(cramertj) make it so this is only called at least once per object so
+// as to avoid creating multiple errors.
+pub fn require_description(
+ errors: &Errors,
+ err_span: Span,
+ desc: &Option<Description>,
+ kind: &str, // the thing being described ("type" or "field"),
+) -> String {
+ desc.as_ref().map(|d| d.content.value().trim().to_owned()).unwrap_or_else(|| {
+ errors.err_span(
+ err_span,
+ &format!(
+ "#[derive(FromArgs)] {} with no description.
+Add a doc comment or an `#[argh(description = \"...\")]` attribute.",
+ kind
+ ),
+ );
+ "".to_string()
+ })
+}
+
+/// 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 mut description = String::from("");
+ if let Some(desc) = &field.attrs.description {
+ description = desc.content.value().trim().to_owned();
+ }
+ positional_description_format(out, &field_name, &description)
+}
+
+fn positional_description_format(out: &mut String, name: &str, description: &str) {
+ let info = argh_shared::CommandInfo { name: &*name, description };
+ argh_shared::write_description(out, &info);
+}
+
+/// Describes an option like this:
+/// -f, --force force, ignore minor errors. This description
+/// is so long that it wraps to the next line.
+fn option_description(errors: &Errors, out: &mut String, field: &StructField<'_>) {
+ let short = field.attrs.short.as_ref().map(|s| s.value());
+ let long_with_leading_dashes = field.long_name.as_ref().expect("missing long name for option");
+ let description =
+ require_description(errors, field.name.span(), &field.attrs.description, "field");
+
+ option_description_format(out, short, long_with_leading_dashes, &description)
+}
+
+fn option_description_format(
+ out: &mut String,
+ short: Option<char>,
+ long_with_leading_dashes: &str,
+ description: &str,
+) {
+ let mut name = String::new();
+ if let Some(short) = short {
+ name.push('-');
+ name.push(short);
+ name.push_str(", ");
+ }
+ name.push_str(long_with_leading_dashes);
+
+ let info = argh_shared::CommandInfo { name: &*name, description };
+ argh_shared::write_description(out, &info);
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..a123d4e
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,948 @@
+#![recursion_limit = "256"]
+// Copyright (c) 2020 Google LLC All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/// Implementation of the `FromArgs` and `argh(...)` derive attributes.
+///
+/// For more thorough documentation, see the `argh` crate itself.
+extern crate proc_macro;
+
+use {
+ crate::{
+ errors::Errors,
+ parse_attrs::{FieldAttrs, FieldKind, TypeAttrs},
+ },
+ proc_macro2::{Span, TokenStream},
+ quote::{quote, quote_spanned, ToTokens},
+ std::str::FromStr,
+ syn::{spanned::Spanned, LitStr},
+};
+
+mod errors;
+mod help;
+mod parse_attrs;
+
+/// Entrypoint for `#[derive(FromArgs)]`.
+#[proc_macro_derive(FromArgs, attributes(argh))]
+pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let ast = syn::parse_macro_input!(input as syn::DeriveInput);
+ let gen = impl_from_args(&ast);
+ gen.into()
+}
+
+/// Transform the input into a token stream containing any generated implementations,
+/// 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::Union(_) => {
+ errors.err(input, "`#[derive(FromArgs)]` cannot be applied to unions");
+ TokenStream::new()
+ }
+ };
+ errors.to_tokens(&mut output_tokens);
+ output_tokens
+}
+
+/// The kind of optionality a parameter has.
+enum Optionality {
+ None,
+ Defaulted(TokenStream),
+ Optional,
+ Repeating,
+}
+
+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,
+ }
+ }
+}
+
+impl Optionality {
+ /// Whether or not this is `Optionality::None`
+ fn is_required(&self) -> bool {
+ if let Optionality::None = self {
+ true
+ } else {
+ false
+ }
+ }
+}
+
+/// A field of a `#![derive(FromArgs)]` struct with attributes and some other
+/// notable metadata appended.
+struct StructField<'a> {
+ /// The original parsed field
+ field: &'a syn::Field,
+ /// The parsed attributes of the field
+ attrs: FieldAttrs,
+ /// The field name. This is contained optionally inside `field`,
+ /// but is duplicated non-optionally here to indicate that all field that
+ /// have reached this point must have a field name, and it no longer
+ /// needs to be unwrapped.
+ name: &'a syn::Ident,
+ /// Similar to `name` above, this is contained optionally inside `FieldAttrs`,
+ /// but here is fully present to indicate that we only have to consider fields
+ /// with a valid `kind` at this point.
+ kind: FieldKind,
+ // If `field.ty` is `Vec<T>` or `Option<T>`, this is `T`, otherwise it's `&field.ty`.
+ // This is used to enable consistent parsing code between optional and non-optional
+ // keyed and subcommand fields.
+ ty_without_wrapper: &'a syn::Type,
+ // Whether the field represents an optional value, such as an `Option` subcommand field
+ // or an `Option` or `Vec` keyed argument, or if it has a `default`.
+ optionality: Optionality,
+ // The `--`-prefixed name of the option, if one exists.
+ long_name: Option<String>,
+}
+
+impl<'a> StructField<'a> {
+ /// Attempts to parse a field of a `#[derive(FromArgs)]` struct, pulling out the
+ /// fields required for code generation.
+ fn new(errors: &Errors, field: &'a syn::Field, attrs: FieldAttrs) -> Option<Self> {
+ let name = field.ident.as_ref().expect("missing ident for named field");
+
+ // Ensure that one "kind" is present (switch, option, subcommand, positional)
+ let kind = if let Some(field_type) = &attrs.field_type {
+ field_type.kind
+ } else {
+ errors.err(
+ field,
+ concat!(
+ "Missing `argh` field kind attribute.\n",
+ "Expected one of: `switch`, `option`, `subcommand`, `positional`",
+ ),
+ );
+ return None;
+ };
+
+ // Parse out whether a field is optional (`Option` or `Vec`).
+ let optionality;
+ let ty_without_wrapper;
+ match kind {
+ FieldKind::Switch => {
+ if !ty_expect_switch(errors, &field.ty) {
+ return None;
+ }
+ optionality = Optionality::Optional;
+ ty_without_wrapper = &field.ty;
+ }
+ FieldKind::Option | FieldKind::Positional => {
+ if let Some(default) = &attrs.default {
+ let tokens = match TokenStream::from_str(&default.value()) {
+ Ok(tokens) => tokens,
+ Err(_) => {
+ errors.err(&default, "Invalid tokens: unable to lex `default` value");
+ return None;
+ }
+ };
+ // Set the span of the generated tokens to the string literal
+ let tokens: TokenStream = tokens
+ .into_iter()
+ .map(|mut tree| {
+ tree.set_span(default.span());
+ tree
+ })
+ .collect();
+ optionality = Optionality::Defaulted(tokens);
+ ty_without_wrapper = &field.ty;
+ } else {
+ let mut inner = None;
+ optionality = if let Some(x) = ty_inner(&["Option"], &field.ty) {
+ inner = Some(x);
+ Optionality::Optional
+ } else if let Some(x) = ty_inner(&["Vec"], &field.ty) {
+ inner = Some(x);
+ Optionality::Repeating
+ } else {
+ Optionality::None
+ };
+ ty_without_wrapper = inner.unwrap_or(&field.ty);
+ }
+ }
+ FieldKind::SubCommand => {
+ let inner = ty_inner(&["Option"], &field.ty);
+ optionality =
+ if inner.is_some() { Optionality::Optional } else { Optionality::None };
+ ty_without_wrapper = inner.unwrap_or(&field.ty);
+ }
+ }
+
+ // Determine the "long" name of options and switches.
+ // 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()));
+ if long_name == "help" {
+ errors.err(field, "Custom `--help` flags are not supported.");
+ }
+ let long_name = format!("--{}", long_name);
+ Some(long_name)
+ }
+ FieldKind::SubCommand | FieldKind::Positional => None,
+ };
+
+ 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())
+ }
+}
+
+/// Implements `FromArgs` and `TopLevelCommand` or `SubCommand` for a `#[derive(FromArgs)]` struct.
+fn impl_from_args_struct(
+ errors: &Errors,
+ name: &syn::Ident,
+ type_attrs: &TypeAttrs,
+ ds: &syn::DataStruct,
+) -> TokenStream {
+ let fields = match &ds.fields {
+ syn::Fields::Named(fields) => fields,
+ syn::Fields::Unnamed(_) => {
+ errors.err(
+ &ds.struct_token,
+ "`#![derive(FromArgs)]` is not currently supported on tuple structs",
+ );
+ return TokenStream::new();
+ }
+ syn::Fields::Unit => {
+ errors.err(&ds.struct_token, "#![derive(FromArgs)]` cannot be applied to unit structs");
+ return TokenStream::new();
+ }
+ };
+
+ let fields: Vec<_> = fields
+ .named
+ .iter()
+ .filter_map(|field| {
+ let attrs = FieldAttrs::parse(errors, field);
+ StructField::new(errors, field, attrs)
+ })
+ .collect();
+
+ ensure_only_last_positional_is_optional(errors, &fields);
+
+ let impl_span = Span::call_site();
+
+ let from_args_method = impl_from_args_struct_from_args(errors, type_attrs, &fields);
+
+ 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 trait_impl = quote_spanned! { impl_span =>
+ impl argh::FromArgs for #name {
+ #from_args_method
+
+ #redact_arg_values_method
+ }
+
+ #top_or_sub_cmd_impl
+ };
+
+ trait_impl
+}
+
+fn impl_from_args_struct_from_args<'a>(
+ errors: &Errors,
+ 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 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);
+ let positional_field_names = positional_fields.iter().map(|field| field.name.to_string());
+ let last_positional_is_repeating = positional_fields
+ .last()
+ .map(|field| field.optionality == Optionality::Repeating)
+ .unwrap_or(false);
+
+ let flag_output_table = fields.iter().filter_map(|field| {
+ let field_name = &field.field.ident;
+ match field.kind {
+ FieldKind::Option => Some(quote! { argh::ParseStructOption::Value(&mut #field_name) }),
+ FieldKind::Switch => Some(quote! { argh::ParseStructOption::Flag(&mut #field_name) }),
+ FieldKind::SubCommand | FieldKind::Positional => None,
+ }
+ });
+
+ 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() {
+ errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field);
+ }
+
+ let impl_span = Span::call_site();
+
+ let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span);
+
+ let append_missing_requirements =
+ append_missing_requirements(&missing_requirements_ident, &fields);
+
+ let parse_subcommands = if let Some(subcommand) = subcommand {
+ let name = subcommand.name;
+ let ty = subcommand.ty_without_wrapper;
+ quote_spanned! { impl_span =>
+ Some(argh::ParseStructSubCommand {
+ subcommands: <#ty as argh::SubCommands>::COMMANDS,
+ parse_func: &mut |__command, __remaining_args| {
+ #name = Some(<#ty as argh::FromArgs>::from_args(__command, __remaining_args)?);
+ Ok(())
+ },
+ })
+ }
+ } else {
+ quote_spanned! { impl_span => None }
+ };
+
+ // 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 method_impl = quote_spanned! { impl_span =>
+ fn from_args(__cmd_name: &[&str], __args: &[&str])
+ -> std::result::Result<Self, argh::EarlyExit>
+ {
+ #( #init_fields )*
+
+ argh::parse_struct_args(
+ __cmd_name,
+ __args,
+ argh::ParseStructOptions {
+ arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ],
+ slots: &mut [ #( #flag_output_table, )* ],
+ },
+ argh::ParseStructPositionals {
+ positionals: &mut [
+ #(
+ argh::ParseStructPositional {
+ name: #positional_field_names,
+ slot: &mut #positional_field_idents as &mut argh::ParseValueSlot,
+ },
+ )*
+ ],
+ last_is_repeating: #last_positional_is_repeating,
+ },
+ #parse_subcommands,
+ &|| #help,
+ )?;
+
+ let mut #missing_requirements_ident = argh::MissingRequirements::default();
+ #(
+ #append_missing_requirements
+ )*
+ #missing_requirements_ident.err_on_any()?;
+
+ Ok(Self {
+ #( #unwrap_fields, )*
+ })
+ }
+ };
+
+ method_impl
+}
+
+fn impl_from_args_struct_redact_arg_values<'a>(
+ errors: &Errors,
+ 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 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);
+ let positional_field_names = positional_fields.iter().map(|field| field.name.to_string());
+ let last_positional_is_repeating = positional_fields
+ .last()
+ .map(|field| field.optionality == Optionality::Repeating)
+ .unwrap_or(false);
+
+ let flag_output_table = fields.iter().filter_map(|field| {
+ let field_name = &field.field.ident;
+ match field.kind {
+ FieldKind::Option => Some(quote! { argh::ParseStructOption::Value(&mut #field_name) }),
+ FieldKind::Switch => Some(quote! { argh::ParseStructOption::Flag(&mut #field_name) }),
+ FieldKind::SubCommand | FieldKind::Positional => None,
+ }
+ });
+
+ 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() {
+ errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field);
+ }
+
+ let impl_span = Span::call_site();
+
+ let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span);
+
+ let append_missing_requirements =
+ append_missing_requirements(&missing_requirements_ident, &fields);
+
+ let redact_subcommands = if let Some(subcommand) = subcommand {
+ let name = subcommand.name;
+ let ty = subcommand.ty_without_wrapper;
+ quote_spanned! { impl_span =>
+ Some(argh::ParseStructSubCommand {
+ subcommands: <#ty as argh::SubCommands>::COMMANDS,
+ parse_func: &mut |__command, __remaining_args| {
+ #name = Some(<#ty as argh::FromArgs>::redact_arg_values(__command, __remaining_args)?);
+ Ok(())
+ },
+ })
+ }
+ } else {
+ 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() }
+ } else {
+ quote! { __cmd_name.last().expect("no subcommand name").to_string() }
+ };
+
+ // 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 method_impl = quote_spanned! { impl_span =>
+ fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> {
+ #( #init_fields )*
+
+ argh::parse_struct_args(
+ __cmd_name,
+ __args,
+ argh::ParseStructOptions {
+ arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ],
+ slots: &mut [ #( #flag_output_table, )* ],
+ },
+ argh::ParseStructPositionals {
+ positionals: &mut [
+ #(
+ argh::ParseStructPositional {
+ name: #positional_field_names,
+ slot: &mut #positional_field_idents as &mut argh::ParseValueSlot,
+ },
+ )*
+ ],
+ last_is_repeating: #last_positional_is_repeating,
+ },
+ #redact_subcommands,
+ &|| #help,
+ )?;
+
+ let mut #missing_requirements_ident = argh::MissingRequirements::default();
+ #(
+ #append_missing_requirements
+ )*
+ #missing_requirements_ident.err_on_any()?;
+
+ let mut __redacted = vec![
+ #cmd_name,
+ ];
+
+ #( #unwrap_fields )*
+
+ Ok(__redacted)
+ }
+ };
+
+ method_impl
+}
+
+/// Ensures that only the last positional arg is non-required.
+fn ensure_only_last_positional_is_optional(errors: &Errors, fields: &[StructField<'_>]) {
+ let mut first_non_required_span = None;
+ for field in fields {
+ if field.kind == FieldKind::Positional {
+ if let Some(first) = first_non_required_span {
+ errors.err_span(
+ first,
+ "Only the last positional argument may be `Option`, `Vec`, or defaulted.",
+ );
+ errors.err(&field.field, "Later positional argument declared here.");
+ return;
+ }
+ if !field.optionality.is_required() {
+ first_non_required_span = Some(field.field.span());
+ }
+ }
+ }
+}
+
+/// Implement `argh::TopLevelCommand` or `argh::SubCommand` as appropriate.
+fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs) -> TokenStream {
+ let description =
+ help::require_description(errors, name.span(), &type_attrs.description, "type");
+ if type_attrs.is_subcommand.is_none() {
+ // Not a subcommand
+ quote! {
+ impl argh::TopLevelCommand for #name {}
+ }
+ } else {
+ let empty_str = syn::LitStr::new("", Span::call_site());
+ let subcommand_name = type_attrs.name.as_ref().unwrap_or_else(|| {
+ errors.err(name, "`#[argh(name = \"...\")]` attribute is required for subcommands");
+ &empty_str
+ });
+ quote! {
+ impl argh::SubCommand for #name {
+ const COMMAND: &'static argh::CommandInfo = &argh::CommandInfo {
+ name: #subcommand_name,
+ description: #description,
+ };
+ }
+ }
+ }
+}
+
+/// Declare a local slots to store each field in during parsing.
+///
+/// Most fields are stored in `Option<FieldType>` locals.
+/// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a
+/// function that knows how to decode the appropriate value.
+fn declare_local_storage_for_from_args_fields<'a>(
+ fields: &'a [StructField<'a>],
+) -> impl Iterator<Item = TokenStream> + 'a {
+ fields.iter().map(|field| {
+ let field_name = &field.field.ident;
+ let field_type = &field.ty_without_wrapper;
+
+ // Wrap field types in `Option` if they aren't already `Option` or `Vec`-wrapped.
+ let field_slot_type = match field.optionality {
+ Optionality::Optional | Optionality::Repeating => (&field.field.ty).into_token_stream(),
+ Optionality::None | Optionality::Defaulted(_) => {
+ quote! { std::option::Option<#field_type> }
+ }
+ };
+
+ match field.kind {
+ FieldKind::Option | FieldKind::Positional => {
+ let from_str_fn = match &field.attrs.from_str_fn {
+ Some(from_str_fn) => from_str_fn.into_token_stream(),
+ None => {
+ quote! {
+ <#field_type as argh::FromArgValue>::from_arg_value
+ }
+ }
+ };
+
+ quote! {
+ let mut #field_name: argh::ParseValueSlotTy<#field_slot_type, #field_type>
+ = argh::ParseValueSlotTy {
+ slot: std::default::Default::default(),
+ parse_func: |_, value| { #from_str_fn(value) },
+ };
+ }
+ }
+ FieldKind::SubCommand => {
+ quote! { let mut #field_name: #field_slot_type = None; }
+ }
+ FieldKind::Switch => {
+ quote! { let mut #field_name: #field_slot_type = argh::Flag::default(); }
+ }
+ }
+ })
+}
+
+/// Unwrap non-optional fields and take options out of their tuple slots.
+fn unwrap_from_args_fields<'a>(
+ fields: &'a [StructField<'a>],
+) -> impl Iterator<Item = TokenStream> + 'a {
+ fields.iter().map(|field| {
+ 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::Optional | Optionality::Repeating => {
+ quote! { #field_name: #field_name.slot }
+ }
+ Optionality::Defaulted(tokens) => {
+ quote! {
+ #field_name: #field_name.slot.unwrap_or_else(|| #tokens)
+ }
+ }
+ },
+ FieldKind::Switch => field_name.into_token_stream(),
+ FieldKind::SubCommand => match field.optionality {
+ Optionality::None => quote! { #field_name: #field_name.unwrap() },
+ Optionality::Optional | Optionality::Repeating => field_name.into_token_stream(),
+ Optionality::Defaulted(_) => unreachable!(),
+ },
+ }
+ })
+}
+
+/// Declare a local slots to store each field in during parsing.
+///
+/// Most fields are stored in `Option<FieldType>` locals.
+/// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a
+/// function that knows how to decode the appropriate value.
+fn declare_local_storage_for_redacted_fields<'a>(
+ fields: &'a [StructField<'a>],
+) -> impl Iterator<Item = TokenStream> + 'a {
+ fields.iter().map(|field| {
+ let field_name = &field.field.ident;
+
+ match field.kind {
+ FieldKind::Switch => {
+ quote! {
+ let mut #field_name = argh::RedactFlag {
+ slot: None,
+ };
+ }
+ }
+ FieldKind::Option => {
+ let field_slot_type = match field.optionality {
+ Optionality::Repeating => {
+ quote! { std::vec::Vec<String> }
+ }
+ Optionality::None | Optionality::Optional | Optionality::Defaulted(_) => {
+ quote! { std::option::Option<String> }
+ }
+ };
+
+ quote! {
+ let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> =
+ argh::ParseValueSlotTy {
+ slot: std::default::Default::default(),
+ parse_func: |arg, _| { Ok(arg.to_string()) },
+ };
+ }
+ }
+ FieldKind::Positional => {
+ let field_slot_type = match field.optionality {
+ Optionality::Repeating => {
+ quote! { std::vec::Vec<String> }
+ }
+ Optionality::None | Optionality::Optional | Optionality::Defaulted(_) => {
+ quote! { std::option::Option<String> }
+ }
+ };
+
+ let arg_name = field.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()) },
+ };
+ }
+ }
+ FieldKind::SubCommand => {
+ quote! { let mut #field_name: std::option::Option<std::vec::Vec<String>> = None; }
+ }
+ }
+ })
+}
+
+/// Unwrap non-optional fields and take options out of their tuple slots.
+fn unwrap_redacted_fields<'a>(
+ fields: &'a [StructField<'a>],
+) -> impl Iterator<Item = TokenStream> + 'a {
+ fields.iter().map(|field| {
+ let field_name = field.name;
+
+ match field.kind {
+ FieldKind::Switch => {
+ quote! {
+ if let Some(__field_name) = #field_name.slot {
+ __redacted.push(__field_name);
+ }
+ }
+ }
+ FieldKind::Option => match field.optionality {
+ Optionality::Repeating => {
+ quote! {
+ __redacted.extend(#field_name.slot.into_iter());
+ }
+ }
+ Optionality::None | Optionality::Optional | Optionality::Defaulted(_) => {
+ quote! {
+ if let Some(__field_name) = #field_name.slot {
+ __redacted.push(__field_name);
+ }
+ }
+ }
+ },
+ FieldKind::Positional => {
+ quote! {
+ __redacted.extend(#field_name.slot.into_iter());
+ }
+ }
+ FieldKind::SubCommand => {
+ quote! {
+ if let Some(__subcommand_args) = #field_name {
+ __redacted.extend(__subcommand_args.into_iter());
+ }
+ }
+ }
+ }
+ })
+}
+
+/// Entries of tokens like `("--some-flag-key", 5)` that map from a flag key string
+/// to an index in the output table.
+fn flag_str_to_output_table_map_entries<'a>(fields: &'a [StructField<'a>]) -> Vec<TokenStream> {
+ let mut flag_str_to_output_table_map = vec![];
+ for (i, (field, long_name)) in fields
+ .iter()
+ .filter_map(|field| field.long_name.as_ref().map(|long_name| (field, long_name)))
+ .enumerate()
+ {
+ if let Some(short) = &field.attrs.short {
+ let short = format!("-{}", short.value());
+ flag_str_to_output_table_map.push(quote! { (#short, #i) });
+ }
+
+ flag_str_to_output_table_map.push(quote! { (#long_name, #i) });
+ }
+ flag_str_to_output_table_map
+}
+
+/// For each non-optional field, add an entry to the `argh::MissingRequirements`.
+fn append_missing_requirements<'a>(
+ // missing_requirements_ident
+ mri: &syn::Ident,
+ fields: &'a [StructField<'a>],
+) -> impl Iterator<Item = TokenStream> + 'a {
+ let mri = mri.clone();
+ fields.iter().filter(|f| f.optionality.is_required()).map(move |field| {
+ let field_name = field.name;
+ match field.kind {
+ FieldKind::Switch => unreachable!("switches are always optional"),
+ FieldKind::Positional => {
+ let name = field.arg_name();
+ quote! {
+ if #field_name.slot.is_none() {
+ #mri.missing_positional_arg(#name)
+ }
+ }
+ }
+ FieldKind::Option => {
+ let name = field.long_name.as_ref().expect("options always have a long name");
+ quote! {
+ if #field_name.slot.is_none() {
+ #mri.missing_option(#name)
+ }
+ }
+ }
+ FieldKind::SubCommand => {
+ let ty = field.ty_without_wrapper;
+ quote! {
+ if #field_name.is_none() {
+ #mri.missing_subcommands(
+ <#ty as argh::SubCommands>::COMMANDS,
+ )
+ }
+ }
+ }
+ }
+ })
+}
+
+/// Require that a type can be a `switch`.
+/// Throws an error for all types except booleans and integers
+fn ty_expect_switch(errors: &Errors, ty: &syn::Type) -> bool {
+ fn ty_can_be_switch(ty: &syn::Type) -> bool {
+ if let syn::Type::Path(path) = ty {
+ if path.qself.is_some() {
+ return false;
+ }
+ if path.path.segments.len() != 1 {
+ return false;
+ }
+ let ident = &path.path.segments[0].ident;
+ ["bool", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"]
+ .iter()
+ .any(|path| ident == path)
+ } else {
+ false
+ }
+ }
+
+ let res = ty_can_be_switch(ty);
+ if !res {
+ errors.err(ty, "switches must be of type `bool` or integer type");
+ }
+ res
+}
+
+/// Returns `Some(T)` if a type is `wrapper_name<T>` for any `wrapper_name` in `wrapper_names`.
+fn ty_inner<'a>(wrapper_names: &[&str], ty: &'a syn::Type) -> Option<&'a syn::Type> {
+ if let syn::Type::Path(path) = ty {
+ if path.qself.is_some() {
+ return None;
+ }
+ // Since we only check the last path segment, it isn't necessarily the case that
+ // we're referring to `std::vec::Vec` or `std::option::Option`, but there isn't
+ // a fool proof way to check these since name resolution happens after macro expansion,
+ // so this is likely "good enough" (so long as people don't have their own types called
+ // `Option` or `Vec` that take one generic parameter they're looking to parse).
+ let last_segment = path.path.segments.last()?;
+ if !wrapper_names.iter().any(|name| last_segment.ident == *name) {
+ return None;
+ }
+ if let syn::PathArguments::AngleBracketed(gen_args) = &last_segment.arguments {
+ let generic_arg = gen_args.args.first()?;
+ if let syn::GenericArgument::Type(ty) = &generic_arg {
+ return Some(ty);
+ }
+ }
+ }
+ None
+}
+
+/// Implements `FromArgs` and `SubCommands` for a `#![derive(FromArgs)]` enum.
+fn impl_from_args_enum(
+ errors: &Errors,
+ name: &syn::Ident,
+ type_attrs: &TypeAttrs,
+ de: &syn::DataEnum,
+) -> TokenStream {
+ parse_attrs::check_enum_type_attrs(errors, type_attrs, &de.enum_token.span);
+
+ // An enum variant like `<name>(<ty>)`
+ struct SubCommandVariant<'a> {
+ name: &'a syn::Ident,
+ ty: &'a syn::Type,
+ }
+
+ 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 })
+ })
+ .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<_>>();
+
+ quote! {
+ impl argh::FromArgs for #name {
+ fn from_args(command_name: &[&str], args: &[&str])
+ -> std::result::Result<Self, argh::EarlyExit>
+ {
+ let subcommand_name = *command_name.last().expect("no subcommand name");
+ #(
+ if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name {
+ return Ok(#name_repeating::#variant_names(
+ <#variant_ty as argh::FromArgs>::from_args(command_name, args)?
+ ));
+ }
+ )*
+ unreachable!("no subcommand matched")
+ }
+
+ 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");
+ #(
+ 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")
+ }
+ }
+
+ impl argh::SubCommands for #name {
+ const COMMANDS: &'static [&'static argh::CommandInfo] = &[#(
+ <#variant_ty as argh::SubCommand>::COMMAND,
+ )*];
+ }
+ }
+}
+
+/// Returns `Some(Bar)` if the field is a single-field unnamed variant like `Foo(Bar)`.
+/// Otherwise, generates an error.
+fn enum_only_single_field_unnamed_variants<'a>(
+ errors: &Errors,
+ variant_fields: &'a syn::Fields,
+) -> Option<&'a syn::Type> {
+ macro_rules! with_enum_suggestion {
+ ($help_text:literal) => {
+ concat!(
+ $help_text,
+ "\nInstead, use a variant with a single unnamed field for each subcommand:\n",
+ " enum MyCommandEnum {\n",
+ " SubCommandOne(SubCommandOne),\n",
+ " SubCommandTwo(SubCommandTwo),\n",
+ " }",
+ )
+ };
+ }
+
+ match variant_fields {
+ syn::Fields::Named(fields) => {
+ errors.err(
+ fields,
+ with_enum_suggestion!(
+ "`#![derive(FromArgs)]` `enum`s do not support variants with named fields."
+ ),
+ );
+ None
+ }
+ syn::Fields::Unit => {
+ errors.err(
+ variant_fields,
+ with_enum_suggestion!(
+ "`#![derive(FromArgs)]` does not support `enum`s with no variants."
+ ),
+ );
+ None
+ }
+ syn::Fields::Unnamed(fields) => {
+ if fields.unnamed.len() != 1 {
+ errors.err(
+ fields,
+ with_enum_suggestion!(
+ "`#![derive(FromArgs)]` `enum` variants must only contain one field."
+ ),
+ );
+ None
+ } else {
+ // `unwrap` is okay because of the length check above.
+ let first_field = fields.unnamed.first().unwrap();
+ Some(&first_field.ty)
+ }
+ }
+ }
+}
diff --git a/src/parse_attrs.rs b/src/parse_attrs.rs
new file mode 100644
index 0000000..83807ed
--- /dev/null
+++ b/src/parse_attrs.rs
@@ -0,0 +1,561 @@
+// Copyright (c) 2020 Google LLC All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+use {
+ crate::errors::Errors,
+ proc_macro2::Span,
+ std::collections::hash_map::{Entry, HashMap},
+};
+
+/// Attributes applied to a field of a `#![derive(FromArgs)]` struct.
+#[derive(Default)]
+pub struct FieldAttrs {
+ pub default: Option<syn::LitStr>,
+ pub description: Option<Description>,
+ pub from_str_fn: Option<syn::Ident>,
+ pub field_type: Option<FieldType>,
+ pub long: Option<syn::LitStr>,
+ pub short: Option<syn::LitChar>,
+ pub arg_name: Option<syn::LitStr>,
+}
+
+/// The purpose of a particular field on a `#![derive(FromArgs)]` struct.
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum FieldKind {
+ /// Switches are booleans that are set to "true" by passing the flag.
+ Switch,
+ /// Options are `--key value`. They may be optional (using `Option`),
+ /// or repeating (using `Vec`), or required (neither `Option` nor `Vec`)
+ Option,
+ /// Subcommand fields (of which there can be at most one) refer to enums
+ /// containing one of several potential subcommands. They may be optional
+ /// (using `Option`) or required (no `Option`).
+ SubCommand,
+ /// Positional arguments are parsed literally if the input
+ /// does not begin with `-` or `--` and is not a subcommand.
+ /// They are parsed in declaration order, and only the last positional
+ /// argument in a type may be an `Option`, `Vec`, or have a default value.
+ Positional,
+}
+
+/// The type of a field on a `#![derive(FromArgs)]` struct.
+///
+/// This is a simple wrapper around `FieldKind` which includes the `syn::Ident`
+/// of the attribute containing the field kind.
+pub struct FieldType {
+ pub kind: FieldKind,
+ pub ident: syn::Ident,
+}
+
+/// A description of a `#![derive(FromArgs)]` struct.
+///
+/// Defaults to the docstring if one is present, or `#[argh(description = "...")]`
+/// if one is provided.
+pub struct Description {
+ /// Whether the description was an explicit annotation or whether it was a doc string.
+ pub explicit: bool,
+ pub content: syn::LitStr,
+}
+
+impl FieldAttrs {
+ pub fn parse(errors: &Errors, field: &syn::Field) -> Self {
+ let mut this = Self::default();
+
+ for attr in &field.attrs {
+ if is_doc_attr(attr) {
+ parse_attr_doc(errors, attr, &mut this.description);
+ continue;
+ }
+
+ 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("arg_name") {
+ 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) {
+ this.parse_attr_default(errors, m);
+ }
+ } else if name.is_ident("description") {
+ 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) {
+ this.parse_attr_from_str_fn(errors, m);
+ }
+ } else if name.is_ident("long") {
+ 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) {
+ this.parse_attr_short(errors, m);
+ }
+ } else if name.is_ident("subcommand") {
+ parse_attr_field_type(
+ errors,
+ meta,
+ FieldKind::SubCommand,
+ &mut this.field_type,
+ );
+ } else if name.is_ident("switch") {
+ parse_attr_field_type(errors, meta, FieldKind::Switch, &mut this.field_type);
+ } else if name.is_ident("positional") {
+ parse_attr_field_type(
+ errors,
+ meta,
+ FieldKind::Positional,
+ &mut this.field_type,
+ );
+ } 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`",
+ ),
+ );
+ }
+ }
+ }
+
+ if let (Some(default), Some(field_type)) = (&this.default, &this.field_type) {
+ match field_type.kind {
+ FieldKind::Option | FieldKind::Positional => {}
+ FieldKind::SubCommand | FieldKind::Switch => errors.err(
+ default,
+ "`default` may only be specified on `#[argh(option)]` \
+ or `#[argh(positional)]` fields",
+ ),
+ }
+ }
+
+ if let Some(d) = &this.description {
+ check_option_description(errors, d.content.value().trim(), d.content.span());
+ }
+
+ this
+ }
+
+ fn parse_attr_from_str_fn(&mut self, errors: &Errors, m: &syn::MetaList) {
+ parse_attr_fn_name(errors, m, "from_str_fn", &mut self.from_str_fn)
+ }
+
+ fn parse_attr_default(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ parse_attr_single_string(errors, m, "default", &mut self.default);
+ }
+
+ fn parse_attr_arg_name(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ parse_attr_single_string(errors, m, "arg_name", &mut self.arg_name);
+ }
+
+ fn parse_attr_long(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ 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");
+ }
+ }
+
+ fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ if let Some(first) = &self.short {
+ errors.duplicate_attrs("short", first, m);
+ } else if let Some(lit_char) = errors.expect_lit_char(&m.lit) {
+ self.short = Some(lit_char.clone());
+ if !lit_char.value().is_ascii() {
+ errors.err(lit_char, "Short names must be ASCII");
+ }
+ }
+ }
+}
+
+fn parse_attr_fn_name(
+ errors: &Errors,
+ m: &syn::MetaList,
+ attr_name: &str,
+ slot: &mut Option<syn::Ident>,
+) {
+ if let Some(first) = slot {
+ errors.duplicate_attrs(attr_name, first, m);
+ }
+
+ if m.nested.len() != 1 {
+ errors.err(&m.nested, "Expected a single argument containing the function name");
+ return;
+ }
+
+ // `unwrap` will not fail because of the call above
+ let nested = m.nested.first().unwrap();
+ if let Some(path) = errors.expect_nested_meta(nested).and_then(|m| errors.expect_meta_word(m)) {
+ *slot = path.get_ident().cloned();
+ }
+}
+
+fn parse_attr_field_type(
+ errors: &Errors,
+ meta: &syn::Meta,
+ kind: FieldKind,
+ slot: &mut Option<FieldType>,
+) {
+ 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() });
+ }
+ }
+ }
+}
+
+// Whether the attribute is one like `#[<name> ...]`
+fn is_matching_attr(name: &str, attr: &syn::Attribute) -> bool {
+ attr.path.segments.len() == 1 && attr.path.segments[0].ident == name
+}
+
+/// Checks for `#[doc ...]`, which is generated by doc comments.
+fn is_doc_attr(attr: &syn::Attribute) -> bool {
+ is_matching_attr("doc", attr)
+}
+
+/// Checks for `#[argh ...]`
+fn is_argh_attr(attr: &syn::Attribute) -> bool {
+ is_matching_attr("argh", attr)
+}
+
+fn attr_to_meta_subtype<R: Clone>(
+ errors: &Errors,
+ attr: &syn::Attribute,
+ f: impl FnOnce(&syn::Meta) -> Option<&R>,
+) -> Option<R> {
+ match attr.parse_meta() {
+ Ok(meta) => f(&meta).cloned(),
+ Err(e) => {
+ errors.push(e);
+ None
+ }
+ }
+}
+
+fn attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList> {
+ attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_list(m))
+}
+
+fn attr_to_meta_name_value(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaNameValue> {
+ attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_name_value(m))
+}
+
+/// Filters out non-`#[argh(...)]` attributes and converts to `syn::MetaList`.
+fn argh_attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList> {
+ if !is_argh_attr(attr) {
+ return None;
+ }
+ attr_to_meta_list(errors, attr)
+}
+
+/// Represents a `#[derive(FromArgs)]` type's top-level attributes.
+#[derive(Default)]
+pub struct TypeAttrs {
+ pub is_subcommand: Option<syn::Ident>,
+ pub name: Option<syn::LitStr>,
+ pub description: Option<Description>,
+ pub examples: Vec<syn::LitStr>,
+ pub notes: Vec<syn::LitStr>,
+ pub error_codes: Vec<(syn::LitInt, syn::LitStr)>,
+}
+
+impl TypeAttrs {
+ /// Parse top-level `#[argh(...)]` attributes
+ pub fn parse(errors: &Errors, derive_input: &syn::DeriveInput) -> Self {
+ let mut this = TypeAttrs::default();
+
+ for attr in &derive_input.attrs {
+ if is_doc_attr(attr) {
+ parse_attr_doc(errors, attr, &mut this.description);
+ continue;
+ }
+
+ 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("description") {
+ 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) {
+ this.parse_attr_error_code(errors, m);
+ }
+ } else if name.is_ident("example") {
+ 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) {
+ this.parse_attr_name(errors, m);
+ }
+ } else if name.is_ident("note") {
+ 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())
+ {
+ this.parse_attr_subcommand(errors, ident);
+ }
+ } else {
+ errors.err(
+ &meta,
+ concat!(
+ "Invalid type-level `argh` attribute\n",
+ "Expected one of: `description`, `error_code`, `example`, `name`, ",
+ "`note`, `subcommand`",
+ ),
+ );
+ }
+ }
+ }
+
+ this.check_error_codes(errors);
+ this
+ }
+
+ /// Checks that error codes are within range for `i32` and that they are
+ /// never duplicated.
+ fn check_error_codes(&self, errors: &Errors) {
+ // map from error code to index
+ let mut map: HashMap<u64, usize> = HashMap::new();
+ for (index, (lit_int, _lit_str)) in self.error_codes.iter().enumerate() {
+ let value = match lit_int.base10_parse::<u64>() {
+ Ok(v) => v,
+ Err(e) => {
+ errors.push(e);
+ continue;
+ }
+ };
+ if value > (std::i32::MAX as u64) {
+ errors.err(lit_int, "Error code out of range for `i32`");
+ }
+ match map.entry(value) {
+ Entry::Occupied(previous) => {
+ let previous_index = *previous.get();
+ let (previous_lit_int, _previous_lit_str) = &self.error_codes[previous_index];
+ errors.err(lit_int, &format!("Duplicate error code {}", value));
+ errors.err(
+ previous_lit_int,
+ &format!("Error code {} previously defined here", value),
+ );
+ }
+ Entry::Vacant(slot) => {
+ slot.insert(index);
+ }
+ }
+ }
+ }
+
+ fn parse_attr_error_code(&mut self, errors: &Errors, ml: &syn::MetaList) {
+ if ml.nested.len() != 2 {
+ errors.err(&ml, "Expected two arguments, an error number and a string description");
+ return;
+ }
+
+ let err_code = &ml.nested[0];
+ let err_msg = &ml.nested[1];
+
+ let err_code = errors.expect_nested_lit(err_code).and_then(|l| errors.expect_lit_int(l));
+ let err_msg = errors.expect_nested_lit(err_msg).and_then(|l| errors.expect_lit_str(l));
+
+ if let (Some(err_code), Some(err_msg)) = (err_code, err_msg) {
+ self.error_codes.push((err_code.clone(), err_msg.clone()));
+ }
+ }
+
+ fn parse_attr_example(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ parse_attr_multi_string(errors, m, &mut self.examples)
+ }
+
+ fn parse_attr_name(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ parse_attr_single_string(errors, m, "name", &mut self.name);
+ if let Some(name) = &self.name {
+ if name.value() == "help" {
+ errors.err(name, "Custom `help` commands are not supported.");
+ }
+ }
+ }
+
+ fn parse_attr_note(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
+ parse_attr_multi_string(errors, m, &mut self.notes)
+ }
+
+ fn parse_attr_subcommand(&mut self, errors: &Errors, ident: &syn::Ident) {
+ if let Some(first) = &self.is_subcommand {
+ errors.duplicate_attrs("subcommand", first, ident);
+ } else {
+ self.is_subcommand = Some(ident.clone());
+ }
+ }
+}
+
+fn check_option_description(errors: &Errors, desc: &str, span: Span) {
+ let chars = &mut desc.trim().chars();
+ match (chars.next(), chars.next()) {
+ (Some(x), _) if x.is_lowercase() => {}
+ // If both the first and second letter are not lowercase,
+ // this is likely an initialism which should be allowed.
+ (Some(x), Some(y)) if !x.is_lowercase() && !y.is_lowercase() => {}
+ _ => {
+ errors.err_span(span, "Descriptions must begin with a lowercase letter");
+ }
+ }
+}
+
+fn parse_attr_single_string(
+ errors: &Errors,
+ m: &syn::MetaNameValue,
+ name: &str,
+ slot: &mut Option<syn::LitStr>,
+) {
+ if let Some(first) = slot {
+ errors.duplicate_attrs(name, first, m);
+ } else if let Some(lit_str) = errors.expect_lit_str(&m.lit) {
+ *slot = Some(lit_str.clone());
+ }
+}
+
+fn parse_attr_multi_string(errors: &Errors, m: &syn::MetaNameValue, list: &mut Vec<syn::LitStr>) {
+ if let Some(lit_str) = errors.expect_lit_str(&m.lit) {
+ list.push(lit_str.clone());
+ }
+}
+
+fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Description>) {
+ let nv = if let Some(nv) = attr_to_meta_name_value(errors, attr) {
+ nv
+ } else {
+ return;
+ };
+
+ // Don't replace an existing description.
+ if slot.as_ref().map(|d| d.explicit).unwrap_or(false) {
+ return;
+ }
+
+ if let Some(lit_str) = errors.expect_lit_str(&nv.lit) {
+ let lit_str = if let Some(previous) = slot {
+ let previous = &previous.content;
+ let previous_span = previous.span();
+ syn::LitStr::new(&(previous.value() + &*lit_str.value()), previous_span)
+ } else {
+ lit_str.clone()
+ };
+ *slot = Some(Description { explicit: false, content: lit_str });
+ }
+}
+
+fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Option<Description>) {
+ let lit_str = if let Some(lit_str) = errors.expect_lit_str(&m.lit) { lit_str } else { return };
+
+ // Don't allow multiple explicit (non doc-comment) descriptions
+ if let Some(description) = slot {
+ if description.explicit {
+ errors.duplicate_attrs("description", &description.content, lit_str);
+ }
+ }
+
+ *slot = Some(Description { explicit: true, content: lit_str.clone() });
+}
+
+/// Checks that a `#![derive(FromArgs)]` enum has an `#[argh(subcommand)]`
+/// attribute and that it does not have any other type-level `#[argh(...)]` attributes.
+pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span) {
+ let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes } = type_attrs;
+
+ // Ensure that `#[argh(subcommand)]` is present.
+ if is_subcommand.is_none() {
+ errors.err_span(
+ type_span.clone(),
+ concat!(
+ "`#![derive(FromArgs)]` on `enum`s can only be used to enumerate subcommands.\n",
+ "Consider adding `#[argh(subcommand)]` to the `enum` declaration.",
+ ),
+ );
+ }
+
+ // Error on all other type-level attributes.
+ if let Some(name) = name {
+ err_unused_enum_attr(errors, name);
+ }
+ if let Some(description) = description {
+ if description.explicit {
+ err_unused_enum_attr(errors, &description.content);
+ }
+ }
+ if let Some(example) = examples.first() {
+ err_unused_enum_attr(errors, example);
+ }
+ if let Some(note) = notes.first() {
+ err_unused_enum_attr(errors, note);
+ }
+ if let Some(err_code) = error_codes.first() {
+ err_unused_enum_attr(errors, &err_code.0);
+ }
+}
+
+/// 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,
+ concat!(
+ "Unused `argh` attribute on `#![derive(FromArgs)]` enum. ",
+ "Such `enum`s can only be used to dispatch to subcommands, ",
+ "and should only contain the #[argh(subcommand)] attribute.",
+ ),
+ );
+}