use std::iter::FromIterator; use proc_macro_error::{abort, ResultExt}; use quote::ToTokens; use syn::{ self, parenthesized, parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, Attribute, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Token, }; pub fn parse_clap_attributes(all_attrs: &[Attribute]) -> Vec { all_attrs .iter() .filter(|attr| attr.path.is_ident("clap") || attr.path.is_ident("structopt")) .flat_map(|attr| { attr.parse_args_with(Punctuated::::parse_terminated) .unwrap_or_abort() }) .collect() } #[allow(clippy::large_enum_variant)] #[derive(Clone)] pub enum ClapAttr { // single-identifier attributes Short(Ident), Long(Ident), ValueParser(Ident), Action(Ident), Env(Ident), Flatten(Ident), ValueEnum(Ident), FromGlobal(Ident), Subcommand(Ident), VerbatimDocComment(Ident), ExternalSubcommand(Ident), About(Ident), Author(Ident), Version(Ident), // ident = "string literal" RenameAllEnv(Ident, LitStr), RenameAll(Ident, LitStr), NameLitStr(Ident, LitStr), // parse(parser_kind [= parser_func]) Parse(Ident, ParserSpec), // ident [= arbitrary_expr] Skip(Ident, Option), // ident = arbitrary_expr NameExpr(Ident, Expr), DefaultValueT(Ident, Option), DefaultValueOsT(Ident, Option), NextDisplayOrder(Ident, Expr), NextHelpHeading(Ident, Expr), HelpHeading(Ident, Expr), // ident(arbitrary_expr,*) MethodCall(Ident, Vec), } impl Parse for ClapAttr { fn parse(input: ParseStream) -> syn::Result { use self::ClapAttr::*; let name: Ident = input.parse()?; let name_str = name.to_string(); if input.peek(Token![=]) { // `name = value` attributes. let assign_token = input.parse::()?; // skip '=' if input.peek(LitStr) { let lit: LitStr = input.parse()?; match &*name_str { "rename_all" => Ok(RenameAll(name, lit)), "rename_all_env" => Ok(RenameAllEnv(name, lit)), "skip" => { let expr = ExprLit { attrs: vec![], lit: Lit::Str(lit), }; let expr = Expr::Lit(expr); Ok(Skip(name, Some(expr))) } "next_display_order" => { let expr = ExprLit { attrs: vec![], lit: Lit::Str(lit), }; let expr = Expr::Lit(expr); Ok(NextDisplayOrder(name, expr)) } "next_help_heading" => { let expr = ExprLit { attrs: vec![], lit: Lit::Str(lit), }; let expr = Expr::Lit(expr); Ok(NextHelpHeading(name, 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::() { Ok(expr) => match &*name_str { "skip" => Ok(Skip(name, Some(expr))), "default_value_t" => Ok(DefaultValueT(name, Some(expr))), "default_value_os_t" => Ok(DefaultValueOsT(name, Some(expr))), "next_display_order" => Ok(NextDisplayOrder(name, expr)), "next_help_heading" => Ok(NextHelpHeading(name, expr)), "help_heading" => Ok(HelpHeading(name, expr)), _ => Ok(NameExpr(name, expr)), }, Err(_) => abort! { assign_token, "expected `string literal` or `expression` after `=`" }, } } } else if input.peek(syn::token::Paren) { // `name(...)` attributes. let nested; parenthesized!(nested in input); match name_str.as_ref() { "parse" => { let parser_specs: Punctuated = nested.parse_terminated(ParserSpec::parse)?; if parser_specs.len() == 1 { Ok(Parse(name, parser_specs[0].clone())) } else { abort!(name, "parse must have exactly one argument") } } "raw" => match nested.parse::() { Ok(bool_token) => { let expr = ExprLit { attrs: vec![], lit: Lit::Bool(bool_token), }; let expr = Expr::Lit(expr); Ok(MethodCall(name, vec![expr])) } Err(_) => { abort!(name, "`#[clap(raw(...))` attributes are removed, \ they are replaced with raw methods"; help = "if you meant to call `clap::Arg::raw()` method \ you should use bool literal, like `raw(true)` or `raw(false)`"; note = raw_method_suggestion(nested); ); } }, _ => { let method_args: Punctuated<_, Token![,]> = nested.parse_terminated(Expr::parse)?; Ok(MethodCall(name, Vec::from_iter(method_args))) } } } else { // Attributes represented with a sole identifier. match name_str.as_ref() { "long" => Ok(Long(name)), "short" => Ok(Short(name)), "value_parser" => Ok(ValueParser(name)), "action" => Ok(Action(name)), "env" => Ok(Env(name)), "flatten" => Ok(Flatten(name)), "arg_enum" => Ok(ValueEnum(name)), "value_enum" => Ok(ValueEnum(name)), "from_global" => Ok(FromGlobal(name)), "subcommand" => Ok(Subcommand(name)), "external_subcommand" => Ok(ExternalSubcommand(name)), "verbatim_doc_comment" => Ok(VerbatimDocComment(name)), "default_value" => { abort!(name, "`#[clap(default_value)` attribute (without a value) has been replaced by `#[clap(default_value_t)]`."; help = "Change the attribute to `#[clap(default_value_t)]`"; ) } "default_value_t" => Ok(DefaultValueT(name, None)), "default_value_os_t" => Ok(DefaultValueOsT(name, None)), "about" => (Ok(About(name))), "author" => (Ok(Author(name))), "version" => Ok(Version(name)), "skip" => Ok(Skip(name, None)), _ => abort!(name, "unexpected attribute: {}", name_str), } } } } #[derive(Clone)] pub struct ParserSpec { pub kind: Ident, pub eq_token: Option, pub parse_func: Option, } impl Parse for ParserSpec { fn parse(input: ParseStream<'_>) -> syn::Result { let kind = input .parse() .map_err(|_| input.error("parser specification must start with identifier"))?; let eq_token = input.parse()?; let parse_func = match eq_token { None => None, Some(_) => Some(input.parse()?), }; Ok(ParserSpec { kind, eq_token, parse_func, }) } } fn raw_method_suggestion(ts: ParseBuffer) -> String { let do_parse = move || -> Result<(Ident, Punctuated), syn::Error> { let name = ts.parse()?; let _eq: Token![=] = ts.parse()?; let val: LitStr = ts.parse()?; let exprs = val.parse_with(Punctuated::::parse_terminated)?; Ok((name, exprs)) }; fn to_string(val: &T) -> String { val.to_token_stream() .to_string() .replace(' ', "") .replace(',', ", ") } if let Ok((name, exprs)) = do_parse() { let suggestion = if exprs.len() == 1 { let val = to_string(&exprs[0]); format!(" = {}", val) } else { let val = exprs .into_iter() .map(|expr| to_string(&expr)) .collect::>() .join(", "); format!("({})", val) }; format!( "if you need to call `clap::Arg/Command::{}` method you \ can do it like this: #[clap({}{})]", name, name, suggestion ) } else { "if you need to call some method from `clap::Arg/Command` \ you should use raw method, see \ https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#raw-attributes" .into() } }