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