Search code examples
rustdoctestrust-macrosrust-proc-macros

Infer the name of the calling crate to populate a doctest in a procedural macro


I'm creating a procedural macro that auto-generates a library from some configuration file (it's a register layout, but that's not important for the question).

I would like the library to auto-generate the documentation accompanying the auto-library and include doc tests that should run with cargo test. Now, I've implemented most of this, but there is one issue I can't see a solution to.

Say we have a library called my_lib in which we invoke the macro to populate it:

use my_macro_lib::hello;

hello!();

which expands to something like:

/// `foo` will always return `true`
/// ```
/// use my_lib;
/// assert!(my_lib::foo());
/// ```
pub fn foo() -> bool {
    true
}

This will run as expected - cargo doc will do the right thing and cargo test will run the doctests as expected.

The problem is that in this example, use my_lib is hard-coded into the my_macro_lib which is clearly undesirable.

How can I create a macro which infers the name of crate that is doing the calling?

I tried using a macro_rules! inside the procedural macro to expand $crate, but this breaks the hygiene rules.


Solution

  • You can obtain the name of the crate that is using your macro by reading the CARGO_PKG_NAME environment variable. Note that you have to read it via std::env (at "runtime" of your macro) and not via env! (which would be when your proc macro crate is compiled).

    #[proc_macro]
    pub fn hello(input: TokenStream) -> TokenStream {
        let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
        let use_statement = format!("use {}::foo;", crate_name);
    
        let output = quote! {
            /// `foo` will always return `true`
            /// ```
            #[doc = #use_statement]
            /// assert!(foo());
            /// ```
            pub fn foo() -> bool {
                true
            }
        };
    
        output.into()
    }
    

    There are a few complications here regarding interpolating things in doc comments. Interpolating like /// #an_ident does not work as doc comments are parsed in a special way. The only way we can do this is to create a string and using the #[doc = ...] syntax. It's a bit annoying because you have to create strings before the quote! invocation, but it works.

    However, I don't think it is guaranteed that this works. Currently proc macros can access all of the environment (including the file system, network, ...). To my knowledge, proc macros aren't guaranteed this access and proc macros might be sandboxed in the future. So this solution is not perfect yet, but it works for now (and probably for quite some time still).


    An alternative would be to just let the user pass the crate name to your macro:

    hello!(my_lib);
    

    If your macro is only invoked once per crate, this is probably the preferred solution. If your macro is invoked a lot, then repeating the crate name might be annoying.