Revision control
Copy as Markdown
Other Tools
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<TokenStream> {
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<TokenStream> {
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([<foo bar>]))]
/// 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() {}
}