bpaf_derive/
top.rs

1use proc_macro2::{Span, TokenStream};
2use quote::{quote, ToTokens};
3use syn::{
4    braced, parenthesized,
5    parse::{Parse, ParseStream},
6    parse_quote,
7    punctuated::Punctuated,
8    token,
9    visit_mut::VisitMut,
10    Attribute, Error, Expr, Ident, ItemFn, LitChar, LitStr, Result, Visibility,
11};
12
13use crate::{
14    attrs::{parse_bpaf_doc_attrs, EnumPrefix, PostDecor, StrictName},
15    custom_path::CratePathReplacer,
16    field::StructField,
17    help::Help,
18    td::{CommandCfg, EAttr, Ed, Mode, OptionsCfg, ParserCfg, TopInfo},
19    utils::{to_kebab_case, to_snake_case, LineIter},
20};
21
22#[derive(Debug)]
23/// Top level container
24pub(crate) struct Top {
25    // {{{
26    /// Name of a parsed or produced type, possibly with generics
27    ty: Ident,
28
29    /// Visibility, derived from the type visibility or top level annotation
30    vis: Visibility,
31
32    /// Name of a generated function, usually derived from type,
33    /// but can be specified with generate
34    generate: Ident,
35
36    /// single branch or multipe branches for enum
37    body: Body,
38    mode: Mode,
39    boxed: bool,
40    adjacent: bool,
41    attrs: Vec<PostDecor>,
42    bpaf_path: Option<syn::Path>,
43}
44
45fn ident_to_long(ident: &Ident) -> LitStr {
46    LitStr::new(&to_kebab_case(&ident.to_string()), ident.span())
47}
48
49fn ident_to_short(ident: &Ident) -> LitChar {
50    LitChar::new(
51        to_kebab_case(&ident.to_string()).chars().next().unwrap(),
52        ident.span(),
53    )
54}
55
56impl Parse for Top {
57    fn parse(input: ParseStream) -> Result<Self> {
58        let attrs = input.call(Attribute::parse_outer)?;
59        let (top_decor, mut help) = parse_bpaf_doc_attrs::<TopInfo>(&attrs)?;
60        let TopInfo {
61            private,
62            custom_name,
63            boxed,
64            mut mode,
65            attrs,
66            ignore_rustdoc,
67            adjacent,
68            bpaf_path,
69        } = top_decor.unwrap_or_default();
70
71        if ignore_rustdoc {
72            help = None;
73        }
74        let vis = input.parse::<Visibility>()?;
75
76        let mut body = Body::parse(input)?;
77        let ty = body.ty();
78
79        if let Mode::Command { command, .. } = &mut mode {
80            if let Some(name) = &command.name {
81                body.set_named_command(name.span())?;
82            } else {
83                body.set_unnamed_command()?;
84                command.name = Some(ident_to_long(&ty));
85            }
86        }
87
88        if let Some(help) = help.take() {
89            match &mut mode {
90                Mode::Command {
91                    command: _,
92                    options,
93                } => {
94                    split_options_help(help, options);
95                }
96                Mode::Options { options } => {
97                    split_options_help(help, options);
98                }
99                Mode::Parser { parser } => {
100                    if parser.group_help.is_none() {
101                        parser.group_help = Some(help);
102                    }
103                }
104            }
105        }
106
107        Ok(Top {
108            vis: if private { Visibility::Inherited } else { vis },
109            mode,
110
111            generate: custom_name
112                .unwrap_or_else(|| Ident::new(&to_snake_case(&ty.to_string()), ty.span())),
113            ty,
114            attrs,
115            body,
116            boxed,
117            adjacent,
118            bpaf_path,
119        })
120    }
121}
122
123fn split_options_help(h: Help, opts: &mut OptionsCfg) {
124    match &h {
125        Help::Custom(_) => {
126            if opts.descr.is_none() {
127                opts.descr = Some(h);
128            }
129        }
130        Help::Doc(c) => {
131            let mut chunks = LineIter::from(c.as_str());
132            if let Some(s) = chunks.next() {
133                if opts.descr.is_none() {
134                    opts.descr = Some(Help::Doc(s));
135                }
136            }
137            if let Some(s) = chunks.next() {
138                if !s.is_empty() && opts.header.is_none() {
139                    opts.header = Some(Help::Doc(s));
140                }
141            }
142            if let Some(s) = chunks.rest() {
143                if opts.footer.is_none() {
144                    opts.footer = Some(Help::Doc(s));
145                }
146            }
147        }
148    }
149}
150
151fn split_ehelp_into(h: Help, opts_at: usize, attrs: &mut Vec<EAttr>) {
152    match &h {
153        Help::Custom(_) => attrs.push(EAttr::Descr(h)),
154        Help::Doc(c) => {
155            let mut chunks = LineIter::from(c.as_str());
156            if let Some(s) = chunks.next() {
157                attrs.insert(opts_at, EAttr::Descr(Help::Doc(s)));
158            }
159            if let Some(s) = chunks.next() {
160                if !s.is_empty() {
161                    attrs.insert(opts_at, EAttr::Header(Help::Doc(s)));
162                }
163            }
164            if let Some(s) = chunks.rest() {
165                attrs.insert(opts_at, EAttr::Footer(Help::Doc(s)));
166            }
167        }
168    }
169}
170
171impl ToTokens for Top {
172    fn to_tokens(&self, tokens: &mut TokenStream) {
173        let Top {
174            ty,
175            vis,
176            generate,
177            body,
178            mode,
179            attrs,
180            boxed,
181            adjacent,
182            bpaf_path,
183        } = self;
184        let boxed = if *boxed { quote!(.boxed()) } else { quote!() };
185        let adjacent = if *adjacent {
186            quote!(.adjacent())
187        } else {
188            quote!()
189        };
190
191        let original = match mode {
192            Mode::Command { command, options } => {
193                let OptionsCfg {
194                    cargo_helper: _,
195                    usage,
196                    version,
197                    descr,
198                    footer,
199                    header,
200                    max_width,
201                    fallback_usage,
202                } = options;
203
204                let version = version.as_ref().map(|v| quote!(.version(#v)));
205                let usage = usage.as_ref().map(|v| quote!(.usage(#v)));
206                let descr = descr.as_ref().map(|v| quote!(.descr(#v)));
207                let footer = footer.as_ref().map(|v| quote!(.footer(#v)));
208                let header = header.as_ref().map(|v| quote!(.header(#v)));
209                let max_width = max_width.as_ref().map(|v| quote!(.max_width(#v)));
210                let fallback_usage = if *fallback_usage {
211                    Some(quote!(.fallback_to_usage()))
212                } else {
213                    None
214                };
215                let CommandCfg {
216                    name,
217                    long,
218                    short,
219                    help,
220                } = command;
221                let name = name.as_ref().expect("Internal bpaf_derive error: Command name was not set! This is a bug, please report it.");
222                let long = long.iter().map(|v| quote!(.long(#v)));
223                let short = short.iter().map(|v| quote!(.short(#v)));
224                let help = help.as_ref().map(|v| quote!(.help(#v)));
225                quote! {
226                    #vis fn #generate() -> impl ::bpaf::Parser<#ty> {
227
228                        #[allow(unused_imports)]
229                        use ::bpaf::Parser;
230                        #body
231                        #(.#attrs)*
232                        .to_options()
233                        #fallback_usage
234                        #version
235                        #descr
236                        #header
237                        #footer
238                        #usage
239                        #max_width
240                        .command(#name)
241                        #(#short)*
242                        #(#long)*
243                        #help
244                        #adjacent
245                        #boxed
246                    }
247                }
248            }
249            Mode::Options { options } => {
250                let OptionsCfg {
251                    cargo_helper,
252                    usage,
253                    version,
254                    descr,
255                    footer,
256                    header,
257                    max_width,
258                    fallback_usage,
259                } = options;
260                let body = match cargo_helper {
261                    Some(cargo) => quote!(::bpaf::cargo_helper(#cargo, #body)),
262                    None => quote!(#body),
263                };
264
265                let fallback_usage = if *fallback_usage {
266                    Some(quote!(.fallback_to_usage()))
267                } else {
268                    None
269                };
270                let version = version.as_ref().map(|v| quote!(.version(#v)));
271                let usage = usage.as_ref().map(|v| quote!(.usage(#v)));
272                let descr = descr.as_ref().map(|v| quote!(.descr(#v)));
273                let footer = footer.as_ref().map(|v| quote!(.footer(#v)));
274                let header = header.as_ref().map(|v| quote!(.header(#v)));
275                let max_width = max_width.as_ref().map(|v| quote!(.max_width(#v)));
276
277                quote! {
278                    #vis fn #generate() -> ::bpaf::OptionParser<#ty> {
279                        #[allow(unused_imports)]
280                        use ::bpaf::Parser;
281                        #body
282                        #(.#attrs)*
283                        .to_options()
284                        #fallback_usage
285                        #version
286                        #descr
287                        #header
288                        #footer
289                        #usage
290                        #max_width
291                    }
292                }
293            }
294            Mode::Parser { parser } => {
295                let ParserCfg { group_help } = &parser;
296                let group_help = group_help.as_ref().map(|v| quote!(.group_help(#v)));
297                quote! {
298                    #vis fn #generate() -> impl ::bpaf::Parser<#ty> {
299                        #[allow(unused_imports)]
300                        use ::bpaf::Parser;
301                        #body
302                        #group_help
303                        #adjacent
304                        #(.#attrs)*
305                        #boxed
306                    }
307                }
308            }
309        };
310
311        if let Some(custom_path) = bpaf_path {
312            let mut replaced: ItemFn = parse_quote!(#original);
313            CratePathReplacer::new(parse_quote!(::bpaf), custom_path.clone())
314                .visit_item_fn_mut(&mut replaced);
315            replaced.to_token_stream()
316        } else {
317            original
318        }
319        .to_tokens(tokens)
320    }
321}
322
323// }}}
324
325/// Describes the actual fields,
326/// can be either a single branch for struct or multiple enum variants
327#[derive(Debug, Clone)]
328pub(crate) enum Body {
329    // {{{
330    Single(Branch),
331    Alternatives(Ident, Vec<EnumBranch>),
332}
333
334impl Parse for Body {
335    fn parse(input: ParseStream) -> Result<Self> {
336        if input.peek(token::Struct) {
337            input.parse::<token::Struct>()?;
338            let branch = Self::Single(input.parse::<Branch>()?);
339            if input.peek(token::Semi) {
340                input.parse::<token::Semi>()?;
341            }
342            Ok(branch)
343        } else if input.peek(token::Enum) {
344            input.parse::<token::Enum>()?;
345            let name = input.parse::<Ident>()?;
346            let content;
347            braced!(content in input);
348
349            let branches = content
350                .parse_terminated(ParsedEnumBranch::parse, token::Comma)?
351                .into_iter()
352                .filter_map(|p| p.resolve(&name).transpose())
353                .collect::<Result<Vec<_>>>()?;
354            Ok(Self::Alternatives(name, branches))
355        } else {
356            Err(input.error("Only structs and enums are supported"))
357        }
358    }
359}
360
361impl Body {
362    fn ty(&self) -> Ident {
363        match self {
364            Body::Single(b) => &b.ident,
365            Body::Alternatives(n, _) => n,
366        }
367        .clone()
368    }
369}
370
371impl Body {
372    fn set_named_command(&mut self, span: Span) -> Result<()> {
373        match self {
374            Body::Single(branch) => {
375                branch.set_command();
376                Ok(())
377            }
378            Body::Alternatives(_, _) => Err(Error::new(
379                span,
380                "You can't annotate `enum` with a named command.",
381            )),
382        }
383    }
384
385    fn set_unnamed_command(&mut self) -> Result<()> {
386        match self {
387            Body::Single(b) => {
388                b.set_unnamed_command();
389                Ok(())
390            }
391            Body::Alternatives(_name, _branches) => {
392                /*
393                for branch in branches {
394                    if !branch
395                        .attrs
396                        .iter()
397                        .any(|attr| matches!(attr, EAttr::ToOptions))
398                    {
399                        let name = ident_to_long(&branch.branch.ident);
400                        branch.attrs.insert(0, EAttr::NamedCommand(name));
401                        branch.attrs.insert(0, EAttr::ToOptions);
402                        branch.branch.set_unnamed_command();
403                    }
404                }*/
405                Ok(())
406            }
407        }
408    }
409}
410
411impl ToTokens for Body {
412    fn to_tokens(&self, tokens: &mut TokenStream) {
413        match self {
414            Body::Single(branch) => quote!(#branch),
415            Body::Alternatives(_name, b) if b.len() == 1 => {
416                let branch = &b[0];
417                quote!(#branch)
418            }
419            Body::Alternatives(_name, b) => {
420                let branches = b.iter();
421                let mk = |i| Ident::new(&format!("alt{}", i), Span::call_site());
422                let name_f = b.iter().enumerate().map(|(n, _)| mk(n));
423                let name_t = name_f.clone();
424                quote! {{
425                    #( let #name_f = #branches; )*
426                    ::bpaf::construct!([ #( #name_t, )* ])
427                }}
428            }
429        }
430        .to_tokens(tokens);
431    }
432}
433
434// }}}
435
436/// Generating code for enum branch needs enum name which is not available from
437/// parsing at that moment so operations are performed in two steps:
438/// 1. parse ParsedEnumBranch
439/// 2. resolve it into EnumBranch (or skip if skip is present)
440pub(crate) struct ParsedEnumBranch {
441    // {{{
442    branch: Branch,
443    attrs: Vec<Attribute>,
444}
445
446impl ParsedEnumBranch {
447    fn resolve(self, enum_name: &Ident) -> Result<Option<EnumBranch>> {
448        let ParsedEnumBranch { mut branch, attrs } = self;
449
450        branch.enum_name = Some(EnumPrefix(enum_name.clone()));
451
452        let (enum_decor, mut help) = parse_bpaf_doc_attrs::<Ed>(&attrs)?;
453        let Ed { attrs: ea, skip } = enum_decor.unwrap_or_default();
454        if skip {
455            return Ok(None);
456        }
457
458        let mut attrs = Vec::with_capacity(ea.len());
459        let mut has_options = None;
460        let mut fallback_usage = false;
461        for attr in ea {
462            match attr {
463                EAttr::NamedCommand(_) => {
464                    branch.set_command();
465                    attrs.push(EAttr::ToOptions);
466                    has_options = Some(attrs.len());
467                    attrs.push(attr);
468                }
469                EAttr::UnnamedCommand => {
470                    branch.set_command();
471                    attrs.push(EAttr::ToOptions);
472                    has_options = Some(attrs.len());
473                    attrs.push(EAttr::NamedCommand(ident_to_long(&branch.ident)));
474                }
475
476                EAttr::CommandShort(_) | EAttr::CommandLong(_) => {
477                    // TODO should probably be a bit more careful here,
478                    // new_derive macro addresses that though
479                    attrs.push(attr);
480                }
481
482                EAttr::UnitShort(n) => branch.set_unit_name(StrictName::Short {
483                    name: n.unwrap_or_else(|| ident_to_short(&branch.ident)),
484                }),
485                EAttr::UnitLong(n) => branch.set_unit_name(StrictName::Long {
486                    name: n.unwrap_or_else(|| ident_to_long(&branch.ident)),
487                }),
488                EAttr::Env(name) => branch.set_unit_name(StrictName::Env { name }),
489
490                EAttr::Usage(_) => {
491                    if let Some(o) = attrs.iter().position(|i| matches!(i, EAttr::ToOptions)) {
492                        attrs.insert(o + 1, attr);
493                    } else {
494                        unreachable!();
495                    }
496                }
497                EAttr::Adjacent | EAttr::Hide => attrs.push(attr),
498                EAttr::Header(_) | EAttr::Footer(_) | EAttr::Descr(_) => {
499                    if let Some(o) = attrs.iter().position(|i| matches!(i, EAttr::ToOptions)) {
500                        attrs.insert(o + 1, attr);
501                    }
502                }
503                EAttr::FallbackUsage => fallback_usage = true,
504                EAttr::ToOptions => unreachable!(),
505            }
506        }
507
508        if let Some(opts_at) = has_options {
509            if fallback_usage {
510                attrs.insert(opts_at, EAttr::FallbackUsage);
511            }
512
513            if let Some(h) = std::mem::take(&mut help) {
514                split_ehelp_into(h, opts_at, &mut attrs);
515            }
516        }
517        branch.set_inplicit_name();
518        if let Some(help) = help {
519            branch.push_help(help);
520        }
521
522        Ok(Some(EnumBranch { branch, attrs }))
523    }
524}
525
526impl Parse for ParsedEnumBranch {
527    fn parse(input: ParseStream) -> Result<Self> {
528        Ok(ParsedEnumBranch {
529            attrs: input.call(Attribute::parse_outer)?,
530            branch: input.parse::<Branch>()?,
531        })
532    }
533}
534
535// }}}
536
537#[derive(Debug, Clone)]
538pub(crate) struct EnumBranch {
539    // {{{
540    branch: Branch,
541    attrs: Vec<EAttr>,
542}
543
544impl ToTokens for EnumBranch {
545    fn to_tokens(&self, tokens: &mut TokenStream) {
546        let EnumBranch { branch, attrs } = self;
547        quote!(#branch #(.#attrs)*).to_tokens(tokens);
548    }
549}
550
551// }}}
552
553#[derive(Debug, Clone)]
554pub struct Branch {
555    // {{{
556    pub(crate) enum_name: Option<EnumPrefix>,
557    pub(crate) ident: Ident,
558    pub(crate) fields: FieldSet,
559}
560
561impl Branch {
562    fn set_command(&mut self) {
563        if let FieldSet::Unit(_, _, _) = self.fields {
564            let ident = &self.ident;
565            let enum_name = &self.enum_name;
566            self.fields = FieldSet::Pure(parse_quote!(::bpaf::pure(#enum_name #ident)));
567        }
568    }
569
570    fn set_unnamed_command(&mut self) {
571        if let FieldSet::Unit(_, _, _) = self.fields {
572            self.set_command();
573        }
574    }
575
576    fn set_unit_name(&mut self, name: StrictName) {
577        if let FieldSet::Unit(_, names, _) = &mut self.fields {
578            names.push(name);
579        }
580    }
581
582    fn set_inplicit_name(&mut self) {
583        if let FieldSet::Unit(_, names, _) = &mut self.fields {
584            if !names
585                .iter()
586                .any(|n| matches!(n, StrictName::Long { .. } | StrictName::Short { .. }))
587            {
588                names.push(StrictName::Long {
589                    name: ident_to_long(&self.ident),
590                });
591            }
592        }
593    }
594    fn push_help(&mut self, help: Help) {
595        if let FieldSet::Unit(_, _, h) = &mut self.fields {
596            *h = Some(help);
597            //        } else {
598            //            todo!("use GroupHelp here");
599            // TODO use GroupHelp here?
600        }
601    }
602}
603
604impl Parse for Branch {
605    fn parse(input: ParseStream) -> Result<Self> {
606        let ident = input.parse::<Ident>()?;
607        let content;
608        let fields = if input.peek(token::Paren) {
609            parenthesized!(content in input);
610            FieldSet::Unnamed(content.parse_terminated(StructField::parse_unnamed, token::Comma)?)
611        } else if input.peek(token::Brace) {
612            braced!(content in input);
613            FieldSet::Named(content.parse_terminated(StructField::parse_named, token::Comma)?)
614        } else {
615            if input.peek(token::Semi) {
616                input.parse::<token::Semi>()?;
617            }
618            FieldSet::Unit(ident.clone(), Vec::new(), None)
619        };
620
621        Ok(Branch {
622            enum_name: None,
623            ident,
624            //decor,
625            fields,
626        })
627    }
628}
629
630impl ToTokens for Branch {
631    fn to_tokens(&self, tokens: &mut TokenStream) {
632        let Branch {
633            enum_name,
634            ident,
635            fields,
636        } = self;
637        match fields {
638            FieldSet::Named(fields) => {
639                let name = fields
640                    .iter()
641                    .enumerate()
642                    .map(|(ix, field)| field.var_name(ix));
643                let result = name.clone();
644                let value = fields.iter();
645                quote! {{
646                    #( let #name = #value; )*
647                    ::bpaf::construct!( #enum_name #ident { #( #result , )* })
648                }}
649            }
650            FieldSet::Unnamed(fields) => {
651                let name = fields
652                    .iter()
653                    .enumerate()
654                    .map(|(ix, field)| field.var_name(ix));
655                let result = name.clone();
656                let value = fields.iter();
657                quote! {{
658                    #( let #name = #value; )*
659                    ::bpaf::construct!( #enum_name #ident ( #( #result , )* ))
660                }}
661            }
662            FieldSet::Unit(ident, names, help) => {
663                let help = help.iter();
664                if names.is_empty() {
665                    let name = StrictName::Long {
666                        name: ident_to_long(ident),
667                    };
668                    quote!(::bpaf:: #name.#(help(#help).)* req_flag(#enum_name #ident))
669                } else {
670                    quote!(::bpaf:: #( #names .)* #(help(#help).)* req_flag(#enum_name #ident))
671                }
672            }
673            FieldSet::Pure(x) => quote!(#x),
674        }
675        .to_tokens(tokens);
676    }
677}
678// }}}
679
680#[derive(Debug, Clone)]
681pub(crate) enum FieldSet {
682    // {{{
683    Named(Punctuated<StructField, token::Comma>),
684    Unnamed(Punctuated<StructField, token::Comma>),
685    Unit(Ident, Vec<StrictName>, Option<Help>),
686    Pure(Box<Expr>),
687}