feat(derive): Generic support

This is a port of clap-rs/clap#3023
This commit is contained in:
kuviman 2021-11-14 12:55:24 +04:00 committed by Ed Page
parent 58b0fe537e
commit aa6594b334
5 changed files with 231 additions and 36 deletions

View file

@ -23,7 +23,7 @@ use proc_macro_error::{abort, abort_call_site};
use quote::{format_ident, quote, quote_spanned}; use quote::{format_ident, quote, quote_spanned};
use syn::{ use syn::{
punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, DataStruct, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, DataStruct,
DeriveInput, Field, Fields, Type, DeriveInput, Field, Fields, Generics, Type,
}; };
pub fn derive_args(input: &DeriveInput) -> TokenStream { pub fn derive_args(input: &DeriveInput) -> TokenStream {
@ -35,21 +35,27 @@ pub fn derive_args(input: &DeriveInput) -> TokenStream {
Data::Struct(DataStruct { Data::Struct(DataStruct {
fields: Fields::Named(ref fields), fields: Fields::Named(ref fields),
.. ..
}) => gen_for_struct(ident, &fields.named, &input.attrs), }) => gen_for_struct(ident, &input.generics, &fields.named, &input.attrs),
Data::Struct(DataStruct { Data::Struct(DataStruct {
fields: Fields::Unit, fields: Fields::Unit,
.. ..
}) => gen_for_struct(ident, &Punctuated::<Field, Comma>::new(), &input.attrs), }) => gen_for_struct(
ident,
&input.generics,
&Punctuated::<Field, Comma>::new(),
&input.attrs,
),
_ => abort_call_site!("`#[derive(Args)]` only supports non-tuple structs"), _ => abort_call_site!("`#[derive(Args)]` only supports non-tuple structs"),
} }
} }
pub fn gen_for_struct( pub fn gen_for_struct(
struct_name: &Ident, struct_name: &Ident,
generics: &Generics,
fields: &Punctuated<Field, Comma>, fields: &Punctuated<Field, Comma>,
attrs: &[Attribute], attrs: &[Attribute],
) -> TokenStream { ) -> TokenStream {
let from_arg_matches = gen_from_arg_matches_for_struct(struct_name, fields, attrs); let from_arg_matches = gen_from_arg_matches_for_struct(struct_name, generics, fields, attrs);
let attrs = Attrs::from_struct( let attrs = Attrs::from_struct(
Span::call_site(), Span::call_site(),
@ -62,6 +68,8 @@ pub fn gen_for_struct(
let augmentation = gen_augment(fields, &app_var, &attrs, false); let augmentation = gen_augment(fields, &app_var, &attrs, false);
let augmentation_update = gen_augment(fields, &app_var, &attrs, true); let augmentation_update = gen_augment(fields, &app_var, &attrs, true);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
#from_arg_matches #from_arg_matches
@ -78,7 +86,7 @@ pub fn gen_for_struct(
clippy::suspicious_else_formatting, clippy::suspicious_else_formatting,
)] )]
#[deny(clippy::correctness)] #[deny(clippy::correctness)]
impl clap::Args for #struct_name { impl #impl_generics clap::Args for #struct_name #ty_generics #where_clause {
fn augment_args<'b>(#app_var: clap::App<'b>) -> clap::App<'b> { fn augment_args<'b>(#app_var: clap::App<'b>) -> clap::App<'b> {
#augmentation #augmentation
} }
@ -91,6 +99,7 @@ pub fn gen_for_struct(
pub fn gen_from_arg_matches_for_struct( pub fn gen_from_arg_matches_for_struct(
struct_name: &Ident, struct_name: &Ident,
generics: &Generics,
fields: &Punctuated<Field, Comma>, fields: &Punctuated<Field, Comma>,
attrs: &[Attribute], attrs: &[Attribute],
) -> TokenStream { ) -> TokenStream {
@ -105,6 +114,8 @@ pub fn gen_from_arg_matches_for_struct(
let constructor = gen_constructor(fields, &attrs); let constructor = gen_constructor(fields, &attrs);
let updater = gen_updater(fields, &attrs, true); let updater = gen_updater(fields, &attrs, true);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
#[allow(dead_code, unreachable_code, unused_variables)] #[allow(dead_code, unreachable_code, unused_variables)]
#[allow( #[allow(
@ -119,7 +130,7 @@ pub fn gen_from_arg_matches_for_struct(
clippy::suspicious_else_formatting, clippy::suspicious_else_formatting,
)] )]
#[deny(clippy::correctness)] #[deny(clippy::correctness)]
impl clap::FromArgMatches for #struct_name { impl #impl_generics clap::FromArgMatches for #struct_name #ty_generics #where_clause {
fn from_arg_matches(__clap_arg_matches: &clap::ArgMatches) -> Result<Self, clap::Error> { fn from_arg_matches(__clap_arg_matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
let v = #struct_name #constructor; let v = #struct_name #constructor;
::std::result::Result::Ok(v) ::std::result::Result::Ok(v)

View file

@ -17,7 +17,7 @@ use std::env;
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort_call_site; use proc_macro_error::abort_call_site;
use quote::quote; use quote::quote;
use syn::{Attribute, Data, DataStruct, DeriveInput, Fields, Ident}; use syn::{Attribute, Data, DataStruct, DeriveInput, Fields, Generics, Ident};
use crate::{ use crate::{
attrs::{Attrs, Name, DEFAULT_CASING, DEFAULT_ENV_CASING}, attrs::{Attrs, Name, DEFAULT_CASING, DEFAULT_ENV_CASING},
@ -34,17 +34,21 @@ pub fn derive_into_app(input: &DeriveInput) -> TokenStream {
Data::Struct(DataStruct { Data::Struct(DataStruct {
fields: Fields::Named(_), fields: Fields::Named(_),
.. ..
}) => gen_for_struct(ident, &input.attrs), }) => gen_for_struct(ident, &input.generics, &input.attrs),
Data::Struct(DataStruct { Data::Struct(DataStruct {
fields: Fields::Unit, fields: Fields::Unit,
.. ..
}) => gen_for_struct(ident, &input.attrs), }) => gen_for_struct(ident, &input.generics, &input.attrs),
Data::Enum(_) => gen_for_enum(ident, &input.attrs), Data::Enum(_) => gen_for_enum(ident, &input.generics, &input.attrs),
_ => abort_call_site!("`#[derive(IntoApp)]` only supports non-tuple structs and enums"), _ => abort_call_site!("`#[derive(IntoApp)]` only supports non-tuple structs and enums"),
} }
} }
pub fn gen_for_struct(struct_name: &Ident, attrs: &[Attribute]) -> TokenStream { pub fn gen_for_struct(
struct_name: &Ident,
generics: &Generics,
attrs: &[Attribute],
) -> TokenStream {
let app_name = env::var("CARGO_PKG_NAME").ok().unwrap_or_default(); let app_name = env::var("CARGO_PKG_NAME").ok().unwrap_or_default();
let attrs = Attrs::from_struct( let attrs = Attrs::from_struct(
@ -57,6 +61,8 @@ pub fn gen_for_struct(struct_name: &Ident, attrs: &[Attribute]) -> TokenStream {
let name = attrs.cased_name(); let name = attrs.cased_name();
let app_var = Ident::new("__clap_app", Span::call_site()); let app_var = Ident::new("__clap_app", Span::call_site());
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let tokens = quote! { let tokens = quote! {
#[allow(dead_code, unreachable_code, unused_variables)] #[allow(dead_code, unreachable_code, unused_variables)]
#[allow( #[allow(
@ -71,15 +77,15 @@ pub fn gen_for_struct(struct_name: &Ident, attrs: &[Attribute]) -> TokenStream {
clippy::suspicious_else_formatting, clippy::suspicious_else_formatting,
)] )]
#[deny(clippy::correctness)] #[deny(clippy::correctness)]
impl clap::IntoApp for #struct_name { impl #impl_generics clap::IntoApp for #struct_name #ty_generics #where_clause {
fn into_app<'b>() -> clap::App<'b> { fn into_app<'b>() -> clap::App<'b> {
let #app_var = clap::App::new(#name); let #app_var = clap::App::new(#name);
<#struct_name as clap::Args>::augment_args(#app_var) <Self as clap::Args>::augment_args(#app_var)
} }
fn into_app_for_update<'b>() -> clap::App<'b> { fn into_app_for_update<'b>() -> clap::App<'b> {
let #app_var = clap::App::new(#name); let #app_var = clap::App::new(#name);
<#struct_name as clap::Args>::augment_args_for_update(#app_var) <Self as clap::Args>::augment_args_for_update(#app_var)
} }
} }
}; };
@ -87,7 +93,7 @@ pub fn gen_for_struct(struct_name: &Ident, attrs: &[Attribute]) -> TokenStream {
tokens tokens
} }
pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute]) -> TokenStream { pub fn gen_for_enum(enum_name: &Ident, generics: &Generics, attrs: &[Attribute]) -> TokenStream {
let app_name = env::var("CARGO_PKG_NAME").ok().unwrap_or_default(); let app_name = env::var("CARGO_PKG_NAME").ok().unwrap_or_default();
let attrs = Attrs::from_struct( let attrs = Attrs::from_struct(
@ -100,6 +106,8 @@ pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute]) -> TokenStream {
let name = attrs.cased_name(); let name = attrs.cased_name();
let app_var = Ident::new("__clap_app", Span::call_site()); let app_var = Ident::new("__clap_app", Span::call_site());
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
#[allow(dead_code, unreachable_code, unused_variables)] #[allow(dead_code, unreachable_code, unused_variables)]
#[allow( #[allow(
@ -114,16 +122,16 @@ pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute]) -> TokenStream {
clippy::suspicious_else_formatting, clippy::suspicious_else_formatting,
)] )]
#[deny(clippy::correctness)] #[deny(clippy::correctness)]
impl clap::IntoApp for #enum_name { impl #impl_generics clap::IntoApp for #enum_name #ty_generics #where_clause {
fn into_app<'b>() -> clap::App<'b> { fn into_app<'b>() -> clap::App<'b> {
let #app_var = clap::App::new(#name) let #app_var = clap::App::new(#name)
.setting(clap::AppSettings::SubcommandRequiredElseHelp); .setting(clap::AppSettings::SubcommandRequiredElseHelp);
<#enum_name as clap::Subcommand>::augment_subcommands(#app_var) <Self as clap::Subcommand>::augment_subcommands(#app_var)
} }
fn into_app_for_update<'b>() -> clap::App<'b> { fn into_app_for_update<'b>() -> clap::App<'b> {
let #app_var = clap::App::new(#name); let #app_var = clap::App::new(#name);
<#enum_name as clap::Subcommand>::augment_subcommands_for_update(#app_var) <Self as clap::Subcommand>::augment_subcommands_for_update(#app_var)
} }
} }
} }

