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}