10 releases (3 stable)

2.1.0 Mar 2, 2024
2.0.0 Apr 11, 2023
1.0.0 Mar 1, 2023
0.0.6 Mar 10, 2022
0.0.3 Jan 28, 2022

#11 in Procedural macros

Download history 23242/week @ 2023-12-23 36701/week @ 2023-12-30 54875/week @ 2024-01-06 65044/week @ 2024-01-13 67648/week @ 2024-01-20 63636/week @ 2024-01-27 62698/week @ 2024-02-03 67821/week @ 2024-02-10 81240/week @ 2024-02-17 75919/week @ 2024-02-24 63741/week @ 2024-03-02 67509/week @ 2024-03-09 68113/week @ 2024-03-16 65611/week @ 2024-03-23 94371/week @ 2024-03-30 74694/week @ 2024-04-06

313,888 downloads per month
Used in 486 crates (6 directly)

MIT/Apache

20KB
308 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)

Features

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.

rustfmt-free formatting: pretty

When built with feature pretty, the output is formatted with prettier-please. Note that this adds additional compiletime overhead and weight to the crate as a trade off not needing any host side tooling.

The formatting output will, for any significant amount of lines of code, differ from the output of rustfmt.

Dependencies

~0.4–0.8MB
~18K SLoC