Skip to content

Commit

Permalink
fix(derive)!: Compile-error on nested subcommands
Browse files Browse the repository at this point in the history
Before, partial command lines would panic at runtime.  Now it'll be a
compile error

For example:
```
pub enum Opt {
  Daemon(DaemonCommand),
}

pub enum DaemonCommand {
  Start,
  Stop,
}
```

Gives:
```
error[E0277]: the trait bound `DaemonCommand: clap::Args` is not satisfied
   --> clap_derive/tests/subcommands.rs:297:16
    |
297 |         Daemon(DaemonCommand),
    |                ^^^^^^^^^^^^^ the trait `clap::Args` is not implemented for `DaemonCommand`
    |
    = note: required by `augment_args`
```

To nest this, you currently need `enum -> struct -> enum`.  A later
change will make it so you can use the `subcommand` attribute within
enums to cover this case.

This is a part of clap-rs#2005
  • Loading branch information
epage committed Jul 14, 2021
1 parent fbcf079 commit 281469f
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 210 deletions.
7 changes: 0 additions & 7 deletions clap_derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,6 @@ pub struct Attrs {
kind: Sp<Kind>,
}

/// Output for the gen_xxx() methods were we need more than a simple stream of tokens.
///
/// The output of a generation method is not only the stream of new tokens but also the attribute
/// information of the current element. These attribute information may contain valuable information
/// for any kind of child arguments.
pub type GenOutput = (TokenStream, Attrs);

impl Method {
pub fn new(name: Ident, args: TokenStream) -> Self {
Method { name, args }
Expand Down
126 changes: 95 additions & 31 deletions clap_derive/src/derives/args.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,94 @@
// Copyright 2018 Guillaume Pinot (@TeXitoi) <[email protected]>,
// Kevin Knapp (@kbknapp) <[email protected]>, and
// Andrew Hobden (@hoverbear) <[email protected]>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// This work was derived from Structopt (https://github.com/TeXitoi/structopt)
// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the
// MIT/Apache 2.0 license.
use proc_macro2::TokenStream;
use proc_macro_error::abort;
use quote::{quote, quote_spanned};
use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, Field, Ident, Type};

use crate::{
attrs::{Attrs, Kind, ParserKind},
attrs::{Attrs, Kind, Name, ParserKind, DEFAULT_CASING, DEFAULT_ENV_CASING},
dummies,
utils::{sub_type, subty_if_name, Sp, Ty},
};

use proc_macro2::{Ident, Span, TokenStream};
use proc_macro_error::{abort, abort_call_site};
use quote::{quote, quote_spanned};
use syn::{
punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, DataStruct,
DeriveInput, Field, Fields, Type,
};

pub fn derive_args(input: &DeriveInput) -> TokenStream {
let ident = &input.ident;

dummies::args(ident);

match input.data {
Data::Struct(DataStruct {
fields: Fields::Named(ref fields),
..
}) => gen_for_struct(ident, &fields.named, &input.attrs),
Data::Struct(DataStruct {
fields: Fields::Unit,
..
}) => gen_for_struct(ident, &Punctuated::<Field, Comma>::new(), &input.attrs),
_ => abort_call_site!("`#[derive(Args)]` only supports non-tuple structs"),
}
}

pub fn gen_for_struct(
struct_name: &Ident,
fields: &Punctuated<Field, Comma>,
attrs: &[Attribute],
) -> TokenStream {
let from_arg_matches = gen_from_arg_matches_for_struct(struct_name, fields, attrs);

let attrs = Attrs::from_struct(
Span::call_site(),
attrs,
Name::Derived(struct_name.clone()),
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);
let app_var = Ident::new("app", Span::call_site());
let augmentation = gen_augment(fields, &app_var, &attrs, false);
let augmentation_update = gen_augment(fields, &app_var, &attrs, true);

quote! {
#from_arg_matches

#[allow(dead_code, unreachable_code, unused_variables)]
#[allow(
clippy::style,
clippy::complexity,
clippy::pedantic,
clippy::restriction,
clippy::perf,
clippy::deprecated,
clippy::nursery,
clippy::cargo
)]
#[deny(clippy::correctness)]
impl clap::Args for #struct_name {
fn augment_args<'b>(#app_var: clap::App<'b>) -> clap::App<'b> {
#augmentation
}
fn augment_args_for_update<'b>(#app_var: clap::App<'b>) -> clap::App<'b> {
#augmentation_update
}
}
}
}

