blob: b8183867466bb0dcad7c0301a8715cc29c6bc51c [file] [log] [blame]
// Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>
//
// 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 crate is custom derive for `StructOpt`. It should not be used
//! directly. See [structopt documentation](https://docs.rs/structopt)
//! for the usage of `#[derive(StructOpt)]`.
#![allow(clippy::large_enum_variant)]
// FIXME: remove when and if our MSRV hits 1.42
#![allow(clippy::match_like_matches_macro)]
#![forbid(unsafe_code)]
extern crate proc_macro;
mod attrs;
mod doc_comments;
mod parse;
mod spanned;
mod ty;
use crate::{
attrs::{Attrs, CasingStyle, Kind, Name, ParserKind},
spanned::Sp,
ty::{is_simple_ty, sub_type, subty_if_name, Ty},
};
use proc_macro2::{Span, TokenStream};
use proc_macro_error::{abort, abort_call_site, proc_macro_error, set_dummy};
use quote::{format_ident, quote, quote_spanned};
use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, *};
/// Default casing style for generated arguments.
const DEFAULT_CASING: CasingStyle = CasingStyle::Kebab;
/// Default casing style for environment variables
const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake;
/// 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.
struct GenOutput {
tokens: TokenStream,
attrs: Attrs,
}
/// Generates the `StructOpt` impl.
#[proc_macro_derive(StructOpt, attributes(structopt))]
#[proc_macro_error]
pub fn structopt(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: DeriveInput = syn::parse(input).unwrap();
let gen = impl_structopt(&input);
gen.into()
}
/// Generate a block of code to add arguments/subcommands corresponding to
/// the `fields` to an app.
fn gen_augmentation(
fields: &Punctuated<Field, Comma>,
app_var: &Ident,
parent_attribute: &Attrs,
) -> TokenStream {
let mut subcmds = fields.iter().filter_map(|field| {
let attrs = Attrs::from_field(
field,
Some(parent_attribute),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let kind = attrs.kind();
if let Kind::Subcommand(ty) = &*kind {
let subcmd_type = match (**ty, sub_type(&field.ty)) {
(Ty::Option, Some(sub_type)) => sub_type,
_ => &field.ty,
};
let required = if **ty == Ty::Option {
quote!()
} else {
quote_spanned! { kind.span()=>
let #app_var = #app_var.setting(
::structopt::clap::AppSettings::SubcommandRequiredElseHelp
);
}
};
let span = field.span();
let ts = quote! {
let #app_var = <#subcmd_type as ::structopt::StructOptInternal>::augment_clap(
#app_var
);
#required
};
Some((span, ts))
} else {
None
}
});
let subcmd = subcmds.next().map(|(_, ts)| ts);
if let Some((span, _)) = subcmds.next() {
abort!(
span,
"multiple subcommand sets are not allowed, that's the second"
);
}
let args = fields.iter().filter_map(|field| {
let attrs = Attrs::from_field(
field,
Some(parent_attribute),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let kind = attrs.kind();
match &*kind {
Kind::ExternalSubcommand => abort!(
kind.span(),
"`external_subcommand` is only allowed on enum variants"
),
Kind::Subcommand(_) | Kind::Skip(_) => None,
Kind::Flatten => {
let ty = &field.ty;
Some(quote_spanned! { kind.span()=>
let #app_var = <#ty as ::structopt::StructOptInternal>::augment_clap(#app_var);
let #app_var = if <#ty as ::structopt::StructOptInternal>::is_subcommand() {
#app_var.setting(::structopt::clap::AppSettings::SubcommandRequiredElseHelp)
} else {
#app_var
};
})
}
Kind::Arg(ty) => {
let convert_type = match **ty {
Ty::Vec | Ty::Option => sub_type(&field.ty).unwrap_or(&field.ty),
Ty::OptionOption | Ty::OptionVec => {
sub_type(&field.ty).and_then(sub_type).unwrap_or(&field.ty)
}
_ => &field.ty,
};
let occurrences = *attrs.parser().kind == ParserKind::FromOccurrences;
let flag = *attrs.parser().kind == ParserKind::FromFlag;
let parser = attrs.parser();
let func = &parser.func;
let validator = match *parser.kind {
ParserKind::TryFromStr => quote_spanned! { func.span()=>
.validator(|s| {
#func(s.as_str())
.map(|_: #convert_type| ())
.map_err(|e| e.to_string())
})
},
ParserKind::TryFromOsStr => quote_spanned! { func.span()=>
.validator_os(|s| #func(&s).map(|_: #convert_type| ()))
},
_ => quote!(),
};
let modifier = match **ty {
Ty::Bool => quote_spanned! { ty.span()=>
.takes_value(false)
.multiple(false)
},
Ty::Option => quote_spanned! { ty.span()=>
.takes_value(true)
.multiple(false)
#validator
},
Ty::OptionOption => quote_spanned! { ty.span()=>
.takes_value(true)
.multiple(false)
.min_values(0)
.max_values(1)
#validator
},
Ty::OptionVec => quote_spanned! { ty.span()=>
.takes_value(true)
.multiple(true)
.min_values(0)
#validator
},
Ty::Vec => quote_spanned! { ty.span()=>
.takes_value(true)
.multiple(true)
#validator
},
Ty::Other if occurrences => quote_spanned! { ty.span()=>
.takes_value(false)
.multiple(true)
},
Ty::Other if flag => quote_spanned! { ty.span()=>
.takes_value(false)
.multiple(false)
},
Ty::Other => {
let required = !attrs.has_method("default_value");
quote_spanned! { ty.span()=>
.takes_value(true)
.multiple(false)
.required(#required)
#validator
}
}
};
let name = attrs.cased_name();
let methods = attrs.field_methods();
Some(quote_spanned! { field.span()=>
let #app_var = #app_var.arg(
::structopt::clap::Arg::with_name(#name)
#modifier
#methods
);
})
}
}
});
let app_methods = parent_attribute.top_level_methods();
let version = parent_attribute.version();
quote! {{
let #app_var = #app_var#app_methods;
#( #args )*
#subcmd
#app_var#version
}}
}
fn gen_constructor(fields: &Punctuated<Field, Comma>, parent_attribute: &Attrs) -> TokenStream {
// This ident is used in several match branches below,
// and the `quote[_spanned]` invocations have different spans.
//
// Given that this ident is used in several places and
// that the branches are located inside of a loop, it is possible that
// this ident will be given _different_ spans in different places, and
// thus will not be the _same_ ident anymore. To make sure the `matches`
// is always the same, we factor it out.
let matches = format_ident!("matches");
let fields = fields.iter().map(|field| {
let attrs = Attrs::from_field(
field,
Some(parent_attribute),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let field_name = field.ident.as_ref().unwrap();
let kind = attrs.kind();
match &*kind {
Kind::ExternalSubcommand => abort!(
kind.span(),
"`external_subcommand` is allowed only on enum variants"
),
Kind::Subcommand(ty) => {
let subcmd_type = match (**ty, sub_type(&field.ty)) {
(Ty::Option, Some(sub_type)) => sub_type,
_ => &field.ty,
};
let unwrapper = match **ty {
Ty::Option => quote!(),
_ => quote_spanned!( ty.span()=> .unwrap() ),
};
quote_spanned! { kind.span()=>
#field_name: <#subcmd_type as ::structopt::StructOptInternal>::from_subcommand(
#matches.subcommand())
#unwrapper
}
}
Kind::Flatten => quote_spanned! { kind.span()=>
#field_name: ::structopt::StructOpt::from_clap(#matches)
},
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) => {
use crate::attrs::ParserKind::*;
let parser = attrs.parser();
let func = &parser.func;
let span = parser.kind.span();
let (value_of, values_of, parse) = match *parser.kind {
FromStr => (
quote_spanned!(span=> value_of),
quote_spanned!(span=> values_of),
func.clone(),
),
TryFromStr => (
quote_spanned!(span=> value_of),
quote_spanned!(span=> values_of),
quote_spanned!(func.span()=> |s| #func(s).unwrap()),
),
FromOsStr => (
quote_spanned!(span=> value_of_os),
quote_spanned!(span=> values_of_os),
func.clone(),
),
TryFromOsStr => (
quote_spanned!(span=> value_of_os),
quote_spanned!(span=> values_of_os),
quote_spanned!(func.span()=> |s| #func(s).unwrap()),
),
FromOccurrences => (
quote_spanned!(span=> occurrences_of),
quote!(),
func.clone(),
),
FromFlag => (quote!(), quote!(), func.clone()),
};
let flag = *attrs.parser().kind == ParserKind::FromFlag;
let occurrences = *attrs.parser().kind == ParserKind::FromOccurrences;
let name = attrs.cased_name();
let field_value = match **ty {
Ty::Bool => quote_spanned!(ty.span()=> #matches.is_present(#name)),
Ty::Option => quote_spanned! { ty.span()=>
#matches.#value_of(#name)
.map(#parse)
},
Ty::OptionOption => quote_spanned! { ty.span()=>
if #matches.is_present(#name) {
Some(#matches.#value_of(#name).map(#parse))
} else {
None
}
},
Ty::OptionVec => quote_spanned! { ty.span()=>
if #matches.is_present(#name) {
Some(#matches.#values_of(#name)
.map_or_else(Vec::new, |v| v.map(#parse).collect()))
} else {
None
}
},
Ty::Vec => quote_spanned! { ty.span()=>
#matches.#values_of(#name)
.map_or_else(Vec::new, |v| v.map(#parse).collect())
},
Ty::Other if occurrences => quote_spanned! { ty.span()=>
#parse(#matches.#value_of(#name))
},
Ty::Other if flag => quote_spanned! { ty.span()=>
#parse(#matches.is_present(#name))
},
Ty::Other => quote_spanned! { ty.span()=>
#matches.#value_of(#name)
.map(#parse)
.unwrap()
},
};
quote_spanned!(field.span()=> #field_name: #field_value )
}
}
});
quote! {{
#( #fields ),*
}}
}
fn gen_from_clap(
struct_name: &Ident,
fields: &Punctuated<Field, Comma>,
parent_attribute: &Attrs,
) -> TokenStream {
let field_block = gen_constructor(fields, parent_attribute);
quote! {
fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self {
#struct_name #field_block
}
}
}
fn gen_clap(attrs: &[Attribute]) -> GenOutput {
let name = std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default();
let attrs = Attrs::from_struct(
Span::call_site(),
attrs,
Name::Assigned(quote!(#name)),
None,
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);
let tokens = {
let name = attrs.cased_name();
quote!(::structopt::clap::App::new(#name))
};
GenOutput { tokens, attrs }
}
fn gen_clap_struct(struct_attrs: &[Attribute]) -> GenOutput {
let initial_clap_app_gen = gen_clap(struct_attrs);
let clap_tokens = initial_clap_app_gen.tokens;
let augmented_tokens = quote! {
fn clap<'a, 'b>() -> ::structopt::clap::App<'a, 'b> {
let app = #clap_tokens;
<Self as ::structopt::StructOptInternal>::augment_clap(app)
}
};
GenOutput {
tokens: augmented_tokens,
attrs: initial_clap_app_gen.attrs,
}
}
fn gen_augment_clap(fields: &Punctuated<Field, Comma>, parent_attribute: &Attrs) -> TokenStream {
let app_var = Ident::new("app", Span::call_site());
let augmentation = gen_augmentation(fields, &app_var, parent_attribute);
quote! {
fn augment_clap<'a, 'b>(
#app_var: ::structopt::clap::App<'a, 'b>
) -> ::structopt::clap::App<'a, 'b> {
#augmentation
}
}
}
fn gen_clap_enum(enum_attrs: &[Attribute]) -> GenOutput {
let initial_clap_app_gen = gen_clap(enum_attrs);
let clap_tokens = initial_clap_app_gen.tokens;
let tokens = quote! {
fn clap<'a, 'b>() -> ::structopt::clap::App<'a, 'b> {
let app = #clap_tokens
.setting(::structopt::clap::AppSettings::SubcommandRequiredElseHelp);
<Self as ::structopt::StructOptInternal>::augment_clap(app)
}
};
GenOutput {
tokens,
attrs: initial_clap_app_gen.attrs,
}
}
fn gen_augment_clap_enum(
variants: &Punctuated<Variant, Comma>,
parent_attribute: &Attrs,
) -> TokenStream {
use syn::Fields::*;
let subcommands = variants.iter().map(|variant| {
let attrs = Attrs::from_struct(
variant.span(),
&variant.attrs,
Name::Derived(variant.ident.clone()),
Some(parent_attribute),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let kind = attrs.kind();
match &*kind {
Kind::ExternalSubcommand => {
let app_var = Ident::new("app", Span::call_site());
quote_spanned! { attrs.kind().span()=>
let #app_var = #app_var.setting(
::structopt::clap::AppSettings::AllowExternalSubcommands
);
}
},
Kind::Flatten => {
match variant.fields {
Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => {
let ty = &unnamed[0];
quote! {
let app = <#ty as ::structopt::StructOptInternal>::augment_clap(app);
}
},
_ => abort!(
variant,
"`flatten` is usable only with single-typed tuple variants"
),
}
},
_ => {
let app_var = Ident::new("subcommand", Span::call_site());
let arg_block = match variant.fields {
Named(ref fields) => gen_augmentation(&fields.named, &app_var, &attrs),
Unit => quote!( #app_var ),
Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => {
let ty = &unnamed[0];
quote_spanned! { ty.span()=>
{
let #app_var = <#ty as ::structopt::StructOptInternal>::augment_clap(
#app_var
);
if <#ty as ::structopt::StructOptInternal>::is_subcommand() {
#app_var.setting(
::structopt::clap::AppSettings::SubcommandRequiredElseHelp
)
} else {
#app_var
}
}
}
}
Unnamed(..) => abort!(variant, "non single-typed tuple enums are not supported"),
};
let name = attrs.cased_name();
let from_attrs = attrs.top_level_methods();
let version = attrs.version();
quote! {
let app = app.subcommand({
let #app_var = ::structopt::clap::SubCommand::with_name(#name);
let #app_var = #arg_block;
#app_var#from_attrs#version
});
}
},
}
});
let app_methods = parent_attribute.top_level_methods();
let version = parent_attribute.version();
quote! {
fn augment_clap<'a, 'b>(
app: ::structopt::clap::App<'a, 'b>
) -> ::structopt::clap::App<'a, 'b> {
let app = app #app_methods;
#( #subcommands )*;
app #version
}
}
}
fn gen_from_clap_enum(name: &Ident) -> TokenStream {
quote! {
fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self {
<#name as ::structopt::StructOptInternal>::from_subcommand(matches.subcommand())
.expect("structopt misuse: You likely tried to #[flatten] a struct \
that contains #[subcommand]. This is forbidden.")
}
}
}
fn gen_from_subcommand(
name: &Ident,
variants: &Punctuated<Variant, Comma>,
parent_attribute: &Attrs,
) -> TokenStream {
use syn::Fields::*;
let mut ext_subcmd = None;
let (flatten_variants, variants): (Vec<_>, Vec<_>) = variants
.iter()
.filter_map(|variant| {
let attrs = Attrs::from_struct(
variant.span(),
&variant.attrs,
Name::Derived(variant.ident.clone()),
Some(parent_attribute),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
let variant_name = &variant.ident;
if let Kind::ExternalSubcommand = *attrs.kind() {
if ext_subcmd.is_some() {
abort!(
attrs.kind().span(),
"Only one variant can be marked with `external_subcommand`, \
this is the second"
);
}
let ty = match variant.fields {
Unnamed(ref fields) if fields.unnamed.len() == 1 => &fields.unnamed[0].ty,
_ => abort!(
variant,
"The enum variant marked with `external_attribute` must be \
a single-typed tuple, and the type must be either `Vec<String>` \
or `Vec<OsString>`."
),
};
let (span, str_ty, values_of) = match subty_if_name(ty, "Vec") {
Some(subty) => {
if is_simple_ty(subty, "String") {
(
subty.span(),
quote!(::std::string::String),
quote!(values_of),
)
} else {
(
subty.span(),
quote!(::std::ffi::OsString),
quote!(values_of_os),
)
}
}
None => abort!(
ty,
"The type must be either `Vec<String>` or `Vec<OsString>` \
to be used with `external_subcommand`."
),
};
ext_subcmd = Some((span, variant_name, str_ty, values_of));
None
} else {
Some((variant, attrs))
}
})
.partition(|(_, attrs)| match &*attrs.kind() {
Kind::Flatten => true,
_ => false,
});
let other = format_ident!("other");
let matches = format_ident!("matches");
let external = match ext_subcmd {
Some((span, var_name, str_ty, values_of)) => quote_spanned! { span=>
match #other {
("", ::std::option::Option::None) => None,
(external, Some(#matches)) => {
::std::option::Option::Some(#name::#var_name(
::std::iter::once(#str_ty::from(external))
.chain(
#matches.#values_of("").into_iter().flatten().map(#str_ty::from)
)
.collect::<::std::vec::Vec<_>>()
))
}
(external, None) => {
::std::option::Option::Some(#name::#var_name(
::std::iter::once(#str_ty::from(external))
.collect::<::std::vec::Vec<_>>()
))
}
}
},
None => quote!(None),
};
let match_arms = variants.iter().map(|(variant, attrs)| {
let sub_name = attrs.cased_name();
let variant_name = &variant.ident;
let constructor_block = match variant.fields {
Named(ref fields) => gen_constructor(&fields.named, &attrs),
Unit => quote!(),
Unnamed(ref fields) if fields.unnamed.len() == 1 => {
let ty = &fields.unnamed[0];
quote!( ( <#ty as ::structopt::StructOpt>::from_clap(#matches) ) )
}
Unnamed(..) => abort!(
variant.ident,
"non single-typed tuple enums are not supported"
),
};
quote! {
(#sub_name, Some(#matches)) => {
Some(#name :: #variant_name #constructor_block)
}
}
});
let child_subcommands = flatten_variants.iter().map(|(variant, _attrs)| {
let variant_name = &variant.ident;
match variant.fields {
Unnamed(ref fields) if fields.unnamed.len() == 1 => {
let ty = &fields.unnamed[0];
quote! {
if let Some(res) =
<#ty as ::structopt::StructOptInternal>::from_subcommand(#other)
{
return Some(#name :: #variant_name (res));
}
}
}
_ => abort!(
variant,
"`flatten` is usable only with single-typed tuple variants"
),
}
});
quote! {
fn from_subcommand<'a, 'b>(
sub: (&'b str, Option<&'b ::structopt::clap::ArgMatches<'a>>)
) -> Option<Self> {
match sub {
#( #match_arms, )*
#other => {
#( #child_subcommands )else*;
#external
}
}
}
}
}
#[cfg(feature = "paw")]
fn gen_paw_impl(name: &Ident) -> TokenStream {
quote! {
impl ::structopt::paw::ParseArgs for #name {
type Error = std::io::Error;
fn parse_args() -> std::result::Result<Self, Self::Error> {
Ok(<#name as ::structopt::StructOpt>::from_args())
}
}
}
}
#[cfg(not(feature = "paw"))]
fn gen_paw_impl(_: &Ident) -> TokenStream {
TokenStream::new()
}
fn impl_structopt_for_struct(
name: &Ident,
fields: &Punctuated<Field, Comma>,
attrs: &[Attribute],
) -> TokenStream {
let basic_clap_app_gen = gen_clap_struct(attrs);
let augment_clap = gen_augment_clap(fields, &basic_clap_app_gen.attrs);
let from_clap = gen_from_clap(name, fields, &basic_clap_app_gen.attrs);
let paw_impl = gen_paw_impl(name);
let clap_tokens = basic_clap_app_gen.tokens;
quote! {
#[allow(unused_variables)]
#[allow(unknown_lints)]
#[allow(
clippy::style,
clippy::complexity,
clippy::pedantic,
clippy::restriction,
clippy::perf,
clippy::deprecated,
clippy::nursery,
clippy::cargo
)]
#[deny(clippy::correctness)]
#[allow(dead_code, unreachable_code)]
impl ::structopt::StructOpt for #name {
#clap_tokens
#from_clap
}
#[allow(unused_variables)]
#[allow(unknown_lints)]
#[allow(
clippy::style,
clippy::complexity,
clippy::pedantic,
clippy::restriction,
clippy::perf,
clippy::deprecated,
clippy::nursery,
clippy::cargo
)]
#[deny(clippy::correctness)]
#[allow(dead_code, unreachable_code)]
impl ::structopt::StructOptInternal for #name {
#augment_clap
fn is_subcommand() -> bool { false }
}
#paw_impl
}
}
fn impl_structopt_for_enum(
name: &Ident,
variants: &Punctuated<Variant, Comma>,
attrs: &[Attribute],
) -> TokenStream {
let basic_clap_app_gen = gen_clap_enum(attrs);
let clap_tokens = basic_clap_app_gen.tokens;
let attrs = basic_clap_app_gen.attrs;
let augment_clap = gen_augment_clap_enum(variants, &attrs);
let from_clap = gen_from_clap_enum(name);
let from_subcommand = gen_from_subcommand(name, variants, &attrs);
let paw_impl = gen_paw_impl(name);
quote! {
#[allow(unknown_lints)]
#[allow(unused_variables, dead_code, unreachable_code)]
#[allow(
clippy::style,
clippy::complexity,
clippy::pedantic,
clippy::restriction,
clippy::perf,
clippy::deprecated,
clippy::nursery,
clippy::cargo
)]
#[deny(clippy::correctness)]
impl ::structopt::StructOpt for #name {
#clap_tokens
#from_clap
}
#[allow(unused_variables)]
#[allow(unknown_lints)]
#[allow(
clippy::style,
clippy::complexity,
clippy::pedantic,
clippy::restriction,
clippy::perf,
clippy::deprecated,
clippy::nursery,
clippy::cargo
)]
#[deny(clippy::correctness)]
#[allow(dead_code, unreachable_code)]
impl ::structopt::StructOptInternal for #name {
#augment_clap
#from_subcommand
fn is_subcommand() -> bool { true }
}
#paw_impl
}
}
fn impl_structopt(input: &DeriveInput) -> TokenStream {
use syn::Data::*;
let struct_name = &input.ident;
set_dummy(quote! {
impl ::structopt::StructOpt for #struct_name {
fn clap<'a, 'b>() -> ::structopt::clap::App<'a, 'b> {
unimplemented!()
}
fn from_clap(_matches: &::structopt::clap::ArgMatches) -> Self {
unimplemented!()
}
}
impl ::structopt::StructOptInternal for #struct_name {}
});
match input.data {
Struct(DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) => impl_structopt_for_struct(struct_name, &fields.named, &input.attrs),
Enum(ref e) => impl_structopt_for_enum(struct_name, &e.variants, &input.attrs),
_ => abort_call_site!("structopt only supports non-tuple structs and enums"),
}
}