Skip to main content

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