pub fn gen_from_arg_matches_for_struct(
struct_name: &Ident,
fields: &Punctuated<Field, Comma>,
parent_attribute: &Attrs,
attrs: &[Attribute],
) -> TokenStream {
let constructor = gen_constructor(fields, parent_attribute);
let updater = gen_updater(fields, parent_attribute, true);
let attrs = Attrs::from_struct(
Span::call_site(),
attrs,
Name::Derived(struct_name.clone()),
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);

let constructor = gen_constructor(fields, &attrs);
let updater = gen_updater(fields, &attrs, true);

quote! {
#[allow(dead_code, unreachable_code, unused_variables)]
Expand All @@ -43,8 +104,9 @@ pub fn gen_from_arg_matches_for_struct(
)]
#[deny(clippy::correctness)]
impl clap::FromArgMatches for #struct_name {
fn from_arg_matches(arg_matches: &clap::ArgMatches) -> Self {
#struct_name #constructor
fn from_arg_matches(arg_matches: &clap::ArgMatches) -> Option<Self> {
let v = #struct_name #constructor;
Some(v)
}

fn update_from_arg_matches(&mut self, arg_matches: &clap::ArgMatches) {
Expand Down Expand Up @@ -123,7 +185,7 @@ pub fn gen_augment(
Kind::Flatten => {
let ty = &field.ty;
Some(quote_spanned! { kind.span()=>
let #app_var = <#ty as clap::IntoApp>::augment_clap(#app_var);
let #app_var = <#ty as clap::Args>::augment_args(#app_var);
})
}
Kind::Arg(ty) => {
Expand Down Expand Up @@ -289,22 +351,24 @@ pub fn gen_constructor(fields: &Punctuated<Field, Comma>, parent_attribute: &Att
};
quote_spanned! { kind.span()=>
#field_name: {
<#subcmd_type as clap::Subcommand>::from_subcommand(#arg_matches.subcommand())
<#subcmd_type as clap::FromArgMatches>::from_arg_matches(#arg_matches)
#unwrapper
}
}
}

Kind::Flatten => quote_spanned! { kind.span()=>
#field_name: clap::FromArgMatches::from_arg_matches(#arg_matches)
#field_name: clap::FromArgMatches::from_arg_matches(#arg_matches).unwrap()
},

Kind::Skip(val) => match val {
None => quote_spanned!(kind.span()=> #field_name: Default::default()),
Some(val) => quote_spanned!(kind.span()=> #field_name: (#val).into()),
},

Kind::Arg(ty) | Kind::FromGlobal(ty) => gen_parsers(&attrs, ty, field_name, field, None),
Kind::Arg(ty) | Kind::FromGlobal(ty) => {
gen_parsers(&attrs, ty, field_name, field, None)
}
}
});

Expand Down Expand Up @@ -350,16 +414,16 @@ pub fn gen_updater(
};

let updater = quote_spanned! { ty.span()=>
<#subcmd_type as clap::Subcommand>::update_from_subcommand(#field_name, #arg_matches.subcommand());
<#subcmd_type as clap::FromArgMatches>::update_from_arg_matches(#field_name, #arg_matches);
};

let updater = match **ty {
Ty::Option => quote_spanned! { kind.span()=>
if let Some(#field_name) = #field_name.as_mut() {
#updater
} else {
*#field_name = <#subcmd_type as clap::Subcommand>::from_subcommand(
#arg_matches.subcommand()
*#field_name = <#subcmd_type as clap::FromArgMatches>::from_arg_matches(
#arg_matches
)
}
},
Expand Down
8 changes: 3 additions & 5 deletions clap_derive/src/derives/clap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,25 @@ fn gen_for_struct(
fields: &Punctuated<Field, Comma>,
attrs: &[Attribute],
) -> TokenStream {
let (into_app, attrs) = into_app::gen_for_struct(name, fields, attrs);
let from_arg_matches = args::gen_from_arg_matches_for_struct(name, fields, &attrs);
let into_app = into_app::gen_for_struct(name, attrs);
let args = args::gen_for_struct(name, fields, attrs);

quote! {
impl clap::Clap for #name {}

#into_app
#from_arg_matches
#args
}
}

fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStream {
let into_app = into_app::gen_for_enum(name, attrs);
let from_arg_matches = subcommand::gen_from_arg_matches_for_enum(name);
let subcommand = subcommand::gen_for_enum(name, attrs, e);

quote! {
impl clap::Clap for #name {}

#into_app
#from_arg_matches
#subcommand
}
}
91 changes: 26 additions & 65 deletions clap_derive/src/derives/into_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,10 @@ use std::env;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort_call_site;
use quote::quote;
use syn::{
punctuated::Punctuated, token::Comma, Attribute, Data, DataStruct, DeriveInput, Field, Fields,
Ident,
};
use syn::{Attribute, Data, DataStruct, DeriveInput, Fields, Ident};

use crate::{
attrs::{Attrs, GenOutput, Name, DEFAULT_CASING, DEFAULT_ENV_CASING},
derives::args,
attrs::{Attrs, Name, DEFAULT_CASING, DEFAULT_ENV_CASING},
dummies,
utils::Sp,
};
Expand All @@ -36,25 +32,29 @@ pub fn derive_into_app(input: &DeriveInput) -> TokenStream {

match input.data {
Data::Struct(DataStruct {
fields: Fields::Named(ref fields),
fields: Fields::Named(_),
..
}) => gen_for_struct(ident, &fields.named, &input.attrs).0,
}) => gen_for_struct(ident, &input.attrs),
Data::Struct(DataStruct {
fields: Fields::Unit,
..
}) => gen_for_struct(ident, &Punctuated::<Field, Comma>::new(), &input.attrs).0,
}) => gen_for_struct(ident, &input.attrs),
Data::Enum(_) => gen_for_enum(ident, &input.attrs),
_ => abort_call_site!("`#[derive(IntoApp)]` only supports non-tuple structs and enums"),
}
}

