1 // Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>
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.
9 use crate::doc_comments
::process_doc_comment
;
10 use crate::{parse::*, spanned::Sp, ty::Ty}
;
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}
;
19 self, ext
::IdentExt
, spanned
::Spanned
, Attribute
, Expr
, Ident
, LitStr
, MetaNameValue
, Type
,
39 pub kind
: Sp
<ParserKind
>,
40 pub func
: TokenStream
,
43 #[derive(Debug, PartialEq, Clone)]
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.
58 /// Keep all letters lowercase and indicate word boundaries with hyphens.
60 /// Indicate word boundaries with uppercase letter, including the first word.
62 /// Keep all letters uppercase and indicate word boundaries with underscores.
64 /// Keep all letters lowercase and indicate word boundaries with underscores.
66 /// Use the original attribute name defined in the code.
68 /// Keep all letters lowercase and remove word boundaries.
70 /// Keep all letters uppercase and remove word boundaries.
77 Assigned(TokenStream
),
83 casing
: Sp
<CasingStyle
>,
84 env_casing
: Sp
<CasingStyle
>,
86 doc_comment
: Vec
<Method
>,
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
,
99 pub fn new(name
: Ident
, args
: TokenStream
) -> Self {
100 Method { name, args }
103 fn from_lit_or_env(ident
: Ident
, lit
: Option
<LitStr
>, env_var
: &str) -> Self {
104 let mut lit
= match lit
{
107 None
=> match env
::var(env_var
) {
108 Ok(val
) => LitStr
::new(&val
, ident
.span()),
111 "cannot derive `{}` from Cargo.toml", ident
;
112 note
= "`{}` environment variable is not set", env_var
;
113 help
= "use `{} = \"...\"` to set {} manually", ident
, ident
;
119 if ident
== "author" {
120 let edited
= process_author_str(&lit
.value());
121 lit
= LitStr
::new(&edited
, lit
.span());
124 Method
::new(ident
, quote
!(#lit))
128 impl ToTokens
for Method
{
129 fn to_tokens(&self, ts
: &mut TokenStream
) {
130 let Method { ref name, ref args }
= self;
131 quote
!(.#name(#args)).to_tokens(ts);
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
)
142 fn from_spec(parse_ident
: Ident
, spec
: ParserSpec
) -> Sp
<Self> {
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
),
155 let func
= match spec
.parse_func
{
157 FromStr
| FromOsStr
=> {
158 quote_spanned
!(spec
.kind
.span()=> ::std
::convert
::From
::from
)
160 TryFromStr
=> quote_spanned
!(spec
.kind
.span()=> ::std
::str::FromStr
::from_str
),
161 TryFromOsStr
=> abort
!(
163 "you must set parser for `try_from_os_str` explicitly"
165 FromOccurrences
=> quote_spanned
!(spec
.kind
.span()=> { |v| v as _ }
),
166 FromFlag
=> quote_spanned
!(spec
.kind
.span()=> ::std
::convert
::From
::from
),
169 Some(func
) => match func
{
170 syn
::Expr
::Path(_
) => quote
!(#func),
171 _
=> abort
!(func
, "`parse` argument must be a function path"),
175 let kind
= Sp
::new(kind
, spec
.kind
.span());
176 let parser
= Parser { kind, func }
;
177 Sp
::new(parser
, parse_ident
.span())
182 fn from_lit(name
: LitStr
) -> Sp
<Self> {
185 let normalized
= name
.value().to_camel_case().to_lowercase();
186 let cs
= |kind
| Sp
::new(kind
, name
.span());
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
),
203 pub fn translate(self, style
: CasingStyle
) -> TokenStream
{
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(),
217 Lower
=> s
.to_snake_case().replace("_", ""),
218 Upper
=> s
.to_shouty_snake_case().replace("_", ""),
220 quote_spanned
!(ident
.span()=> #s)
230 parent_attrs
: Option
<&Attrs
>,
232 casing
: Sp
<CasingStyle
>,
233 env_casing
: Sp
<CasingStyle
>,
235 let no_version
= parent_attrs
237 .map(|attrs
| attrs
.no_version
.clone())
247 parser
: Parser
::default_spanned(default_span
),
252 verbatim_doc_comment
: None
,
254 has_custom_parser
: false,
255 kind
: Sp
::new(Kind
::Arg(Sp
::new(Ty
::Other
, default_span
)), default_span
),
259 fn push_method(&mut self, name
: Ident
, arg
: impl ToTokens
) {
261 self.name
= Name
::Assigned(quote
!(#arg));
262 } else if name
== "version" {
263 self.version
= Some(Method
::new(name
, quote
!(#arg)));
265 self.methods
.push(Method
::new(name
, quote
!(#arg)))
269 fn push_attrs(&mut self, attrs
: &[Attribute
]) {
270 use crate::parse
::StructOptAttr
::*;
272 for attr
in parse_structopt_attributes(attrs
) {
274 Short(ident
) | Long(ident
) => {
275 self.push_method(ident
, self.name
.clone().translate(*self.casing
));
279 self.push_method(ident
, self.name
.clone().translate(*self.env_casing
));
282 Subcommand(ident
) => {
283 let ty
= Sp
::call_site(Ty
::Other
);
284 let kind
= Sp
::new(Kind
::Subcommand(ty
), ident
.span());
288 ExternalSubcommand(ident
) => {
289 self.kind
= Sp
::new(Kind
::ExternalSubcommand
, ident
.span());
293 let kind
= Sp
::new(Kind
::Flatten
, ident
.span());
297 Skip(ident
, expr
) => {
298 let kind
= Sp
::new(Kind
::Skip(expr
), ident
.span());
302 NoVersion(ident
) => self.no_version
= Some(ident
),
304 VerbatimDocComment(ident
) => self.verbatim_doc_comment
= Some(ident
),
306 DefaultValue(ident
, lit
) => {
307 let val
= if let Some(lit
) = lit
{
310 let ty
= if let Some(ty
) = self.ty
.as_ref() {
315 "#[structopt(default_value)] (without an argument) can be used \
316 only on field level";
319 https://docs.rs/structopt/0.3.5/structopt/#magical-methods")
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())
334 self.methods
.push(Method
::new(ident
, val
));
337 About(ident
, about
) => {
338 self.about
= Some(Method
::from_lit_or_env(
341 "CARGO_PKG_DESCRIPTION",
345 Author(ident
, author
) => {
346 self.author
= Some(Method
::from_lit_or_env(ident
, author
, "CARGO_PKG_AUTHORS"));
349 Version(ident
, version
) => {
350 self.push_method(ident
, version
);
353 NameLitStr(name
, lit
) => {
354 self.push_method(name
, lit
);
357 NameExpr(name
, expr
) => {
358 self.push_method(name
, expr
);
361 MethodCall(name
, args
) => self.push_method(name
, quote
!(#(#args),*)),
363 RenameAll(_
, casing_lit
) => {
364 self.casing
= CasingStyle
::from_lit(casing_lit
);
367 RenameAllEnv(_
, casing_lit
) => {
368 self.env_casing
= CasingStyle
::from_lit(casing_lit
);
371 Parse(ident
, spec
) => {
372 self.has_custom_parser
= true;
373 self.parser
= Parser
::from_spec(ident
, spec
);
379 fn push_doc_comment(&mut self, attrs
: &[Attribute
], name
: &str) {
383 let comment_parts
: Vec
<_
> = attrs
385 .filter(|attr
| attr
.path
.is_ident("doc"))
387 if let Ok(NameValue(MetaNameValue { lit: Str(s), .. }
)) = attr
.parse_meta() {
390 // non #[doc = "..."] attributes are not our concern
391 // we leave them for rustc to handle
398 process_doc_comment(comment_parts
, name
, self.verbatim_doc_comment
.is_none());
405 parent_attrs
: Option
<&Attrs
>,
406 argument_casing
: Sp
<CasingStyle
>,
407 env_casing
: Sp
<CasingStyle
>,
410 let mut res
= Self::new(span
, name
, parent_attrs
, None
, argument_casing
, env_casing
);
411 res
.push_attrs(attrs
);
412 res
.push_doc_comment(attrs
, "about");
414 if res
.has_custom_parser
{
417 "`parse` attribute is only allowed on fields"
421 Kind
::Subcommand(_
) => abort
!(res
.kind
.span(), "subcommand is only allowed on fields"),
422 Kind
::Skip(_
) if !allow_skip
=> {
423 abort
!(res
.kind
.span(), "skip is only allowed on fields")
425 Kind
::Arg(_
) | Kind
::ExternalSubcommand
| Kind
::Flatten
| Kind
::Skip(_
) => res
,
431 parent_attrs
: Option
<&Attrs
>,
432 struct_casing
: Sp
<CasingStyle
>,
433 env_casing
: Sp
<CasingStyle
>,
435 let name
= field
.ident
.clone().unwrap();
436 let mut res
= Self::new(
440 Some(field
.ty
.clone()),
444 res
.push_attrs(&field
.attrs
);
445 res
.push_doc_comment(&field
.attrs
, "help");
449 if res
.has_custom_parser
{
452 "parse attribute is not allowed for flattened entry"
455 if res
.has_explicit_methods() {
458 "methods are not allowed for flattened entry"
462 if res
.has_doc_methods() {
463 res
.doc_comment
= vec
![];
467 Kind
::ExternalSubcommand
=> {}
469 Kind
::Subcommand(_
) => {
470 if res
.has_custom_parser
{
473 "parse attribute is not allowed for subcommand"
476 if res
.has_explicit_methods() {
479 "methods in attributes are not allowed for subcommand"
483 let ty
= Ty
::from_syn_ty(&field
.ty
);
485 Ty
::OptionOption
=> {
488 "Option<Option<T>> type is not allowed for subcommand"
494 "Option<Vec<T>> type is not allowed for subcommand"
500 res
.kind
= Sp
::new(Kind
::Subcommand(ty
), res
.kind
.span());
503 if res
.has_explicit_methods() {
506 "methods are not allowed for skipped fields"
510 Kind
::Arg(orig_ty
) => {
511 let mut ty
= Ty
::from_syn_ty(&field
.ty
);
512 if res
.has_custom_parser
{
514 Ty
::Option
| Ty
::Vec
| Ty
::OptionVec
=> (),
515 _
=> ty
= Sp
::new(Ty
::Other
, ty
.span()),
521 if res
.is_positional() && !res
.has_custom_parser
{
523 "`bool` cannot be used as positional parameter with default parser";
524 help
= "if you want to create a flag add `long` or `short`";
525 help
= "If you really want a boolean parameter \
526 add an explicit parser, for example `parse(try_from_str)`";
527 note
= "see also https://github.com/TeXitoi/structopt/tree/master/examples/true_or_false.rs";
530 if let Some(m
) = res
.find_method("default_value") {
531 abort
!(m
.name
, "default_value is meaningless for bool")
533 if let Some(m
) = res
.find_method("required") {
534 abort
!(m
.name
, "required is meaningless for bool")
538 if let Some(m
) = res
.find_method("default_value") {
539 abort
!(m
.name
, "default_value is meaningless for Option")
541 if let Some(m
) = res
.find_method("required") {
542 abort
!(m
.name
, "required is meaningless for Option")
545 Ty
::OptionOption
=> {
546 if res
.is_positional() {
549 "Option<Option<T>> type is meaningless for positional argument"
554 if res
.is_positional() {
557 "Option<Vec<T>> type is meaningless for positional argument"
564 res
.kind
= Sp
::new(Kind
::Arg(ty
), orig_ty
.span());
571 fn set_kind(&mut self, kind
: Sp
<Kind
>) {
572 if let Kind
::Arg(_
) = *self.kind
{
577 "subcommand, flatten and skip cannot be used together"
582 pub fn has_method(&self, name
: &str) -> bool
{
583 self.find_method(name
).is_some()
586 pub fn find_method(&self, name
: &str) -> Option
<&Method
> {
587 self.methods
.iter().find(|m
| m
.name
== name
)
590 /// generate methods from attributes on top of struct or enum
591 pub fn top_level_methods(&self) -> TokenStream
{
592 let author
= &self.author
;
593 let about
= &self.about
;
594 let methods
= &self.methods
;
595 let doc_comment
= &self.doc_comment
;
597 quote
!( #(#doc_comment)* #author #about #(#methods)* )
600 /// generate methods on top of a field
601 pub fn field_methods(&self) -> TokenStream
{
602 let methods
= &self.methods
;
603 let doc_comment
= &self.doc_comment
;
604 quote
!( #(#doc_comment)* #(#methods)* )
607 pub fn version(&self) -> TokenStream
{
608 match (&self.no_version
, &self.version
) {
609 (None
, Some(m
)) => m
.to_token_stream(),
611 (None
, None
) => std
::env
::var("CARGO_PKG_VERSION")
612 .map(|version
| quote
!( .version(#version) ))
613 .unwrap_or_default(),
619 pub fn cased_name(&self) -> TokenStream
{
620 self.name
.clone().translate(*self.casing
)
623 pub fn parser(&self) -> &Sp
<Parser
> {
627 pub fn kind(&self) -> Sp
<Kind
> {
631 pub fn casing(&self) -> Sp
<CasingStyle
> {
635 pub fn env_casing(&self) -> Sp
<CasingStyle
> {
636 self.env_casing
.clone()
639 pub fn is_positional(&self) -> bool
{
642 .all(|m
| m
.name
!= "long" && m
.name
!= "short")
645 pub fn has_explicit_methods(&self) -> bool
{
648 .any(|m
| m
.name
!= "help" && m
.name
!= "long_help")
651 pub fn has_doc_methods(&self) -> bool
{
652 !self.doc_comment
.is_empty()
653 || self.methods
.iter().any(|m
| {
655 || m
.name
== "long_help"
657 || m
.name
== "long_about"
662 /// replace all `:` with `, ` when not inside the `<>`
664 /// `"author1:author2:author3" => "author1, author2, author3"`
665 /// `"author1 <http://website1.com>:author2" => "author1 <http://website1.com>, author2"
666 fn process_author_str(author
: &str) -> String
{
667 let mut res
= String
::with_capacity(author
.len());
668 let mut inside_angle_braces
= 0usize
;
670 for ch
in author
.chars() {
671 if inside_angle_braces
> 0 && ch
== '
>'
{
672 inside_angle_braces
-= 1;
674 } else if ch
== '
<'
{
675 inside_angle_braces
+= 1;
677 } else if inside_angle_braces
== 0 && ch
== '
:'
{