pastey/
attr.rs

1use crate::error::Result;
2use crate::segment::{self, Segment};
3use proc_macro::{Delimiter, Group, Spacing, Span, TokenStream, TokenTree};
4use std::iter;
5use std::mem;
6use std::str::FromStr;
7
8pub fn expand_attr(
9    attr: TokenStream,
10    span: Span,
11    contains_paste: &mut bool,
12) -> Result<TokenStream> {
13    let mut tokens = attr.clone().into_iter();
14    let mut leading_colons = 0; // $(::)?
15    let mut leading_path = 0; // $($ident)::+
16
17    let mut token;
18    let group = loop {
19        token = tokens.next();
20        match token {
21            // colon after `$(:)?`
22            Some(TokenTree::Punct(ref punct))
23                if punct.as_char() == ':' && leading_colons < 2 && leading_path == 0 =>
24            {
25                leading_colons += 1;
26            }
27            // ident after `$(::)? $($ident ::)*`
28            Some(TokenTree::Ident(_)) if leading_colons != 1 && leading_path % 3 == 0 => {
29                leading_path += 1;
30            }
31            // colon after `$(::)? $($ident ::)* $ident $(:)?`
32            Some(TokenTree::Punct(ref punct)) if punct.as_char() == ':' && leading_path % 3 > 0 => {
33                leading_path += 1;
34            }
35            // eq+value after `$(::)? $($ident)::+`
36            Some(TokenTree::Punct(ref punct))
37                if punct.as_char() == '=' && leading_path % 3 == 1 =>
38            {
39                let mut count = 0;
40                if tokens.inspect(|_| count += 1).all(|tt| is_stringlike(&tt)) && count > 1 {
41                    *contains_paste = true;
42                    let leading = leading_colons + leading_path;
43                    return do_paste_name_value_attr(attr, span, leading);
44                }
45                return Ok(attr);
46            }
47            // parens after `$(::)? $($ident)::+`
48            Some(TokenTree::Group(ref group))
49                if group.delimiter() == Delimiter::Parenthesis && leading_path % 3 == 1 =>
50            {
51                break group;
52            }
53            // bail out
54            _ => return Ok(attr),
55        }
56    };
57
58    // There can't be anything else after the first group in a valid attribute.
59    if tokens.next().is_some() {
60        return Ok(attr);
61    }
62
63    let mut group_contains_paste = false;
64    let mut expanded = TokenStream::new();
65    let mut nested_attr = TokenStream::new();
66    for tt in group.stream() {
67        match &tt {
68            TokenTree::Punct(punct) if punct.as_char() == ',' => {
69                expanded.extend(expand_attr(
70                    nested_attr,
71                    group.span(),
72                    &mut group_contains_paste,
73                )?);
74                expanded.extend(iter::once(tt));
75                nested_attr = TokenStream::new();
76            }
77            _ => nested_attr.extend(iter::once(tt)),
78        }
79    }
80
81    if !nested_attr.is_empty() {
82        expanded.extend(expand_attr(
83            nested_attr,
84            group.span(),
85            &mut group_contains_paste,
86        )?);
87    }
88
89    if group_contains_paste {
90        *contains_paste = true;
91        let mut group = Group::new(Delimiter::Parenthesis, expanded);
92        group.set_span(span);
93        Ok(attr
94            .into_iter()
95            // Just keep the initial ident in `#[ident(...)]`.
96            .take(leading_colons + leading_path)
97            .chain(iter::once(TokenTree::Group(group)))
98            .collect())
99    } else {
100        Ok(attr)
101    }
102}
103
104fn do_paste_name_value_attr(attr: TokenStream, span: Span, leading: usize) -> Result<TokenStream> {
105    let mut expanded = TokenStream::new();
106    let mut tokens = attr.into_iter().peekable();
107    expanded.extend(tokens.by_ref().take(leading + 1)); // `doc =`
108
109    let mut segments = segment::parse(&mut tokens)?;
110
111    for segment in &mut segments {
112        if let Segment::String(string) = segment {
113            if let Some(open_quote) = string.value.find('"') {
114                if open_quote == 0 {
115                    string.value.truncate(string.value.len() - 1);
116                    string.value.remove(0);
117                } else {
118                    let begin = open_quote + 1;
119                    let end = string.value.rfind('"').unwrap();
120                    let raw_string = mem::take(&mut string.value);
121                    for ch in raw_string[begin..end].chars() {
122                        string.value.extend(ch.escape_default());
123                    }
124                }
125            }
126        }
127    }
128
129    let mut lit = segment::paste(&segments)?;
130
131    if lit.starts_with("r#") {
132        // Raw mode doesn't have any impact when using in attribute
133        lit.remove(0);
134        lit.remove(0);
135    }
136
137    lit.insert(0, '"');
138    lit.push('"');
139
140    let mut lit = TokenStream::from_str(&lit)
141        .unwrap()
142        .into_iter()
143        .next()
144        .unwrap();
145    lit.set_span(span);
146    expanded.extend(iter::once(lit));
147    Ok(expanded)
148}
149
150fn is_stringlike(token: &TokenTree) -> bool {
151    match token {
152        TokenTree::Ident(_) => true,
153        TokenTree::Literal(literal) => {
154            let repr = literal.to_string();
155            !repr.starts_with('b') && !repr.starts_with('\'')
156        }
157        TokenTree::Group(group) => {
158            if group.delimiter() != Delimiter::None {
159                return false;
160            }
161            let mut inner = group.stream().into_iter();
162            match inner.next() {
163                Some(first) => inner.next().is_none() && is_stringlike(&first),
164                None => false,
165            }
166        }
167        TokenTree::Punct(punct) => {
168            punct.as_char() == '\'' || punct.as_char() == ':' && punct.spacing() == Spacing::Alone
169        }
170    }
171}
172
173#[cfg(doctest)]
174#[doc(hidden)]
175mod doc_tests {
176    /// ```
177    /// use pastey::paste;
178    /// paste! {
179    ///     #[doc = "Hello " "World"]
180    ///     pub struct DocStringPaste;
181    /// }
182    /// ```
183    fn test_doc_string_paste() {}
184
185    /// ```
186    /// use pastey::paste;
187    /// paste! {
188    ///     #[doc = "hello"]
189    ///     pub struct DocSingleToken;
190    /// }
191    /// ```
192    fn test_doc_single_token() {}
193
194    /// ```
195    /// use pastey::paste;
196    /// paste! {
197    ///     #[derive(Clone, Copy)]
198    ///     struct DocDeriveAttr(u8);
199    /// }
200    /// ```
201    fn test_derive_attr_in_paste() {}
202
203    /// ```
204    /// use pastey::paste;
205    /// paste! {
206    ///     #[cfg_attr(not(all()), allow([<foo bar>]))]
207    ///     pub struct DocPasteInAttr;
208    /// }
209    /// ```
210    fn test_paste_in_attr_paren() {}
211
212    /// ```
213    /// use pastey::paste;
214    /// paste! {
215    ///     #[cfg_attr(not(all()), ::foo::bar(baz))]
216    ///     pub struct DocAbsPath;
217    /// }
218    /// ```
219    fn test_absolute_path_attr() {}
220
221    /// ```
222    /// use pastey::paste;
223    /// paste! {
224    ///     #[doc = r"Hello " "World"]
225    ///     pub struct DocRawStr;
226    /// }
227    /// ```
228    fn test_raw_str_doc_attr() {}
229
230    /// ```
231    /// use pastey::paste;
232    /// macro_rules! m {
233    ///     ($val:ident) => {
234    ///         paste! {
235    ///             #[cfg_attr(not(all()), allow(ident = $val "world"))]
236    ///             pub struct DocNoneGroupStringlike;
237    ///         }
238    ///     }
239    /// }
240    /// m!(hello);
241    /// ```
242    fn test_none_group_stringlike() {}
243
244    /// ```
245    /// use pastey::paste;
246    /// macro_rules! with_doc_path {
247    ///     ($m:ident) => {
248    ///         paste! {
249    ///             #[doc = stringify!($m::Item)]
250    ///             pub fn doc_none_group_before_double_colon() {}
251    ///         }
252    ///     }
253    /// }
254    ///      ///
255    /// with_doc_path!(my_mod);
256    /// doc_none_group_before_double_colon();
257    /// ```
258    fn test_none_group_before_double_colon_in_attr_context() {}
259}