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
Contributor

bors commented Nov 27, 2022

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

@bors
Copy link
Contributor

bors commented Nov 28, 2022

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

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

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
Contributor

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 3 times, most recently from b11b685 to dc752cf Compare October 5, 2024 04:55
@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.

@nyurik
Copy link
Contributor Author

nyurik commented Oct 19, 2024

@xFrednet @Alexendoo @llogiq friendly ping - it seems this 2 years old PR might actually get merged / put to vote, as I don't see any outstanding issues with it?

@Alexendoo
Copy link
Member

https://rust-lang.zulipchat.com/#narrow/channel/257328-clippy/topic/Can.20we.20start.20FCP.20for.20.60.23.5Bclippy.3A.3Aformat_args.5D.60.20in.20user.20code.3F hasn't had any concerns raised so yeah seems fine

We should ensure this is documented somewhere though

@nyurik
Copy link
Contributor Author

nyurik commented Oct 20, 2024

@Alexendoo where would be the best place to document that 3rd party crates can now use this new attribute? Documentation should also include recommended way to declare format-using macros (i.e. to avoid trailing comma gotchas, etc). The only concern for 3rd party crates is how will they deal with the older Clippy versions - wouldn't there be some warning if clippy doesn't recognize an attribute?

@Alexendoo
Copy link
Member

Probably a new section in the book for if we add others

older Clippy versions - wouldn't there be some warning if clippy doesn't recognize an attribute?

If the crate author uses Clippy I believe they would have to be using one that supports the attribute, but consuming the dep shouldn't show a warning if it's older than the attribute. We should add a test for this though

@nyurik nyurik force-pushed the handle-all-fmt branch 4 times, most recently from 6d144ac to b799d4c Compare October 21, 2024 07:08
@nyurik
Copy link
Contributor Author

nyurik commented Oct 21, 2024

Thx, rebased and added documentation page to the book.

@nyurik
Copy link
Contributor Author

nyurik commented Oct 21, 2024

P.S. using undefined attribute is an error, not a warning - that's why #[rustversion::attr(since(1.84), clippy::format_args)] should be the recommended usage pattern.

Copy link
Member

@flip1995 flip1995 left a comment

Choose a reason for hiding this comment

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

The only thing that still needs to be figured out is the behavior when those macros are used in a dependency. Aka would adding this (without the rust version::attr) raise the MSRV of a crate?

book/src/lib_usage.md Outdated Show resolved Hide resolved
clippy_utils/src/attrs.rs Show resolved Hide resolved
@@ -36,7 +37,9 @@ pub fn is_format_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
FORMAT_MACRO_DIAG_ITEMS.contains(&name)
} else {
false
// Allow users to tag any macro as being format!-like
// TODO: consider deleting FORMAT_MACRO_DIAG_ITEMS and using just this method
Copy link
Member

Choose a reason for hiding this comment

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

This should be a follow up PR to the rust repo, after this PR is merged and synced

book/src/SUMMARY.md Outdated Show resolved Hide resolved
book/src/lib_usage.md Outdated Show resolved Hide resolved
@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from 8e0ebf1 to a728e2c Compare November 12, 2024 21:19
Copy link
Member

@flip1995 flip1995 left a comment

Choose a reason for hiding this comment

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

LGTM. Just 2 minor comments.

book/src/attribs.md Outdated Show resolved Hide resolved
clippy_utils/src/attrs.rs Show resolved Hide resolved
@nyurik nyurik force-pushed the handle-all-fmt branch 2 times, most recently from 9c829be to b3ccc3f Compare November 14, 2024 17:35
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!`
@flip1995
Copy link
Member

I think this is good to go now. @Alexendoo any other change requests? If not r=me

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.

9 participants