pub fn gen_for_struct(
struct_name: &Ident,
fields: &Punctuated<Field, Comma>,
attrs: &[Attribute],
) -> GenOutput {
let (into_app, attrs) = gen_into_app_fn(attrs);
let augment_clap = gen_augment_clap_fn(fields, &attrs);
pub fn gen_for_struct(struct_name: &Ident, attrs: &[Attribute]) -> TokenStream {
let app_name = env::var("CARGO_PKG_NAME").ok().unwrap_or_default();

let attrs = Attrs::from_struct(
Span::call_site(),
attrs,
Name::Assigned(quote!(#app_name)),
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);
let name = attrs.cased_name();

let tokens = quote! {
#[allow(dead_code, unreachable_code, unused_variables)]
Expand All @@ -70,12 +70,19 @@ pub fn gen_for_struct(
)]
#[deny(clippy::correctness)]
impl clap::IntoApp for #struct_name {
#into_app
#augment_clap
fn into_app<'b>() -> clap::App<'b> {
let app = clap::App::new(#name);
<#struct_name as clap::Args>::augment_args(app)
}

fn into_app_for_update<'b>() -> clap::App<'b> {
let app = clap::App::new(#name);
<#struct_name as clap::Args>::augment_args_for_update(app)
}
}
};

(tokens, attrs)
tokens
}

pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute]) -> TokenStream {
Expand Down Expand Up @@ -107,59 +114,13 @@ pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute]) -> TokenStream {
fn into_app<'b>() -> clap::App<'b> {
let app = clap::App::new(#name)
.setting(clap::AppSettings::SubcommandRequiredElseHelp);
<#enum_name as clap::IntoApp>::augment_clap(app)
}

fn augment_clap<'b>(app: clap::App<'b>) -> clap::App<'b> {
<#enum_name as clap::Subcommand>::augment_subcommands(app)
}

fn into_app_for_update<'b>() -> clap::App<'b> {
let app = clap::App::new(#name);
<#enum_name as clap::IntoApp>::augment_clap_for_update(app)
}

fn augment_clap_for_update<'b>(app: clap::App<'b>) -> clap::App<'b> {
<#enum_name as clap::Subcommand>::augment_subcommands_for_update(app)
}
}
}
}

fn gen_into_app_fn(attrs: &[Attribute]) -> GenOutput {
let app_name = env::var("CARGO_PKG_NAME").ok().unwrap_or_default();

let attrs = Attrs::from_struct(
Span::call_site(),
attrs,
Name::Assigned(quote!(#app_name)),
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);
let name = attrs.cased_name();

let tokens = quote! {
fn into_app<'b>() -> clap::App<'b> {
Self::augment_clap(clap::App::new(#name))
}
fn into_app_for_update<'b>() -> clap::App<'b> {
Self::augment_clap_for_update(clap::App::new(#name))
}
};

(tokens, attrs)
}

fn gen_augment_clap_fn(fields: &Punctuated<Field, Comma>, parent_attribute: &Attrs) -> TokenStream {
let app_var = Ident::new("app", Span::call_site());
let augmentation = args::gen_augment(fields, &app_var, parent_attribute, false);
let augmentation_update = args::gen_augment(fields, &app_var, parent_attribute, true);
quote! {
fn augment_clap<'b>(#app_var: clap::App<'b>) -> clap::App<'b> {
#augmentation
}
fn augment_clap_for_update<'b>(#app_var: clap::App<'b>) -> clap::App<'b> {
#augmentation_update
}
}
}
2 changes: 1 addition & 1 deletion clap_derive/src/derives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ mod subcommand;

pub use self::clap::derive_clap;
pub use arg_enum::derive_arg_enum;
// pub use from_arg_matches::derive_from_arg_matches;
pub use args::derive_args;
pub use into_app::derive_into_app;
pub use subcommand::derive_subcommand;
Loading

0 comments on commit 281469f

Please sign in to comment.