View file

@ -22,7 +22,7 @@ use proc_macro_error::abort_call_site;
use quote::quote; use quote::quote;
use syn::{ use syn::{
self, punctuated::Punctuated, token::Comma, Attribute, Data, DataEnum, DataStruct, DeriveInput, self, punctuated::Punctuated, token::Comma, Attribute, Data, DataEnum, DataStruct, DeriveInput,
Field, Fields, Ident, Field, Fields, Generics, Ident,
}; };
pub fn derive_parser(input: &DeriveInput) -> TokenStream { pub fn derive_parser(input: &DeriveInput) -> TokenStream {
@ -34,18 +34,23 @@ pub fn derive_parser(input: &DeriveInput) -> TokenStream {
.. ..
}) => { }) => {
dummies::parser_struct(ident); dummies::parser_struct(ident);
gen_for_struct(ident, &fields.named, &input.attrs) gen_for_struct(ident, &input.generics, &fields.named, &input.attrs)
} }
Data::Struct(DataStruct { Data::Struct(DataStruct {
fields: Fields::Unit, fields: Fields::Unit,
.. ..
}) => { }) => {
dummies::parser_struct(ident); dummies::parser_struct(ident);
gen_for_struct(ident, &Punctuated::<Field, Comma>::new(), &input.attrs) gen_for_struct(
ident,
&input.generics,
&Punctuated::<Field, Comma>::new(),
&input.attrs,
)
} }
Data::Enum(ref e) => { Data::Enum(ref e) => {
dummies::parser_enum(ident); dummies::parser_enum(ident);
gen_for_enum(ident, &input.attrs, e) gen_for_enum(ident, &input.generics, &input.attrs, e)
} }
_ => abort_call_site!("`#[derive(Parser)]` only supports non-tuple structs and enums"), _ => abort_call_site!("`#[derive(Parser)]` only supports non-tuple structs and enums"),
} }
@ -53,26 +58,36 @@ pub fn derive_parser(input: &DeriveInput) -> TokenStream {
fn gen_for_struct( fn gen_for_struct(
name: &Ident, name: &Ident,
generics: &Generics,
fields: &Punctuated<Field, Comma>, fields: &Punctuated<Field, Comma>,
attrs: &[Attribute], attrs: &[Attribute],
) -> TokenStream { ) -> TokenStream {
let into_app = into_app::gen_for_struct(name, attrs); let into_app = into_app::gen_for_struct(name, generics, attrs);
let args = args::gen_for_struct(name, fields, attrs); let args = args::gen_for_struct(name, generics, fields, attrs);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
impl clap::Parser for #name {} impl #impl_generics clap::Parser for #name #ty_generics #where_clause {}
#into_app #into_app
#args #args
} }
} }
fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStream { fn gen_for_enum(
let into_app = into_app::gen_for_enum(name, attrs); name: &Ident,
let subcommand = subcommand::gen_for_enum(name, attrs, e); generics: &Generics,
attrs: &[Attribute],
e: &DataEnum,
) -> TokenStream {
let into_app = into_app::gen_for_enum(name, generics, attrs);
let subcommand = subcommand::gen_for_enum(name, generics, attrs, e);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
impl clap::Parser for #name {} impl #impl_generics clap::Parser for #name #ty_generics #where_clause {}
#into_app #into_app
#subcommand #subcommand

View file

@ -23,7 +23,7 @@ use proc_macro_error::{abort, abort_call_site};
use quote::{format_ident, quote, quote_spanned}; use quote::{format_ident, quote, quote_spanned};
use syn::{ use syn::{
punctuated::Punctuated, spanned::Spanned, Attribute, Data, DataEnum, DeriveInput, punctuated::Punctuated, spanned::Spanned, Attribute, Data, DataEnum, DeriveInput,
FieldsUnnamed, Token, Variant, FieldsUnnamed, Generics, Token, Variant,
}; };
pub fn derive_subcommand(input: &DeriveInput) -> TokenStream { pub fn derive_subcommand(input: &DeriveInput) -> TokenStream {
@ -32,13 +32,18 @@ pub fn derive_subcommand(input: &DeriveInput) -> TokenStream {
dummies::subcommand(ident); dummies::subcommand(ident);
match input.data { match input.data {
Data::Enum(ref e) => gen_for_enum(ident, &input.attrs, e), Data::Enum(ref e) => gen_for_enum(ident, &input.generics, &input.attrs, e),
_ => abort_call_site!("`#[derive(Subcommand)]` only supports enums"), _ => abort_call_site!("`#[derive(Subcommand)]` only supports enums"),
} }
} }
pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStream { pub fn gen_for_enum(
let from_arg_matches = gen_from_arg_matches_for_enum(enum_name, attrs, e); enum_name: &Ident,
generics: &Generics,
attrs: &[Attribute],
e: &DataEnum,
) -> TokenStream {
let from_arg_matches = gen_from_arg_matches_for_enum(enum_name, generics, attrs, e);
let attrs = Attrs::from_struct( let attrs = Attrs::from_struct(
Span::call_site(), Span::call_site(),
@ -51,6 +56,8 @@ pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute], e: &DataEnum) -> Tok
let augmentation_update = gen_augment(&e.variants, &attrs, true); let augmentation_update = gen_augment(&e.variants, &attrs, true);
let has_subcommand = gen_has_subcommand(&e.variants, &attrs); let has_subcommand = gen_has_subcommand(&e.variants, &attrs);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
#from_arg_matches #from_arg_matches
@ -67,7 +74,7 @@ pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute], e: &DataEnum) -> Tok
clippy::suspicious_else_formatting, clippy::suspicious_else_formatting,
)] )]
#[deny(clippy::correctness)] #[deny(clippy::correctness)]
impl clap::Subcommand for #enum_name { impl #impl_generics clap::Subcommand for #enum_name #ty_generics #where_clause {
fn augment_subcommands <'b>(__clap_app: clap::App<'b>) -> clap::App<'b> { fn augment_subcommands <'b>(__clap_app: clap::App<'b>) -> clap::App<'b> {
#augmentation #augmentation
} }
@ -81,7 +88,12 @@ pub fn gen_for_enum(enum_name: &Ident, attrs: &[Attribute], e: &DataEnum) -> Tok
} }
} }
fn gen_from_arg_matches_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStream { fn gen_from_arg_matches_for_enum(
name: &Ident,
generics: &Generics,
attrs: &[Attribute],
e: &DataEnum,
) -> TokenStream {
let attrs = Attrs::from_struct( let attrs = Attrs::from_struct(
Span::call_site(), Span::call_site(),
attrs, attrs,
@ -93,6 +105,8 @@ fn gen_from_arg_matches_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum
let from_arg_matches = gen_from_arg_matches(name, &e.variants, &attrs); let from_arg_matches = gen_from_arg_matches(name, &e.variants, &attrs);
let update_from_arg_matches = gen_update_from_arg_matches(name, &e.variants, &attrs); let update_from_arg_matches = gen_update_from_arg_matches(name, &e.variants, &attrs);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! { quote! {
#[allow(dead_code, unreachable_code, unused_variables, unused_braces)] #[allow(dead_code, unreachable_code, unused_variables, unused_braces)]
#[allow( #[allow(
@ -107,7 +121,7 @@ fn gen_from_arg_matches_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum
clippy::suspicious_else_formatting, clippy::suspicious_else_formatting,
)] )]
#[deny(clippy::correctness)] #[deny(clippy::correctness)]
impl clap::FromArgMatches for #name { impl #impl_generics clap::FromArgMatches for #name #ty_generics #where_clause {
#from_arg_matches #from_arg_matches
#update_from_arg_matches #update_from_arg_matches
} }

