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 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
}
#[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");
}
}