runtime_macros/lib.rs
1// Copyright (c) 2018-2022 Jeremy Davis (jeremydavis519@gmail.com)
2//
3// Licensed under the Apache License, Version 2.0 (located at /LICENSE-APACHE
4// or http://www.apache.org/licenses/LICENSE-2.0), or the MIT license
5// (located at /LICENSE-MIT or http://opensource.org/licenses/MIT), at your
6// option. The file may not be copied, modified, or distributed except
7// according to those terms.
8//
9// Unless required by applicable law or agreed to in writing, this software
10// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11// ANY KIND, either express or implied. See the applicable license for the
12// specific language governing permissions and limitations under that license.
13
14//! This crate offers a way to emulate the process of procedural macro expansion at run time.
15//! It is intended for use with code coverage tools like [`tarpaulin`], which can't measure
16//! the code coverage of anything that happens at compile time.
17//!
18//! Currently, `runtime-macros` only works with `functionlike!` procedural macros. Custom
19//! derive may be supported in the future if there's demand.
20//!
21//! [`tarpaulin`]: https://crates.io/crates/cargo-tarpaulin
22//!
23//! To use it, add a test case to your procedural macro crate that calls `emulate_macro_expansion`
24//! on a `.rs` file that calls the macro. Most likely, all the files you'll want to use it on will
25//! be in your `/tests` directory. Once you've completed this step, any code coverage tool that
26//! works with your crate's test cases will be able to report on how thoroughly you've tested the
27//! macro.
28//!
29//! See the `/examples` directory in the [repository] for working examples.
30//!
31//! [repository]: https://github.com/jeremydavis519/runtime-macros
32
33extern crate proc_macro;
34extern crate quote;
35extern crate syn;
36
37use {
38 quote::ToTokens,
39 std::{
40 fs,
41 io::Read,
42 panic::{self, AssertUnwindSafe},
43 },
44};
45
46/// Searches the given Rust source code file for function-like macro calls and calls the functions
47/// that define how to expand them.
48///
49/// Each time it finds one, this function calls the corresponding procedural macro function, passing
50/// it the inner `TokenStream` just as if the macro were being expanded. The only effect is to
51/// verify that the macro doesn't panic, as the expansion is not actually applied to the AST or the
52/// source code.
53///
54/// Note that this parser only handles Rust's syntax, so it cannot resolve paths to see if they
55/// are equivalent to the given one. The paths used to reference the macro must be exactly equal
56/// to the one given in order to be expanded by this function. For example, if `macro_path` is
57/// `"foo"` and the file provided calls the macro using `bar::foo!`, this function will not know
58/// to expand it, and the macro's code coverage will be underestimated.
59///
60/// Also, this function uses `proc_macro2::TokenStream`, not the standard `proc_macro::TokenStream`.
61/// The Rust compiler disallows using the `proc_macro` API for anything except defining a procedural
62/// macro (i.e. we can't use it at runtime). You can convert between the two types using their
63/// `into` methods, as shown below.
64///
65/// # Returns
66///
67/// `Ok` on success, or an instance of [`Error`] indicating any error that occurred when trying to
68/// read or parse the file.
69///
70/// [`Error`]: enum.Error.html
71///
72/// # Example
73///
74/// ```
75/// # use runtime_macros::emulate_functionlike_macro_expansion;
76///
77/// # /*
78/// #[proc_macro]
79/// fn remove(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
80/// // This stub just allows us to use `proc_macro2` instead of `proc_macro`.
81/// remove_internal(ts.into()).into()
82/// }
83/// # */
84///
85/// fn remove_internal(_: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
86/// // This macro just eats its input and replaces it with nothing.
87/// proc_macro2::TokenStream::new()
88/// }
89///
90/// # /*
91/// #[test]
92/// # */
93/// fn macro_code_coverage() {
94/// # /*
95/// let file = std::fs::File::open("tests/tests.rs").unwrap();
96/// # */
97/// # let file = std::fs::File::open(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs")).unwrap();
98/// emulate_functionlike_macro_expansion(file, &[("remove", remove_internal)]).unwrap();
99/// }
100/// # macro_code_coverage();
101/// ```
102pub fn emulate_functionlike_macro_expansion<'a, F>(
103 mut file: fs::File,
104 macro_paths_and_proc_macro_fns: &[(&'a str, F)],
105) -> Result<(), Error>
106where
107 F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
108{
109 struct MacroVisitor<'a, F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream> {
110 macro_paths_and_proc_macro_fns: AssertUnwindSafe<Vec<(syn::Path, &'a F)>>,
111 }
112 impl<'a, 'ast, F> syn::visit::Visit<'ast> for MacroVisitor<'a, F>
113 where
114 F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
115 {
116 fn visit_macro(&mut self, macro_item: &'ast syn::Macro) {
117 for (path, proc_macro_fn) in self.macro_paths_and_proc_macro_fns.iter() {
118 if macro_item.path == *path {
119 proc_macro_fn(macro_item.tokens.clone().into());
120 }
121 }
122 }
123 }
124
125 let mut content = String::new();
126 file.read_to_string(&mut content)
127 .map_err(|e| Error::IoError(e))?;
128
129 let ast =
130 AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(|e| Error::ParseError(e))?);
131 let macro_paths_and_proc_macro_fns = AssertUnwindSafe(
132 macro_paths_and_proc_macro_fns
133 .iter()
134 .map(|(s, f)| Ok((syn::parse_str(s)?, f)))
135 .collect::<Result<Vec<(syn::Path, &F)>, _>>()
136 .map_err(|e| Error::ParseError(e))?,
137 );
138
139 panic::catch_unwind(|| {
140 syn::visit::visit_file(
141 &mut MacroVisitor::<F> {
142 macro_paths_and_proc_macro_fns,
143 },
144 &*ast,
145 );
146 })
147 .map_err(|_| {
148 Error::ParseError(syn::parse::Error::new(
149 proc_macro2::Span::call_site().into(),
150 "macro expansion panicked",
151 ))
152 })?;
153
154 Ok(())
155}
156
157/// Searches the given Rust source code file for derive macro calls and calls the functions that
158/// define how to expand them.
159///
160/// This function behaves just like [`emulate_functionlike_macro_expansion`], but with derive macros
161/// like `#[derive(Foo)]` instead of function-like macros like `foo!()`. See that function's
162/// documentation for details and an example of use.
163///
164/// [`emulate_functionlike_macro_expansion`]: fn.emulate_functionlike_macro_expansion.html
165pub fn emulate_derive_macro_expansion<'a, F>(
166 mut file: fs::File,
167 macro_paths_and_proc_macro_fns: &[(&'a str, F)],
168) -> Result<(), Error>
169where
170 F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
171{
172 struct MacroVisitor<'a, F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream> {
173 macro_paths_and_proc_macro_fns: AssertUnwindSafe<Vec<(syn::Path, &'a F)>>,
174 }
175 impl<'a, 'ast, F> syn::visit::Visit<'ast> for MacroVisitor<'a, F>
176 where
177 F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
178 {
179 fn visit_item(&mut self, item: &'ast syn::Item) {
180 macro_rules! visit {
181 ( $($ident:ident),* ) => {
182 match *item {
183 $(syn::Item::$ident(ref item) => {
184 for attr in item.attrs.iter() {
185 let meta = match &attr.meta {
186 syn::Meta::List(list) => list,
187 _ => continue
188 };
189
190 match meta.path.get_ident() {
191 Some(x) => {
192 if x != "derive" {
193 continue;
194 }
195 },
196 None => continue
197 }
198
199 match meta.parse_nested_meta(|meta| {
200 for (path, proc_macro_fn) in self.macro_paths_and_proc_macro_fns.iter() {
201 if meta.path == *path {
202 proc_macro_fn(/* attributes? */ item.to_token_stream());
203 }
204 }
205 Ok(())
206 }) {
207 Ok(_) => {},
208 Err(err) => panic!("Error parsing nested meta: {}", err),
209 };
210 }
211 },)*
212 _ => {}
213 }
214 }
215 }
216 visit!(
217 Const,
218 Enum,
219 ExternCrate,
220 Fn,
221 ForeignMod,
222 Impl,
223 Macro,
224 Mod,
225 Static,
226 Struct,
227 Trait,
228 TraitAlias,
229 Type,
230 Union,
231 Use
232 );
233 }
234 }
235
236 let mut content = String::new();
237 file.read_to_string(&mut content)
238 .map_err(|e| Error::IoError(e))?;
239
240 let ast =
241 AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(|e| Error::ParseError(e))?);
242 let macro_paths_and_proc_macro_fns = AssertUnwindSafe(
243 macro_paths_and_proc_macro_fns
244 .iter()
245 .map(|(s, f)| Ok((syn::parse_str(s)?, f)))
246 .collect::<Result<Vec<(syn::Path, &F)>, _>>()
247 .map_err(|e| Error::ParseError(e))?,
248 );
249
250 panic::catch_unwind(|| {
251 syn::visit::visit_file(
252 &mut MacroVisitor::<F> {
253 macro_paths_and_proc_macro_fns,
254 },
255 &*ast,
256 );
257 })
258 .map_err(|_| {
259 Error::ParseError(syn::parse::Error::new(
260 proc_macro2::Span::call_site().into(),
261 "macro expansion panicked",
262 ))
263 })?;
264
265 Ok(())
266}
267
268/// Searches the given Rust source code file for attribute-like macro calls and calls the functions
269/// that define how to expand them.
270///
271/// This function behaves just like [`emulate_functionlike_macro_expansion`], but with attribute-like
272/// macros like `#[foo]` instead of function-like macros like `foo!()`. See that function's
273/// documentation for details and an example of use.
274///
275/// [`emulate_functionlike_macro_expansion`]: fn.emulate_functionlike_macro_expansion.html
276pub fn emulate_attributelike_macro_expansion<'a, F>(
277 mut file: fs::File,
278 macro_paths_and_proc_macro_fns: &[(&'a str, F)],
279) -> Result<(), Error>
280where
281 F: Fn(proc_macro2::TokenStream, proc_macro2::TokenStream) -> proc_macro2::TokenStream,
282{
283 struct MacroVisitor<
284 'a,
285 F: Fn(proc_macro2::TokenStream, proc_macro2::TokenStream) -> proc_macro2::TokenStream,
286 > {
287 macro_paths_and_proc_macro_fns: AssertUnwindSafe<Vec<(syn::Path, &'a F)>>,
288 }
289 impl<'a, 'ast, F> syn::visit::Visit<'ast> for MacroVisitor<'a, F>
290 where
291 F: Fn(proc_macro2::TokenStream, proc_macro2::TokenStream) -> proc_macro2::TokenStream,
292 {
293 fn visit_item(&mut self, item: &'ast syn::Item) {
294 macro_rules! visit {
295 ( $($ident:ident),* ) => {
296 match *item {
297 $(syn::Item::$ident(ref item) => {
298 for attr in item.attrs.iter() {
299 let (path, args) = match &attr.meta {
300 syn::Meta::Path(path) => (path, proc_macro2::TokenStream::new()),
301 syn::Meta::List(list) => (&list.path, list.tokens.clone().into()),
302 _ => continue
303 };
304
305 for (proc_macro_path, proc_macro_fn) in self.macro_paths_and_proc_macro_fns.iter() {
306 if path == proc_macro_path {
307 proc_macro_fn(args.clone(), item.to_token_stream());
308 }
309 }
310 }
311 },)*
312 _ => {}
313 }
314 }
315 }
316 visit!(
317 Const,
318 Enum,
319 ExternCrate,
320 Fn,
321 ForeignMod,
322 Impl,
323 Macro,
324 Mod,
325 Static,
326 Struct,
327 Trait,
328 TraitAlias,
329 Type,
330 Union,
331 Use
332 );
333 }
334 }
335
336 let mut content = String::new();
337 file.read_to_string(&mut content)
338 .map_err(|e| Error::IoError(e))?;
339
340 let ast =
341 AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(|e| Error::ParseError(e))?);
342 let macro_paths_and_proc_macro_fns = AssertUnwindSafe(
343 macro_paths_and_proc_macro_fns
344 .iter()
345 .map(|(s, f)| Ok((syn::parse_str(s)?, f)))
346 .collect::<Result<Vec<(syn::Path, &F)>, _>>()
347 .map_err(|e| Error::ParseError(e))?,
348 );
349
350 panic::catch_unwind(|| {
351 syn::visit::visit_file(
352 &mut MacroVisitor::<F> {
353 macro_paths_and_proc_macro_fns,
354 },
355 &*ast,
356 );
357 })
358 .map_err(|_| {
359 Error::ParseError(syn::parse::Error::new(
360 proc_macro2::Span::call_site().into(),
361 "macro expansion panicked",
362 ))
363 })?;
364
365 Ok(())
366}
367
368/// The error type for `emulate_*_macro_expansion`. If anything goes wrong during the file loading
369/// or macro expansion, this type describes it.
370#[derive(Debug)]
371pub enum Error {
372 IoError(std::io::Error),
373 ParseError(syn::parse::Error),
374}
375
376impl std::fmt::Display for Error {
377 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
378 match self {
379 Error::IoError(e) => e.fmt(f),
380 Error::ParseError(e) => e.fmt(f),
381 }
382 }
383}
384
385impl std::error::Error for Error {
386 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
387 match self {
388 Error::IoError(e) => e.source(),
389 Error::ParseError(e) => e.source(),
390 }
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 extern crate cargo_tarpaulin;
397 use self::cargo_tarpaulin::config::Config;
398 use self::cargo_tarpaulin::launch_tarpaulin;
399 use std::panic;
400 use std::{env, time};
401
402 #[test]
403 fn proc_macro_coverage() {
404 // All the tests are in this one function so they'll run sequentially. Something about how
405 // Tarpaulin works seems to dislike having two instances running in parallel.
406
407 {
408 // Function-like
409 let mut config = Config::default();
410 let test_dir = env::current_dir()
411 .unwrap()
412 .join("examples")
413 .join("custom_assert");
414 config.set_manifest(test_dir.join("Cargo.toml"));
415 config.test_timeout = time::Duration::from_secs(60);
416 let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
417 assert_eq!(return_code, 0);
418 }
419
420 {
421 // Attribute-like
422 let mut config = Config::default();
423 let test_dir = env::current_dir()
424 .unwrap()
425 .join("examples")
426 .join("reference_counting");
427 config.set_manifest(test_dir.join("Cargo.toml"));
428 config.test_timeout = time::Duration::from_secs(60);
429 let (_trace_map, return_code) = match launch_tarpaulin(&config, &None) {
430 Ok(ret) => ret,
431 Err(err) => panic!("{}", err),
432 };
433 assert_eq!(return_code, 0);
434 }
435 }
436}