diff --git a/bpaf_derive/Cargo.toml b/bpaf_derive/Cargo.toml index 8b2ce047..9f661700 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 " ] +authors = ["Michael Baykov "] 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 ee4780c9..4f665acf 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, } -#[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 00000000..e0db3069 --- /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::>(); + } + } + + 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>( + input: &'a UseTree, + query_len: usize, + mut query_iter: Q, + mut matched_parts: Vec<&'a UseTree>, + ) -> Option<(Vec<&'a UseTree>, Option)> { + 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 { + 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 437ef0dc..16a36a37 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), Doc(String), diff --git a/bpaf_derive/src/lib.rs b/bpaf_derive/src/lib.rs index 4f4d9c32..fe0c0a03 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 f8893b38..1fa68420 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, pub env: Vec, diff --git a/bpaf_derive/src/td.rs b/bpaf_derive/src/td.rs index 741e4a60..eac83d1f 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::{ @@ -91,6 +91,9 @@ pub(crate) struct TopInfo { pub(crate) adjacent: bool, pub(crate) mode: Mode, pub(crate) attrs: Vec, + + /// Custom absolute path to the `bpaf` crate. + pub(crate) bpaf_path: Option, } impl Default for TopInfo { @@ -105,6 +108,7 @@ impl Default for TopInfo { }, attrs: Vec::new(), ignore_rustdoc: false, + bpaf_path: None, } } } @@ -171,6 +175,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::()?; @@ -239,6 +244,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::(input)?); } else if let Some(pd) = PostDecor::parse(input, &kw)? { attrs.push(pd); } else { @@ -274,6 +281,7 @@ impl Parse for TopInfo { adjacent, mode, attrs, + bpaf_path, }) } } @@ -358,7 +366,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 a8f3f926..4544d72d 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, + bpaf_path: Option, } 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: _, @@ -279,6 +286,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) } @@ -288,8 +321,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), @@ -493,7 +526,7 @@ impl Parse for ParsedEnumBranch { // }}} -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct EnumBranch { // {{{ branch: Branch, @@ -509,12 +542,12 @@ impl ToTokens for EnumBranch { // }}} -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Branch { // {{{ - enum_name: Option, - ident: Ident, - fields: FieldSet, + pub(crate) enum_name: Option, + pub(crate) ident: Ident, + pub(crate) fields: FieldSet, } impl Branch { @@ -636,7 +669,7 @@ impl ToTokens for Branch { } // }}} -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) enum FieldSet { // {{{ Named(Punctuated), diff --git a/bpaf_derive/src/top_tests.rs b/bpaf_derive/src/top_tests.rs index 7d4c1590..51c96c7c 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 { + #[allow(unused_imports)] + use ::indirector::bpaf::Parser; + { + let alt0 = { + let field = ::indirector::bpaf::long("field").argument::("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() { diff --git a/bpaf_derive/src/utils.rs b/bpaf_derive/src/utils.rs index 2b384320..d3ef497a 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(input: ParseStream) -> Result { @@ -20,6 +20,11 @@ pub(crate) fn parse_opt_arg(input: ParseStream) -> Result> { } } +pub(crate) fn parse_name_value(input: ParseStream) -> Result { + let _ = input.parse::(); + input.parse::() +} + pub(crate) fn parse_arg2(input: ParseStream) -> Result<(A, B)> { let content; let _ = parenthesized!(content in input);