Skip to content

Commit

Permalink
fix(derive): Ensure App help_heading is applied
Browse files Browse the repository at this point in the history
We normally set all app attributes at the end.  This can be changed but
will require some work to ensure
- Top-level item's doc cmment ins our over flattened
- We still support `Args` / `Subcommand` be used to initialize an `App` when
  creating a subcommand

In the mean time, this special cases `help_heading` to happen first.
We'll need this special casing anyways to address #2803 since we'll need
to capture the old help heading before addings args and then restore it
after.  I guess we could unconditionally do that but its extra work /
boilerplate for when people have to dig into their what the derives do.

Fixes #2785
  • Loading branch information
epage committed Oct 15, 2021
1 parent f9208ae commit 22edac6
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 10 deletions.
16 changes: 14 additions & 2 deletions clap_derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pub struct Attrs {
author: Option<Method>,
version: Option<Method>,
verbatim_doc_comment: Option<Ident>,
help_heading: Option<Method>,
is_enum: bool,
has_custom_parser: bool,
kind: Sp<Kind>,
Expand Down Expand Up @@ -285,6 +286,7 @@ impl Attrs {
author: None,
version: None,
verbatim_doc_comment: None,
help_heading: None,
is_enum: false,
has_custom_parser: false,
kind: Sp::new(Kind::Arg(Sp::new(Ty::Other, default_span)), default_span),
Expand Down Expand Up @@ -383,6 +385,10 @@ impl Attrs {
self.methods.push(Method::new(raw_ident, val));
}

HelpHeading(ident, expr) => {
self.help_heading = Some(Method::new(ident, quote!(#expr)));
}

About(ident, about) => {
let method = Method::from_lit_or_env(ident, about, "CARGO_PKG_DESCRIPTION");
self.methods.push(method);
Expand Down Expand Up @@ -773,7 +779,12 @@ impl Attrs {
}

/// generate methods from attributes on top of struct or enum
pub fn top_level_methods(&self) -> TokenStream {
pub fn initial_top_level_methods(&self) -> TokenStream {
let help_heading = self.help_heading.as_ref().into_iter();
quote!( #(#help_heading)* )
}

pub fn final_top_level_methods(&self) -> TokenStream {
let version = &self.version;
let author = &self.author;
let methods = &self.methods;
Expand All @@ -786,7 +797,8 @@ impl Attrs {
pub fn field_methods(&self) -> proc_macro2::TokenStream {
let methods = &self.methods;
let doc_comment = &self.doc_comment;
quote!( #(#doc_comment)* #(#methods)* )
let help_heading = self.help_heading.as_ref().into_iter();
quote!( #(#doc_comment)* #(#help_heading)* #(#methods)* )
}

pub fn cased_name(&self) -> TokenStream {
Expand Down
6 changes: 4 additions & 2 deletions clap_derive/src/derives/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,13 @@ pub fn gen_augment(
}
});

let app_methods = parent_attribute.top_level_methods();
let initial_app_methods = parent_attribute.initial_top_level_methods();
let final_app_methods = parent_attribute.final_top_level_methods();
quote! {{
let #app_var = #app_var#initial_app_methods;
#( #args )*
#subcmd
#app_var#app_methods
#app_var#final_app_methods
}}
}

Expand Down
18 changes: 12 additions & 6 deletions clap_derive/src/derives/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,15 @@ fn gen_augment(
};

let name = attrs.cased_name();
let from_attrs = attrs.top_level_methods();
let initial_app_methods = parent_attribute.initial_top_level_methods();
let final_from_attrs = attrs.final_top_level_methods();
let subcommand = quote! {
let #app_var = #app_var.subcommand({
let #subcommand_var = clap::App::new(#name);
let #subcommand_var = #subcommand_var#initial_app_methods;
let #subcommand_var = #arg_block;
let #subcommand_var = #subcommand_var.setting(::clap::AppSettings::SubcommandRequiredElseHelp);
#subcommand_var#from_attrs
#subcommand_var#final_from_attrs
});
};
Some(subcommand)
Expand Down Expand Up @@ -257,12 +259,14 @@ fn gen_augment(
};

let name = attrs.cased_name();
let from_attrs = attrs.top_level_methods();
let initial_app_methods = parent_attribute.initial_top_level_methods();
let final_from_attrs = attrs.final_top_level_methods();
let subcommand = quote! {
let #app_var = #app_var.subcommand({
let #subcommand_var = clap::App::new(#name);
let #subcommand_var = #subcommand_var#initial_app_methods;
let #subcommand_var = #arg_block;
#subcommand_var#from_attrs
#subcommand_var#final_from_attrs
});
};
Some(subcommand)
Expand All @@ -271,10 +275,12 @@ fn gen_augment(
})
.collect();

let app_methods = parent_attribute.top_level_methods();
let initial_app_methods = parent_attribute.initial_top_level_methods();
let final_app_methods = parent_attribute.final_top_level_methods();
quote! {
let #app_var = #app_var#initial_app_methods;
#( #subcommands )*;
#app_var #app_methods
#app_var #final_app_methods
}
}

Expand Down
11 changes: 11 additions & 0 deletions clap_derive/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub enum ClapAttr {
// ident = arbitrary_expr
NameExpr(Ident, Expr),
DefaultValueT(Ident, Option<Expr>),
HelpHeading(Ident, Expr),

// ident(arbitrary_expr,*)
MethodCall(Ident, Vec<Expr>),
Expand Down Expand Up @@ -100,13 +101,23 @@ impl Parse for ClapAttr {
Ok(Skip(name, Some(expr)))
}

"help_heading" => {
let expr = ExprLit {
attrs: vec![],
lit: Lit::Str(lit),
};
let expr = Expr::Lit(expr);
Ok(HelpHeading(name, expr))
}

_ => Ok(NameLitStr(name, lit)),
}
} else {
match input.parse::<Expr>() {
Ok(expr) => match &*name_str {
"skip" => Ok(Skip(name, Some(expr))),
"default_value_t" => Ok(DefaultValueT(name, Some(expr))),
"help_heading" => Ok(HelpHeading(name, expr)),
_ => Ok(NameExpr(name, expr)),
},

Expand Down
99 changes: 99 additions & 0 deletions clap_derive/tests/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use clap::{Args, IntoApp, Parser};

#[test]
fn arg_help_heading_applied() {
#[derive(Debug, Clone, Parser)]
struct CliOptions {
#[clap(long)]
#[clap(help_heading = Some("HEADING A"))]
should_be_in_section_a: Option<u32>,

#[clap(long)]
no_section: Option<u32>,
}

let app = CliOptions::into_app();

let should_be_in_section_a = app
.get_arguments()
.find(|a| a.get_name() == "should-be-in-section-a")
.unwrap();
assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A"));

let should_be_in_section_b = app
.get_arguments()
.find(|a| a.get_name() == "no-section")
.unwrap();
assert_eq!(should_be_in_section_b.get_help_heading(), None);
}

#[test]
fn app_help_heading_applied() {
#[derive(Debug, Clone, Parser)]
#[clap(help_heading = "DEFAULT")]
struct CliOptions {
#[clap(long)]
#[clap(help_heading = Some("HEADING A"))]
should_be_in_section_a: Option<u32>,

#[clap(long)]
should_be_in_default_section: Option<u32>,
}

let app = CliOptions::into_app();

let should_be_in_section_a = app
.get_arguments()
.find(|a| a.get_name() == "should-be-in-section-a")
.unwrap();
assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A"));

let should_be_in_default_section = app
.get_arguments()
.find(|a| a.get_name() == "should-be-in-default-section")
.unwrap();
assert_eq!(
should_be_in_default_section.get_help_heading(),
Some("DEFAULT")
);
}

#[test]
fn app_help_heading_flattened() {
#[derive(Debug, Clone, Parser)]
struct CliOptions {
#[clap(flatten)]
options_a: OptionsA,

#[clap(flatten)]
options_b: OptionsB,
}

#[derive(Debug, Clone, Args)]
#[clap(help_heading = "HEADING A")]
struct OptionsA {
#[clap(long)]
should_be_in_section_a: Option<u32>,
}

#[derive(Debug, Clone, Args)]
#[clap(help_heading = "HEADING B")]
struct OptionsB {
#[clap(long)]
should_be_in_section_b: Option<u32>,
}

let app = CliOptions::into_app();

let should_be_in_section_a = app
.get_arguments()
.find(|a| a.get_name() == "should-be-in-section-a")
.unwrap();
assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A"));

let should_be_in_section_b = app
.get_arguments()
.find(|a| a.get_name() == "should-be-in-section-b")
.unwrap();
assert_eq!(should_be_in_section_b.get_help_heading(), Some("HEADING B"));
}

0 comments on commit 22edac6

Please sign in to comment.