extern crate proc_macro; mod enum_hack; use proc_macro2::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}; use proc_macro_hack::proc_macro_hack; use quote::{quote, ToTokens}; use std::iter::FromIterator; use syn::parse::{Error, Parse, ParseStream, Parser, Result}; use syn::{parenthesized, parse_macro_input, Lit, LitStr, Token}; #[proc_macro] pub fn item(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as PasteInput); proc_macro::TokenStream::from(input.expanded) } #[proc_macro] pub fn item_with_macros(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as PasteInput); proc_macro::TokenStream::from(enum_hack::wrap(input.expanded)) } #[proc_macro_hack] pub fn expr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as PasteInput); let output = input.expanded; proc_macro::TokenStream::from(quote!({ #output })) } #[doc(hidden)] #[proc_macro_derive(EnumHack)] pub fn enum_hack(input: proc_macro::TokenStream) -> proc_macro::TokenStream { enum_hack::extract(input) } struct PasteInput { expanded: TokenStream, } impl Parse for PasteInput { fn parse(input: ParseStream) -> Result { let mut expanded = TokenStream::new(); while !input.is_empty() { match input.parse()? { TokenTree::Group(group) => { let delimiter = group.delimiter(); let content = group.stream(); let span = group.span(); if delimiter == Delimiter::Bracket && is_paste_operation(&content) { let segments = parse_bracket_as_segments.parse2(content)?; let pasted = paste_segments(span, &segments)?; pasted.to_tokens(&mut expanded); } else if is_none_delimited_single_ident_or_lifetime(delimiter, &content) { content.to_tokens(&mut expanded); } else { let nested = PasteInput::parse.parse2(content)?; let mut group = Group::new(delimiter, nested.expanded); group.set_span(span); group.to_tokens(&mut expanded); } } other => other.to_tokens(&mut expanded), } } Ok(PasteInput { expanded }) } } fn is_paste_operation(input: &TokenStream) -> bool { let input = input.clone(); parse_bracket_as_segments.parse2(input).is_ok() } // https://github.com/dtolnay/paste/issues/26 fn is_none_delimited_single_ident_or_lifetime(delimiter: Delimiter, input: &TokenStream) -> bool { if delimiter != Delimiter::None { return false; } #[derive(PartialEq)] enum State { Init, Ident, Apostrophe, Lifetime, } let mut state = State::Init; for tt in input.clone() { state = match (state, &tt) { (State::Init, TokenTree::Ident(_)) => State::Ident, (State::Init, TokenTree::Punct(punct)) if punct.as_char() == '\'' => State::Apostrophe, (State::Apostrophe, TokenTree::Ident(_)) => State::Lifetime, _ => return false, }; } state == State::Ident || state == State::Lifetime } enum Segment { String(String), Apostrophe(Span), Env(LitStr), Modifier(Token![:], Ident), } fn parse_bracket_as_segments(input: ParseStream) -> Result> { input.parse::()?; let segments = parse_segments(input)?; input.parse::]>()?; if !input.is_empty() { return Err(input.error("invalid input")); } Ok(segments) } fn parse_segments(input: ParseStream) -> Result> { let mut segments = Vec::new(); while !(input.is_empty() || input.peek(Token![>])) { match input.parse()? { TokenTree::Ident(ident) => { let mut fragment = ident.to_string(); if fragment.starts_with("r#") { fragment = fragment.split_off(2); } if fragment == "env" && input.peek(Token![!]) { input.parse::()?; let arg; parenthesized!(arg in input); let var: LitStr = arg.parse()?; segments.push(Segment::Env(var)); } else { segments.push(Segment::String(fragment)); } } TokenTree::Literal(lit) => { let value = match syn::parse_str(&lit.to_string())? { Lit::Str(string) => string.value().replace('-', "_"), Lit::Int(_) => lit.to_string(), _ => return Err(Error::new(lit.span(), "unsupported literal")), }; segments.push(Segment::String(value)); } TokenTree::Punct(punct) => match punct.as_char() { '_' => segments.push(Segment::String("_".to_string())), '\'' => segments.push(Segment::Apostrophe(punct.span())), ':' => segments.push(Segment::Modifier(Token![:](punct.span()), input.parse()?)), _ => return Err(Error::new(punct.span(), "unexpected punct")), }, TokenTree::Group(group) => { if group.delimiter() == Delimiter::None { let nested = parse_segments.parse2(group.stream())?; segments.extend(nested); } else { return Err(Error::new(group.span(), "unexpected token")); } } } } Ok(segments) } fn paste_segments(span: Span, segments: &[Segment]) -> Result { let mut evaluated = Vec::new(); let mut is_lifetime = false; for segment in segments { match segment { Segment::String(segment) => { evaluated.push(segment.clone()); } Segment::Apostrophe(span) => { if is_lifetime { return Err(Error::new(*span, "unexpected lifetime")); } is_lifetime = true; } Segment::Env(var) => { let resolved = match std::env::var(var.value()) { Ok(resolved) => resolved, Err(_) => { return Err(Error::new(var.span(), "no such env var")); } }; let resolved = resolved.replace('-', "_"); evaluated.push(resolved); } Segment::Modifier(colon, ident) => { let span = quote!(#colon #ident); let last = match evaluated.pop() { Some(last) => last, None => return Err(Error::new_spanned(span, "unexpected modifier")), }; if ident == "lower" { evaluated.push(last.to_lowercase()); } else if ident == "upper" { evaluated.push(last.to_uppercase()); } else if ident == "snake" { let mut acc = String::new(); let mut prev = '_'; for ch in last.chars() { if ch.is_uppercase() && prev != '_' { acc.push('_'); } acc.push(ch); prev = ch; } evaluated.push(acc.to_lowercase()); } else if ident == "camel" { let mut acc = String::new(); let mut prev = '_'; for ch in last.chars() { if ch != '_' { if prev == '_' { for chu in ch.to_uppercase() { acc.push(chu); } } else if prev.is_uppercase() { for chl in ch.to_lowercase() { acc.push(chl); } } else { acc.push(ch); } } prev = ch; } evaluated.push(acc); } else { return Err(Error::new_spanned(span, "unsupported modifier")); } } } } let pasted = evaluated.into_iter().collect::(); let ident = TokenTree::Ident(Ident::new(&pasted, span)); let tokens = if is_lifetime { let apostrophe = TokenTree::Punct(Punct::new('\'', Spacing::Joint)); vec![apostrophe, ident] } else { vec![ident] }; Ok(TokenStream::from_iter(tokens)) }