• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>
2 //
3 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6 // option. This file may not be copied, modified, or distributed
7 // except according to those terms.
8 
9 use crate::doc_comments::process_doc_comment;
10 use crate::{parse::*, spanned::Sp, ty::Ty};
11 
12 use std::env;
13 
14 use heck::{CamelCase, KebabCase, MixedCase, ShoutySnakeCase, SnakeCase};
15 use proc_macro2::{Span, TokenStream};
16 use proc_macro_error::abort;
17 use quote::{quote, quote_spanned, ToTokens};
18 use syn::{
19     self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Ident, LitStr, MetaNameValue, Type,
20 };
21 
22 #[derive(Clone)]
23 pub enum Kind {
24     Arg(Sp<Ty>),
25     Subcommand(Sp<Ty>),
26     ExternalSubcommand,
27     Flatten,
28     Skip(Option<Expr>),
29 }
30 
31 #[derive(Clone)]
32 pub struct Method {
33     name: Ident,
34     args: TokenStream,
35 }
36 
37 #[derive(Clone)]
38 pub struct Parser {
39     pub kind: Sp<ParserKind>,
40     pub func: TokenStream,
41 }
42 
43 #[derive(Debug, PartialEq, Clone)]
44 pub enum ParserKind {
45     FromStr,
46     TryFromStr,
47     FromOsStr,
48     TryFromOsStr,
49     FromOccurrences,
50     FromFlag,
51 }
52 
53 /// Defines the casing for the attributes long representation.
54 #[derive(Copy, Clone, Debug, PartialEq)]
55 pub enum CasingStyle {
56     /// Indicate word boundaries with uppercase letter, excluding the first word.
57     Camel,
58     /// Keep all letters lowercase and indicate word boundaries with hyphens.
59     Kebab,
60     /// Indicate word boundaries with uppercase letter, including the first word.
61     Pascal,
62     /// Keep all letters uppercase and indicate word boundaries with underscores.
63     ScreamingSnake,
64     /// Keep all letters lowercase and indicate word boundaries with underscores.
65     Snake,
66     /// Use the original attribute name defined in the code.
67     Verbatim,
68     /// Keep all letters lowercase and remove word boundaries.
69     Lower,
70     /// Keep all letters uppercase and remove word boundaries.
71     Upper,
72 }
73 
74 #[derive(Clone)]
75 pub enum Name {
76     Derived(Ident),
77     Assigned(TokenStream),
78 }
79 
80 #[derive(Clone)]
81 pub struct Attrs {
82     name: Name,
83     casing: Sp<CasingStyle>,
84     env_casing: Sp<CasingStyle>,
85     ty: Option<Type>,
86     doc_comment: Vec<Method>,
87     methods: Vec<Method>,
88     parser: Sp<Parser>,
89     author: Option<Method>,
90     about: Option<Method>,
91     version: Option<Method>,
92     no_version: Option<Ident>,
93     verbatim_doc_comment: Option<Ident>,
94     has_custom_parser: bool,
95     kind: Sp<Kind>,
96 }
97 
98 impl Method {
new(name: Ident, args: TokenStream) -> Self99     pub fn new(name: Ident, args: TokenStream) -> Self {
100         Method { name, args }
101     }
102 
from_lit_or_env(ident: Ident, lit: Option<LitStr>, env_var: &str) -> Option<Self>103     fn from_lit_or_env(ident: Ident, lit: Option<LitStr>, env_var: &str) -> Option<Self> {
104         let mut lit = match lit {
105             Some(lit) => lit,
106 
107             None => match env::var(env_var) {
108                 Ok(val) => LitStr::new(&val, ident.span()),
109                 Err(_) => {
110                     abort!(ident,
111                         "cannot derive `{}` from Cargo.toml", ident;
112                         note = "`{}` environment variable is not set", env_var;
113                         help = "use `{} = \"...\"` to set {} manually", ident, ident;
114                     );
115                 }
116             },
117         };
118 
119         if ident == "author" {
120             let edited = process_author_str(&lit.value());
121             lit = LitStr::new(&edited, lit.span());
122         }
123 
124         Some(Method::new(ident, quote!(#lit)))
125     }
126 }
127 
128 impl ToTokens for Method {
to_tokens(&self, ts: &mut TokenStream)129     fn to_tokens(&self, ts: &mut TokenStream) {
130         let Method { ref name, ref args } = self;
131         quote!(.#name(#args)).to_tokens(ts);
132     }
133 }
134 
135 impl Parser {
default_spanned(span: Span) -> Sp<Self>136     fn default_spanned(span: Span) -> Sp<Self> {
137         let kind = Sp::new(ParserKind::TryFromStr, span);
138         let func = quote_spanned!(span=> ::std::str::FromStr::from_str);
139         Sp::new(Parser { kind, func }, span)
140     }
141 
from_spec(parse_ident: Ident, spec: ParserSpec) -> Sp<Self>142     fn from_spec(parse_ident: Ident, spec: ParserSpec) -> Sp<Self> {
143         use ParserKind::*;
144 
145         let kind = match &*spec.kind.to_string() {
146             "from_str" => FromStr,
147             "try_from_str" => TryFromStr,
148             "from_os_str" => FromOsStr,
149             "try_from_os_str" => TryFromOsStr,
150             "from_occurrences" => FromOccurrences,
151             "from_flag" => FromFlag,
152             s => abort!(spec.kind, "unsupported parser `{}`", s),
153         };
154 
155         let func = match spec.parse_func {
156             None => match kind {
157                 FromStr | FromOsStr => {
158                     quote_spanned!(spec.kind.span()=> ::std::convert::From::from)
159                 }
160                 TryFromStr => quote_spanned!(spec.kind.span()=> ::std::str::FromStr::from_str),
161                 TryFromOsStr => abort!(
162                     spec.kind,
163                     "you must set parser for `try_from_os_str` explicitly"
164                 ),
165                 FromOccurrences => quote_spanned!(spec.kind.span()=> { |v| v as _ }),
166                 FromFlag => quote_spanned!(spec.kind.span()=> ::std::convert::From::from),
167             },
168 
169             Some(func) => match func {
170                 syn::Expr::Path(_) => quote!(#func),
171                 _ => abort!(func, "`parse` argument must be a function path"),
172             },
173         };
174 
175         let kind = Sp::new(kind, spec.kind.span());
176         let parser = Parser { kind, func };
177         Sp::new(parser, parse_ident.span())
178     }
179 }
180 
181 impl CasingStyle {
from_lit(name: LitStr) -> Sp<Self>182     fn from_lit(name: LitStr) -> Sp<Self> {
183         use CasingStyle::*;
184 
185         let normalized = name.value().to_camel_case().to_lowercase();
186         let cs = |kind| Sp::new(kind, name.span());
187 
188         match normalized.as_ref() {
189             "camel" | "camelcase" => cs(Camel),
190             "kebab" | "kebabcase" => cs(Kebab),
191             "pascal" | "pascalcase" => cs(Pascal),
192             "screamingsnake" | "screamingsnakecase" => cs(ScreamingSnake),
193             "snake" | "snakecase" => cs(Snake),
194             "verbatim" | "verbatimcase" => cs(Verbatim),
195             "lower" | "lowercase" => cs(Lower),
196             "upper" | "uppercase" => cs(Upper),
197             s => abort!(name, "unsupported casing: `{}`", s),
198         }
199     }
200 }
201 
202 impl Name {
translate(self, style: CasingStyle) -> TokenStream203     pub fn translate(self, style: CasingStyle) -> TokenStream {
204         use CasingStyle::*;
205 
206         match self {
207             Name::Assigned(tokens) => tokens,
208             Name::Derived(ident) => {
209                 let s = ident.unraw().to_string();
210                 let s = match style {
211                     Pascal => s.to_camel_case(),
212                     Kebab => s.to_kebab_case(),
213                     Camel => s.to_mixed_case(),
214                     ScreamingSnake => s.to_shouty_snake_case(),
215                     Snake => s.to_snake_case(),
216                     Verbatim => s,
217                     Lower => s.to_snake_case().replace("_", ""),
218                     Upper => s.to_shouty_snake_case().replace("_", ""),
219                 };
220                 quote_spanned!(ident.span()=> #s)
221             }
222         }
223     }
224 }
225 
226 impl Attrs {
new( default_span: Span, name: Name, parent_attrs: Option<&Attrs>, ty: Option<Type>, casing: Sp<CasingStyle>, env_casing: Sp<CasingStyle>, ) -> Self227     fn new(
228         default_span: Span,
229         name: Name,
230         parent_attrs: Option<&Attrs>,
231         ty: Option<Type>,
232         casing: Sp<CasingStyle>,
233         env_casing: Sp<CasingStyle>,
234     ) -> Self {
235         let no_version = parent_attrs
236             .as_ref()
237             .map(|attrs| attrs.no_version.clone())
238             .unwrap_or(None);
239 
240         Self {
241             name,
242             ty,
243             casing,
244             env_casing,
245             doc_comment: vec![],
246             methods: vec![],
247             parser: Parser::default_spanned(default_span),
248             about: None,
249             author: None,
250             version: None,
251             no_version,
252             verbatim_doc_comment: None,
253 
254             has_custom_parser: false,
255             kind: Sp::new(Kind::Arg(Sp::new(Ty::Other, default_span)), default_span),
256         }
257     }
258 
push_method(&mut self, name: Ident, arg: impl ToTokens)259     fn push_method(&mut self, name: Ident, arg: impl ToTokens) {
260         if name == "name" {
261             self.name = Name::Assigned(quote!(#arg));
262         } else if name == "version" {
263             self.version = Some(Method::new(name, quote!(#arg)));
264         } else {
265             self.methods.push(Method::new(name, quote!(#arg)))
266         }
267     }
268 
push_attrs(&mut self, attrs: &[Attribute])269     fn push_attrs(&mut self, attrs: &[Attribute]) {
270         use crate::parse::StructOptAttr::*;
271 
272         for attr in parse_structopt_attributes(attrs) {
273             match attr {
274                 Short(ident) | Long(ident) => {
275                     self.push_method(ident, self.name.clone().translate(*self.casing));
276                 }
277 
278                 Env(ident) => {
279                     self.push_method(ident, self.name.clone().translate(*self.env_casing));
280                 }
281 
282                 Subcommand(ident) => {
283                     let ty = Sp::call_site(Ty::Other);
284                     let kind = Sp::new(Kind::Subcommand(ty), ident.span());
285                     self.set_kind(kind);
286                 }
287 
288                 ExternalSubcommand(ident) => {
289                     self.kind = Sp::new(Kind::ExternalSubcommand, ident.span());
290                 }
291 
292                 Flatten(ident) => {
293                     let kind = Sp::new(Kind::Flatten, ident.span());
294                     self.set_kind(kind);
295                 }
296 
297                 Skip(ident, expr) => {
298                     let kind = Sp::new(Kind::Skip(expr), ident.span());
299                     self.set_kind(kind);
300                 }
301 
302                 NoVersion(ident) => self.no_version = Some(ident),
303 
304                 VerbatimDocComment(ident) => self.verbatim_doc_comment = Some(ident),
305 
306                 DefaultValue(ident, lit) => {
307                     let val = if let Some(lit) = lit {
308                         quote!(#lit)
309                     } else {
310                         let ty = if let Some(ty) = self.ty.as_ref() {
311                             ty
312                         } else {
313                             abort!(
314                                 ident,
315                                 "#[structopt(default_value)] (without an argument) can be used \
316                                 only on field level";
317 
318                                 note = "see \
319                                     https://docs.rs/structopt/0.3.5/structopt/#magical-methods")
320                         };
321 
322                         quote_spanned!(ident.span()=> {
323                             ::structopt::lazy_static::lazy_static! {
324                                 static ref DEFAULT_VALUE: &'static str = {
325                                     let val = <#ty as ::std::default::Default>::default();
326                                     let s = ::std::string::ToString::to_string(&val);
327                                     ::std::boxed::Box::leak(s.into_boxed_str())
328                                 };
329                             }
330                             *DEFAULT_VALUE
331                         })
332                     };
333 
334                     self.methods.push(Method::new(ident, val));
335                 }
336 
337                 About(ident, about) => {
338                     self.about = Method::from_lit_or_env(ident, about, "CARGO_PKG_DESCRIPTION");
339                 }
340 
341                 Author(ident, author) => {
342                     self.author = Method::from_lit_or_env(ident, author, "CARGO_PKG_AUTHORS");
343                 }
344 
345                 Version(ident, version) => {
346                     self.push_method(ident, version);
347                 }
348 
349                 NameLitStr(name, lit) => {
350                     self.push_method(name, lit);
351                 }
352 
353                 NameExpr(name, expr) => {
354                     self.push_method(name, expr);
355                 }
356 
357                 MethodCall(name, args) => self.push_method(name, quote!(#(#args),*)),
358 
359                 RenameAll(_, casing_lit) => {
360                     self.casing = CasingStyle::from_lit(casing_lit);
361                 }
362 
363                 RenameAllEnv(_, casing_lit) => {
364                     self.env_casing = CasingStyle::from_lit(casing_lit);
365                 }
366 
367                 Parse(ident, spec) => {
368                     self.has_custom_parser = true;
369                     self.parser = Parser::from_spec(ident, spec);
370                 }
371             }
372         }
373     }
374 
push_doc_comment(&mut self, attrs: &[Attribute], name: &str)375     fn push_doc_comment(&mut self, attrs: &[Attribute], name: &str) {
376         use crate::Lit::*;
377         use crate::Meta::*;
378 
379         let comment_parts: Vec<_> = attrs
380             .iter()
381             .filter(|attr| attr.path.is_ident("doc"))
382             .filter_map(|attr| {
383                 if let Ok(NameValue(MetaNameValue { lit: Str(s), .. })) = attr.parse_meta() {
384                     Some(s.value())
385                 } else {
386                     // non #[doc = "..."] attributes are not our concern
387                     // we leave them for rustc to handle
388                     None
389                 }
390             })
391             .collect();
392 
393         self.doc_comment =
394             process_doc_comment(comment_parts, name, self.verbatim_doc_comment.is_none());
395     }
396 
from_struct( span: Span, attrs: &[Attribute], name: Name, parent_attrs: Option<&Attrs>, argument_casing: Sp<CasingStyle>, env_casing: Sp<CasingStyle>, ) -> Self397     pub fn from_struct(
398         span: Span,
399         attrs: &[Attribute],
400         name: Name,
401         parent_attrs: Option<&Attrs>,
402         argument_casing: Sp<CasingStyle>,
403         env_casing: Sp<CasingStyle>,
404     ) -> Self {
405         let mut res = Self::new(span, name, parent_attrs, None, argument_casing, env_casing);
406         res.push_attrs(attrs);
407         res.push_doc_comment(attrs, "about");
408 
409         if res.has_custom_parser {
410             abort!(
411                 res.parser.span(),
412                 "`parse` attribute is only allowed on fields"
413             );
414         }
415         match &*res.kind {
416             Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"),
417             Kind::Skip(_) => abort!(res.kind.span(), "skip is only allowed on fields"),
418             Kind::Arg(_) | Kind::ExternalSubcommand | Kind::Flatten => res,
419         }
420     }
421 
from_field( field: &syn::Field, parent_attrs: Option<&Attrs>, struct_casing: Sp<CasingStyle>, env_casing: Sp<CasingStyle>, ) -> Self422     pub fn from_field(
423         field: &syn::Field,
424         parent_attrs: Option<&Attrs>,
425         struct_casing: Sp<CasingStyle>,
426         env_casing: Sp<CasingStyle>,
427     ) -> Self {
428         let name = field.ident.clone().unwrap();
429         let mut res = Self::new(
430             field.span(),
431             Name::Derived(name),
432             parent_attrs,
433             Some(field.ty.clone()),
434             struct_casing,
435             env_casing,
436         );
437         res.push_attrs(&field.attrs);
438         res.push_doc_comment(&field.attrs, "help");
439 
440         match &*res.kind {
441             Kind::Flatten => {
442                 if res.has_custom_parser {
443                     abort!(
444                         res.parser.span(),
445                         "parse attribute is not allowed for flattened entry"
446                     );
447                 }
448                 if res.has_explicit_methods() {
449                     abort!(
450                         res.kind.span(),
451                         "methods are not allowed for flattened entry"
452                     );
453                 }
454 
455                 if res.has_doc_methods() {
456                     res.doc_comment = vec![];
457                 }
458             }
459 
460             Kind::ExternalSubcommand => {}
461 
462             Kind::Subcommand(_) => {
463                 if res.has_custom_parser {
464                     abort!(
465                         res.parser.span(),
466                         "parse attribute is not allowed for subcommand"
467                     );
468                 }
469                 if res.has_explicit_methods() {
470                     abort!(
471                         res.kind.span(),
472                         "methods in attributes are not allowed for subcommand"
473                     );
474                 }
475 
476                 let ty = Ty::from_syn_ty(&field.ty);
477                 match *ty {
478                     Ty::OptionOption => {
479                         abort!(
480                             field.ty,
481                             "Option<Option<T>> type is not allowed for subcommand"
482                         );
483                     }
484                     Ty::OptionVec => {
485                         abort!(
486                             field.ty,
487                             "Option<Vec<T>> type is not allowed for subcommand"
488                         );
489                     }
490                     _ => (),
491                 }
492 
493                 res.kind = Sp::new(Kind::Subcommand(ty), res.kind.span());
494             }
495             Kind::Skip(_) => {
496                 if res.has_explicit_methods() {
497                     abort!(
498                         res.kind.span(),
499                         "methods are not allowed for skipped fields"
500                     );
501                 }
502             }
503             Kind::Arg(orig_ty) => {
504                 let mut ty = Ty::from_syn_ty(&field.ty);
505                 if res.has_custom_parser {
506                     match *ty {
507                         Ty::Option | Ty::Vec | Ty::OptionVec => (),
508                         _ => ty = Sp::new(Ty::Other, ty.span()),
509                     }
510                 }
511 
512                 match *ty {
513                     Ty::Bool => {
514                         if res.is_positional() && !res.has_custom_parser {
515                             abort!(field.ty,
516                                 "`bool` cannot be used as positional parameter with default parser";
517                                 help = "if you want to create a flag add `long` or `short`";
518                                 help = "If you really want a boolean parameter \
519                                     add an explicit parser, for example `parse(try_from_str)`";
520                                 note = "see also https://github.com/TeXitoi/structopt/tree/master/examples/true_or_false.rs";
521                             )
522                         }
523                         if let Some(m) = res.find_method("default_value") {
524                             abort!(m.name, "default_value is meaningless for bool")
525                         }
526                         if let Some(m) = res.find_method("required") {
527                             abort!(m.name, "required is meaningless for bool")
528                         }
529                     }
530                     Ty::Option => {
531                         if let Some(m) = res.find_method("default_value") {
532                             abort!(m.name, "default_value is meaningless for Option")
533                         }
534                         if let Some(m) = res.find_method("required") {
535                             abort!(m.name, "required is meaningless for Option")
536                         }
537                     }
538                     Ty::OptionOption => {
539                         if res.is_positional() {
540                             abort!(
541                                 field.ty,
542                                 "Option<Option<T>> type is meaningless for positional argument"
543                             )
544                         }
545                     }
546                     Ty::OptionVec => {
547                         if res.is_positional() {
548                             abort!(
549                                 field.ty,
550                                 "Option<Vec<T>> type is meaningless for positional argument"
551                             )
552                         }
553                     }
554 
555                     _ => (),
556                 }
557                 res.kind = Sp::new(Kind::Arg(ty), orig_ty.span());
558             }
559         }
560 
561         res
562     }
563 
set_kind(&mut self, kind: Sp<Kind>)564     fn set_kind(&mut self, kind: Sp<Kind>) {
565         if let Kind::Arg(_) = *self.kind {
566             self.kind = kind;
567         } else {
568             abort!(
569                 kind.span(),
570                 "subcommand, flatten and skip cannot be used together"
571             );
572         }
573     }
574 
has_method(&self, name: &str) -> bool575     pub fn has_method(&self, name: &str) -> bool {
576         self.find_method(name).is_some()
577     }
578 
find_method(&self, name: &str) -> Option<&Method>579     pub fn find_method(&self, name: &str) -> Option<&Method> {
580         self.methods.iter().find(|m| m.name == name)
581     }
582 
583     /// generate methods from attributes on top of struct or enum
top_level_methods(&self) -> TokenStream584     pub fn top_level_methods(&self) -> TokenStream {
585         let author = &self.author;
586         let about = &self.about;
587         let methods = &self.methods;
588         let doc_comment = &self.doc_comment;
589 
590         quote!( #(#doc_comment)* #author #about #(#methods)*  )
591     }
592 
593     /// generate methods on top of a field
field_methods(&self) -> TokenStream594     pub fn field_methods(&self) -> TokenStream {
595         let methods = &self.methods;
596         let doc_comment = &self.doc_comment;
597         quote!( #(#doc_comment)* #(#methods)* )
598     }
599 
version(&self) -> TokenStream600     pub fn version(&self) -> TokenStream {
601         match (&self.no_version, &self.version) {
602             (None, Some(m)) => m.to_token_stream(),
603 
604             (None, None) => std::env::var("CARGO_PKG_VERSION")
605                 .map(|version| quote!( .version(#version) ))
606                 .unwrap_or_default(),
607 
608             _ => quote!(),
609         }
610     }
611 
cased_name(&self) -> TokenStream612     pub fn cased_name(&self) -> TokenStream {
613         self.name.clone().translate(*self.casing)
614     }
615 
parser(&self) -> &Sp<Parser>616     pub fn parser(&self) -> &Sp<Parser> {
617         &self.parser
618     }
619 
kind(&self) -> Sp<Kind>620     pub fn kind(&self) -> Sp<Kind> {
621         self.kind.clone()
622     }
623 
casing(&self) -> Sp<CasingStyle>624     pub fn casing(&self) -> Sp<CasingStyle> {
625         self.casing.clone()
626     }
627 
env_casing(&self) -> Sp<CasingStyle>628     pub fn env_casing(&self) -> Sp<CasingStyle> {
629         self.env_casing.clone()
630     }
631 
is_positional(&self) -> bool632     pub fn is_positional(&self) -> bool {
633         self.methods
634             .iter()
635             .all(|m| m.name != "long" && m.name != "short")
636     }
637 
has_explicit_methods(&self) -> bool638     pub fn has_explicit_methods(&self) -> bool {
639         self.methods
640             .iter()
641             .any(|m| m.name != "help" && m.name != "long_help")
642     }
643 
has_doc_methods(&self) -> bool644     pub fn has_doc_methods(&self) -> bool {
645         !self.doc_comment.is_empty()
646             || self.methods.iter().any(|m| {
647                 m.name == "help"
648                     || m.name == "long_help"
649                     || m.name == "about"
650                     || m.name == "long_about"
651             })
652     }
653 }
654 
655 /// replace all `:` with `, ` when not inside the `<>`
656 ///
657 /// `"author1:author2:author3" => "author1, author2, author3"`
658 /// `"author1 <http://website1.com>:author2" => "author1 <http://website1.com>, author2"
process_author_str(author: &str) -> String659 fn process_author_str(author: &str) -> String {
660     let mut res = String::with_capacity(author.len());
661     let mut inside_angle_braces = 0usize;
662 
663     for ch in author.chars() {
664         if inside_angle_braces > 0 && ch == '>' {
665             inside_angle_braces -= 1;
666             res.push(ch);
667         } else if ch == '<' {
668             inside_angle_braces += 1;
669             res.push(ch);
670         } else if inside_angle_braces == 0 && ch == ':' {
671             res.push_str(", ");
672         } else {
673             res.push(ch);
674         }
675     }
676 
677     res
678 }
679