#proc #hint #macro #proc-macro #file

expander

Expands proc macro output to a file, to enable easier debugging

6 releases

0.0.6 Mar 10, 2022
0.0.5 Feb 24, 2022
0.0.3 Jan 28, 2022

#36 in Procedural macros

Download history 14587/week @ 2022-10-18 10216/week @ 2022-10-25 12603/week @ 2022-11-01 15358/week @ 2022-11-08 12467/week @ 2022-11-15 12985/week @ 2022-11-22 14449/week @ 2022-11-29 15296/week @ 2022-12-06 13120/week @ 2022-12-13 13017/week @ 2022-12-20 7362/week @ 2022-12-27 13566/week @ 2023-01-03 14602/week @ 2023-01-10 16653/week @ 2023-01-17 14244/week @ 2023-01-24 12941/week @ 2023-01-31

61,508 downloads per month
Used in 4 crates (2 directly)

MIT/Apache

16KB
236 lines

crates.io CI commits-since rust 1.51.0+ badge

expander

Expands a proc-macro into a file, and uses a include! directive in place.

Advantages

  • Only expands a particular proc-macro, not all of them. I.e. tracing is notorious for expanding into a significant amount of boilerplate with i.e. cargo expand
  • Get good errors when your generated code is not perfect yet

Usage

In your proc-macro, use it like:


#[proc_macro_attribute]
pub fn baz(_attr: proc_macro::TokenStream, input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // wrap as per usual for `proc-macro2::TokenStream`, here dropping `attr` for simplicity
    baz2(input.into()).into()
}


 // or any other macro type
fn baz2(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
    let modified = quote::quote!{
        #[derive(Debug, Clone, Copy)]
        #input
    };

    let expanded = Expander::new("baz")
        .add_comment("This is generated code!".to_owned())
        .fmt(Edition::_2021)
        .verbose(true)
        // common way of gating this, by making it part of the default feature set
        .dry(cfg!(feature="no-file-expansion"))
        .write_to_out_dir(modified.clone()).unwrap_or_else(|e| {
            eprintln!("Failed to write to file: {:?}", e);
            modified
        });
    expanded
}

will expand into

include!("/absolute/path/to/your/project/target/debug/build/expander-49db7ae3a501e9f4/out/baz-874698265c6c4afd1044a1ced12437c901a26034120b464626128281016424db.rs");

where the file content will be

#[derive(Debug, Clone, Copy)]
struct X {
    y: [u8:32],
}

Exemplary output

An error in your proc-macro, i.e. an excess ;, is shown as


   Compiling expander v0.0.4-alpha.0 (/somewhere/expander)
error: macro expansion ignores token `;` and any following
 --> tests/multiple.rs:1:1
  |
1 | #[baz::baz]
  | ^^^^^^^^^^^ caused by the macro expansion here
  |
  = note: the usage of `baz::baz!` is likely invalid in item context

error: macro expansion ignores token `;` and any following
 --> tests/multiple.rs:4:1
  |
4 | #[baz::baz]
  | ^^^^^^^^^^^ caused by the macro expansion here
  |
  = note: the usage of `baz::baz!` is likely invalid in item context

error: could not compile `expander` due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
error: build failed

becomes


   Compiling expander v0.0.4-alpha.0 (/somewhere/expander)
expander: writing /somewhere/expander/target/debug/build/expander-8cb9d7a52d4e83d1/out/baz-874698265c6c.rs
error: expected item, found `;`
 --> /somewhere/expander/target/debug/build/expander-8cb9d7a52d4e83d1/out/baz-874698265c6c.rs:2:42
  |
2 | #[derive(Debug, Clone, Copy)] struct A ; ;
  |                                          ^

expander: writing /somewhere/expander/target/debug/build/expander-8cb9d7a52d4e83d1/out/baz-73b3d5b9bc46.rs
error: expected item, found `;`
 --> /somewhere/expander/target/debug/build/expander-8cb9d7a52d4e83d1/out/baz-73b3d5b9bc46.rs:2:42
  |
2 | #[derive(Debug, Clone, Copy)] struct B ; ;
  |                                          ^

error: could not compile `expander` due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
error: build failed

which shows exactly where in the generated code, the produce of your proc-macro, rustc found an invalid token sequence.

Now this was a simple example, doing this with macros that would expand to multiple tens of thousand lines of code when expanded with cargo-expand, and still in a few thousand that your particular one generates, it's a life saver to know what caused the issue rather than having to use eprintln! to print a unformated string to the terminal.

Hint: You can quickly toggle this by using .dry(true || false)

Special handling: syn

By default expander is built with feature syndicate which adds fn maybe_write_* to struct Expander, which aids handling of Result<TokenStream, syn::Error> for the commonly used rust parsing library syn.

Reasoning

syn::Error::new(Span::call_site(),"yikes!").into_token_stream(self) becomes compile_error!("yikes!") which provides better info to the user (that's you!) than when serializing it to file, since the provided span for the syn::Error is printed differently - being pointed to the compile_error! invocation in the generated file is not helpful, and rustc can point to the span instead.

Dependencies

~410–650KB
~15K SLoC