use proc_macro2::{TokenStream, TokenTree};
use quote::{ToTokens, quote};
use syn::ext::IdentExt;
use syn::{GenericParam, Ident};
pub(crate) fn mentions(tokens: &impl ToTokens, name: &str) -> bool {
fn walk(tokens: TokenStream, name: &str) -> bool {
tokens.into_iter().any(|token| match token {
TokenTree::Ident(ident) => ident == name,
TokenTree::Group(group) => walk(group.stream(), name),
_ => false,
})
}
walk(tokens.to_token_stream(), name)
}
pub(crate) fn lifetimes(tokens: &impl ToTokens) -> Vec<Ident> {
fn walk(tokens: TokenStream, found: &mut Vec<Ident>) {
let mut apostrophe = false;
for token in tokens {
match token {
TokenTree::Group(group) => {
walk(group.stream(), found);
apostrophe = false;
}
TokenTree::Punct(punct) => apostrophe = punct.as_char() == '\'',
TokenTree::Ident(ident) => {
if apostrophe && ident != "static" && ident != "_" && !found.contains(&ident) {
found.push(ident);
}
apostrophe = false;
}
TokenTree::Literal(_) => apostrophe = false,
}
}
}
let mut found = Vec::new();
walk(tokens.to_token_stream(), &mut found);
found
}
pub(crate) fn rename(tokens: &impl ToTokens, renames: &[(String, TokenStream)]) -> TokenStream {
fn find<'a>(renames: &'a [(String, TokenStream)], name: &str) -> Option<&'a TokenStream> {
renames
.iter()
.find(|(from, _)| from == name)
.map(|(_, to)| to)
}
fn walk(tokens: TokenStream, renames: &[(String, TokenStream)]) -> TokenStream {
let mut out = TokenStream::new();
let mut apostrophe: Option<TokenTree> = None;
for token in tokens {
match token {
TokenTree::Ident(ident) => match apostrophe.take() {
Some(punct) => match find(renames, &format!("'{ident}")) {
Some(to) => out.extend(to.clone()),
None => {
out.extend([punct, TokenTree::Ident(ident)]);
}
},
None => match find(renames, &ident.to_string()) {
Some(to) => out.extend(to.clone()),
None => out.extend([TokenTree::Ident(ident)]),
},
},
TokenTree::Punct(punct) if punct.as_char() == '\'' => {
out.extend(apostrophe.take());
apostrophe = Some(TokenTree::Punct(punct));
}
TokenTree::Group(group) => {
out.extend(apostrophe.take());
let inner = walk(group.stream(), renames);
out.extend([TokenTree::Group(proc_macro2::Group::new(
group.delimiter(),
inner,
))]);
}
other => {
out.extend(apostrophe.take());
out.extend([other]);
}
}
}
out.extend(apostrophe);
out
}
walk(tokens.to_token_stream(), renames)
}
pub(crate) fn ours(attr: &syn::Attribute, name: &str) -> bool {
let path = attr.path();
if path.is_ident(name) {
return true;
}
path.segments.last().is_some_and(|last| last.ident == name)
&& path.segments.first().is_some_and(|first| {
first.ident == "closed_trait" || first.ident == "closed_trait_macros"
})
}
pub(crate) fn render(tokens: &impl ToTokens) -> String {
tokens
.to_token_stream()
.to_string()
.replace(" <", "<")
.replace("< ", "<")
.replace(" >", ">")
.replace("> ", ">")
.replace(" ,", ",")
.replace(" ::", "::")
.replace(":: ", "::")
.replace(" :", ":")
.replace("& ", "&")
}
pub(crate) fn name_of(param: &GenericParam) -> String {
match param {
GenericParam::Lifetime(param) => param.lifetime.ident.to_string(),
GenericParam::Type(param) => param.ident.to_string(),
GenericParam::Const(param) => param.ident.to_string(),
}
}
pub(crate) fn argument(param: &GenericParam) -> TokenStream {
match param {
GenericParam::Lifetime(param) => {
let lifetime = ¶m.lifetime;
quote!(#lifetime)
}
GenericParam::Type(param) => {
let ident = ¶m.ident;
quote!(#ident)
}
GenericParam::Const(param) => {
let ident = ¶m.ident;
quote!(#ident)
}
}
}
pub(crate) fn snake_case(ident: &Ident) -> String {
let name = ident.unraw().to_string();
let characters: Vec<char> = name.chars().collect();
let mut snake = String::new();
for (index, character) in characters.iter().enumerate() {
if character.is_uppercase() {
let after_lowercase = index > 0 && !characters[index - 1].is_uppercase();
if after_lowercase {
snake.push('_');
}
snake.extend(character.to_lowercase());
} else {
snake.push(*character);
}
}
snake
}
pub(crate) fn fresh(taken: impl IntoIterator<Item = String>, base: &str) -> Ident {
let taken: Vec<String> = taken.into_iter().collect();
let span = proc_macro2::Span::call_site();
if !taken.iter().any(|name| name == base) {
return Ident::new(base, span);
}
let mut suffix = 2;
loop {
let candidate = format!("{base}{suffix}");
if !taken.contains(&candidate) {
return Ident::new(&candidate, span);
}
suffix += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::{Type, parse_quote};
#[test]
fn mentions_looks_inside_generic_arguments() {
let ty: Type = parse_quote!(Wrapper<Vec<Self>>);
assert!(mentions(&ty, "Self"));
assert!(mentions(&ty, "Vec"));
assert!(!mentions(&ty, "Other"));
}
#[test]
fn lifetimes_are_in_order_without_repeats() {
let ty: Type = parse_quote!(Foo<'b, 'a, 'b>);
let found: Vec<String> = lifetimes(&ty).iter().map(Ident::to_string).collect();
assert_eq!(found, ["b", "a"]);
}
#[test]
fn lifetimes_skips_the_ones_no_impl_can_declare() {
let ty: Type = parse_quote!(Foo<'static, '_, 'a>);
let found: Vec<String> = lifetimes(&ty).iter().map(Ident::to_string).collect();
assert_eq!(found, ["a"]);
}
#[test]
fn lifetimes_tells_a_lifetime_from_a_type_of_the_same_name() {
let ty: Type = parse_quote!(Foo<src, 'src>);
let found: Vec<String> = lifetimes(&ty).iter().map(Ident::to_string).collect();
assert_eq!(found, ["src"], "only the one behind an apostrophe");
}
#[test]
fn render_writes_types_the_way_a_person_would() {
let ty: Type = parse_quote!(a::Boxed<T>);
assert_eq!(render(&ty), "a::Boxed<T>");
let predicate: syn::WherePredicate = parse_quote!(Self: Clone);
assert_eq!(render(&predicate), "Self: Clone");
let ty: Type = parse_quote!(&'a [u8]);
assert_eq!(render(&ty), "&'a [u8]");
}
#[test]
fn snake_case_splits_on_the_capitals_a_person_would() {
assert_eq!(snake_case(&parse_quote!(Shape)), "shape");
assert_eq!(snake_case(&parse_quote!(HttpRequest)), "http_request");
assert_eq!(snake_case(&parse_quote!(already_snake)), "already_snake");
}
#[test]
fn snake_case_leaves_a_run_of_capitals_alone() {
assert_eq!(snake_case(&parse_quote!(HTTPRequest)), "httprequest");
}
#[test]
fn a_parameters_name_drops_the_apostrophe() {
assert_eq!(name_of(&parse_quote!('a)), "a");
assert_eq!(name_of(&parse_quote!(T: Clone)), "T");
assert_eq!(name_of(&parse_quote!(const N: usize)), "N");
}
#[test]
fn an_argument_drops_the_bounds() {
assert_eq!(render(&argument(&parse_quote!(T: Clone + Send))), "T");
assert_eq!(render(&argument(&parse_quote!('a))), "'a");
assert_eq!(render(&argument(&parse_quote!(const N: usize))), "N");
}
#[test]
fn fresh_takes_the_base_when_nothing_holds_it() {
assert_eq!(fresh(Vec::new(), "S").to_string(), "S");
assert_eq!(fresh(vec!["T".to_owned()], "S").to_string(), "S");
}
#[test]
fn fresh_counts_past_every_name_already_held() {
let taken = |names: &[&str]| names.iter().map(|n| n.to_string()).collect::<Vec<_>>();
assert_eq!(fresh(taken(&["S"]), "S").to_string(), "S2");
assert_eq!(fresh(taken(&["S", "S2"]), "S").to_string(), "S3");
assert_eq!(fresh(taken(&["S", "S3"]), "S").to_string(), "S2");
}
fn renamed(ty: TokenStream, renames: &[(&str, &str)]) -> String {
let renames: Vec<(String, TokenStream)> = renames
.iter()
.map(|(from, to)| {
let to: TokenStream = to.parse().expect("the replacement parses");
((*from).to_owned(), to)
})
.collect();
render(&rename(&ty, &renames))
}
#[test]
fn rename_replaces_whole_idents_only() {
assert_eq!(renamed(quote!(Boxed<U>), &[("U", "T")]), "Boxed<T>");
assert_eq!(renamed(quote!(Boxed<SomeU>), &[("U", "T")]), "Boxed<SomeU>");
}
#[test]
fn rename_descends_into_nested_arguments() {
assert_eq!(
renamed(quote!(Boxed<Vec<(U, u8)>>), &[("U", "T")]),
"Boxed<Vec<(T, u8)>>"
);
}
#[test]
fn rename_tells_a_lifetime_from_a_type_of_the_same_name() {
assert_eq!(
renamed(quote!(Slice<'a, a>), &[("'a", "'b")]),
"Slice<'b, a>"
);
assert_eq!(renamed(quote!(Slice<'a, a>), &[("a", "T")]), "Slice<'a, T>");
}
#[test]
fn rename_leaves_everything_else_alone() {
assert_eq!(renamed(quote!(Plain), &[("U", "T")]), "Plain");
assert_eq!(renamed(quote!(Boxed<U>), &[]), "Boxed<U>");
assert_eq!(renamed(quote!(Slice<'a>), &[("U", "T")]), "Slice<'a>");
}
}