Skip to content

Commit

Permalink
bpaf_derive: allow specification of a custom crate path to resolve pr…
Browse files Browse the repository at this point in the history
…oc_macro hygiene related issues that might develop in some situations
  • Loading branch information
bzm3r committed Jan 5, 2024
1 parent 5f001a2 commit cf60ce4
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 22 deletions.
4 changes: 2 additions & 2 deletions bpaf_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand Down
11 changes: 6 additions & 5 deletions bpaf_derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl ToTokens for TurboFish<'_> {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Consumer {
Switch {
span: Span,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -243,7 +243,7 @@ impl ToTokens for PostDecor {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum PostParse {
Adjacent { span: Span },
Catch { span: Span },
Expand Down Expand Up @@ -273,7 +273,7 @@ impl PostParse {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum PostDecor {
Complete {
span: Span,
Expand Down Expand Up @@ -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 {
Expand Down
205 changes: 205 additions & 0 deletions bpaf_derive/src/custom_path.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion bpaf_derive/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use syn::{
Expr, Result,
};

#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum Help {
Custom(Box<Expr>),
Doc(String),
Expand Down
1 change: 1 addition & 0 deletions bpaf_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod top_tests;
mod help;

mod td;
mod custom_path;

use top::Top;

Expand Down
2 changes: 1 addition & 1 deletion bpaf_derive/src/named_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
12 changes: 10 additions & 2 deletions bpaf_derive/src/td.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -91,6 +91,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 {
Expand All @@ -105,6 +108,7 @@ impl Default for TopInfo {
},
attrs: Vec::new(),
ignore_rustdoc: false,
bpaf_path: None,
}
}
}
Expand Down Expand Up @@ -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::<Ident>()?;

Expand Down Expand Up @@ -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::<syn::Path>(input)?);
} else if let Some(pd) = PostDecor::parse(input, &kw)? {
attrs.push(pd);
} else {
Expand Down Expand Up @@ -274,6 +281,7 @@ impl Parse for TopInfo {
adjacent,
mode,
attrs,
bpaf_path,
})
}
}
Expand Down Expand Up @@ -358,7 +366,7 @@ impl Parse for Ed {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum EAttr {
NamedCommand(LitStr),
UnnamedCommand,
Expand Down
Loading

0 comments on commit cf60ce4

Please sign in to comment.