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) -> Self103 fn from_lit_or_env(ident: Ident, lit: Option<LitStr>, env_var: &str) -> 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 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 = Some(Method::from_lit_or_env(
339 ident,
340 about,
341 "CARGO_PKG_DESCRIPTION",
342 ));
343 }
344
345 Author(ident, author) => {
346 self.author = Some(Method::from_lit_or_env(ident, author, "CARGO_PKG_AUTHORS"));
347 }
348
349 Version(ident, version) => {
350 self.push_method(ident, version);
351 }
352
353 NameLitStr(name, lit) => {
354 self.push_method(name, lit);
355 }
356
357 NameExpr(name, expr) => {
358 self.push_method(name, expr);
359 }
360
361 MethodCall(name, args) => self.push_method(name, quote!(#(#args),*)),
362
363 RenameAll(_, casing_lit) => {
364 self.casing = CasingStyle::from_lit(casing_lit);
365 }
366
367 RenameAllEnv(_, casing_lit) => {
368 self.env_casing = CasingStyle::from_lit(casing_lit);
369 }
370
371 Parse(ident, spec) => {
372 self.has_custom_parser = true;
373 self.parser = Parser::from_spec(ident, spec);
374 }
375 }
376 }
377 }
378
push_doc_comment(&mut self, attrs: &[Attribute], name: &str)379 fn push_doc_comment(&mut self, attrs: &[Attribute], name: &str) {
380 use crate::Lit::*;
381 use crate::Meta::*;
382
383 let comment_parts: Vec<_> = attrs
384 .iter()
385 .filter(|attr| attr.path.is_ident("doc"))
386 .filter_map(|attr| {
387 if let Ok(NameValue(MetaNameValue { lit: Str(s), .. })) = attr.parse_meta() {
388 Some(s.value())
389 } else {
390 // non #[doc = "..."] attributes are not our concern
391 // we leave them for rustc to handle
392 None
393 }
394 })
395 .collect();
396
397 self.doc_comment =
398 process_doc_comment(comment_parts, name, self.verbatim_doc_comment.is_none());
399 }
400
from_struct( span: Span, attrs: &[Attribute], name: Name, parent_attrs: Option<&Attrs>, argument_casing: Sp<CasingStyle>, env_casing: Sp<CasingStyle>, ) -> Self401 pub fn from_struct(
402 span: Span,
403 attrs: &[Attribute],
404 name: Name,
405 parent_attrs: Option<&Attrs>,
406 argument_casing: Sp<CasingStyle>,
407 env_casing: Sp<CasingStyle>,
408 ) -> Self {
409 let mut res = Self::new(span, name, parent_attrs, None, argument_casing, env_casing);
410 res.push_attrs(attrs);
411 res.push_doc_comment(attrs, "about");
412
413 if res.has_custom_parser {
414 abort!(
415 res.parser.span(),
416 "`parse` attribute is only allowed on fields"
417 );
418 }
419 match &*res.kind {
420 Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"),
421 Kind::Skip(_) => abort!(res.kind.span(), "skip is only allowed on fields"),
422 Kind::Arg(_) | Kind::ExternalSubcommand | Kind::Flatten => res,
423 }
424 }
425
from_field( field: &syn::Field, parent_attrs: Option<&Attrs>, struct_casing: Sp<CasingStyle>, env_casing: Sp<CasingStyle>, ) -> Self426 pub fn from_field(
427 field: &syn::Field,
428 parent_attrs: Option<&Attrs>,
429 struct_casing: Sp<CasingStyle>,
430 env_casing: Sp<CasingStyle>,
431 ) -> Self {
432 let name = field.ident.clone().unwrap();
433 let mut res = Self::new(
434 field.span(),
435 Name::Derived(name),
436 parent_attrs,
437 Some(field.ty.clone()),
438 struct_casing,
439 env_casing,
440 );
441 res.push_attrs(&field.attrs);
442 res.push_doc_comment(&field.attrs, "help");
443
444 match &*res.kind {
445 Kind::Flatten => {
446 if res.has_custom_parser {
447 abort!(
448 res.parser.span(),
449 "parse attribute is not allowed for flattened entry"
450 );
451 }
452 if res.has_explicit_methods() {
453 abort!(
454 res.kind.span(),
455 "methods are not allowed for flattened entry"
456 );
457 }
458
459 if res.has_doc_methods() {
460 res.doc_comment = vec![];
461 }
462 }
463
464 Kind::ExternalSubcommand => {}
465
466 Kind::Subcommand(_) => {
467 if res.has_custom_parser {
468 abort!(
469 res.parser.span(),
470 "parse attribute is not allowed for subcommand"
471 );
472 }
473 if res.has_explicit_methods() {
474 abort!(
475 res.kind.span(),
476 "methods in attributes are not allowed for subcommand"
477 );
478 }
479
480 let ty = Ty::from_syn_ty(&field.ty);
481 match *ty {
482 Ty::OptionOption => {
483 abort!(
484 field.ty,
485 "Option<Option<T>> type is not allowed for subcommand"
486 );
487 }
488 Ty::OptionVec => {
489 abort!(
490 field.ty,
491 "Option<Vec<T>> type is not allowed for subcommand"
492 );
493 }
494 _ => (),
495 }
496
497 res.kind = Sp::new(Kind::Subcommand(ty), res.kind.span());
498 }
499 Kind::Skip(_) => {
500 if res.has_explicit_methods() {
501 abort!(
502 res.kind.span(),
503 "methods are not allowed for skipped fields"
504 );
505 }
506 }
507 Kind::Arg(orig_ty) => {
508 let mut ty = Ty::from_syn_ty(&field.ty);
509 if res.has_custom_parser {
510 match *ty {
511 Ty::Option | Ty::Vec | Ty::OptionVec => (),
512 _ => ty = Sp::new(Ty::Other, ty.span()),
513 }
514 }
515
516 match *ty {
517 Ty::Bool => {
518 if res.is_positional() && !res.has_custom_parser {
519 abort!(field.ty,
520 "`bool` cannot be used as positional parameter with default parser";
521 help = "if you want to create a flag add `long` or `short`";
522 help = "If you really want a boolean parameter \
523 add an explicit parser, for example `parse(try_from_str)`";
524 note = "see also https://github.com/TeXitoi/structopt/tree/master/examples/true_or_false.rs";
525 )
526 }
527 if let Some(m) = res.find_method("default_value") {
528 abort!(m.name, "default_value is meaningless for bool")
529 }
530 if let Some(m) = res.find_method("required") {
531 abort!(m.name, "required is meaningless for bool")
532 }
533 }
534 Ty::Option => {
535 if let Some(m) = res.find_method("default_value") {
536 abort!(m.name, "default_value is meaningless for Option")
537 }
538 if let Some(m) = res.find_method("required") {
539 abort!(m.name, "required is meaningless for Option")
540 }
541 }
542 Ty::OptionOption => {
543 if res.is_positional() {
544 abort!(
545 field.ty,
546 "Option<Option<T>> type is meaningless for positional argument"
547 )
548 }
549 }
550 Ty::OptionVec => {
551 if res.is_positional() {
552 abort!(
553 field.ty,
554 "Option<Vec<T>> type is meaningless for positional argument"
555 )
556 }
557 }
558
559 _ => (),
560 }
561 res.kind = Sp::new(Kind::Arg(ty), orig_ty.span());
562 }
563 }
564
565 res
566 }
567
set_kind(&mut self, kind: Sp<Kind>)568 fn set_kind(&mut self, kind: Sp<Kind>) {
569 if let Kind::Arg(_) = *self.kind {
570 self.kind = kind;
571 } else {
572 abort!(
573 kind.span(),
574 "subcommand, flatten and skip cannot be used together"
575 );
576 }
577 }
578
has_method(&self, name: &str) -> bool579 pub fn has_method(&self, name: &str) -> bool {
580 self.find_method(name).is_some()
581 }
582
find_method(&self, name: &str) -> Option<&Method>583 pub fn find_method(&self, name: &str) -> Option<&Method> {
584 self.methods.iter().find(|m| m.name == name)
585 }
586
587 /// generate methods from attributes on top of struct or enum
top_level_methods(&self) -> TokenStream588 pub fn top_level_methods(&self) -> TokenStream {
589 let author = &self.author;
590 let about = &self.about;
591 let methods = &self.methods;
592 let doc_comment = &self.doc_comment;
593
594 quote!( #(#doc_comment)* #author #about #(#methods)* )
595 }
596
597 /// generate methods on top of a field
field_methods(&self) -> TokenStream598 pub fn field_methods(&self) -> TokenStream {
599 let methods = &self.methods;
600 let doc_comment = &self.doc_comment;
601 quote!( #(#doc_comment)* #(#methods)* )
602 }
603
version(&self) -> TokenStream604 pub fn version(&self) -> TokenStream {
605 match (&self.no_version, &self.version) {
606 (None, Some(m)) => m.to_token_stream(),
607
608 (None, None) => std::env::var("CARGO_PKG_VERSION")
609 .map(|version| quote!( .version(#version) ))
610 .unwrap_or_default(),
611
612 _ => quote!(),
613 }
614 }
615
cased_name(&self) -> TokenStream616 pub fn cased_name(&self) -> TokenStream {
617 self.name.clone().translate(*self.casing)
618 }
619
parser(&self) -> &Sp<Parser>620 pub fn parser(&self) -> &Sp<Parser> {
621 &self.parser
622 }
623
kind(&self) -> Sp<Kind>624 pub fn kind(&self) -> Sp<Kind> {
625 self.kind.clone()
626 }
627
casing(&self) -> Sp<CasingStyle>628 pub fn casing(&self) -> Sp<CasingStyle> {
629 self.casing.clone()
630 }
631
env_casing(&self) -> Sp<CasingStyle>632 pub fn env_casing(&self) -> Sp<CasingStyle> {
633 self.env_casing.clone()
634 }
635
is_positional(&self) -> bool636 pub fn is_positional(&self) -> bool {
637 self.methods
638 .iter()
639 .all(|m| m.name != "long" && m.name != "short")
640 }
641
has_explicit_methods(&self) -> bool642 pub fn has_explicit_methods(&self) -> bool {
643 self.methods
644 .iter()
645 .any(|m| m.name != "help" && m.name != "long_help")
646 }
647
has_doc_methods(&self) -> bool648 pub fn has_doc_methods(&self) -> bool {
649 !self.doc_comment.is_empty()
650 || self.methods.iter().any(|m| {
651 m.name == "help"
652 || m.name == "long_help"
653 || m.name == "about"
654 || m.name == "long_about"
655 })
656 }
657 }
658
659 /// replace all `:` with `, ` when not inside the `<>`
660 ///
661 /// `"author1:author2:author3" => "author1, author2, author3"`
662 /// `"author1 <http://website1.com>:author2" => "author1 <http://website1.com>, author2"
process_author_str(author: &str) -> String663 fn process_author_str(author: &str) -> String {
664 let mut res = String::with_capacity(author.len());
665 let mut inside_angle_braces = 0usize;
666
667 for ch in author.chars() {
668 if inside_angle_braces > 0 && ch == '>' {
669 inside_angle_braces -= 1;
670 res.push(ch);
671 } else if ch == '<' {
672 inside_angle_braces += 1;
673 res.push(ch);
674 } else if inside_angle_braces == 0 && ch == ':' {
675 res.push_str(", ");
676 } else {
677 res.push(ch);
678 }
679 }
680
681 res
682 }
683