use crate::error::Result; use crate::segment::{self, Segment}; use proc_macro::{Delimiter, Group, Spacing, Span, TokenStream, TokenTree}; use std::iter; use std::mem; use std::str::FromStr; pub fn expand_attr( attr: TokenStream, span: Span, contains_paste: &mut bool, ) -> Result { let mut tokens = attr.clone().into_iter(); let mut leading_colons = 0; // $(::)? let mut leading_path = 0; // $($ident)::+ let mut token; let group = loop { token = tokens.next(); match token { // colon after `$(:)?` Some(TokenTree::Punct(ref punct)) if punct.as_char() == ':' && leading_colons < 2 && leading_path == 0 => { leading_colons += 1; } // ident after `$(::)? $($ident ::)*` Some(TokenTree::Ident(_)) if leading_colons != 1 && leading_path % 3 == 0 => { leading_path += 1; } // colon after `$(::)? $($ident ::)* $ident $(:)?` Some(TokenTree::Punct(ref punct)) if punct.as_char() == ':' && leading_path % 3 > 0 => { leading_path += 1; } // eq+value after `$(::)? $($ident)::+` Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' && leading_path % 3 == 1 => { let mut count = 0; if tokens.inspect(|_| count += 1).all(|tt| is_stringlike(&tt)) && count > 1 { *contains_paste = true; let leading = leading_colons + leading_path; return do_paste_name_value_attr(attr, span, leading); } return Ok(attr); } // parens after `$(::)? $($ident)::+` Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Parenthesis && leading_path % 3 == 1 => { break group; } // bail out _ => return Ok(attr), } }; // There can't be anything else after the first group in a valid attribute. if tokens.next().is_some() { return Ok(attr); } let mut group_contains_paste = false; let mut expanded = TokenStream::new(); let mut nested_attr = TokenStream::new(); for tt in group.stream() { match &tt { TokenTree::Punct(punct) if punct.as_char() == ',' => { expanded.extend(expand_attr( nested_attr, group.span(), &mut group_contains_paste, )?); expanded.extend(iter::once(tt)); nested_attr = TokenStream::new(); } _ => nested_attr.extend(iter::once(tt)), } } if !nested_attr.is_empty() { expanded.extend(expand_attr( nested_attr, group.span(), &mut group_contains_paste, )?); } if group_contains_paste { *contains_paste = true; let mut group = Group::new(Delimiter::Parenthesis, expanded); group.set_span(span); Ok(attr .into_iter() // Just keep the initial ident in `#[ident(...)]`. .take(leading_colons + leading_path) .chain(iter::once(TokenTree::Group(group))) .collect()) } else { Ok(attr) } } fn do_paste_name_value_attr(attr: TokenStream, span: Span, leading: usize) -> Result { let mut expanded = TokenStream::new(); let mut tokens = attr.into_iter().peekable(); expanded.extend(tokens.by_ref().take(leading + 1)); // `doc =` let mut segments = segment::parse(&mut tokens)?; for segment in &mut segments { if let Segment::String(string) = segment { if let Some(open_quote) = string.value.find('"') { if open_quote == 0 { string.value.truncate(string.value.len() - 1); string.value.remove(0); } else { let begin = open_quote + 1; let end = string.value.rfind('"').unwrap(); let raw_string = mem::take(&mut string.value); for ch in raw_string[begin..end].chars() { string.value.extend(ch.escape_default()); } } } } } let mut lit = segment::paste(&segments)?; if lit.starts_with("r#") { // Raw mode doesn't have any impact when using in attribute lit.remove(0); lit.remove(0); } lit.insert(0, '"'); lit.push('"'); let mut lit = TokenStream::from_str(&lit) .unwrap() .into_iter() .next() .unwrap(); lit.set_span(span); expanded.extend(iter::once(lit)); Ok(expanded) } fn is_stringlike(token: &TokenTree) -> bool { match token { TokenTree::Ident(_) => true, TokenTree::Literal(literal) => { let repr = literal.to_string(); !repr.starts_with('b') && !repr.starts_with('\'') } TokenTree::Group(group) => { if group.delimiter() != Delimiter::None { return false; } let mut inner = group.stream().into_iter(); match inner.next() { Some(first) => inner.next().is_none() && is_stringlike(&first), None => false, } } TokenTree::Punct(punct) => { punct.as_char() == '\'' || punct.as_char() == ':' && punct.spacing() == Spacing::Alone } } } #[cfg(doctest)] #[doc(hidden)] mod doc_tests { /// ``` /// use pastey::paste; /// paste! { /// #[doc = "Hello " "World"] /// pub struct DocStringPaste; /// } /// ``` fn test_doc_string_paste() {} /// ``` /// use pastey::paste; /// paste! { /// #[doc = "hello"] /// pub struct DocSingleToken; /// } /// ``` fn test_doc_single_token() {} /// ``` /// use pastey::paste; /// paste! { /// #[derive(Clone, Copy)] /// struct DocDeriveAttr(u8); /// } /// ``` fn test_derive_attr_in_paste() {} /// ``` /// use pastey::paste; /// paste! { /// #[cfg_attr(not(all()), allow([]))] /// pub struct DocPasteInAttr; /// } /// ``` fn test_paste_in_attr_paren() {} /// ``` /// use pastey::paste; /// paste! { /// #[cfg_attr(not(all()), ::foo::bar(baz))] /// pub struct DocAbsPath; /// } /// ``` fn test_absolute_path_attr() {} /// ``` /// use pastey::paste; /// paste! { /// #[doc = r"Hello " "World"] /// pub struct DocRawStr; /// } /// ``` fn test_raw_str_doc_attr() {} /// ``` /// use pastey::paste; /// macro_rules! m { /// ($val:ident) => { /// paste! { /// #[cfg_attr(not(all()), allow(ident = $val "world"))] /// pub struct DocNoneGroupStringlike; /// } /// } /// } /// m!(hello); /// ``` fn test_none_group_stringlike() {} /// ``` /// use pastey::paste; /// macro_rules! with_doc_path { /// ($m:ident) => { /// paste! { /// #[doc = stringify!($m::Item)] /// pub fn doc_none_group_before_double_colon() {} /// } /// } /// } /// /// /// with_doc_path!(my_mod); /// doc_none_group_before_double_colon(); /// ``` fn test_none_group_before_double_colon_in_attr_context() {} }