Specifying file path in macro - macro fails, but inlined version works

78 Views Asked by At

I'm trying to write some macros to clean up boilerplate around my intros/headers, to enforce that I'm organizing things in the same way, and to have a one-spot place to mandate any changes to such. I wanted to write one for organizing tests.

The structure I've settled on is for unit tests to go in a "test" sub-directory within a module's parent folder. So, for a leaf module general.rs, the header looks like:

#[cfg(test)]
#[path = "test/general_test.rs"]
mod general_test;

I'm trying to generate this via macro call:

#[macro_export]
macro_rules! declare_tests {
    ($mod_name : ident) => {
        #[cfg(test)]
        #[path = concat!("test/", stringify!($mod_name), ".rs")]
        mod $mod_name;
    };
}

But this fails:

crate::declare_tests!(general_test);
unresolved module, can't find module file: general/general_test.rs, or general/general_test/mod.rsrust-analyzerE0583

But if my IDE inlines it to inspect the problem, the problem goes away:

crate::declare_tests!(general_test);

inlines as

#[cfg(test)]
#[path = concat!("test/",stringify!(general_test),".rs")]
mod general_test;

which inlines as

#[cfg(test)]
#[path = "test/general_test.rs"]
mod general_test;

which works.

I think maybe my IDE (vscode) is doing these in the wrong order, but I can't tell what the right order would be?

Curious what's up with the inlining, but of course a better solution overall would also be welcome. (I plan to integrate this with std::module_path! to further reduce boilerplate/standardize, once I get this step working.)

EDIT:

With some experimentation:

#[macro_export]
macro_rules! declare_tests_helper {
    ($mod_path : expr, $mod_name : tt) => {
        #[cfg(test)]
        #[path = $mod_path]
        mod $mod_name;
        
    };
}
...
crate::declare_tests_helper!("test/general_test.rs", general_test);

works, tho doesn't help me as much as I'd like - I still can't pass another macro's result as an argument, because the outer macro is expanded first.

I'm wondering if it's possible to do this with a procedural macro, and (hopefully) greater control over order of execution?

2

There are 2 best solutions below

3
kmdreko On

You cannot use concat! in a #[path] attribute.

#[path = concat!("test/", "general_test", ".rs")]
mod general_test;
error: malformed `path` attribute input
 --> src/main.rs:6:1
  |
6 | #[path = concat!("test/", "general_test", ".rs")]
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: must be of the form: `#[path = "file"]`

Telling your IDE to inline the concat! macro is not what the compiler does when evaluating the #[path] attribute. In general, macros are evaluated by the outside-inward; so the concat! is not evaluated before the #[path] starts trying to use it.

Running function-like macros eagerly inside attributes macros only happen in limited circumstances. See this PR for more info on allowing #[doc = include!(...)] but it mentions "the attributes path, ... do not support this". And see this issue for #[path] to support it.

0
Edward Peters On

It's not pretty but it works! (My first proc macro, sure it's not ideal.)

#[proc_macro]
pub fn test_setup(_item: TokenStream) -> TokenStream {
    let first = _item.into_iter().next().unwrap();
    let id = match first{
        TokenTree::Ident(ident) => ident.to_string(),
        _ => panic!("Needed identifier"),
    };

    let imports = format!{"#[cfg(test)]
    #[path = \"test/{id}_test.rs\"]
    mod {id}_test;"};
     let statement = imports.parse().unwrap();
     statement
}

Use site:

proc_macros::test_setup!(general);

The original case wasn't working because the outer macro #[path = <>] was expanded first, and saw the un-expanded declarative macros. With procedural macros you can control the order of execution and actually generate the #[path] call with the arguments in place.