Repository: pacak/bpaf
Pull request #332 (2024-01-05 07:25:09)

bpaf_derive: allow specification of a custom crate path to resolve proc_macro hygiene related issues that might develop in some situations
Suppose that we have a crate called `indirector`. 

`indirector` publicly re-exports `bpaf`:

```rust
pub use bpaf;
pub use bpaf::Bpaf;
```

Then, we have a crate called `user`, which imports `indirector`, and uses `bpaf` through it:

```rust
use indirector::bpaf::{Parser, Bpaf};
```

This will cause a hygiene related issue, because currently `bpaf_derive` assumes that `bpaf` is available globally (via `::bpaf`) [in line with the official recommendations](https://doc.rust-lang.org/reference/procedural-macros.html#procedural-macro-hygiene).

There was a [discussion](https://users.rust-lang.org/t/how-to-express-crate-path-in-procedural-macros/91274/1) on how to address this sort of issue, which lead me to [experiment](https://github.com/bzm3r/hygiene_mnwe) with the [two solutions](https://users.rust-lang.org/t/how-to-express-crate-path-in-procedural-macros/91274/17) listed there:

* using `extern crate`: this [does not work](https://github.com/bzm3r/hygiene_mnwe/tree/extern-crate) because because it is not relevant to our situation, as it is for fixing the use of `bpaf_derive` *within* `bpaf` 
* [using the `serde` solution](https://serde.rs/container-attrs.html#crate): this [works](https://github.com/bzm3r/hygiene_mnwe/tree/explicit-path)

So I implemented a simple version of it. I tried to keep the changes as small as possible, but was forced to increase how large a change was made due to the realities of how [`use some::path` is handled in `syn`](https://docs.rs/syn/latest/syn/struct.ItemUse.html).

I have included [one simple test](https://github.com/bzm3r/bpaf/blob/cf60ce4b054a67b637e6ccee5edae55e76cba2fd/bpaf_derive/src/top_tests.rs#L1180), and have also tested it out in the use case where I first ran into this issue. 


pacak: I did consider this scenario a while ago and decided not to do anything about it back then. Reexports make sense if you have a library and and you want your users to derive some implementations using some specific version of a third crate, usually because that's what your library expects to consume. With `bpaf`, at least when used the intended way, consumer is usually a crate with a binary and authors of that library tend to control all the places where instances can be derived. That said this MR adds some flexibility to how it can be used and doesn't add any external dependencies so I don't mind merging this.

Any ideas how much compilation time this adds?
bzm3r: > I did consider this scenario a while ago and decided not to do anything about it back then. Reexports make sense if you have a library and and you want your users to derive some implementations using some specific version of a third crate, usually because that's what your library expects to consume. With `bpaf`, at least when used the intended way, consumer is usually a crate with a binary and authors of that library tend to control all the places where instances can be derived. That said this MR adds some flexibility to how it can be used and doesn't add any external dependencies so I don't mind merging this.
> 
> Any ideas how much compilation time this adds?

Sorry for taking a while to respond to this. It was not because I was ignoring it, but rather because I fell into a rabbit hole of trying to be accurate. 

Long story short, I couldn't detect significant differences between compilation time, beyond what seems to be natural variation between build runs. I figured it might be due to my compilation setup (sccache + mold), but it seemed to be insensitive to that. Then I fell into the rabbit hole of performing many build runs to create a statistical comparison (that work is about 80% done), but then I fell into a side-rabbit-hole related to Nix/NixOS and how I'm setting up the Rust dev environments...

:facepalm: 

So it's a work in progress, and a task that is taking longer than intended. Perhaps there might be some future benefit to it, but likely I just got distracted...there's probably already a good statistical tool for measuring build differences, now that I think about it...probably some variant of `criterion`...so my rabbit hole is also likely ultimately pointless. I guess I did learn about [cargo build --timings](https://doc.rust-lang.org/cargo/reference/timings.html)....

Sigh, but for now, I guess, the short answer is: I couldn't detect any differences, even after being careful with cleaning previous build artifacts.

Patch:
diff --git a/bpaf_derive/Cargo.toml b/bpaf_derive/Cargo.toml
index 8b2ce04..9f66170 100644
--- a/bpaf_derive/Cargo.toml
+++ b/bpaf_derive/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2018"
 categories = ["command-line-interface"]
 description = "Derive macros for bpaf Command Line Argument Parser"
 keywords = ["args", "arguments", "cli", "parser", "parse"]
-authors = [ "Michael Baykov <manpacket@gmail.com>" ]
+authors = ["Michael Baykov <manpacket@gmail.com>"]
 readme = "README.md"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/pacak/bpaf"
@@ -15,7 +15,7 @@ name = "bpaf_derive"
 proc-macro = true
 
 [dependencies]
-syn = { version = "2.0.2", features = ["full", "extra-traits"] }
+syn = { version = "2.0.2", features = ["full", "extra-traits", "visit-mut"] }
 proc-macro2 = "1.0.27"
 quote = "1.0.9"
 
diff --git a/bpaf_derive/src/attrs.rs b/bpaf_derive/src/attrs.rs
index ee4780c..4f665ac 100644
--- a/bpaf_derive/src/attrs.rs
+++ b/bpaf_derive/src/attrs.rs
@@ -36,7 +36,7 @@ impl ToTokens for TurboFish<'_> {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub enum Consumer {
     Switch {
         span: Span,
@@ -187,7 +187,7 @@ impl ToTokens for StrictName {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum Post {
     /// Those items can change the type of the result
     Parse(PostParse),
@@ -243,7 +243,7 @@ impl ToTokens for PostDecor {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum PostParse {
     Adjacent { span: Span },
     Catch { span: Span },
@@ -273,7 +273,7 @@ impl PostParse {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum PostDecor {
     Complete {
         span: Span,
@@ -528,13 +528,14 @@ impl PostDecor {
         }))
     }
 }
+
 #[derive(Debug)]
 pub(crate) struct CustomHelp {
     pub span: Span,
     pub doc: Box<Expr>,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) struct EnumPrefix(pub Ident);
 
 impl ToTokens for EnumPrefix {
diff --git a/bpaf_derive/src/custom_path.rs b/bpaf_derive/src/custom_path.rs
new file mode 100644
index 0000000..e0db306
--- /dev/null
+++ b/bpaf_derive/src/custom_path.rs
@@ -0,0 +1,205 @@
+use syn::{
+    punctuated::Punctuated,
+    token::{self, PathSep},
+    visit_mut::{self, VisitMut},
+    PathSegment, UseName, UsePath, UseRename, UseTree,
+};
+
+/// Implements [`syn::visit_mut::VisitMut`] to find
+/// those [`Path`](syn::Path)s which match
+/// [`query`](Self::target) and replace them with [`target`](Self::target).
+pub(crate) struct CratePathReplacer {
+    /// The prefix to search for within an input path.
+    query: syn::Path,
+    /// The prefix we wish the input path to have.
+    target: syn::Path,
+}
+
+impl CratePathReplacer {
+    pub(crate) fn new(target: syn::Path, replacement: syn::Path) -> Self {
+        CratePathReplacer {
+            query: target,
+            target: replacement,
+        }
+    }
+
+    /// Check if both [`query`](Self::query) and `input` have the same leading
+    /// path segment (`::`) responsible for marking [a path as
+    /// "global"](https://doc.rust-lang.org/reference/procedural-macros.html#procedural-macro-hygiene).
+    ///
+    /// If these do not match, no replacement will be performed.
+    fn path_global_match(&self, input: &mut syn::Path) -> bool {
+        self.query.leading_colon.is_some() && input.leading_colon.is_some()
+    }
+
+    /// Check if the initial segments of `input` match [`query`](Self::query).
+    ///
+    /// If these do not match, no replacement will be performed.
+    fn path_segments_match(&self, input: &mut syn::Path) -> bool {
+        self.query
+            .segments
+            .iter()
+            .zip(input.segments.iter())
+            .all(|(f, o)| f == o)
+    }
+
+    /// Replaces the prefix of `input` with those of [`target`](Self::target) if
+    /// the `input` path's prefix matches [`query`](Self::query).
+    fn replace_path_if_match(&self, input: &mut syn::Path) {
+        if self.path_global_match(input) && self.path_segments_match(input) {
+            input.leading_colon = self.target.leading_colon;
+            input.segments = self
+                .target
+                .segments
+                .clone()
+                .into_iter()
+                .chain(
+                    input
+                        .segments
+                        .iter()
+                        .skip(self.query.segments.iter().count())
+                        .cloned(),
+                )
+                .collect::<Punctuated<_, _>>();
+        }
+    }
+
+    fn item_use_global_match(&self, input: &syn::ItemUse) -> bool {
+        self.query.leading_colon == input.leading_colon
+    }
+
+    fn item_use_segments_match<'a, Q: Iterator<Item = &'a PathSegment>>(
+        input: &'a UseTree,
+        query_len: usize,
+        mut query_iter: Q,
+        mut matched_parts: Vec<&'a UseTree>,
+    ) -> Option<(Vec<&'a UseTree>, Option<UseTree>)> {
+        if let Some(next_to_match) = query_iter.next() {
+            match input {
+                UseTree::Path(path) => {
+                    if next_to_match.ident == path.ident {
+                        matched_parts.push(input);
+                        return Self::item_use_segments_match(
+                            path.tree.as_ref(),
+                            query_len,
+                            query_iter,
+                            matched_parts,
+                        );
+                    }
+                }
+                UseTree::Name(name) => {
+                    if next_to_match.ident == name.ident {
+                        if query_iter.next().is_some() {
+                            return None;
+                        } else {
+                            matched_parts.push(input);
+                        }
+                    }
+                }
+                UseTree::Rename(rename) => {
+                    if next_to_match.ident == rename.ident {
+                        if query_iter.next().is_some() {
+                            return None;
+                        } else {
+                            matched_parts.push(input);
+                        }
+                    }
+                }
+                UseTree::Glob(_) => {}
+                UseTree::Group(_) => {}
+            }
+        }
+
+        if query_len == matched_parts.len() {
+            Some((matched_parts, Some(input.clone())))
+        } else {
+            None
+        }
+    }
+
+    fn append_suffix_to_target(
+        &self,
+        matched_parts: Vec<&UseTree>,
+        suffix: Option<UseTree>,
+    ) -> UseTree {
+        let last_input_match = matched_parts
+            .last()
+            .expect("If a match exists, then it the matched prefix must be non-empty.");
+        let mut rev_target_ids = self.target.segments.iter().map(|s| s.ident.clone()).rev();
+        let mut result_tree = match last_input_match {
+            UseTree::Path(_) => {
+                if let Some(suffix_tree) = suffix {
+                    UseTree::Path(UsePath {
+                        ident: rev_target_ids.next().expect(
+                            "error while making a `UseTree::Path`: target should not be empty",
+                        ),
+                        colon2_token: PathSep::default(),
+                        tree: Box::new(suffix_tree),
+                    })
+                } else {
+                    unreachable!("If the last part of the matched input was a path, then there must be some suffix left to attach to complete it.")
+                }
+            }
+            UseTree::Name(_) => {
+                assert!(suffix.is_none(), "If the last part of the matched input was a syn::UseTree::Name, then there shouldn't be any suffix left to attach to the prefix.");
+                UseTree::Name(UseName {
+                    ident: rev_target_ids
+                        .next()
+                        .expect("error while making a `UseTree::Name`: target should not be empty"),
+                })
+            }
+            UseTree::Rename(original_rename) => {
+                assert!(suffix.is_none(), "If the last part of the matched input was a syn::UseTree::Rename, then there shouldn't be any suffix left to attach to the prefix.");
+                UseTree::Rename(UseRename {
+                    ident: rev_target_ids.next().expect(
+                        "error while making a `UseTree::Rename`: target should not be empty",
+                    ),
+                    as_token: token::As::default(),
+                    rename: original_rename.rename.clone(),
+                })
+            }
+            UseTree::Glob(_) => unreachable!(
+                "There is no functionality for matching against a syn::UseTree::Group."
+            ),
+            UseTree::Group(_) => unreachable!(
+                "There is no functionality for matching against a syn::UseTree::Group."
+            ),
+        };
+        for id in rev_target_ids {
+            result_tree = UseTree::Path(UsePath {
+                ident: id,
+                colon2_token: PathSep::default(),
+                tree: Box::new(result_tree),
+            })
+        }
+        result_tree
+    }
+
+    /// Replaces the prefix of `input` with those of [`target`](Self::target) if
+    /// the `input` path's prefix matches [`query`](Self::query).
+    fn replace_item_use_if_match(&self, input: &mut syn::ItemUse) {
+        if self.item_use_global_match(input) {
+            if let Some((matched_prefix, suffix)) = Self::item_use_segments_match(
+                &input.tree,
+                self.query.segments.len(),
+                self.query.segments.iter(),
+                vec![],
+            ) {
+                input.leading_colon = self.target.leading_colon;
+                input.tree = self.append_suffix_to_target(matched_prefix, suffix);
+            }
+        }
+    }
+}
+
+impl VisitMut for CratePathReplacer {
+    fn visit_path_mut(&mut self, path: &mut syn::Path) {
+        self.replace_path_if_match(path);
+        visit_mut::visit_path_mut(self, path);
+    }
+
+    fn visit_item_use_mut(&mut self, item_use: &mut syn::ItemUse) {
+        self.replace_item_use_if_match(item_use);
+        visit_mut::visit_item_use_mut(self, item_use);
+    }
+}
diff --git a/bpaf_derive/src/help.rs b/bpaf_derive/src/help.rs
index 437ef0d..16a36a3 100644
--- a/bpaf_derive/src/help.rs
+++ b/bpaf_derive/src/help.rs
@@ -5,7 +5,7 @@ use syn::{
     Expr, Result,
 };
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum Help {
     Custom(Box<Expr>),
     Doc(String),
diff --git a/bpaf_derive/src/lib.rs b/bpaf_derive/src/lib.rs
index 4f4d9c3..fe0c0a0 100644
--- a/bpaf_derive/src/lib.rs
+++ b/bpaf_derive/src/lib.rs
@@ -16,6 +16,7 @@ mod top_tests;
 mod help;
 
 mod td;
+mod custom_path;
 
 use top::Top;
 
diff --git a/bpaf_derive/src/named_field.rs b/bpaf_derive/src/named_field.rs
index f8893b3..1fa6842 100644
--- a/bpaf_derive/src/named_field.rs
+++ b/bpaf_derive/src/named_field.rs
@@ -15,7 +15,7 @@ use crate::{
     utils::to_snake_case,
 };
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) struct StructField {
     pub name: Option<Ident>,
     pub env: Vec<StrictName>,
diff --git a/bpaf_derive/src/td.rs b/bpaf_derive/src/td.rs
index 28ce23b..9a3d585 100644
--- a/bpaf_derive/src/td.rs
+++ b/bpaf_derive/src/td.rs
@@ -1,7 +1,7 @@
 use crate::{
     attrs::PostDecor,
     help::Help,
-    utils::{parse_arg, parse_opt_arg},
+    utils::{parse_arg, parse_name_value, parse_opt_arg},
 };
 use quote::{quote, ToTokens};
 use syn::{
@@ -92,6 +92,9 @@ pub(crate) struct TopInfo {
     pub(crate) adjacent: bool,
     pub(crate) mode: Mode,
     pub(crate) attrs: Vec<PostDecor>,
+
+    /// Custom absolute path to the `bpaf` crate.
+    pub(crate) bpaf_path: Option<syn::Path>,
 }
 
 impl Default for TopInfo {
@@ -106,6 +109,7 @@ impl Default for TopInfo {
             },
             attrs: Vec::new(),
             ignore_rustdoc: false,
+            bpaf_path: None,
         }
     }
 }
@@ -172,6 +176,7 @@ impl Parse for TopInfo {
         let mut adjacent = false;
         let mut attrs = Vec::new();
         let mut first = true;
+        let mut bpaf_path = None;
         loop {
             let kw = input.parse::<Ident>()?;
 
@@ -240,6 +245,8 @@ impl Parse for TopInfo {
             } else if kw == "help" {
                 let help = parse_arg(input)?;
                 with_command(&kw, command.as_mut(), |cfg| cfg.help = Some(help))?;
+            } else if kw == "bpaf_path" {
+                bpaf_path.replace(parse_name_value::<syn::Path>(input)?);
             } else if kw == "max_width" {
                 let max_width = parse_arg(input)?;
                 with_options(&kw, options.as_mut(), |opt| opt.max_width = Some(max_width))?;
@@ -278,6 +285,7 @@ impl Parse for TopInfo {
             adjacent,
             mode,
             attrs,
+            bpaf_path,
         })
     }
 }
@@ -362,7 +370,7 @@ impl Parse for Ed {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum EAttr {
     NamedCommand(LitStr),
     UnnamedCommand,
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index 94063db..1ed338c 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -5,11 +5,14 @@ use syn::{
     parse::{Parse, ParseStream},
     parse_quote,
     punctuated::Punctuated,
-    token, Attribute, Error, Expr, Ident, LitChar, LitStr, Result, Visibility,
+    token,
+    visit_mut::VisitMut,
+    Attribute, Error, Expr, Ident, ItemFn, LitChar, LitStr, Result, Visibility,
 };
 
 use crate::{
     attrs::{parse_bpaf_doc_attrs, EnumPrefix, PostDecor, StrictName},
+    custom_path::CratePathReplacer,
     field::StructField,
     help::Help,
     td::{CommandCfg, EAttr, Ed, Mode, OptionsCfg, ParserCfg, TopInfo},
@@ -36,6 +39,7 @@ pub(crate) struct Top {
     boxed: bool,
     adjacent: bool,
     attrs: Vec<PostDecor>,
+    bpaf_path: Option<syn::Path>,
 }
 
 fn ident_to_long(ident: &Ident) -> LitStr {
@@ -61,6 +65,7 @@ impl Parse for Top {
             attrs,
             ignore_rustdoc,
             adjacent,
+            bpaf_path,
         } = top_decor.unwrap_or_default();
 
         if ignore_rustdoc {
@@ -110,6 +115,7 @@ impl Parse for Top {
             body,
             boxed,
             adjacent,
+            bpaf_path,
         })
     }
 }
@@ -173,6 +179,7 @@ impl ToTokens for Top {
             attrs,
             boxed,
             adjacent,
+            bpaf_path,
         } = self;
         let boxed = if *boxed { quote!(.boxed()) } else { quote!() };
         let adjacent = if *adjacent {
@@ -181,7 +188,7 @@ impl ToTokens for Top {
             quote!()
         };
 
-        match mode {
+        let original = match mode {
             Mode::Command { command, options } => {
                 let OptionsCfg {
                     cargo_helper: _,
@@ -286,6 +293,32 @@ impl ToTokens for Top {
                     }
                 }
             }
+        };
+
+        if let Some(custom_path) = bpaf_path {
+            //     syn::parse2(original)
+            //     .map_err(|e| {
+            //         syn::Error::new(
+            //     e.span(),
+            //     format!("Failed to parse originally generated macro output as an ItemFn: {e}"),
+            // )
+            //     })
+            //     .unwrap();
+            let mut replaced: ItemFn = parse_quote!(#original);
+            // syn::parse2(quote!(::bpaf))
+            // .map_err(|e| {
+            //     syn::Error::new(
+            //         e.span(),
+            //         format!("Failed to convert quote!(::bpaf) into a Path: {e}"),
+            //     )
+            // })
+            // .unwrap()
+            CratePathReplacer::new(parse_quote!(::bpaf), custom_path.clone())
+                .visit_item_fn_mut(&mut replaced);
+
+            replaced.to_token_stream()
+        } else {
+            original
         }
         .to_tokens(tokens)
     }
@@ -295,8 +328,8 @@ impl ToTokens for Top {
 
 /// Describes the actual fields,
 /// can be either a single branch for struct or multiple enum variants
-#[derive(Debug)]
-enum Body {
+#[derive(Debug, Clone)]
+pub(crate) enum Body {
     // {{{
     Single(Branch),
     Alternatives(Ident, Vec<EnumBranch>),
@@ -500,7 +533,7 @@ impl Parse for ParsedEnumBranch {
 
 // }}}
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) struct EnumBranch {
     // {{{
     branch: Branch,
@@ -516,12 +549,12 @@ impl ToTokens for EnumBranch {
 
 // }}}
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct Branch {
     // {{{
-    enum_name: Option<EnumPrefix>,
-    ident: Ident,
-    fields: FieldSet,
+    pub(crate) enum_name: Option<EnumPrefix>,
+    pub(crate) ident: Ident,
+    pub(crate) fields: FieldSet,
 }
 
 impl Branch {
@@ -643,7 +676,7 @@ impl ToTokens for Branch {
 }
 // }}}
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum FieldSet {
     // {{{
     Named(Punctuated<StructField, token::Comma>),
diff --git a/bpaf_derive/src/utils.rs b/bpaf_derive/src/utils.rs
index 2b38432..d3ef497 100644
--- a/bpaf_derive/src/utils.rs
+++ b/bpaf_derive/src/utils.rs
@@ -1,7 +1,7 @@
 use syn::{
     parenthesized,
     parse::{Parse, ParseStream},
-    token, Attribute, Expr, LitChar, LitStr, Result,
+    token, Attribute, Expr, LitChar, LitStr, Result, Token,
 };
 
 pub(crate) fn parse_arg<T: Parse>(input: ParseStream) -> Result<T> {
@@ -20,6 +20,11 @@ pub(crate) fn parse_opt_arg<T: Parse>(input: ParseStream) -> Result<Option<T>> {
     }
 }
 
+pub(crate) fn parse_name_value<T: Parse>(input: ParseStream) -> Result<T> {
+    let _ = input.parse::<Token![=]>();
+    input.parse::<T>()
+}
+
 pub(crate) fn parse_arg2<A: Parse, B: Parse>(input: ParseStream) -> Result<(A, B)> {
     let content;
     let _ = parenthesized!(content in input);


Test patch:
diff --git a/bpaf_derive/src/top_tests.rs b/bpaf_derive/src/top_tests.rs
index 7d4c159..51c96c7 100644
--- a/bpaf_derive/src/top_tests.rs
+++ b/bpaf_derive/src/top_tests.rs
@@ -1176,6 +1176,60 @@ fn top_comment_is_group_help_struct() {
     assert_eq!(top.to_token_stream().to_string(), expected.to_string());
 }
 
+#[test]
+fn custom_bpaf_path() {
+    let input: Top = parse_quote! {
+        // those are options
+        #[bpaf(options, header(h), footer(f), bpaf_path = ::indirector::bpaf)]
+        enum Opt {
+            #[bpaf(command("foo"))]
+            /// foo doc
+            ///
+            ///
+            /// header
+            ///
+            ///
+            /// footer
+            Foo { field: usize },
+            /// bar doc
+            #[bpaf(command, adjacent)]
+            Bar { field: bool }
+        }
+    };
+
+    let expected = quote! {
+        fn opt() -> ::indirector::bpaf::OptionParser<Opt> {
+            #[allow(unused_imports)]
+            use ::indirector::bpaf::Parser;
+            {
+                let alt0 = {
+                    let field = ::indirector::bpaf::long("field").argument::<usize>("ARG");
+                    ::indirector::bpaf::construct!(Opt::Foo { field, })
+                }
+                .to_options()
+                .footer("footer")
+                .header("header")
+                .descr("foo doc")
+                .command("foo");
+
+                let alt1 = {
+                    let field = ::indirector::bpaf::long("field").switch();
+                    ::indirector::bpaf::construct!(Opt::Bar { field, })
+                }
+                .to_options()
+                .descr("bar doc")
+                .command("bar")
+                .adjacent();
+                ::indirector::bpaf::construct!([alt0, alt1, ])
+            }
+            .to_options()
+            .header(h)
+            .footer(f)
+        }
+    };
+    assert_eq!(input.to_token_stream().to_string(), expected.to_string());
+}
+
 /*
 #[test]
 fn push_down_command() {