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}