proc_macro_utils/
lib.rs

1//! Some useful functions on [`proc_macro`] and [`proc_macro2`] types
2//!
3//! E.g. [pushing tokens onto `TokenStream`](TokenStreamExt::push) and [testing
4//! for specific punctuation on `TokenTree` and `Punct`](TokenTreePunct)
5//!
6//! It also adds the [`assert_tokens!`] and [`assert_expansion!`] macros to
7//! improve unit testability for `proc-macros`.
8#![warn(clippy::pedantic, missing_docs)]
9#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10#![deny(rustdoc::all)]
11
12#[cfg(doc)]
13use proc_macro2::{Punct, Spacing};
14
15#[cfg(feature = "proc-macro")]
16extern crate proc_macro;
17
18/// Parsing of simple rust structures without syn
19#[cfg(feature = "parser")]
20mod parser;
21#[cfg(feature = "parser")]
22pub use parser::TokenParser;
23
24#[cfg(feature = "parser")]
25#[macro_use]
26mod assert;
27
28#[cfg(feature = "parser")]
29#[doc(hidden)]
30pub mod __private;
31
32mod sealed {
33    pub trait Sealed {}
34
35    macro_rules! sealed {
36        [$($ty:ident),* $(,)?] => {$(
37            impl Sealed for proc_macro::$ty {}
38            impl Sealed for proc_macro2::$ty {}
39        )*};
40    }
41
42    sealed![TokenStream, TokenTree, Punct, Literal, Group];
43}
44
45macro_rules! once {
46    (($($tts:tt)*) $($tail:tt)*) => {
47        $($tts)*
48    };
49}
50
51macro_rules! attr {
52    (($($attr:tt)*), $($item:tt)+) => {
53        $(#$attr)* $($item)+
54    };
55}
56
57macro_rules! trait_def {
58    ($item_attr:tt, $trait:ident, $($fn_attr:tt, $fn:ident, $({$($gen:tt)*})?, $args:tt, $($ret:ty)?),*) => {
59        attr!($item_attr,
60        pub trait $trait: crate::sealed::Sealed {
61            $(attr!($fn_attr, fn $fn $($($gen)*)? $args $(-> $ret)?;);)*
62        });
63    };
64}
65
66macro_rules! trait_impl {
67    ($trait:ident, $type:ident, $($fn_attr:tt, $fn:ident, $({$($gen:tt)*})?, $args:tt, $($ret:ty)?, $stmts:tt),*) => {
68        impl $trait for $type {
69            $(attr!($fn_attr, fn $fn $($($gen)*)? $args $(-> $ret)? $stmts);)*
70        }
71    };
72}
73
74macro_rules! impl_via_trait {
75    ($(
76        $(#$trait_attr:tt)*
77        impl $trait:ident for $type:ident {
78            $($(#$fn_attr:tt)*
79            fn $fn:ident $({$($gen:tt)*})? ($($args:tt)*)  $(-> $ret:ty)? { $($stmts:tt)* })*
80        }
81    )+) => {
82        once!($((trait_def!(($($trait_attr)*), $trait, $(($($fn_attr)*), $fn,$({$($gen)*})?, ($($args)*), $($ret)?),*);))+);
83        #[cfg(feature = "proc-macro")]
84        const _: () = {
85            use proc_macro::*;
86            $(trait_impl!($trait, $type, $(($($fn_attr)*), $fn, $({$($gen)*})?, ($($args)*), $($ret)?, {$($stmts)*}),*);)+
87        };
88        #[cfg(feature = "proc-macro2")]
89        const _:() = {
90            use proc_macro2::*;
91            $(trait_impl!($trait, $type, $(($($fn_attr)*), $fn, $({$($gen)*})?, ($($args)*), $($ret)?, {$($stmts)*}),*);)+
92        };
93    };
94    (
95        mod $mod:ident, $mod2:ident {
96            $(
97                $(#$trait_attr:tt)*
98                impl $trait:ident$($doc:literal)?, $trait2:ident$($doc2:literal)?  for $type:ident {
99                    $($(#$fn_attr:tt)*
100                    fn $fn:ident $({$($gen:tt)*})? ($($args:tt)*) $(-> $ret:ty)? { $($stmts:tt)* })*
101                }
102            )+
103        }
104    ) => {
105        #[cfg(feature = "proc-macro")]
106        once!(($(pub use $mod::$trait;)+));
107        #[cfg(feature = "proc-macro")]
108        mod $mod {
109            use proc_macro::*;
110            once!($((trait_def!(($($trait_attr)* $([doc=$doc])?), $trait, $(($($fn_attr)*), $fn, $({$($gen)*})?, ($($args)*), $($ret)?),*);))+);
111            $(trait_impl!($trait, $type, $(($($fn_attr)*), $fn, $({$($gen)*})?, ($($args)*), $($ret)?, {$($stmts)*}),*);)+
112        }
113        #[cfg(feature = "proc-macro2")]
114        once!(($(pub use $mod2::$trait2;)+));
115        #[cfg(feature = "proc-macro2")]
116        mod $mod2 {
117            use proc_macro2::*;
118            once!($((trait_def!(($($trait_attr)*$([doc=$doc2])?), $trait2, $(($($fn_attr)*), $fn, $({$($gen)*})?, ($($args)*), $($ret)?),*);))+);
119            $(trait_impl!($trait2, $type, $(($($fn_attr)*), $fn, $({$($gen)*})?, ($($args)*), $($ret)?, {$($stmts)*}),*);)+
120        }
121    };
122}
123
124impl_via_trait! {
125    mod token_stream_ext, token_stream2_ext {
126        /// Generic extensions for
127        impl TokenStreamExt "[`proc_macro::TokenStream`]", TokenStream2Ext "[`proc_macro2::TokenStream`]" for TokenStream {
128            /// Pushes a single [`TokenTree`] onto the token stream.
129            fn push(&mut self, token: TokenTree) {
130                self.extend(std::iter::once(token))
131            }
132            /// Creates a [`TokenParser`](crate::TokenParser) from this token stream.
133            #[cfg(feature = "parser")]
134            fn parser(self) -> crate::TokenParser<proc_macro2::token_stream::IntoIter> {
135                #[allow(clippy::useless_conversion)]
136                proc_macro2::TokenStream::from(self).into()
137            }
138
139            /// Creates a [`TokenParser`](crate::TokenParser) from this token stream.
140            ///
141            /// Allows to specify the length of the [peeker buffer](crate::TokenParser#peeking).
142            #[cfg(feature = "parser")]
143            fn parser_generic{<const PEEKER_LEN: usize>}(self) -> crate::TokenParser<proc_macro2::token_stream::IntoIter, PEEKER_LEN> {
144                #[allow(clippy::useless_conversion)]
145                proc_macro2::TokenStream::from(self).into()
146            }
147        }
148    }
149}
150
151macro_rules! token_tree_ext {
152    ($($a:literal, $token:literal, $is:ident, $as:ident, $into:ident, $variant:ident);+$(;)?) => {
153        impl_via_trait! {
154            mod token_tree_ext, token_tree2_ext {
155                /// Generic extensions for
156                impl TokenTreeExt "[`proc_macro::TokenTree`]", TokenTree2Ext "[`proc_macro2::TokenTree`]"  for TokenTree {
157                    $(
158                        #[doc = concat!("Tests if the token tree is ", $a, " ", $token, ".")]
159                        #[must_use]
160                        fn $is(&self) -> bool {
161                            matches!(self, Self::$variant(_))
162                        }
163                        #[doc = concat!("Get the [`", stringify!($variant), "`] inside this token tree, or [`None`] if it isn't ", $a, " ", $token, ".")]
164                        #[must_use]
165                        fn $as(&self) -> Option<&$variant> {
166                            if let Self::$variant(inner) = &self {
167                                Some(inner)
168                            } else {
169                                None
170                            }
171                        }
172                        #[doc = concat!("Get the [`", stringify!($variant), "`] inside this token tree, or [`None`] if it isn't ", $a, " ", $token, ".")]
173                        #[must_use]
174                        fn $into(self) -> Option<$variant> {
175                            if let Self::$variant(inner) = self {
176                                Some(inner)
177                            } else {
178                                None
179                            }
180                        }
181                    )*
182                }
183            }
184        }
185    };
186}
187
188token_tree_ext!(
189    "a", "group", is_group, group, into_group, Group;
190    "an", "ident", is_ident, ident, into_ident, Ident;
191    "a", "punctuation", is_punct, punct, into_punct, Punct;
192    "a", "literal", is_literal, literal, into_literal, Literal;
193);
194
195macro_rules! punctuations {
196    ($($char:literal as $name:ident),*) => {
197        impl_via_trait!{
198            /// Trait to test for punctuation
199            impl TokenTreePunct for TokenTree {
200                $(#[doc = concat!("Tests if the token is `", $char, "`")]
201                #[must_use]
202                fn $name(&self) -> bool {
203                    matches!(self, TokenTree::Punct(punct) if punct.$name())
204                })*
205                /// Tests if token is followed by some none punctuation token or whitespace.
206                #[must_use]
207                fn is_alone(&self) -> bool {
208                    matches!(self, TokenTree::Punct(punct) if punct.is_alone())
209                }
210                /// Tests if token is followed by another punct and can potentially be combined into
211                /// a multi-character operator.
212                #[must_use]
213                fn is_joint(&self) -> bool {
214                    matches!(self, TokenTree::Punct(punct) if punct.is_joint())
215                }
216                /// If sets the [`spacing`](Punct::spacing) of a punct to [`Alone`](Spacing::Alone).
217                #[must_use]
218                fn alone(self) -> Self {
219                    match self {
220                        Self::Punct(p) => Self::Punct(p.alone()),
221                        it => it
222                    }
223                }
224            }
225            impl TokenTreePunct for Punct {
226                $(fn $name(&self) -> bool {
227                    self.as_char() == $char
228                })*
229                fn is_alone(&self) -> bool {
230                    self.spacing() == Spacing::Alone
231                }
232                fn is_joint(&self) -> bool {
233                    self.spacing() == Spacing::Joint
234                }
235                fn alone(self) -> Self {
236                    if self.is_alone() {
237                        self
238                    } else {
239                        let mut this = Punct::new(self.as_char(), Spacing::Alone);
240                        this.set_span(self.span());
241                        this
242                    }
243                }
244            }
245        }
246    };
247}
248
249punctuations![
250    '=' as is_equals,
251    '<' as is_less_than,
252    '>' as is_greater_than,
253    '!' as is_exclamation,
254    '~' as is_tilde,
255    '+' as is_plus,
256    '-' as is_minus,
257    '*' as is_asterix, // TODO naming
258    '/' as is_slash,
259    '%' as is_percent,
260    '^' as is_caret,
261    '&' as is_and,
262    '|' as is_pipe,
263    '@' as is_at,
264    '.' as is_dot,
265    ',' as is_comma,
266    ';' as is_semi,
267    ':' as is_colon,
268    '#' as is_pound,
269    '$' as is_dollar,
270    '?' as is_question,
271    '\'' as is_quote // TODO naming
272];
273
274macro_rules! delimited {
275    ($($delimiter:ident as $name:ident : $doc:literal),*) => {
276        impl_via_trait!{
277            /// Trait to test for delimiters of groups
278            impl Delimited for TokenTree {
279                $(#[doc = concat!("Tests if the token is a group with ", $doc)]
280                #[must_use]
281                fn $name(&self) -> bool {
282                    matches!(self, TokenTree::Group(group) if group.$name())
283                })*
284            }
285            impl Delimited for Group {
286                $(#[doc = concat!("Tests if a group has ", $doc)]
287                #[must_use]
288                fn $name(&self) -> bool {
289                    matches!(self.delimiter(), Delimiter::$delimiter)
290                })*
291            }
292        }
293    };
294}
295
296delimited![
297    Parenthesis as is_parenthesized: " parentheses (`( ... )`)",
298    Brace as is_braced: " braces (`{ ... }`)",
299    Bracket as is_bracketed: " brackets (`[ ... ]`)",
300    None as is_implicitly_delimited: " no delimiters (`Ø ... Ø`)"
301];
302
303impl_via_trait! {
304    /// Trait to parse literals
305    impl TokenTreeLiteral for TokenTree {
306        /// Tests if the token is a string literal.
307        #[must_use]
308        fn is_string(&self) -> bool {
309            self.literal().is_some_and(TokenTreeLiteral::is_string)
310        }
311
312        /// Returns the string contents if it is a string literal.
313        #[must_use]
314        fn string(&self) -> Option<String> {
315            self.literal().and_then(TokenTreeLiteral::string)
316        }
317    }
318
319    impl TokenTreeLiteral for Literal {
320        fn is_string(&self) -> bool {
321            let s = self.to_string();
322            s.starts_with('"') || s.starts_with("r\"") || s.starts_with("r#")
323        }
324        fn string(&self) -> Option<String> {
325            let lit = self.to_string();
326            if lit.starts_with('"') {
327                Some(resolve_escapes(&lit[1..lit.len() - 1]))
328            } else if lit.starts_with('r') {
329                let pounds = lit.chars().skip(1).take_while(|&c| c == '#').count();
330                Some(lit[2 + pounds..lit.len() - pounds - 1].to_owned())
331            } else {
332                None
333            }
334        }
335    }
336}
337
338// Implemented following https://doc.rust-lang.org/reference/tokens.html#string-literals
339// #[allow(clippy::needless_continue)]
340fn resolve_escapes(mut s: &str) -> String {
341    let mut out = String::new();
342    while !s.is_empty() {
343        if s.starts_with('\\') {
344            match s.as_bytes()[1] {
345                b'x' => {
346                    out.push(
347                        char::from_u32(u32::from_str_radix(&s[2..=3], 16).expect("valid escape"))
348                            .expect("valid escape"),
349                    );
350                    s = &s[4..];
351                }
352                b'u' => {
353                    let len = s[3..].find('}').expect("valid escape");
354                    out.push(
355                        char::from_u32(u32::from_str_radix(&s[3..len], 16).expect("valid escape"))
356                            .expect("valid escape"),
357                    );
358                    s = &s[3 + len..];
359                }
360                b'n' => {
361                    out.push('\n');
362                    s = &s[2..];
363                }
364                b'r' => {
365                    out.push('\r');
366                    s = &s[2..];
367                }
368                b't' => {
369                    out.push('\t');
370                    s = &s[2..];
371                }
372                b'\\' => {
373                    out.push('\\');
374                    s = &s[2..];
375                }
376                b'0' => {
377                    out.push('\0');
378                    s = &s[2..];
379                }
380                b'\'' => {
381                    out.push('\'');
382                    s = &s[2..];
383                }
384                b'"' => {
385                    out.push('"');
386                    s = &s[2..];
387                }
388                b'\n' => {
389                    s = &s[..s[2..]
390                        .find(|c: char| !c.is_ascii_whitespace())
391                        .unwrap_or(s.len())];
392                }
393                c => unreachable!(
394                    "TokenStream string literals should only contain valid escapes, found `\\{c}`"
395                ),
396            }
397        } else {
398            let len = s.find('\\').unwrap_or(s.len());
399            out.push_str(&s[..len]);
400            s = &s[len..];
401        }
402    }
403    out
404}
405
406#[cfg(all(test, feature = "proc-macro2"))]
407mod test {
408    use proc_macro2::{Punct, Spacing, TokenTree};
409    use quote::quote;
410
411    use super::*;
412
413    #[test]
414    fn punctuation() {
415        let mut tokens = quote! {=<>!$~+-*/%^|@.,;:#$?'a}.into_iter();
416        assert!(tokens.next().unwrap().is_equals());
417        assert!(tokens.next().unwrap().is_less_than());
418        assert!(tokens.next().unwrap().is_greater_than());
419        assert!(tokens.next().unwrap().is_exclamation());
420        assert!(tokens.next().unwrap().is_dollar());
421        assert!(tokens.next().unwrap().is_tilde());
422        assert!(tokens.next().unwrap().is_plus());
423        assert!(tokens.next().unwrap().is_minus());
424        assert!(tokens.next().unwrap().is_asterix());
425        assert!(tokens.next().unwrap().is_slash());
426        assert!(tokens.next().unwrap().is_percent());
427        assert!(tokens.next().unwrap().is_caret());
428        assert!(tokens.next().unwrap().is_pipe());
429        assert!(tokens.next().unwrap().is_at());
430        assert!(tokens.next().unwrap().is_dot());
431        assert!(tokens.next().unwrap().is_comma());
432        assert!(tokens.next().unwrap().is_semi());
433        assert!(tokens.next().unwrap().is_colon());
434        assert!(tokens.next().unwrap().is_pound());
435        assert!(tokens.next().unwrap().is_dollar());
436        assert!(tokens.next().unwrap().is_question());
437        assert!(tokens.next().unwrap().is_quote());
438    }
439
440    #[test]
441    fn token_stream_ext() {
442        let mut tokens = quote!(a);
443        tokens.push(TokenTree::Punct(Punct::new(',', Spacing::Alone)));
444        assert_eq!(tokens.to_string(), "a ,");
445    }
446
447    #[test]
448    fn token_tree_ext() {
449        let mut tokens = quote!({group} ident + "literal").into_iter().peekable();
450        assert!(tokens.peek().unwrap().is_group());
451        assert!(matches!(
452            tokens.next().unwrap().group().unwrap().to_string().as_str(),
453            "{ group }" | "{group}"
454        ));
455        assert!(tokens.peek().unwrap().is_ident());
456        assert_eq!(tokens.next().unwrap().ident().unwrap().to_string(), "ident");
457        assert!(tokens.peek().unwrap().is_punct());
458        assert_eq!(tokens.next().unwrap().punct().unwrap().to_string(), "+");
459        assert!(tokens.peek().unwrap().is_literal());
460        assert_eq!(
461            tokens.next().unwrap().literal().unwrap().to_string(),
462            "\"literal\""
463        );
464    }
465
466    #[test]
467    fn test() {}
468}