// Copyright 2018 Guillaume Pinot (@TeXitoi) , // Kevin Knapp (@kbknapp) , and // Ana Hobden (@hoverbear) // // Licensed under the Apache License, Version 2.0 or the MIT license // , 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 std::env; use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; use proc_macro2::{self, Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::DeriveInput; use syn::{self, ext::IdentExt, spanned::Spanned, Attribute, Field, Ident, LitStr, Type, Variant}; use crate::attr::*; use crate::utils::{extract_doc_comment, format_doc_comment, inner_type, is_simple_ty, Sp, Ty}; /// Default casing style for generated arguments. pub const DEFAULT_CASING: CasingStyle = CasingStyle::Kebab; /// Default casing style for environment variables pub const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake; #[derive(Clone)] pub struct Item { name: Name, casing: Sp, env_casing: Sp, ty: Option, doc_comment: Vec, methods: Vec, deprecations: Vec, value_parser: Option, action: Option, verbatim_doc_comment: bool, force_long_help: bool, next_display_order: Option, next_help_heading: Option, is_enum: bool, is_positional: bool, skip_group: bool, group_id: Name, group_methods: Vec, kind: Sp, } impl Item { pub fn from_args_struct(input: &DeriveInput, name: Name) -> Result { let ident = input.ident.clone(); let span = input.ident.span(); let attrs = &input.attrs; let argument_casing = Sp::new(DEFAULT_CASING, span); let env_casing = Sp::new(DEFAULT_ENV_CASING, span); let kind = Sp::new(Kind::Command(Sp::new(Ty::Other, span)), span); let mut res = Self::new(name, ident, None, argument_casing, env_casing, kind); let parsed_attrs = ClapAttr::parse_all(attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; res.push_doc_comment(attrs, "about", Some("long_about")); Ok(res) } pub fn from_subcommand_enum(input: &DeriveInput, name: Name) -> Result { let ident = input.ident.clone(); let span = input.ident.span(); let attrs = &input.attrs; let argument_casing = Sp::new(DEFAULT_CASING, span); let env_casing = Sp::new(DEFAULT_ENV_CASING, span); let kind = Sp::new(Kind::Command(Sp::new(Ty::Other, span)), span); let mut res = Self::new(name, ident, None, argument_casing, env_casing, kind); let parsed_attrs = ClapAttr::parse_all(attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; res.push_doc_comment(attrs, "about", Some("long_about")); Ok(res) } pub fn from_value_enum(input: &DeriveInput, name: Name) -> Result { let ident = input.ident.clone(); let span = input.ident.span(); let attrs = &input.attrs; let argument_casing = Sp::new(DEFAULT_CASING, span); let env_casing = Sp::new(DEFAULT_ENV_CASING, span); let kind = Sp::new(Kind::Value, span); let mut res = Self::new(name, ident, None, argument_casing, env_casing, kind); let parsed_attrs = ClapAttr::parse_all(attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; // Ignoring `push_doc_comment` as there is no top-level clap builder to add documentation // to if res.has_explicit_methods() { abort!( res.methods[0].name.span(), "{} doesn't exist for `ValueEnum` enums", res.methods[0].name ); } Ok(res) } pub fn from_subcommand_variant( variant: &Variant, struct_casing: Sp, env_casing: Sp, ) -> Result { let name = variant.ident.clone(); let ident = variant.ident.clone(); let span = variant.span(); let ty = match variant.fields { syn::Fields::Unnamed(syn::FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { Ty::from_syn_ty(&unnamed[0].ty) } syn::Fields::Named(_) | syn::Fields::Unnamed(..) | syn::Fields::Unit => { Sp::new(Ty::Other, span) } }; let kind = Sp::new(Kind::Command(ty), span); let mut res = Self::new( Name::Derived(name), ident, None, struct_casing, env_casing, kind, ); let parsed_attrs = ClapAttr::parse_all(&variant.attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; if matches!(&*res.kind, Kind::Command(_) | Kind::Subcommand(_)) { res.push_doc_comment(&variant.attrs, "about", Some("long_about")); } match &*res.kind { Kind::Flatten(_) => { if res.has_explicit_methods() { abort!( res.kind.span(), "methods are not allowed for flattened entry" ); } } Kind::Subcommand(_) | Kind::ExternalSubcommand | Kind::FromGlobal(_) | Kind::Skip(_, _) | Kind::Command(_) | Kind::Value | Kind::Arg(_) => (), } Ok(res) } pub fn from_value_enum_variant( variant: &Variant, argument_casing: Sp, env_casing: Sp, ) -> Result { let ident = variant.ident.clone(); let span = variant.span(); let kind = Sp::new(Kind::Value, span); let mut res = Self::new( Name::Derived(variant.ident.clone()), ident, None, argument_casing, env_casing, kind, ); let parsed_attrs = ClapAttr::parse_all(&variant.attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; if matches!(&*res.kind, Kind::Value) { res.push_doc_comment(&variant.attrs, "help", None); } Ok(res) } pub fn from_args_field( field: &Field, struct_casing: Sp, env_casing: Sp, ) -> Result { let name = field.ident.clone().unwrap(); let ident = field.ident.clone().unwrap(); let span = field.span(); let ty = Ty::from_syn_ty(&field.ty); let kind = Sp::new(Kind::Arg(ty), span); let mut res = Self::new( Name::Derived(name), ident, Some(field.ty.clone()), struct_casing, env_casing, kind, ); let parsed_attrs = ClapAttr::parse_all(&field.attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; if matches!(&*res.kind, Kind::Arg(_)) { res.push_doc_comment(&field.attrs, "help", Some("long_help")); } match &*res.kind { Kind::Flatten(_) => { if res.has_explicit_methods() { abort!( res.kind.span(), "methods are not allowed for flattened entry" ); } } Kind::Subcommand(_) => { if res.has_explicit_methods() { abort!( res.kind.span(), "methods in attributes are not allowed for subcommand" ); } } Kind::Skip(_, _) | Kind::FromGlobal(_) | Kind::Arg(_) | Kind::Command(_) | Kind::Value | Kind::ExternalSubcommand => {} } Ok(res) } fn new( name: Name, ident: Ident, ty: Option, casing: Sp, env_casing: Sp, kind: Sp, ) -> Self { let group_id = Name::Derived(ident); Self { name, ty, casing, env_casing, doc_comment: vec![], methods: vec![], deprecations: vec![], value_parser: None, action: None, verbatim_doc_comment: false, force_long_help: false, next_display_order: None, next_help_heading: None, is_enum: false, is_positional: true, skip_group: false, group_id, group_methods: vec![], kind, } } fn push_method(&mut self, kind: AttrKind, name: Ident, arg: impl ToTokens) { self.push_method_(kind, name, arg.to_token_stream()); } fn push_method_(&mut self, kind: AttrKind, name: Ident, arg: TokenStream) { if name == "id" { match kind { AttrKind::Command | AttrKind::Value => { self.deprecations.push(Deprecation { span: name.span(), id: "id_is_only_for_arg", version: "4.0.0", description: format!( "`#[{}(id)] was allowed by mistake, instead use `#[{}(name)]`", kind.as_str(), kind.as_str() ), }); self.name = Name::Assigned(arg); } AttrKind::Group => { self.group_id = Name::Assigned(arg); } AttrKind::Arg | AttrKind::Clap | AttrKind::StructOpt => { self.name = Name::Assigned(arg); } } } else if name == "name" { match kind { AttrKind::Arg => { self.deprecations.push(Deprecation { span: name.span(), id: "id_is_only_for_arg", version: "4.0.0", description: format!( "`#[{}(name)] was allowed by mistake, instead use `#[{}(id)]` or `#[{}(value_name)]`", kind.as_str(), kind.as_str(), kind.as_str() ), }); self.name = Name::Assigned(arg); } AttrKind::Group => self.group_methods.push(Method::new(name, arg)), AttrKind::Command | AttrKind::Value | AttrKind::Clap | AttrKind::StructOpt => { self.name = Name::Assigned(arg); } } } else if name == "value_parser" { self.value_parser = Some(ValueParser::Explicit(Method::new(name, arg))); } else if name == "action" { self.action = Some(Action::Explicit(Method::new(name, arg))); } else { if name == "short" || name == "long" { self.is_positional = false; } match kind { AttrKind::Group => self.group_methods.push(Method::new(name, arg)), _ => self.methods.push(Method::new(name, arg)), }; } } fn infer_kind(&mut self, attrs: &[ClapAttr]) -> Result<(), syn::Error> { for attr in attrs { if let Some(AttrValue::Call(_)) = &attr.value { continue; } let actual_attr_kind = *attr.kind.get(); let kind = match &attr.magic { Some(MagicAttrName::FromGlobal) => { if attr.value.is_some() { let expr = attr.value_or_abort()?; abort!(expr, "attribute `{}` does not accept a value", attr.name); } let ty = self .kind() .ty() .cloned() .unwrap_or_else(|| Sp::new(Ty::Other, self.kind.span())); let kind = Sp::new(Kind::FromGlobal(ty), attr.name.clone().span()); Some(kind) } Some(MagicAttrName::Subcommand) if attr.value.is_none() => { if attr.value.is_some() { let expr = attr.value_or_abort()?; abort!(expr, "attribute `{}` does not accept a value", attr.name); } let ty = self .kind() .ty() .cloned() .unwrap_or_else(|| Sp::new(Ty::Other, self.kind.span())); let kind = Sp::new(Kind::Subcommand(ty), attr.name.clone().span()); Some(kind) } Some(MagicAttrName::ExternalSubcommand) if attr.value.is_none() => { if attr.value.is_some() { let expr = attr.value_or_abort()?; abort!(expr, "attribute `{}` does not accept a value", attr.name); } let kind = Sp::new(Kind::ExternalSubcommand, attr.name.clone().span()); Some(kind) } Some(MagicAttrName::Flatten) if attr.value.is_none() => { if attr.value.is_some() { let expr = attr.value_or_abort()?; abort!(expr, "attribute `{}` does not accept a value", attr.name); } let ty = self .kind() .ty() .cloned() .unwrap_or_else(|| Sp::new(Ty::Other, self.kind.span())); let kind = Sp::new(Kind::Flatten(ty), attr.name.clone().span()); Some(kind) } Some(MagicAttrName::Skip) if actual_attr_kind != AttrKind::Group => { let expr = attr.value.clone(); let kind = Sp::new( Kind::Skip(expr, self.kind.attr_kind()), attr.name.clone().span(), ); Some(kind) } _ => None, }; if let Some(kind) = kind { self.set_kind(kind)?; } } Ok(()) } fn push_attrs(&mut self, attrs: &[ClapAttr]) -> Result<(), syn::Error> { for attr in attrs { let actual_attr_kind = *attr.kind.get(); let expected_attr_kind = self.kind.attr_kind(); match (actual_attr_kind, expected_attr_kind) { (AttrKind::Clap, _) | (AttrKind::StructOpt, _) => { self.deprecations.push(Deprecation::attribute( "4.0.0", actual_attr_kind, expected_attr_kind, attr.kind.span(), )); } (AttrKind::Group, AttrKind::Command) => {} _ if attr.kind != expected_attr_kind => { abort!( attr.kind.span(), "Expected `{}` attribute instead of `{}`", expected_attr_kind.as_str(), actual_attr_kind.as_str() ); } _ => {} } if let Some(AttrValue::Call(tokens)) = &attr.value { // Force raw mode with method call syntax self.push_method(*attr.kind.get(), attr.name.clone(), quote!(#(#tokens),*)); continue; } match &attr.magic { Some(MagicAttrName::Short) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.push_method( *attr.kind.get(), attr.name.clone(), self.name.clone().translate_char(*self.casing), ); } Some(MagicAttrName::Long) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.push_method(*attr.kind.get(), attr.name.clone(), self.name.clone().translate(*self.casing)); } Some(MagicAttrName::ValueParser) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.deprecations.push(Deprecation { span: attr.name.span(), id: "bare_value_parser", version: "4.0.0", description: "`#[arg(value_parser)]` is now the default and is no longer needed`".to_owned(), }); self.value_parser = Some(ValueParser::Implicit(attr.name.clone())); } Some(MagicAttrName::Action) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.deprecations.push(Deprecation { span: attr.name.span(), id: "bare_action", version: "4.0.0", description: "`#[arg(action)]` is now the default and is no longer needed`".to_owned(), }); self.action = Some(Action::Implicit(attr.name.clone())); } Some(MagicAttrName::Env) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.push_method( *attr.kind.get(), attr.name.clone(), self.name.clone().translate(*self.env_casing), ); } Some(MagicAttrName::ValueEnum) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.is_enum = true } Some(MagicAttrName::VerbatimDocComment) if attr.value.is_none() => { self.verbatim_doc_comment = true } Some(MagicAttrName::About) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Command])?; if let Some(method) = Method::from_env(attr.name.clone(), "CARGO_PKG_DESCRIPTION")? { self.methods.push(method); } } Some(MagicAttrName::LongAbout) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Command])?; self.force_long_help = true; } Some(MagicAttrName::LongHelp) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Arg])?; self.force_long_help = true; } Some(MagicAttrName::Author) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Command])?; if let Some(method) = Method::from_env(attr.name.clone(), "CARGO_PKG_AUTHORS")? { self.methods.push(method); } } Some(MagicAttrName::Version) if attr.value.is_none() => { assert_attr_kind(attr, &[AttrKind::Command])?; if let Some(method) = Method::from_env(attr.name.clone(), "CARGO_PKG_VERSION")? { self.methods.push(method); } } Some(MagicAttrName::DefaultValueT) => { assert_attr_kind(attr, &[AttrKind::Arg])?; let ty = if let Some(ty) = self.ty.as_ref() { ty } else { abort!( attr.name.clone(), "#[arg(default_value_t)] (without an argument) can be used \ only on field level\n\n= note: {note}\n\n", note = "see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") }; let val = if let Some(expr) = &attr.value { quote!(#expr) } else { quote!(<#ty as ::std::default::Default>::default()) }; let val = if attrs .iter() .any(|a| a.magic == Some(MagicAttrName::ValueEnum)) { quote_spanned!(attr.name.clone().span()=> { static DEFAULT_VALUE: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); let s = DEFAULT_VALUE.get_or_init(|| { let val: #ty = #val; clap::ValueEnum::to_possible_value(&val).unwrap().get_name().to_owned() }); let s: &'static str = &*s; s }) } else { quote_spanned!(attr.name.clone().span()=> { static DEFAULT_VALUE: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); let s = DEFAULT_VALUE.get_or_init(|| { let val: #ty = #val; ::std::string::ToString::to_string(&val) }); let s: &'static str = &*s; s }) }; let raw_ident = Ident::new("default_value", attr.name.clone().span()); self.methods.push(Method::new(raw_ident, val)); } Some(MagicAttrName::DefaultValuesT) => { assert_attr_kind(attr, &[AttrKind::Arg])?; let ty = if let Some(ty) = self.ty.as_ref() { ty } else { abort!( attr.name.clone(), "#[arg(default_values_t)] (without an argument) can be used \ only on field level\n\n= note: {note}\n\n", note = "see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") }; let expr = attr.value_or_abort()?; let container_type = Ty::from_syn_ty(ty); if *container_type != Ty::Vec { abort!( attr.name.clone(), "#[arg(default_values_t)] can be used only on Vec types\n\n= note: {note}\n\n", note = "see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") } let inner_type = inner_type(ty); // Use `Borrow<#inner_type>` so we accept `&Vec<#inner_type>` and // `Vec<#inner_type>`. let val = if attrs .iter() .any(|a| a.magic == Some(MagicAttrName::ValueEnum)) { quote_spanned!(attr.name.clone().span()=> { { fn iter_to_vals(iterable: impl IntoIterator) -> impl Iterator where T: ::std::borrow::Borrow<#inner_type> { iterable .into_iter() .map(|val| { clap::ValueEnum::to_possible_value(val.borrow()).unwrap().get_name().to_owned() }) } static DEFAULT_STRINGS: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); static DEFAULT_VALUES: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); DEFAULT_VALUES.get_or_init(|| { DEFAULT_STRINGS.get_or_init(|| iter_to_vals(#expr).collect()).iter().map(::std::string::String::as_str).collect() }).iter().copied() } }) } else { quote_spanned!(attr.name.clone().span()=> { { fn iter_to_vals(iterable: impl IntoIterator) -> impl Iterator where T: ::std::borrow::Borrow<#inner_type> { iterable.into_iter().map(|val| val.borrow().to_string()) } static DEFAULT_STRINGS: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); static DEFAULT_VALUES: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); DEFAULT_VALUES.get_or_init(|| { DEFAULT_STRINGS.get_or_init(|| iter_to_vals(#expr).collect()).iter().map(::std::string::String::as_str).collect() }).iter().copied() } }) }; self.methods.push(Method::new( Ident::new("default_values", attr.name.clone().span()), val, )); } Some(MagicAttrName::DefaultValueOsT) => { assert_attr_kind(attr, &[AttrKind::Arg])?; let ty = if let Some(ty) = self.ty.as_ref() { ty } else { abort!( attr.name.clone(), "#[arg(default_value_os_t)] (without an argument) can be used \ only on field level\n\n= note: {note}\n\n", note = "see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") }; let val = if let Some(expr) = &attr.value { quote!(#expr) } else { quote!(<#ty as ::std::default::Default>::default()) }; let val = if attrs .iter() .any(|a| a.magic == Some(MagicAttrName::ValueEnum)) { quote_spanned!(attr.name.clone().span()=> { static DEFAULT_VALUE: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); let s = DEFAULT_VALUE.get_or_init(|| { let val: #ty = #val; clap::ValueEnum::to_possible_value(&val).unwrap().get_name().to_owned() }); let s: &'static str = &*s; s }) } else { quote_spanned!(attr.name.clone().span()=> { static DEFAULT_VALUE: ::std::sync::OnceLock<::std::ffi::OsString> = ::std::sync::OnceLock::new(); let s = DEFAULT_VALUE.get_or_init(|| { let val: #ty = #val; ::std::ffi::OsString::from(val) }); let s: &'static ::std::ffi::OsStr = &*s; s }) }; let raw_ident = Ident::new("default_value", attr.name.clone().span()); self.methods.push(Method::new(raw_ident, val)); } Some(MagicAttrName::DefaultValuesOsT) => { assert_attr_kind(attr, &[AttrKind::Arg])?; let ty = if let Some(ty) = self.ty.as_ref() { ty } else { abort!( attr.name.clone(), "#[arg(default_values_os_t)] (without an argument) can be used \ only on field level\n\n= note: {note}\n\n", note = "see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") }; let expr = attr.value_or_abort()?; let container_type = Ty::from_syn_ty(ty); if *container_type != Ty::Vec { abort!( attr.name.clone(), "#[arg(default_values_os_t)] can be used only on Vec types\n\n= note: {note}\n\n", note = "see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") } let inner_type = inner_type(ty); // Use `Borrow<#inner_type>` so we accept `&Vec<#inner_type>` and // `Vec<#inner_type>`. let val = if attrs .iter() .any(|a| a.magic == Some(MagicAttrName::ValueEnum)) { quote_spanned!(attr.name.clone().span()=> { { fn iter_to_vals(iterable: impl IntoIterator) -> impl Iterator where T: ::std::borrow::Borrow<#inner_type> { iterable .into_iter() .map(|val| { clap::ValueEnum::to_possible_value(val.borrow()).unwrap().get_name().to_owned().into() }) } static DEFAULT_STRINGS: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); static DEFAULT_VALUES: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); DEFAULT_VALUES.get_or_init(|| { DEFAULT_STRINGS.get_or_init(|| iter_to_vals(#expr).collect()).iter().map(::std::ffi::OsString::as_os_str).collect() }).iter().copied() } }) } else { quote_spanned!(attr.name.clone().span()=> { { fn iter_to_vals(iterable: impl IntoIterator) -> impl Iterator where T: ::std::borrow::Borrow<#inner_type> { iterable.into_iter().map(|val| val.borrow().into()) } static DEFAULT_STRINGS: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); static DEFAULT_VALUES: ::std::sync::OnceLock> = ::std::sync::OnceLock::new(); DEFAULT_VALUES.get_or_init(|| { DEFAULT_STRINGS.get_or_init(|| iter_to_vals(#expr).collect()).iter().map(::std::ffi::OsString::as_os_str).collect() }).iter().copied() } }) }; self.methods.push(Method::new( Ident::new("default_values", attr.name.clone().span()), val, )); } Some(MagicAttrName::NextDisplayOrder) => { assert_attr_kind(attr, &[AttrKind::Command])?; let expr = attr.value_or_abort()?; self.next_display_order = Some(Method::new(attr.name.clone(), quote!(#expr))); } Some(MagicAttrName::NextHelpHeading) => { assert_attr_kind(attr, &[AttrKind::Command])?; let expr = attr.value_or_abort()?; self.next_help_heading = Some(Method::new(attr.name.clone(), quote!(#expr))); } Some(MagicAttrName::RenameAll) => { let lit = attr.lit_str_or_abort()?; self.casing = CasingStyle::from_lit(lit)?; } Some(MagicAttrName::RenameAllEnv) => { assert_attr_kind(attr, &[AttrKind::Command, AttrKind::Arg])?; let lit = attr.lit_str_or_abort()?; self.env_casing = CasingStyle::from_lit(lit)?; } Some(MagicAttrName::Skip) if actual_attr_kind == AttrKind::Group => { self.skip_group = true; } None // Magic only for the default, otherwise just forward to the builder | Some(MagicAttrName::Short) | Some(MagicAttrName::Long) | Some(MagicAttrName::Env) | Some(MagicAttrName::About) | Some(MagicAttrName::LongAbout) | Some(MagicAttrName::LongHelp) | Some(MagicAttrName::Author) | Some(MagicAttrName::Version) => { let expr = attr.value_or_abort()?; self.push_method(*attr.kind.get(), attr.name.clone(), expr); } // Magic only for the default, otherwise just forward to the builder Some(MagicAttrName::ValueParser) | Some(MagicAttrName::Action) => { let expr = attr.value_or_abort()?; self.push_method(*attr.kind.get(), attr.name.clone(), expr); } // Directives that never receive a value Some(MagicAttrName::ValueEnum) | Some(MagicAttrName::VerbatimDocComment) => { let expr = attr.value_or_abort()?; abort!(expr, "attribute `{}` does not accept a value", attr.name); } // Kinds Some(MagicAttrName::FromGlobal) | Some(MagicAttrName::Subcommand) | Some(MagicAttrName::ExternalSubcommand) | Some(MagicAttrName::Flatten) | Some(MagicAttrName::Skip) => { } } } if self.has_explicit_methods() { if let Kind::Skip(_, attr) = &*self.kind { abort!( self.methods[0].name.span(), "`{}` cannot be used with `#[{}(skip)]", self.methods[0].name, attr.as_str(), ); } if let Kind::FromGlobal(_) = &*self.kind { abort!( self.methods[0].name.span(), "`{}` cannot be used with `#[arg(from_global)]", self.methods[0].name, ); } } Ok(()) } fn push_doc_comment(&mut self, attrs: &[Attribute], short_name: &str, long_name: Option<&str>) { let lines = extract_doc_comment(attrs); if !lines.is_empty() { let (short_help, long_help) = format_doc_comment(&lines, !self.verbatim_doc_comment, self.force_long_help); let short_name = format_ident!("{short_name}"); let short = Method::new( short_name, short_help .map(|h| quote!(#h)) .unwrap_or_else(|| quote!(None)), ); self.doc_comment.push(short); if let Some(long_name) = long_name { let long_name = format_ident!("{long_name}"); let long = Method::new( long_name, long_help .map(|h| quote!(#h)) .unwrap_or_else(|| quote!(None)), ); self.doc_comment.push(long); } } } fn set_kind(&mut self, kind: Sp) -> Result<(), syn::Error> { match (self.kind.get(), kind.get()) { (Kind::Arg(_), Kind::FromGlobal(_)) | (Kind::Arg(_), Kind::Subcommand(_)) | (Kind::Arg(_), Kind::Flatten(_)) | (Kind::Arg(_), Kind::Skip(_, _)) | (Kind::Command(_), Kind::Subcommand(_)) | (Kind::Command(_), Kind::Flatten(_)) | (Kind::Command(_), Kind::Skip(_, _)) | (Kind::Command(_), Kind::ExternalSubcommand) | (Kind::Value, Kind::Skip(_, _)) => { self.kind = kind; } (_, _) => { let old = self.kind.name(); let new = kind.name(); abort!(kind.span(), "`{new}` cannot be used with `{old}`"); } } Ok(()) } pub fn find_default_method(&self) -> Option<&Method> { self.methods .iter() .find(|m| m.name == "default_value" || m.name == "default_value_os") } /// generate methods from attributes on top of struct or enum pub fn initial_top_level_methods(&self) -> TokenStream { let next_display_order = self.next_display_order.as_ref().into_iter(); let next_help_heading = self.next_help_heading.as_ref().into_iter(); quote!( #(#next_display_order)* #(#next_help_heading)* ) } pub fn final_top_level_methods(&self) -> TokenStream { let methods = &self.methods; let doc_comment = &self.doc_comment; quote!( #(#doc_comment)* #(#methods)*) } /// generate methods on top of a field pub fn field_methods(&self) -> proc_macro2::TokenStream { let methods = &self.methods; let doc_comment = &self.doc_comment; quote!( #(#doc_comment)* #(#methods)* ) } pub fn group_id(&self) -> TokenStream { self.group_id.clone().raw() } pub fn group_methods(&self) -> TokenStream { let group_methods = &self.group_methods; quote!( #(#group_methods)* ) } pub fn deprecations(&self) -> proc_macro2::TokenStream { let deprecations = &self.deprecations; quote!( #(#deprecations)* ) } pub fn next_display_order(&self) -> TokenStream { let next_display_order = self.next_display_order.as_ref().into_iter(); quote!( #(#next_display_order)* ) } pub fn next_help_heading(&self) -> TokenStream { let next_help_heading = self.next_help_heading.as_ref().into_iter(); quote!( #(#next_help_heading)* ) } pub fn id(&self) -> TokenStream { self.name.clone().raw() } pub fn cased_name(&self) -> TokenStream { self.name.clone().translate(*self.casing) } pub fn value_name(&self) -> TokenStream { self.name.clone().translate(CasingStyle::ScreamingSnake) } pub fn value_parser(&self, field_type: &Type) -> Method { self.value_parser .clone() .map(|p| { let inner_type = inner_type(field_type); p.resolve(inner_type) }) .unwrap_or_else(|| { let inner_type = inner_type(field_type); if let Some(action) = self.action.as_ref() { let span = action.span(); default_value_parser(inner_type, span) } else { let span = self .action .as_ref() .map(|a| a.span()) .unwrap_or_else(|| self.kind.span()); default_value_parser(inner_type, span) } }) } pub fn action(&self, field_type: &Type) -> Method { self.action .clone() .map(|p| p.resolve(field_type)) .unwrap_or_else(|| { if let Some(value_parser) = self.value_parser.as_ref() { let span = value_parser.span(); default_action(field_type, span) } else { let span = self .value_parser .as_ref() .map(|a| a.span()) .unwrap_or_else(|| self.kind.span()); default_action(field_type, span) } }) } pub fn kind(&self) -> Sp { self.kind.clone() } pub fn is_positional(&self) -> bool { self.is_positional } pub fn casing(&self) -> Sp { self.casing } pub fn env_casing(&self) -> Sp { self.env_casing } pub fn has_explicit_methods(&self) -> bool { self.methods .iter() .any(|m| m.name != "help" && m.name != "long_help") } pub fn skip_group(&self) -> bool { self.skip_group } } #[derive(Clone)] enum ValueParser { Explicit(Method), Implicit(Ident), } impl ValueParser { fn resolve(self, _inner_type: &Type) -> Method { match self { Self::Explicit(method) => method, Self::Implicit(ident) => default_value_parser(_inner_type, ident.span()), } } fn span(&self) -> Span { match self { Self::Explicit(method) => method.name.span(), Self::Implicit(ident) => ident.span(), } } } fn default_value_parser(inner_type: &Type, span: Span) -> Method { let func = Ident::new("value_parser", span); Method::new( func, quote_spanned! { span=> clap::value_parser!(#inner_type) }, ) } #[derive(Clone)] pub enum Action { Explicit(Method), Implicit(Ident), } impl Action { pub fn resolve(self, _field_type: &Type) -> Method { match self { Self::Explicit(method) => method, Self::Implicit(ident) => default_action(_field_type, ident.span()), } } pub fn span(&self) -> Span { match self { Self::Explicit(method) => method.name.span(), Self::Implicit(ident) => ident.span(), } } } fn default_action(field_type: &Type, span: Span) -> Method { let ty = Ty::from_syn_ty(field_type); let args = match *ty { Ty::Vec | Ty::OptionVec | Ty::VecVec | Ty::OptionVecVec => { quote_spanned! { span=> clap::ArgAction::Append } } Ty::Option | Ty::OptionOption => { quote_spanned! { span=> clap::ArgAction::Set } } _ => { if is_simple_ty(field_type, "bool") { quote_spanned! { span=> clap::ArgAction::SetTrue } } else { quote_spanned! { span=> clap::ArgAction::Set } } } }; let func = Ident::new("action", span); Method::new(func, args) } #[allow(clippy::large_enum_variant)] #[derive(Clone)] pub enum Kind { Arg(Sp), Command(Sp), Value, FromGlobal(Sp), Subcommand(Sp), Flatten(Sp), Skip(Option, AttrKind), ExternalSubcommand, } impl Kind { pub fn name(&self) -> &'static str { match self { Self::Arg(_) => "arg", Self::Command(_) => "command", Self::Value => "value", Self::FromGlobal(_) => "from_global", Self::Subcommand(_) => "subcommand", Self::Flatten(_) => "flatten", Self::Skip(_, _) => "skip", Self::ExternalSubcommand => "external_subcommand", } } pub fn attr_kind(&self) -> AttrKind { match self { Self::Arg(_) => AttrKind::Arg, Self::Command(_) => AttrKind::Command, Self::Value => AttrKind::Value, Self::FromGlobal(_) => AttrKind::Arg, Self::Subcommand(_) => AttrKind::Command, Self::Flatten(_) => AttrKind::Command, Self::Skip(_, kind) => *kind, Self::ExternalSubcommand => AttrKind::Command, } } pub fn ty(&self) -> Option<&Sp> { match self { Self::Arg(ty) | Self::Command(ty) | Self::Flatten(ty) | Self::FromGlobal(ty) | Self::Subcommand(ty) => Some(ty), Self::Value | Self::Skip(_, _) | Self::ExternalSubcommand => None, } } } #[derive(Clone)] pub struct Method { name: Ident, args: TokenStream, } impl Method { pub fn new(name: Ident, args: TokenStream) -> Self { Method { name, args } } fn from_env(ident: Ident, env_var: &str) -> Result, syn::Error> { let mut lit = match env::var(env_var) { Ok(val) => { if val.is_empty() { return Ok(None); } LitStr::new(&val, ident.span()) } Err(_) => { abort!( ident, "cannot derive `{}` from Cargo.toml\n\n= note: {note}\n\n= help: {help}\n\n", ident, note = format_args!("`{env_var}` environment variable is not set"), help = format_args!("use `{ident} = \"...\"` to set {ident} manually") ); } }; if ident == "author" { let edited = process_author_str(&lit.value()); lit = LitStr::new(&edited, lit.span()); } Ok(Some(Method::new(ident, quote!(#lit)))) } pub(crate) fn args(&self) -> &TokenStream { &self.args } } impl ToTokens for Method { fn to_tokens(&self, ts: &mut proc_macro2::TokenStream) { let Method { ref name, ref args } = self; let tokens = quote!( .#name(#args) ); tokens.to_tokens(ts); } } #[derive(Clone)] pub struct Deprecation { pub span: Span, pub id: &'static str, pub version: &'static str, pub description: String, } impl Deprecation { fn attribute(version: &'static str, old: AttrKind, new: AttrKind, span: Span) -> Self { Self { span, id: "old_attribute", version, description: format!( "Attribute `#[{}(...)]` has been deprecated in favor of `#[{}(...)]`", old.as_str(), new.as_str() ), } } } impl ToTokens for Deprecation { fn to_tokens(&self, ts: &mut proc_macro2::TokenStream) { let tokens = if cfg!(feature = "deprecated") { let Deprecation { span, id, version, description, } = self; let span = *span; let id = Ident::new(id, span); quote_spanned!(span=> { #[deprecated(since = #version, note = #description)] fn #id() {} #id(); }) } else { quote!() }; tokens.to_tokens(ts); } } fn assert_attr_kind(attr: &ClapAttr, possible_kind: &[AttrKind]) -> Result<(), syn::Error> { if *attr.kind.get() == AttrKind::Clap || *attr.kind.get() == AttrKind::StructOpt { // deprecated } else if !possible_kind.contains(attr.kind.get()) { let options = possible_kind .iter() .map(|k| format!("`#[{}({})]`", k.as_str(), attr.name)) .collect::>(); abort!( attr.name, "Unknown `#[{}({})]` attribute ({} exists)", attr.kind.as_str(), attr.name, options.join(", ") ); } Ok(()) } /// replace all `:` with `, ` when not inside the `<>` /// /// `"author1:author2:author3" => "author1, author2, author3"` /// `"author1 :author2" => "author1 , author2" fn process_author_str(author: &str) -> String { let mut res = String::with_capacity(author.len()); let mut inside_angle_braces = 0usize; for ch in author.chars() { if inside_angle_braces > 0 && ch == '>' { inside_angle_braces -= 1; res.push(ch); } else if ch == '<' { inside_angle_braces += 1; res.push(ch); } else if inside_angle_braces == 0 && ch == ':' { res.push_str(", "); } else { res.push(ch); } } res } /// Defines the casing for the attributes long representation. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum CasingStyle { /// Indicate word boundaries with uppercase letter, excluding the first word. Camel, /// Keep all letters lowercase and indicate word boundaries with hyphens. Kebab, /// Indicate word boundaries with uppercase letter, including the first word. Pascal, /// Keep all letters uppercase and indicate word boundaries with underscores. ScreamingSnake, /// Keep all letters lowercase and indicate word boundaries with underscores. Snake, /// Keep all letters lowercase and remove word boundaries. Lower, /// Keep all letters uppercase and remove word boundaries. Upper, /// Use the original attribute name defined in the code. Verbatim, } impl CasingStyle { fn from_lit(name: &LitStr) -> Result, syn::Error> { use self::CasingStyle::*; let normalized = name.value().to_upper_camel_case().to_lowercase(); let cs = |kind| Sp::new(kind, name.span()); let s = match normalized.as_ref() { "camel" | "camelcase" => cs(Camel), "kebab" | "kebabcase" => cs(Kebab), "pascal" | "pascalcase" => cs(Pascal), "screamingsnake" | "screamingsnakecase" => cs(ScreamingSnake), "snake" | "snakecase" => cs(Snake), "lower" | "lowercase" => cs(Lower), "upper" | "uppercase" => cs(Upper), "verbatim" | "verbatimcase" => cs(Verbatim), s => abort!(name, "unsupported casing: `{s}`"), }; Ok(s) } } #[derive(Clone)] pub enum Name { Derived(Ident), Assigned(TokenStream), } impl Name { pub fn raw(self) -> TokenStream { match self { Name::Assigned(tokens) => tokens, Name::Derived(ident) => { let s = ident.unraw().to_string(); quote_spanned!(ident.span()=> #s) } } } pub fn translate(self, style: CasingStyle) -> TokenStream { use CasingStyle::*; match self { Name::Assigned(tokens) => tokens, Name::Derived(ident) => { let s = ident.unraw().to_string(); let s = match style { Pascal => s.to_upper_camel_case(), Kebab => s.to_kebab_case(), Camel => s.to_lower_camel_case(), ScreamingSnake => s.to_shouty_snake_case(), Snake => s.to_snake_case(), Lower => s.to_snake_case().replace('_', ""), Upper => s.to_shouty_snake_case().replace('_', ""), Verbatim => s, }; quote_spanned!(ident.span()=> #s) } } } pub fn translate_char(self, style: CasingStyle) -> TokenStream { use CasingStyle::*; match self { Name::Assigned(tokens) => quote!( (#tokens).chars().next().unwrap() ), Name::Derived(ident) => { let s = ident.unraw().to_string(); let s = match style { Pascal => s.to_upper_camel_case(), Kebab => s.to_kebab_case(), Camel => s.to_lower_camel_case(), ScreamingSnake => s.to_shouty_snake_case(), Snake => s.to_snake_case(), Lower => s.to_snake_case(), Upper => s.to_shouty_snake_case(), Verbatim => s, }; let s = s.chars().next().unwrap(); quote_spanned!(ident.span()=> #s) } } } }