sylvia_runtime_macros/
lib.rs

1// Copyright (c) 2018 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
33use std::fs;
34use std::io::Read;
35use std::panic::{self, AssertUnwindSafe};
36
37use attr_macro_visitor::AttributeMacroVisitor;
38use syn::punctuated::Punctuated;
39use syn::{Path, Token};
40
41mod attr_macro_visitor;
42
43/// Parses the given Rust source file, finding functionlike macro expansions using `macro_path`.
44/// Each time it finds one, it calls `proc_macro_fn`, passing it the inner `TokenStream` just as
45/// if the macro were being expanded. The only effect is to verify that the macro doesn't panic,
46/// as the expansion is not actually applied to the AST or the source code.
47///
48/// Note that this parser only handles Rust's syntax, so it cannot resolve paths to see if they
49/// are equivalent to the given one. The paths used to reference the macro must be exactly equal
50/// to the one given in order to be expanded by this function. For example, if `macro_path` is
51/// `"foo"` and the file provided calls the macro using `bar::foo!`, this function will not know
52/// to expand it, and the macro's code coverage will be underestimated.
53///
54/// Also, this function uses `proc_macro2::TokenStream`, not the standard but partly unstable
55/// `proc_macro::TokenStream`. You can convert between them using their `into` methods, as shown
56/// below.
57///
58/// # Returns
59///
60/// `Ok` on success, or an instance of [`Error`] indicating any error that occurred when trying to
61/// read or parse the file.
62///
63/// [`Error`]: enum.Error.html
64///
65/// # Example
66///
67/// ```ignore
68/// # // This example doesn't compile because procedural macros can only be made in crates with
69/// # // type "proc-macro".
70/// # #![cfg(feature = "proc-macro")]
71/// # extern crate proc_macro;
72/// # extern crate proc_macro2;
73/// #[proc_macro]
74/// fn remove(_: proc_macro::TokenStream) -> proc_macro::TokenStream {
75///     // This macro just eats its input and replaces it with nothing.
76///     proc_macro::TokenStream::empty()
77/// }
78///
79/// extern crate syn;
80///
81/// #[test]
82/// fn macro_code_coverage() {
83///     let file = std::fs::File::open("tests/tests.rs");
84///     emulate_macro_expansion(file, "remove", |ts| remove(ts.into()).into());
85/// }
86/// ```
87pub fn emulate_macro_expansion_fallible<F>(
88    mut file: fs::File,
89    macro_path: &str,
90    proc_macro_fn: F,
91) -> Result<(), Error>
92where
93    F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
94{
95    struct MacroVisitor<F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream> {
96        macro_path: syn::Path,
97        proc_macro_fn: AssertUnwindSafe<F>,
98    }
99    impl<'ast, F> syn::visit::Visit<'ast> for MacroVisitor<F>
100    where
101        F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
102    {
103        fn visit_macro(&mut self, macro_item: &'ast syn::Macro) {
104            if macro_item.path == self.macro_path {
105                (*self.proc_macro_fn)(macro_item.tokens.clone());
106            }
107        }
108    }
109
110    let proc_macro_fn = AssertUnwindSafe(proc_macro_fn);
111
112    let mut content = String::new();
113    file.read_to_string(&mut content)
114        .map_err(|e| Error::IoError(e))?;
115
116    let ast =
117        AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(|e| Error::ParseError(e))?);
118    let macro_path: syn::Path = syn::parse_str(macro_path).map_err(|e| Error::ParseError(e))?;
119
120    panic::catch_unwind(|| {
121        syn::visit::visit_file(
122            &mut MacroVisitor::<F> {
123                macro_path,
124                proc_macro_fn,
125            },
126            &*ast,
127        );
128    })
129    .map_err(|_| {
130        Error::ParseError(syn::parse::Error::new(
131            proc_macro2::Span::call_site(),
132            "macro expansion panicked",
133        ))
134    })?;
135
136    Ok(())
137}
138
139fn uses_derive(attrs: &[syn::Attribute], derive_name: &syn::Path) -> Result<bool, Error> {
140    for attr in attrs {
141        if attr.path().is_ident("derive") {
142            if let syn::Meta::List(ml) = &attr.meta {
143                let nested = ml
144                    .parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated)
145                    .map_err(Error::ParseError)?;
146                let uses_derive = nested.iter().any(|nested_meta| nested_meta == derive_name);
147                if uses_derive {
148                    return Ok(true);
149                }
150            }
151        }
152    }
153    Ok(false)
154}
155
156/// Parses the given Rust source file, finding custom drives macro expansions using `macro_path`.
157/// Each time it finds one, it calls `derive_fn`, passing it a `syn::DeriveInput`.
158///
159/// Note that this parser only handles Rust's syntax, so it cannot resolve paths to see if they
160/// are equivalent to the given one. The paths used to reference the macro must be exactly equal
161/// to the one given in order to be expanded by this function. For example, if `macro_path` is
162/// `"foo"` and the file provided calls the macro using `bar::foo!`, this function will not know
163/// to expand it, and the macro's code coverage will be underestimated.
164///
165/// This function follows the standard syn pattern of implementing most of the logic using the
166/// `proc_macro2` types, leaving only those methods that can only exist for `proc_macro=true`
167/// crates, such as types from `proc_macro` or `syn::parse_macro_input` in the outer function.
168/// This allows use of the inner function in tests which is needed to expand it here.
169///
170/// # Returns
171///
172/// `Ok` on success, or an instance of [`Error`] indicating any error that occurred when trying to
173/// read or parse the file.
174///
175/// [`Error`]: enum.Error.html
176///
177/// # Example
178///
179/// ```ignore
180/// # // This example doesn't compile because procedural macros can only be made in crates with
181/// # // type "proc-macro".
182/// # #![cfg(feature = "proc-macro")]
183/// # extern crate proc_macro;
184///
185/// use quote::quote;
186/// use syn::parse_macro_input;
187///
188/// #[proc_macro_derive(Hello)]
189/// fn hello(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
190///     hello_internal(parse_macro_input!(input as DeriveInput)).into()
191/// }
192///
193/// fn hello_internal(input: syn::DeriveInput) -> proc_macro2::TokenStream {
194///     let ident = input.ident;
195///     quote! {
196///         impl #ident {
197///             fn hello_world() -> String {
198///                 String::from("Hello World")
199///             }
200///         }
201///     }
202/// }
203///
204/// #[test]
205/// fn macro_code_coverage() {
206///     let file = std::fs::File::open("tests/tests.rs");
207///     emulate_derive_expansion_fallible(file, "Hello", hello_internal);
208/// }
209/// ```
210pub fn emulate_derive_expansion_fallible<F>(
211    mut file: fs::File,
212    macro_path: &str,
213    derive_fn: F,
214) -> Result<(), Error>
215where
216    F: Fn(syn::DeriveInput) -> proc_macro2::TokenStream,
217{
218    struct MacroVisitor<F: Fn(syn::DeriveInput) -> proc_macro2::TokenStream> {
219        macro_path: syn::Path,
220        derive_fn: AssertUnwindSafe<F>,
221    }
222    impl<'ast, F> syn::visit::Visit<'ast> for MacroVisitor<F>
223    where
224        F: Fn(syn::DeriveInput) -> proc_macro2::TokenStream,
225    {
226        fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
227            match uses_derive(&node.attrs, &self.macro_path) {
228                Ok(uses) => {
229                    if uses {
230                        (*self.derive_fn)(node.clone().into());
231                    }
232                }
233                Err(e) => panic!(
234                    "Failed expanding derive macro for {:?}: {}",
235                    self.macro_path, e
236                ),
237            }
238        }
239
240        fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
241            match uses_derive(&node.attrs, &self.macro_path) {
242                Ok(uses) => {
243                    if uses {
244                        (*self.derive_fn)(node.clone().into());
245                    }
246                }
247                Err(e) => panic!(
248                    "Failed expanding derive macro for {:?}: {}",
249                    self.macro_path, e
250                ),
251            }
252        }
253    }
254
255    let derive_fn = AssertUnwindSafe(derive_fn);
256
257    let mut content = String::new();
258    file.read_to_string(&mut content)
259        .map_err(|e| Error::IoError(e))?;
260
261    let ast =
262        AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(|e| Error::ParseError(e))?);
263    let macro_path: syn::Path = syn::parse_str(macro_path).map_err(|e| Error::ParseError(e))?;
264
265    panic::catch_unwind(|| {
266        syn::visit::visit_file(
267            &mut MacroVisitor::<F> {
268                macro_path,
269                derive_fn,
270            },
271            &*ast,
272        );
273    })
274    .map_err(|_| {
275        Error::ParseError(syn::parse::Error::new(
276            proc_macro2::Span::call_site(),
277            "macro expansion panicked",
278        ))
279    })?;
280
281    Ok(())
282}
283
284/// Parses the given Rust source file, finding attributes macro expansions using `macro_path`.
285/// Each time it finds one, it calls `derive_fn`, passing it a `syn::DeriveInput`.
286///
287/// Note that this parser only handles Rust's syntax, so it cannot resolve paths to see if they
288/// are equivalent to the given one. The paths used to reference the macro must be exactly equal
289/// to the one given in order to be expanded by this function. For example, if `macro_path` is
290/// `"foo"` and the file provided calls the macro using `#[bar::foo]`, this function will not know
291/// to expand it, and the macro's code coverage will be underestimated. Also it is important, that
292/// this function would expand every matching attribute, so it is important to design your macros
293/// in the way, the attribute do not collide with other attributes used in tests - not only
294/// actual macros, but also attributes eaten by other macros/derives.
295///
296/// This function follows the standard syn pattern of implementing most of the logic using top
297/// use quote::quote;
298/// use syn::parse_macro_input;
299///
300/// #[proc_macro_attribute]
301/// fn hello(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
302///     hello_internal(attr.into(), item.into()).into()
303/// }
304///
305/// fn hello_internal(attr: proc_macro2::TokenStream, item: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
306///     quote!(#item)
307/// }
308///
309/// #[test]
310/// fn macro_code_coverage() {
311///     let file = std::fs::File::open("tests/tests.rs");
312///     emulate_attribute_expansion_fallible(file, "hello", hello_internal);
313/// }
314/// ```
315pub fn emulate_attribute_expansion_fallible<Arg, Res>(
316    mut file: fs::File,
317    macro_path: &str,
318    macro_fn: impl Fn(Arg, Arg) -> Res,
319) -> Result<(), Error>
320where
321    Arg: From<proc_macro2::TokenStream>,
322    Res: Into<proc_macro2::TokenStream>,
323{
324    let macro_fn = AssertUnwindSafe(
325        |attr: proc_macro2::TokenStream, item: proc_macro2::TokenStream| {
326            macro_fn(attr.into(), item.into()).into()
327        },
328    );
329
330    let mut content = String::new();
331    file.read_to_string(&mut content).map_err(Error::IoError)?;
332
333    let ast = AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(Error::ParseError)?);
334    let macro_path: syn::Path = syn::parse_str(macro_path).map_err(Error::ParseError)?;
335
336    panic::catch_unwind(|| {
337        syn::visit::visit_file(&mut AttributeMacroVisitor::new(macro_path, macro_fn), &*ast);
338    })
339    .map_err(|_| {
340        Error::ParseError(syn::parse::Error::new(
341            proc_macro2::Span::call_site(),
342            "macro expansion panicked",
343        ))
344    })?;
345
346    Ok(())
347}
348
349/// This type is like [`emulate_macro_expansion_fallible`] but automatically unwraps any errors it
350/// encounters. As such, it's deprecated due to being less flexible.
351///
352/// [`emulate_macro_expansion_fallible`]: fn.emulate_macro_expansion_fallible.html
353#[deprecated]
354pub fn emulate_macro_expansion<F>(file: fs::File, macro_path: &str, proc_macro_fn: F)
355where
356    F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
357{
358    emulate_macro_expansion_fallible(file, macro_path, proc_macro_fn).unwrap()
359}
360
361/// The error type for [`emulate_macro_expansion_fallible`]. If anything goes wrong during the file
362/// loading or macro expansion, this type describes it.
363///
364/// [`emulate_macro_expansion_fallible`]: fn.emulate_macro_expansion_fallible.html
365#[derive(Debug)]
366pub enum Error {
367    IoError(std::io::Error),
368    ParseError(syn::parse::Error),
369}
370
371impl std::fmt::Display for Error {
372    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
373        match self {
374            Error::IoError(e) => e.fmt(f),
375            Error::ParseError(e) => e.fmt(f),
376        }
377    }
378}
379
380impl std::error::Error for Error {
381    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
382        match self {
383            Error::IoError(e) => e.source(),
384            Error::ParseError(e) => e.source(),
385        }
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use cargo_tarpaulin::config::Config;
392    use cargo_tarpaulin::launch_tarpaulin;
393    use std::{
394        env,
395        panic::UnwindSafe,
396        sync::{Arc, Mutex, Once},
397        time,
398    };
399
400    static mut TARPAULIN_MUTEX: Option<Arc<Mutex<()>>> = None;
401    static SETUP_TEST_MUTEX: Once = Once::new();
402
403    pub(crate) fn test_mutex() -> Arc<Mutex<()>> {
404        unsafe {
405            SETUP_TEST_MUTEX.call_once(|| {
406                TARPAULIN_MUTEX = Some(Arc::new(Mutex::new(())));
407            });
408            Arc::clone(TARPAULIN_MUTEX.as_ref().unwrap())
409        }
410    }
411
412    pub(crate) fn with_test_lock<F, R>(f: F) -> R
413    where
414        R: Send + 'static,
415        F: FnOnce() -> R + Send + UnwindSafe + 'static,
416    {
417        let test_mutex = test_mutex();
418        let test_lock = test_mutex.lock().expect("Failed to acquire test lock");
419        let res = f();
420        drop(test_lock);
421        res
422    }
423
424    #[test]
425    fn proc_macro_coverage() {
426        with_test_lock(|| {
427            let mut config = Config::default();
428            let test_dir = env::current_dir()
429                .unwrap()
430                .join("examples")
431                .join("custom_assert");
432            config.set_manifest(test_dir.join("Cargo.toml"));
433            config.test_timeout = time::Duration::from_secs(60);
434            let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
435            assert_eq!(return_code, 0);
436        })
437    }
438
439    #[test]
440    fn derive_macro_coverage() {
441        with_test_lock(|| {
442            let mut config = Config::default();
443            let test_dir = env::current_dir()
444                .unwrap()
445                .join("examples")
446                .join("custom_derive");
447            config.set_manifest(test_dir.join("Cargo.toml"));
448            config.test_timeout = time::Duration::from_secs(60);
449            let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
450            assert_eq!(return_code, 0);
451        })
452    }
453
454    #[test]
455    fn attribute_macro_coverage() {
456        with_test_lock(|| {
457            let mut config = Config::default();
458            let test_dir = env::current_dir()
459                .unwrap()
460                .join("examples")
461                .join("custom_attribute");
462            config.set_manifest(test_dir.join("Cargo.toml"));
463            config.test_timeout = time::Duration::from_secs(60);
464            let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
465            assert_eq!(return_code, 0);
466        })
467    }
468}