1 use std::iter::FromIterator;
2 
3 use proc_macro_error::{abort, ResultExt};
4 use quote::ToTokens;
5 use syn::{
6     self, parenthesized,
7     parse::{Parse, ParseBuffer, ParseStream},
8     punctuated::Punctuated,
9     Attribute, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Token,
10 };
11 
12 #[allow(clippy::large_enum_variant)]
13 pub enum ClapAttr {
14     // single-identifier attributes
15     Short(Ident),
16     Long(Ident),
17     Env(Ident),
18     Flatten(Ident),
19     ArgEnum(Ident),
20     FromGlobal(Ident),
21     Subcommand(Ident),
22     VerbatimDocComment(Ident),
23     ExternalSubcommand(Ident),
24 
25     // ident [= "string literal"]
26     About(Ident, Option<LitStr>),
27     Author(Ident, Option<LitStr>),
28     Version(Ident, Option<LitStr>),
29 
30     // ident = "string literal"
31     RenameAllEnv(Ident, LitStr),
32     RenameAll(Ident, LitStr),
33     NameLitStr(Ident, LitStr),
34 
35     // parse(parser_kind [= parser_func])
36     Parse(Ident, ParserSpec),
37 
38     // ident [= arbitrary_expr]
39     Skip(Ident, Option<Expr>),
40 
41     // ident = arbitrary_expr
42     NameExpr(Ident, Expr),
43     DefaultValueT(Ident, Option<Expr>),
44     HelpHeading(Ident, Expr),
45 
46     // ident(arbitrary_expr,*)
47     MethodCall(Ident, Vec<Expr>),
48 }
49 
50 impl Parse for ClapAttr {
parse(input: ParseStream) -> syn::Result<Self>51     fn parse(input: ParseStream) -> syn::Result<Self> {
52         use self::ClapAttr::*;
53 
54         let name: Ident = input.parse()?;
55         let name_str = name.to_string();
56 
57         if input.peek(Token![=]) {
58             // `name = value` attributes.
59             let assign_token = input.parse::<Token![=]>()?; // skip '='
60 
61             if input.peek(LitStr) {
62                 let lit: LitStr = input.parse()?;
63                 let lit_str = lit.value();
64 
65                 let check_empty_lit = |s| {
66                     if lit_str.is_empty() {
67                         abort!(
68                             lit,
69                             "`#[clap({} = \"\")` is deprecated, \
70                              now it's default behavior",
71                             s
72                         );
73                     }
74                 };
75 
76                 match &*name_str {
77                     "rename_all" => Ok(RenameAll(name, lit)),
78                     "rename_all_env" => Ok(RenameAllEnv(name, lit)),
79 
80                     "version" => {
81                         check_empty_lit("version");
82                         Ok(Version(name, Some(lit)))
83                     }
84 
85                     "author" => {
86                         check_empty_lit("author");
87                         Ok(Author(name, Some(lit)))
88                     }
89 
90                     "about" => {
91                         check_empty_lit("about");
92                         Ok(About(name, Some(lit)))
93                     }
94 
95                     "skip" => {
96                         let expr = ExprLit {
97                             attrs: vec![],
98                             lit: Lit::Str(lit),
99                         };
100                         let expr = Expr::Lit(expr);
101                         Ok(Skip(name, Some(expr)))
102                     }
103 
104                     "help_heading" => {
105                         let expr = ExprLit {
106                             attrs: vec![],
107                             lit: Lit::Str(lit),
108                         };
109                         let expr = Expr::Lit(expr);
110                         Ok(HelpHeading(name, expr))
111                     }
112 
113                     _ => Ok(NameLitStr(name, lit)),
114                 }
115             } else {
116                 match input.parse::<Expr>() {
117                     Ok(expr) => match &*name_str {
118                         "skip" => Ok(Skip(name, Some(expr))),
119                         "default_value_t" => Ok(DefaultValueT(name, Some(expr))),
120                         "help_heading" => Ok(HelpHeading(name, expr)),
121                         _ => Ok(NameExpr(name, expr)),
122                     },
123 
124                     Err(_) => abort! {
125                         assign_token,
126                         "expected `string literal` or `expression` after `=`"
127                     },
128                 }
129             }
130         } else if input.peek(syn::token::Paren) {
131             // `name(...)` attributes.
132             let nested;
133             parenthesized!(nested in input);
134 
135             match name_str.as_ref() {
136                 "parse" => {
137                     let parser_specs: Punctuated<ParserSpec, Token![,]> =
138                         nested.parse_terminated(ParserSpec::parse)?;
139 
140                     if parser_specs.len() == 1 {
141                         Ok(Parse(name, parser_specs[0].clone()))
142                     } else {
143                         abort!(name, "parse must have exactly one argument")
144                     }
145                 }
146 
147                 "raw" => match nested.parse::<LitBool>() {
148                     Ok(bool_token) => {
149                         let expr = ExprLit {
150                             attrs: vec![],
151                             lit: Lit::Bool(bool_token),
152                         };
153                         let expr = Expr::Lit(expr);
154                         Ok(MethodCall(name, vec![expr]))
155                     }
156 
157                     Err(_) => {
158                         abort!(name,
159                             "`#[clap(raw(...))` attributes are removed, \
160                             they are replaced with raw methods";
161                             help = "if you meant to call `clap::Arg::raw()` method \
162                                 you should use bool literal, like `raw(true)` or `raw(false)`";
163                             note = raw_method_suggestion(nested);
164                         );
165                     }
166                 },
167 
168                 _ => {
169                     let method_args: Punctuated<_, Token![,]> =
170                         nested.parse_terminated(Expr::parse)?;
171                     Ok(MethodCall(name, Vec::from_iter(method_args)))
172                 }
173             }
174         } else {
175             // Attributes represented with a sole identifier.
176             match name_str.as_ref() {
177                 "long" => Ok(Long(name)),
178                 "short" => Ok(Short(name)),
179                 "env" => Ok(Env(name)),
180                 "flatten" => Ok(Flatten(name)),
181                 "arg_enum" => Ok(ArgEnum(name)),
182                 "from_global" => Ok(FromGlobal(name)),
183                 "subcommand" => Ok(Subcommand(name)),
184                 "external_subcommand" => Ok(ExternalSubcommand(name)),
185                 "verbatim_doc_comment" => Ok(VerbatimDocComment(name)),
186 
187                 "default_value" => {
188                     abort!(name,
189                         "`#[clap(default_value)` attribute (without a value) has been replaced by `#[clap(default_value_t)]`.";
190                         help = "Change the attribute to `#[clap(default_value_t)]`";
191                     )
192                 }
193                 "default_value_t" => Ok(DefaultValueT(name, None)),
194                 "about" => (Ok(About(name, None))),
195                 "author" => (Ok(Author(name, None))),
196                 "version" => Ok(Version(name, None)),
197 
198                 "skip" => Ok(Skip(name, None)),
199 
200                 _ => abort!(name, "unexpected attribute: {}", name_str),
201             }
202         }
203     }
204 }
205 
206 #[derive(Clone)]
207 pub struct ParserSpec {
208     pub kind: Ident,
209     pub eq_token: Option<Token![=]>,
210     pub parse_func: Option<Expr>,
211 }
212 
213 impl Parse for ParserSpec {
parse(input: ParseStream<'_>) -> syn::Result<Self>214     fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
215         let kind = input
216             .parse()
217             .map_err(|_| input.error("parser specification must start with identifier"))?;
218         let eq_token = input.parse()?;
219         let parse_func = match eq_token {
220             None => None,
221             Some(_) => Some(input.parse()?),
222         };
223         Ok(ParserSpec {
224             kind,
225             eq_token,
226             parse_func,
227         })
228     }
229 }
230 
raw_method_suggestion(ts: ParseBuffer) -> String231 fn raw_method_suggestion(ts: ParseBuffer) -> String {
232     let do_parse = move || -> Result<(Ident, Punctuated<Expr, Token![,]>), syn::Error> {
233         let name = ts.parse()?;
234         let _eq: Token![=] = ts.parse()?;
235         let val: LitStr = ts.parse()?;
236         let exprs = val.parse_with(Punctuated::<Expr, Token![,]>::parse_terminated)?;
237         Ok((name, exprs))
238     };
239 
240     fn to_string<T: ToTokens>(val: &T) -> String {
241         val.to_token_stream()
242             .to_string()
243             .replace(" ", "")
244             .replace(",", ", ")
245     }
246 
247     if let Ok((name, exprs)) = do_parse() {
248         let suggestion = if exprs.len() == 1 {
249             let val = to_string(&exprs[0]);
250             format!(" = {}", val)
251         } else {
252             let val = exprs
253                 .into_iter()
254                 .map(|expr| to_string(&expr))
255                 .collect::<Vec<_>>()
256                 .join(", ");
257 
258             format!("({})", val)
259         };
260 
261         format!(
262             "if you need to call `clap::Arg/App::{}` method you \
263              can do it like this: #[clap({}{})]",
264             name, name, suggestion
265         )
266     } else {
267         "if you need to call some method from `clap::Arg/App` \
268          you should use raw method, see \
269          https://docs.rs/structopt/0.3/structopt/#raw-methods"
270             .into()
271     }
272 }
273 
parse_clap_attributes(all_attrs: &[Attribute]) -> Vec<ClapAttr>274 pub fn parse_clap_attributes(all_attrs: &[Attribute]) -> Vec<ClapAttr> {
275     all_attrs
276         .iter()
277         .filter(|attr| attr.path.is_ident("clap"))
278         .flat_map(|attr| {
279             attr.parse_args_with(Punctuated::<ClapAttr, Token![,]>::parse_terminated)
280                 .unwrap_or_abort()
281         })
282         .collect()
283 }
284