View file

@ -0,0 +1,147 @@
mod utils;
use clap::{Args, Parser};
#[test]
fn generic_struct_flatten() {
#[derive(Args, PartialEq, Debug)]
struct Inner {
pub answer: isize,
}
#[derive(Parser, PartialEq, Debug)]
struct Outer<T: Args> {
#[clap(flatten)]
pub inner: T,
}
assert_eq!(
Outer {
inner: Inner { answer: 42 }
},
Outer::parse_from(&["--answer", "42"])
)
}
#[test]
fn generic_struct_flatten_w_where_clause() {
#[derive(Args, PartialEq, Debug)]
struct Inner {
pub answer: isize,
}
#[derive(Parser, PartialEq, Debug)]
struct Outer<T>
where
T: Args,
{
#[clap(flatten)]
pub inner: T,
}
assert_eq!(
Outer {
inner: Inner { answer: 42 }
},
Outer::parse_from(&["--answer", "42"])
)
}
#[test]
fn generic_enum() {
#[derive(Args, PartialEq, Debug)]
struct Inner {
pub answer: isize,
}
#[derive(Parser, PartialEq, Debug)]
enum GenericEnum<T: Args> {
Start(T),
Stop,
}
assert_eq!(
GenericEnum::Start(Inner { answer: 42 }),
GenericEnum::parse_from(&["test", "start", "42"])
)
}
#[test]
fn generic_enum_w_where_clause() {
#[derive(Args, PartialEq, Debug)]
struct Inner {
pub answer: isize,
}
#[derive(Parser, PartialEq, Debug)]
enum GenericEnum<T>
where
T: Args,
{
Start(T),
Stop,
}
assert_eq!(
GenericEnum::Start(Inner { answer: 42 }),
GenericEnum::parse_from(&["test", "start", "42"])
)
}
#[test]
fn generic_w_fromstr_trait_bound() {
use std::str::FromStr;
#[derive(Parser, PartialEq, Debug)]
struct Opt<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Sync + Send + 'static,
{
answer: T,
}
assert_eq!(
Opt::<isize> { answer: 42 },
Opt::<isize>::parse_from(&["--answer", "42"])
)
}
#[test]
fn generic_wo_trait_bound() {
use std::time::Duration;
#[derive(Parser, PartialEq, Debug)]
struct Opt<T> {
answer: isize,
#[clap(skip)]
took: Option<T>,
}
assert_eq!(
Opt::<Duration> {
answer: 42,
took: None
},
Opt::<Duration>::parse_from(&["--answer", "42"])
)
}
#[test]
fn generic_where_clause_w_trailing_comma() {
use std::str::FromStr;
#[derive(Parser, PartialEq, Debug)]
struct Opt<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Sync + Send + 'static,
{
pub answer: T,
}
assert_eq!(
Opt::<isize> { answer: 42 },
Opt::<isize>::parse_from(&["--answer", "42"])
)
}