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)
syn
Special handling: 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.