proc_macro_utils/
assert.rs

1/// Allows simple unit testing of proc macro implementations.
2///
3/// This macro only works with functions taking [`proc_macro2::TokenStream`] due
4/// to the [`proc_macro`] API not being available in unit tests. This can be
5/// achieved either by manually creating a separate function:
6/// ```ignore
7/// use proc_macro::TokenStream;
8/// use proc_macro2::TokenStream as TokenStream2;
9/// #[proc_macro]
10/// pub fn actual_macro(input: TokenStream) -> TokenStream {
11///     macro_impl(input.into()).into()
12/// }
13/// fn macro_impl(input: TokenStream2) -> TokenStream2 {
14///     // ...
15/// }
16/// ```
17/// or use a crate like [`manyhow`](https://docs.rs/manyhow/):
18/// ```ignore
19/// use proc_macro2::TokenStream as TokenStream2;
20/// #[manyhow(impl_fn)] // generates `fn actual_macro_impl`
21/// pub fn actual_macro(input: TokenStream2) -> TokenStream2 {
22///     // ...
23/// }
24/// ```
25///
26/// # Function like macros
27/// ```
28/// # use proc_macro_utils::assert_expansion;
29/// # use proc_macro2::TokenStream;
30/// # use quote::quote;
31/// // Dummy attribute macro impl
32/// fn macro_impl(input: TokenStream) -> TokenStream {
33///     quote!(#input)
34/// }
35/// fn macro_impl_result(input: TokenStream) -> Result<TokenStream, ()> {
36///     Ok(quote!(#input))
37/// }
38/// assert_expansion!(
39///     macro_impl!(something test),
40///     { something test }
41/// );
42/// assert_expansion!(
43///     macro_impl![1, 2, 3],
44///     { 1, 2, 3 }
45/// );
46/// assert_expansion!(
47///     macro_impl!{ braced },
48///     { braced }
49/// );
50/// // adding a single function call (without arguments) is also allowed e.g. `unwrap()`
51/// assert_expansion!(
52///     macro_impl_result!(result).unwrap(),
53///     { result }
54/// );
55/// ```
56///
57/// # Derive macros
58/// ```
59/// # use proc_macro_utils::assert_expansion;
60/// # use proc_macro2::TokenStream;
61/// # use quote::quote;
62/// // Dummy derive macro impl
63/// fn macro_impl(item: TokenStream) -> TokenStream {
64///     quote!(#item)
65/// }
66/// fn macro_impl_result(item: TokenStream) -> Result<TokenStream, ()> {
67///     Ok(quote!(#item))
68/// }
69/// assert_expansion!(
70///     #[derive(macro_impl)]
71///     struct A; // the comma after items is optional
72///     { struct A; }
73/// );
74/// assert_expansion!(
75///     #[derive(macro_impl)]
76///     struct A {}
77///     { struct A {} }
78/// );
79/// // adding a single function call (without arguments) is also allowed e.g. `unwrap()`
80/// assert_expansion!(
81///     #[derive(macro_impl_result)]
82///     struct A {}.unwrap()
83///     { struct A {} }
84/// );
85/// // alternatively the proc_macro syntax is compatible
86/// assert_expansion!(
87///     macro_impl!{ struct A {} },
88///     { struct A {} }
89/// );
90/// ```
91///
92/// # Attribute macros
93/// ```
94/// # use proc_macro_utils::assert_expansion;
95/// # use proc_macro2::TokenStream;
96/// # use quote::quote;
97/// // Dummy attribute macro impl
98/// fn macro_impl(input: TokenStream, item: TokenStream) -> TokenStream {
99///     quote!(#input, #item)
100/// }
101/// fn macro_impl_result(input: TokenStream, item: TokenStream) -> Result<TokenStream, ()> {
102///     Ok(quote!(#input, #item))
103/// }
104/// assert_expansion!(
105///     #[macro_impl]
106///     struct A;
107///     { , struct A; }
108/// );
109/// assert_expansion!(
110///     #[macro_impl = "hello"]
111///     fn test() { }, // the comma after items is optional
112///     { "hello", fn test() {} }
113/// );
114/// assert_expansion!(
115///     #[macro_impl(a = 10)]
116///     impl Hello for World {},
117///     { a = 10, impl Hello for World {} }
118/// );
119/// // adding a single function call (without arguments) is also allowed e.g. `unwrap()`
120/// assert_expansion!(
121///     #[macro_impl_result(a = 10)]
122///     impl Hello for World {}.unwrap(),
123///     { a = 10, impl Hello for World {} }
124/// );
125/// ```
126///
127/// # Generic usage
128/// On top of the normal macro inputs a generic input is also supported.
129/// ```
130/// # use proc_macro_utils::assert_expansion;
131/// # use proc_macro2::TokenStream;
132/// # use quote::quote;
133/// fn macro_impl(first: TokenStream, second: TokenStream, third: TokenStream) -> TokenStream {
134///     quote!(#first, #second, #third)
135/// }
136/// fn macro_impl_result(first: TokenStream, second: TokenStream, third: TokenStream) -> Result<TokenStream, ()> {
137///     Ok(quote!(#first, #second, #third))
138/// }
139/// assert_expansion!(
140///     macro_impl({ 1 }, { something }, { ":)" }),
141///     { 1, something, ":)" }
142/// );
143/// // adding a single function call (without arguments) is also allowed e.g. `unwrap()`
144/// assert_expansion!(
145///     macro_impl_result({ 1 }, { something }, { ":)" }).unwrap(),
146///     { 1, something, ":)" }
147/// );
148/// ```
149#[macro_export]
150#[allow(clippy::module_name_repetitions)]
151macro_rules! assert_expansion {
152    ($macro:ident!($($input:tt)*)$(.$fn:ident())?, { $($rhs:tt)* }) => {
153        $crate::assert_expansion!($macro({$($input)*})$(.$fn())?, { $($rhs)* })
154    };
155    ($macro:ident![$($input:tt)*]$(.$fn:ident())?, { $($rhs:tt)* }) => {
156        $crate::assert_expansion!($macro({$($input)*})$(.$fn())?, { $($rhs)* })
157    };
158    ($macro:ident!{$($input:tt)*}$(.$fn:ident())?, { $($rhs:tt)* }) => {
159        $crate::assert_expansion!($macro({$($input)*})$(.$fn())?, { $($rhs)* })
160    };
161    (#[derive($macro:ident)]$item:item$(.$fn:ident())?$(,)? { $($rhs:tt)* }) => {
162        $crate::assert_expansion!($macro({$item})$(.$fn())?, { $($rhs)* })
163    };
164    (#[$macro:ident]$item:item$(.$fn:ident())?$(,)? { $($rhs:tt)* }) => {
165        $crate::assert_expansion!($macro({}, {$item})$(.$fn())?, { $($rhs)* })
166    };
167    (#[$macro:ident = $input:expr]$item:item$(.$fn:ident())?$(,)? { $($rhs:tt)* }) => {
168        $crate::assert_expansion!($macro({$input}, {$item})$(.$fn())?, { $($rhs)* })
169    };
170    (#[$macro:ident($($input:tt)*)]$item:item$(.$fn:ident())?$(,)? { $($rhs:tt)* }) => {
171        $crate::assert_expansion!($macro({$($input)*}, {$item})$(.$fn())?, { $($rhs)* })
172    };
173    ($macro:ident($({$($input:tt)*}),+$(,)?)$(.$fn:ident())?, {$($rhs:tt)*}) => {
174        $crate::assert_tokens!(
175            $crate::__private::proc_macro2::TokenStream::from($macro(
176                $(<$crate::__private::proc_macro2::TokenStream as ::core::str::FromStr>
177                    ::from_str(::core::stringify!($($input)*)).unwrap().into()),+
178            )$(.$fn())?),
179           { $($rhs)* }
180        )
181    };
182}
183
184/// Asserts that the `lhs` matches the tokens wrapped in braces on the `rhs`.
185///
186/// `lhs` needs to be an expression implementing `IntoIterator<Item=TokenTree>`
187/// e.g. [`TokenStream`](proc_macro2::TokenStream) or
188/// [`TokenParser`](crate::TokenParser).
189/// ```
190/// # use quote::quote;
191/// # use proc_macro_utils::assert_tokens;
192/// let some_tokens = quote! { ident, { group } };
193///
194/// assert_tokens! {some_tokens, { ident, {group} }};
195/// ```
196#[macro_export]
197#[cfg(doc)]
198macro_rules! assert_tokens {
199    ($lhs:expr, { $($rhs:tt)* }) => {};
200}
201
202#[macro_export]
203#[cfg(not(doc))]
204#[doc(hidden)]
205#[allow(clippy::module_name_repetitions)]
206macro_rules! assert_tokens {
207    ($lhs:expr, {$($rhs:tt)*}) => {{
208        let mut lhs = $crate::TokenParser::new_generic::<3, _, _>($lhs);
209        $crate::assert_tokens!(@O lhs, "", $($rhs)*);
210    }};
211    (@E $prefix:expr, $expected:tt, $found:tt) => {
212        panic!("expected\n    {}\nfound\n    {}\nat\n    {} {}", stringify!$expected, $found, $prefix, $found);
213    };
214    (@E $prefix:expr, $expected:tt) => {
215        panic!("unexpected end, expected\n    {}\nafter\n    {}", stringify!$expected, $prefix);
216    };
217    (@G $lhs:ident, $fn:ident, $aggr:expr, $sym:literal, $group:tt, {$($inner:tt)*}, $($rhs:tt)*) => {
218        if let Some(lhs) = $lhs.$fn() {
219            let mut lhs = $crate::TokenParser::<_, 3>::from($crate::__private::proc_macro2::Group::stream(&lhs));
220            $crate::assert_tokens!(@O lhs, concat!($aggr, ' ', $sym), $($inner)*);
221        } else if let Some(lhs) = $lhs.next() {
222            $crate::assert_tokens!(@E $aggr, ($group), lhs);
223        } else {
224            $crate::assert_tokens!(@E $aggr, ($group));
225        }
226        $crate::assert_tokens!(@O $lhs, $crate::assert_tokens!(@C $aggr, $group), $($rhs)*);
227    };
228    // These don't add a whitespace in front
229    (@C $lhs:expr, ,) => {
230        concat!($lhs, ',')
231    };
232    (@C $lhs:expr, :) => {
233        concat!($lhs, ':')
234    };
235    (@C $lhs:expr, ;) => {
236        concat!($lhs, ';')
237    };
238    (@C $lhs:expr, .) => {
239        concat!($lhs, '.')
240    };
241    // All other tokens do
242    (@C $lhs:expr, $rhs:tt) => {
243        concat!($lhs, ' ', stringify!($rhs))
244    };
245    (@O $lhs:ident, $aggr:expr,) => { assert!($lhs.is_empty(), "unexpected left over tokens `{}`", $lhs.into_token_stream()); };
246    (@O $lhs:ident, $aggr:expr, ( $($inner:tt)* ) $($rhs:tt)*) => {
247        $crate::assert_tokens!(@G $lhs, next_parenthesized, $aggr, '(', { $($inner)* }, { $($inner)* }, $($rhs)*);
248    };
249    (@O $lhs:ident, $aggr:expr, { $($inner:tt)* } $($rhs:tt)*) => {
250        $crate::assert_tokens!(@G $lhs, next_braced, $aggr, '{', { $($inner)* }, { $($inner)* }, $($rhs)*);
251    };
252    (@O $lhs:ident, $aggr:expr, [ $($inner:tt)* ] $($rhs:tt)*) => {
253        $crate::assert_tokens!(@G $lhs, next_bracketed, $aggr, '[', [ $($inner)* ], { $($inner)* }, $($rhs)*);
254    };
255    (@O $lhs:ident, $aggr:expr, $token:tt $($rhs:tt)*) => {
256        if let Some(lhs) = $lhs.next_macro_rules_tt().map(|t|t.to_string()).or_else(|| $lhs.next().map(|t|t.to_string())) {
257            if(lhs != stringify!($token)) {
258                $crate::assert_tokens!(@E $aggr, ($token), lhs);
259            }
260        } else {
261            $crate::assert_tokens!(@E $aggr, ($token));
262        }
263        $crate::assert_tokens!(@O $lhs, $crate::assert_tokens!(@C $aggr, $token), $($rhs)*);
264    };
265}
266
267#[test]
268fn test() {
269    // TODO testing with quote is incomplete `":::"` can be joint joint alone if
270    // produced directly not with quote.
271    use quote::quote;
272    assert_tokens!(quote!(ident ident, { group/test, vec![a, (a + b)] }, "literal" $), {
273        ident ident, { group /test, vec![a,(a+b)] }, "literal" $
274    });
275    assert_tokens!(quote!(:::), {
276        :::
277    });
278    assert_tokens!(quote!(more:::test::test:: hello :-D $$$ It should just work), {
279        more ::: test ::test:: hello :-D $$$ It should just work
280    });
281
282    assert_tokens!(quote!(:$), {: $});
283}