hodoku/lib.rs
1//! [<img alt="github" src="https://img.shields.io/badge/github-udoprog/hodoku-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/udoprog/hodoku)
2//! [<img alt="crates.io" src="https://img.shields.io/crates/v/hodoku.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/hodoku)
3//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-hodoku-66c2a5?style=for-the-badge&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/hodoku)
4//!
5//! A simple set of macros to aid testing with try operations.
6//!
7//! This crate allows for easily writing functions and expression where `?` is
8//! automatically translated into `.unwrap()`.
9//!
10//! It is syntactically desirable to use `?`. This however causes issues during
11//! testing, because a failing test lacks a stack trace which helps you track
12//! down the exact line that errored.
13//!
14//! ```
15//! # fn function() -> Result<u32, &'static str> { Ok(42) };
16//! #[test]
17//! fn test_case() -> Result<(), &'static str> {
18//! let value = function()?;
19//! assert_eq!(value, 42);
20//! Ok(())
21//! }
22//! ```
23//!
24//! By default you'd get this when `function()?` errors:
25//!
26//! ```text
27//! ---- test_case stdout ----
28//! Error: "bad"
29//! thread 'test_case' panicked at 'assertion failed: `(left == right)`
30//! left: `1`,
31//! right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', <path>\library\test\src\lib.rs:185:5
32//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
33//!
34//!
35//! failures:
36//! test_case
37//! ```
38//!
39//! Note how there's no information on which line the test failed on.
40//!
41//! But with the inclusion of `#[hodoku::function]` you get this:
42//!
43//! ```
44//! # fn function() -> Result<u32, &'static str> { Err("bad") };
45//! #[test]
46//! #[hodoku::function]
47//! fn test_case() -> Result<(), &'static str> {
48//! let value = function()?;
49//! assert_eq!(value, 42);
50//! Ok(())
51//! }
52//! ```
53//!
54//! ```text
55//! ---- test_case stdout ----
56//! thread 'test_case' panicked at 'called `Result::unwrap()` on an `Err` value: "bad"', tests\failing.rs:8:27
57//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
58//!
59//!
60//! failures:
61//! test_case
62//! ```
63//!
64//! This is exactly why we want to make use of `.unwrap()` instead of the try
65//! operator tests. It indicates the exact line that errored.
66//!
67//! <br>
68//!
69//! ## Examples
70//!
71//! Use of `#[hodoku::function]`.
72//!
73//! ```
74//! #[hodoku::function]
75//! fn hello() {
76//! let value = Some(42)?;
77//! assert_eq!(value, 42);
78//! }
79//!
80//! hello();
81//! ```
82//!
83//! Unwrapping expressions:
84//!
85//! ```
86//! let value = hodoku::expr!(Some(42)?);
87//! assert_eq!(value, 42);
88//! ```
89
90#![allow(clippy::test_attr_in_doctest)]
91#![no_std]
92
93use core::array;
94use core::iter;
95
96use proc_macro::Spacing;
97use proc_macro::{Delimiter, Group, Ident, Punct, TokenStream, TokenTree};
98
99/// Process an expression or item marked with an attribute to modify any uses of
100/// the try operator `?` into trailing `.unwrap()`. So `Some(42)?` will be
101/// translated to `Some(42).unwrap()`.
102///
103/// This is useful for adhoc testing.
104///
105/// # Examples
106///
107/// ```
108/// #[hodoku::function]
109/// fn hello() {
110/// let value = Some(42)?;
111/// assert_eq!(value, 42);
112/// }
113///
114/// hello();
115/// ```
116#[proc_macro_attribute]
117pub fn function(args: TokenStream, item: TokenStream) -> TokenStream {
118 if args.into_iter().next().is_some() {
119 panic!("#[hodoku::function]: takes not arguments")
120 }
121
122 process(item)
123}
124
125/// Process an expression to modify any uses of the try operator `?` into
126/// trailing `.unwrap()`. So `expr!(Some(42)?)` will be translated to
127/// `Some(42).unwrap()`.
128///
129/// This is useful for adhoc testing.
130///
131/// # Examples
132///
133/// ```
134/// let value = hodoku::expr!(Some(42)?);
135/// assert_eq!(value, 42);
136/// ```
137#[proc_macro]
138pub fn expr(input: TokenStream) -> TokenStream {
139 process(input)
140}
141
142fn process(item: TokenStream) -> TokenStream {
143 let mut it = item.into_iter();
144 let mut tmp = None::<array::IntoIter<TokenTree, 2>>;
145
146 TokenStream::from_iter(iter::from_fn(move || {
147 if let Some(buf) = tmp.as_mut() {
148 if let Some(tt) = buf.next() {
149 return Some(tt);
150 }
151
152 tmp = None;
153 }
154
155 match it.next()? {
156 TokenTree::Group(g) => Some(TokenTree::Group(Group::new(
157 g.delimiter(),
158 process(g.stream()),
159 ))),
160 TokenTree::Punct(punct) => {
161 if punct.as_char() == '?' {
162 let mut group = Group::new(Delimiter::Parenthesis, TokenStream::default());
163 group.set_span(punct.span());
164
165 tmp = Some(
166 [
167 TokenTree::Ident(Ident::new("unwrap", punct.span())),
168 TokenTree::Group(group),
169 ]
170 .into_iter(),
171 );
172
173 let mut first = Punct::new('.', Spacing::Joint);
174 first.set_span(punct.span());
175 Some(TokenTree::Punct(first))
176 } else {
177 Some(TokenTree::Punct(punct))
178 }
179 }
180 tt => Some(tt),
181 }
182 }))
183}