Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user format-like macros #9948

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

nyurik
Copy link
Contributor

@nyurik nyurik commented Nov 25, 2022

Add support for #[clippy::format_args] attribute that can be attached to any macro to indicate that it functions the same as the built-in format macros like format!, println! and write!


changelog: Enhancement: [format_in_format_args], [recursive_format_impl], [to_string_in_format_args], [uninlined_format_args], [unused_format_specs]: Recognizes #[clippy::format_args] to support custom 3rs party format macros.

@rust-highfive
Copy link

r? @Alexendoo

(rust-highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Nov 25, 2022
@Alexendoo
Copy link
Member

Alexendoo commented Nov 25, 2022

There are some reasonable situations where inline args can't be used, such as

macro_rules! error {
    ($fmt:literal $(, $e:expr)* $(,)?) => { println!(concat!("ERROR: ", $fmt), $($e,)*) }
}

fn main() {
    let x = 1;
    error!("{}", x);
}

Another thing is that it could break macro invocations themselves, initially I wrote that as ($fmt:literal, $($e:expr),*) where replacing it with error!("{x}") would result in error: unexpected end of macro invocation

@nyurik
Copy link
Contributor Author

nyurik commented Nov 25, 2022

@Alexendoo I just added your example to my unit tests - it passes just fine. Do you have a test that fails?

@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from 6b83237 to 0bdf235 Compare November 25, 2022 23:01
@Alexendoo
Copy link
Member

concat! does seem to be safe since it gets the correct span, for the second one an example would be

macro_rules! error {
    ($fmt:literal, $($e:expr),*) => { println!($fmt, $($e,)*) }
}

fn main() {
    let x = 1;
    error!("{}", x);
}

@nyurik
Copy link
Contributor Author

nyurik commented Nov 25, 2022

@Alexendoo that's an excellent catch, thank you! The issue here is with the bug in macro definition (granted that macros are a special beast and a "bug" could be someone's feature). TBH, I am surprised there is no lint that tries to catch this mistake. The bigger question though is if this is something we should or should not process? I.e. something that suggests a cleaner repeated macro arg definition. I would even make this as a suspicious category.

That said, on one hand we have badly created user macros that probably should be fixed. On the other, there is a very common pattern with all sorts of log and other macros that use format strings and could really use this lint. Which of these takes priority?

@nyurik
Copy link
Contributor Author

nyurik commented Nov 26, 2022

@Alexendoo I created a lint suggestion #9959 that would solve this types of issues - having both lints on at the same time would fix the issue i think

@Alexendoo
Copy link
Member

It would be neat to be able to lint macro definitions, however as they are almost entirely represented as a big TokenStream that may be expanded into an arbitrary place linting them isn't easy - https://rust-lang.zulipchat.com/#narrow/stream/257328-clippy/topic/Get.20paths.20in.20a.20macro/near/292040802

Another one to bring up is that a common way of defining things like this is two arms, so that lint wouldn't be generally applicable

($fmt:literal) => ...
($fmt:literal, $($e:expr),*) => ...

e.g. the format_args macro_rules stub is defined like that. There are macros out there that assume the single arg version is a plain string much like the old panic! macro, I found this one in a search https://github.com/scottlamb/moonfire-nvr/blob/d509d3ff40160a58e4fbdd01be240f2be474b693/server/base/error.rs#L159-L166

That would make the suggestion change the behaviour of the code, silently even if the printed variable is otherwise used

My biggest concern is probably all the proc macros out there, the current implementation is not super robust against them. If a proc macro did an internal format!() but happened to use certain input spans we could be matching up a completely different format string than the one actually being expanded

To that end it may be worth keeping an eye on rust-lang/rust#99012 / rust-lang/compiler-team#541

@Alexendoo
Copy link
Member

Another fun one:

macro_rules! used_twice {
    (
        large = $large:literal,
        small = $small:literal,
        $val:expr,
    ) => {{
        if $val < 5 {
            println!($small, $val);
        } else {
            println!($large, $val);
        }
    }}
}

fn main() {
    let x = 1;
    used_twice! {
        large = "large value: {}",
        small = "small value: {}",
        x,
    }
}

@nyurik
Copy link
Contributor Author

nyurik commented Nov 26, 2022

I have a better idea - let's disable Rust macros, as they are clearly a work of Cthulhu, designed to steer us away from the light... as this is clearly a case of "I miss Perl, so I will make things as interesting as possible".

I don't think Clippy should care about such cases TBH as they probably represent 1% - when on the other hand we have 99% of the simple log::warn! type. Moreover, if our lint is used often enough, the 1% edge cases should disappear out right as abominations against humanity. That said, we may want to detect such cases (e.g. have a list of well known crates), and if the first macro in a call chain is not in the well known list, reduce the certainty of replacement from MachineApplicable to MaybeIncorrect.

@nyurik nyurik force-pushed the handle-all-fmt branch 4 times, most recently from 0b3e8b5 to 7239dd6 Compare November 26, 2022 03:59
@llogiq
Copy link
Contributor

llogiq commented Nov 26, 2022

Please keep in mind that we deem false positives a worse outcome than false negatives. I'd be OK with having either a config that makes the lint apply everywhere instead of just format while making the suggestion MaybeIncorrect or having a second lint that is allowed by default that catches those cases aside format though, with a slight preference towards the latter.

