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