Skip to content

Commit e3a7939

Browse files
committed
feat: assert_expansion! macro
1 parent e323372 commit e3a7939

File tree

5 files changed

+249
-31
lines changed

5 files changed

+249
-31
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ jobs:
4444
- name: Test
4545
run: cargo hack test --feature-powerset --all-targets --no-fail-fast --workspace
4646
- name: Doc Test
47-
run: cargo hack test --feature-powerset --doc --no-fail-fast --workspace
47+
run: cargo test --all-features --doc --no-fail-fast --workspace
4848
- name: Build Docs
4949
run: cargo doc --all-features --workspace

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `*_alone` functions to `TokenParser` setting the last token to `Alone` if it is a punctuation.
1515
- `next_n` to `TokenParser` returning the next `n` tokens.
1616
- `peek_range` to `TokenParser` returning a range of tokens.
17+
- `FromStr` implementation for `TokenParser` based on `TokenStream`'s.
18+
- `assert_expansion!` macro to unit test macro implementations.
1719

1820
### Changed
1921
- **Breaking Change** Added const generic buffer size to `TokenParser`.
@@ -27,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2729
- Marked parser functions as must_use.
2830
- `next_expr` and `next_type` set the last tokens `spacing` to `Alone`.
2931

32+
### Fixed
33+
- Allow `assert_tokens` in expression position e.g. unbraced `match` arm
34+
3035
## [0.6.0] - 2023-04-29
3136
- `TokenParser::next_keyword(v)`
3237

src/assert.rs

Lines changed: 198 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,186 @@
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 seperate 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)]
151+
macro_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+
1184
/// Asserts that the `lhs` matches the tokens wrapped in braces on the `rhs`.
2185
///
3186
/// `lhs` needs to be an expression implementing `IntoIterator<Item=TokenTree>`
@@ -21,10 +204,10 @@ macro_rules! assert_tokens {
21204
#[doc(hidden)]
22205
#[allow(clippy::module_name_repetitions)]
23206
macro_rules! assert_tokens {
24-
($lhs:expr, {$($rhs:tt)*}) => {
207+
($lhs:expr, {$($rhs:tt)*}) => {{
25208
let mut lhs = $crate::TokenParser::new_generic::<3, _, _>($lhs);
26-
assert_tokens!(@O lhs, "", $($rhs)*);
27-
};
209+
$crate::assert_tokens!(@O lhs, "", $($rhs)*);
210+
}};
28211
(@E $prefix:expr, $expected:tt, $found:tt) => {
29212
panic!("expected\n {}\nfound\n {}\nat\n {} {}", stringify!$expected, $found, $prefix, $found);
30213
};
@@ -34,13 +217,13 @@ macro_rules! assert_tokens {
34217
(@G $lhs:ident, $fn:ident, $aggr:expr, $sym:literal, $group:tt, {$($inner:tt)*}, $($rhs:tt)*) => {
35218
if let Some(lhs) = $lhs.$fn() {
36219
let mut lhs = $crate::TokenParser::<_, 3>::from($crate::__private::proc_macro2::Group::stream(&lhs));
37-
assert_tokens!(@O lhs, concat!($aggr, ' ', $sym), $($inner)*);
220+
$crate::assert_tokens!(@O lhs, concat!($aggr, ' ', $sym), $($inner)*);
38221
} else if let Some(lhs) = $lhs.next() {
39-
assert_tokens!(@E $aggr, ($group), lhs);
222+
$crate::assert_tokens!(@E $aggr, ($group), lhs);
40223
} else {
41-
assert_tokens!(@E $aggr, ($group));
224+
$crate::assert_tokens!(@E $aggr, ($group));
42225
}
43-
assert_tokens!(@O $lhs, assert_tokens!(@C $aggr, $group), $($rhs)*);
226+
$crate::assert_tokens!(@O $lhs, $crate::assert_tokens!(@C $aggr, $group), $($rhs)*);
44227
};
45228
// These don't add a whitespace in front
46229
(@C $lhs:expr, ,) => {
@@ -61,28 +244,30 @@ macro_rules! assert_tokens {
61244
};
62245
(@O $lhs:ident, $aggr:expr,) => { assert!($lhs.is_empty(), "unexpected left over tokens `{}`", $lhs.into_token_stream()); };
63246
(@O $lhs:ident, $aggr:expr, ( $($inner:tt)* ) $($rhs:tt)*) => {
64-
assert_tokens!(@G $lhs, next_parenthesized, $aggr, '(', { $($inner)* }, { $($inner)* }, $($rhs)*);
247+
$crate::assert_tokens!(@G $lhs, next_parenthesized, $aggr, '(', { $($inner)* }, { $($inner)* }, $($rhs)*);
65248
};
66249
(@O $lhs:ident, $aggr:expr, { $($inner:tt)* } $($rhs:tt)*) => {
67-
assert_tokens!(@G $lhs, next_braced, $aggr, '{', { $($inner)* }, { $($inner)* }, $($rhs)*);
250+
$crate::assert_tokens!(@G $lhs, next_braced, $aggr, '{', { $($inner)* }, { $($inner)* }, $($rhs)*);
68251
};
69252
(@O $lhs:ident, $aggr:expr, [ $($inner:tt)* ] $($rhs:tt)*) => {
70-
assert_tokens!(@G $lhs, next_bracketed, $aggr, '[', [ $($inner)* ], { $($inner)* }, $($rhs)*);
253+
$crate::assert_tokens!(@G $lhs, next_bracketed, $aggr, '[', [ $($inner)* ], { $($inner)* }, $($rhs)*);
71254
};
72255
(@O $lhs:ident, $aggr:expr, $token:tt $($rhs:tt)*) => {
73256
if let Some(lhs) = $lhs.next_punctuation_group().map(|t|t.to_string()).or_else(|| $lhs.next().map(|t|t.to_string())) {
74257
if(lhs != stringify!($token)) {
75-
assert_tokens!(@E $aggr, ($token), lhs);
258+
$crate::assert_tokens!(@E $aggr, ($token), lhs);
76259
}
77260
} else {
78-
assert_tokens!(@E $aggr, ($token));
261+
$crate::assert_tokens!(@E $aggr, ($token));
79262
}
80-
assert_tokens!(@O $lhs, assert_tokens!(@C $aggr, $token), $($rhs)*);
263+
$crate::assert_tokens!(@O $lhs, $crate::assert_tokens!(@C $aggr, $token), $($rhs)*);
81264
};
82265
}
83266

84267
#[test]
85268
fn test() {
269+
// TODO testing with quote is incomplete `":::"` can be joint joint alone if
270+
// produced directly not with quote.
86271
use quote::quote;
87272
assert_tokens!(quote!(ident ident, { group/test, vec![a, (a + b)] }, "literal" $), {
88273
ident ident, { group /test, vec![a,(a+b)] }, "literal" $

src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
//! Some useful functions on `proc_macro` and `proc_macro2` types
1+
//! Some useful functions on [`proc_macro`] and [`proc_macro2`] types
22
//!
33
//! E.g. [pushing tokens onto `TokenStream`](TokenStreamExt::push) and [testing
44
//! 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.
58
#![warn(clippy::pedantic, missing_docs)]
69
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
710
#![deny(rustdoc::all)]
@@ -343,4 +346,7 @@ mod test {
343346
"\"literal\""
344347
);
345348
}
349+
350+
#[test]
351+
fn test() {}
346352
}

0 commit comments

Comments
 (0)