@nyurik
Copy link
Contributor Author

nyurik commented Nov 26, 2022

@llogiq I think we should also take into account how often we get false positive vs false negative. I agree that if they are about the same, false positive is worse, but if we get 100x more false negatives, I would prefer get a few more false positives.

I like the idea of an extra config param, something like include_custom_format_macros (naming?) - this way all lints that currently use is_format_arg would handle 3rd-party format macros.

@llogiq
Copy link
Contributor

llogiq commented Nov 27, 2022

I would fully support that argument for a suspicious lint, but not for a style lint. Please note that a lint rarely works in isolation – clippy usually runs many of them. If one of them gives even a modest amount of false positives, this reduces the usefulness of clippy as a whole. While that may be acceptable for code that is likely problematic and warrants a closer look, I don't think it is needed for style lints which IMHO are a teaching tool.

With that said, as long as the user explicitly asks for it, I'm ok with changing this balance.

@bors
Copy link
Collaborator

bors commented Nov 27, 2022

☔ The latest upstream changes (presumably #9860) made this pull request unmergeable. Please resolve the merge conflicts.

@bors
Copy link
Collaborator

bors commented Nov 28, 2022

☔ The latest upstream changes (presumably #9865) made this pull request unmergeable. Please resolve the merge conflicts.

@bors
Copy link
Collaborator

bors commented Feb 11, 2024

☔ The latest upstream changes (presumably #12269) made this pull request unmergeable. Please resolve the merge conflicts.

@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from b41c598 to 7f29eff Compare February 13, 2024 00:03
@Alexendoo
Copy link
Member

My preferred solution would be the tool attribute applied to macro definitions, e.g.

#[clippy::format_args_compatible]
macro_rules! log {
// ...

Config based relies on assuming third party crate layouts to be widely useful which we're trying not to expand, I think putting it in the macro authors hands is preferable there

If there's a good heuristic that works for automatic detection I would be fine with that, but I'm not sure if there is one

@nyurik
Copy link
Contributor Author

nyurik commented Feb 15, 2024

@Alexendoo do you know of any example of an attribute set on a macro rule, and Clippy using that attribute while parsing the expansion of the macro?

We could introduce a new pedantic (?) lint that would tag all cases as suspicious if they resolve to format_args! internally, but there are guaranteed to be rare false positives. We could even add a link to a guide on how to write proper format macros so they won't break if they use a single string without args.

@Alexendoo
Copy link
Member

I don't think we have any cases where it's used, but MacroCall contains the macro's DefId which can be used to retrieve its attributes

Linting macro definitions is considered too difficult because the only thing you get from unexpanded macros is a token stream

@bors
Copy link
Collaborator

bors commented Apr 12, 2024

☔ The latest upstream changes (presumably #12635) made this pull request unmergeable. Please resolve the merge conflicts.

@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from 4b61a18 to 48385e7 Compare May 26, 2024 00:46
@xFrednet
Copy link
Member

Hey, this is a ping from triage. @Alexendoo can you give this PR a review? It's totally fine if you don't have the time right now, you can reassign the PR to a random team member using r? clippy.

@rustbot ready

@bors
Copy link
Collaborator

bors commented Jul 17, 2024

☔ The latest upstream changes (presumably #13088) made this pull request unmergeable. Please resolve the merge conflicts.

@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from 7d46049 to 6fb3013 Compare July 17, 2024 22:47
@Alexendoo
Copy link
Member

Ah sorry missed that notification @xFrednet, my position is still if we're doing this it should be through an attribute rather than configuration

@bors
Copy link
Collaborator

bors commented Jul 29, 2024

☔ The latest upstream changes (presumably #13173) made this pull request unmergeable. Please resolve the merge conflicts.

@nyurik
Copy link
Contributor Author

nyurik commented Sep 1, 2024

@Alexendoo i updated this PR to rely on #[clippy::format_args] attribute

@nyurik nyurik changed the title Process all format-like macros Support user format-like macros Sep 1, 2024
@bors
Copy link
Collaborator

bors commented Sep 22, 2024

☔ The latest upstream changes (presumably #13440) made this pull request unmergeable. Please resolve the merge conflicts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray file?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file produces no lints but unfixable implies it ought to

Comment on lines 216 to 221
#[clippy::format_args]
macro_rules! my_concat {
($fmt:literal $(, $e:expr)*) => {
println!(concat!("ERROR: ", $fmt), $($e,)*)
}
}

#[clippy::format_args]
macro_rules! my_good_macro {
($fmt:literal $(, $e:expr)* $(,)?) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[clippy::format_args] is applied to many of these macros but only my_good_macro is linting, is that intentional?

@bors
Copy link
Collaborator

bors commented Sep 25, 2024

☔ The latest upstream changes (presumably #13442) made this pull request unmergeable. Please resolve the merge conflicts.

@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from 88054d0 to b11b685 Compare October 5, 2024 04:49
Add support for `#[clippy::format_args]` attribute that can be attached to any macro to indicate that it functions the same as the built-in format macros like `format!`, `println!` and `write!`
@nyurik
Copy link
Contributor Author

nyurik commented Oct 5, 2024

@Alexendoo thx, I think I fixed all the issues with it now, adding a lot of new tests in various relevant lints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants