Rustfmt skips macro arguments?

Running rustfmt on

macro_rules! identity {
    ($x:expr) => {
        $x
    };
}

fn main() {
    foo("first very very long argument", "second very very long argument");
    identity! { foo("first very very long argument", "second very very long argument") };
}

yields

macro_rules! identity {
    ($x:expr) => {
        $x
    };
}

fn main() {
    foo(
        "first very very long argument",
        "second very very long argument",
    );
    identity! { foo("first very very long argument", "second very very long argument") };
}

i.e. the first call to foo() gets formatted as expected while the second call is left unchanged. I suspect this is because macro invocations might be sensitive to whitespace (Are they, though? I'm not sure), but it is quite a nuisance if a large part of your code is nested in e.g. a tokio::select!. Is there any way to tell rustfmt to please format macro arguments as well?

I'm pretty sure the answer is no, but I would love to hear otherwise so we can fix it for tokio::select!.

2 Likes

macro look a bit like annotations

I think the rule is that rustfmt ignores everything inside a macro call when you use {}. You can try to use () to call the macro.

8 Likes

You are absolutely right! I guess I'll be using () from now on...

The reason for this is that macros can have arbitrary inner syntax that rustfmt has no idea how to format.

Treating block like macros opaquely and function like macros like functions is a practical compromise.

7 Likes

Just noticed that this rule doesn't seem to hold for select!. The following doesn't get formatted properly regardless of whether I use () or {}:

#[tokio::main]
async fn main() {
    // This gets formatted as expected
    foo(
        "first very very long argument",
        "second very very long argument",
    );
    tokio::select! (
        _ = futures::future::ready(true) => {
            // This is left unchanged
            foo("first very very long argument", "second very very long argument")
        }
    )
}

Rustfmt probably ignores every block inside the macro call, not just the outer one. Not sure if this is intentional, however.

I always thought it would format the contents of a macro if it parses as normal Rust code. So tracing::info!( "Request failed") might format correctly, but tracing::info!( url ="...","Request failed") might not.

In general, it is simply not possible for a syntax-only formatter to correctly format the input of a macro (i.e., preserve the meaning of the code) without knowing about the semantics of the macro.

For example, I am currently developing an embedded DSL for an ORM, wihch is implemented as a procedural macro. The eDSL comes with its own operators/sygils, for instance relationships are denoted <->. Just like in Rust, it is crucial that a single, atomic operator do not contain whitespace, so for example < - > would not denote the same operator in my eDSL.

However, a formatter that only knows about Rust syntax could rightfully change <-> into < -> based on the reasoning that < and -> are separate operators, and they should be surrounded by whitespace. However, this would cause previously correct code not to compile, because the parsing proc-macro would reject it.

1 Like

I wonder whether it would be possible for rustfmt to parse the macro definition and apply formatting based on the fragment specifier? That's a tall order, obviously...

That would likely be at least possible for declarative macros, but for proc-macros, it would still have to understand what the proc-macro is doing/expecting, which is at least not realistic (and may even be impossible without actually executing the macro – I have a feeling this ultimately reduces to the HP in its full generality.)

1 Like

Yep

1 Like

This will require name resolution, and Rust's name resolution requires macro expansion. And then you cannot run rustfmt on a single file, and it'll make formatting much longer.

What if I could list the macros I want to be formatted in the settings? That would move the name resolution from "every time you format" to "every time you change the macro list", which should be acceptable.

This idea could also be expanded to deal with procedural macros. In this case, rustfmt could allow the user to provide macro-by-example definitions of all the patterns that rustfmt should recognise and format. These definitions would likely not cover the whole range of valid inputs (otherwise the macro would probably have been written in the by-example style in the first place), but it would provide a relatively simple means to at least cover the most obvious cases.

That would require some way to cache the name resolution which is currently not possible. Even incremental compilation doesn't cache name resolution. Furthermore it still doesn't work for formatting single files. Only whole crates and it would break in weird ways with respect to #[cfg()] or macros defining macros.

3 Likes

Off topic, but many times the matcher can be expressed int terms of macro_rules but the expander cannot (or can, but it's too complicated).

1 Like

I'm not sure I expressed my idea clearly enough. At its simplest, what I had in mind was that rustfmt would parse a config file where the user can copy-paste macro_rules definitions, and then rustfmt can use these definitions to format the corresponding macro invocations. This can be thought of as a special type of custom formatting rules. Of course, the copy-pasting becomes tedious fairly quickly, so the next step would be that instead of copy-pasting, I can point to a macro definition and then rustfmt would look it up at runtime. This shouldn't require any name resolution (or at least no sophisticated name resolution), and it would be a pay-for-what-you-use feature: If you leave the config file empty, then this feature would introduce virtually zero overhead.

Not sure how it is expected to work. How you would point on the definition, if not using the path to it? And the path to any entity is subject to name resolution.

Ok, maybe not the best choice of words on my end. Point is, it's fairly straightforward name resolution, and you only incur the name resolution overhead if you sign up for it.