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

add manual_abs_diff lint #14482

Merged
merged 1 commit into from
Apr 9, 2025
Merged

Conversation

yotamofek
Copy link
Contributor

changelog: [manual_abs_diff]: Initial implementation

Hey, first time writing a new lint for clippy, hope I got it right. I think it's pretty self-explanatory!
Added a few fixme test cases, where the lint can be improved to catch more (probably rare) patterns, but opening a PR with this initial implementation to make sure I'm on the right track, and that this lint is acceptable at all.

😁

@rustbot
Copy link
Collaborator

rustbot commented Mar 26, 2025

r? @Jarcho

rustbot has assigned @Jarcho.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Mar 26, 2025
@yotamofek yotamofek force-pushed the manual-abs-diff branch 5 times, most recently from df165b3 to dfc3d13 Compare March 26, 2025 22:37
Copy link
Contributor

@samueltardieu samueltardieu left a comment

Choose a reason for hiding this comment

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

This should probably be restricted to standard integral types. This will be a false positive, with a suggestion causing a compilation error:

#[derive(Eq, PartialEq, PartialOrd)]
struct S(i32);

impl std::ops::Sub for S {
    type Output = S;

    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0 - rhs.0)
    }
}

fn main() {
    let (a, b) = (S(10), S(20));
    let _ = if a < b { b - a } else { a - b };
}

@yotamofek
Copy link
Contributor Author

Right, I'll fix that.
Thanks for the quick review!

@yotamofek yotamofek force-pushed the manual-abs-diff branch 2 times, most recently from ed0bacf to 2332038 Compare March 26, 2025 23:57
@yotamofek yotamofek requested a review from samueltardieu March 26, 2025 23:57
@samueltardieu
Copy link
Contributor

Since I started the review already and have more to say:
r? @samueltardieu

@rustbot rustbot assigned samueltardieu and unassigned Jarcho Mar 27, 2025
Copy link
Contributor

@samueltardieu samueltardieu left a comment

Choose a reason for hiding this comment

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

This is starting to be in a good shape. My issues right now are mostly with style, which feels a bit too verbose compared to most of the Clippy code. It is good not to have huge chunks of code, but creating intermediary types just for the sake of separating functions and using the types once seems a bit too much.

For example, the suggestions I make here should allow you to remove about 60 lines of code from this short lint (more than 35%) while keeping the intent clear and having the check_expr() method go straight to the point.

Comment on lines 71 to 96
if !expr.span.from_expansion()
&& let Some(suggestion) = self.is_manual_abs_diff_pattern(cx, expr)
{
emit_suggestion(cx, &suggestion);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Given the relatively low complexity of the lint, separating the check and emitting phase does not bring a lot of clarity. Moreover, it makes you introduce one additional function, one additional method, and two intermediate Suggestion and Input types.

You can inline the calls, and rewrite them a bit to be more concise. For example, it may not be needed to match all names of the If struct right away, I you can later refer to them by field access. Also, expansing the Spanned { OpKind, … } is often not done, preferring to refer to the .node attribute to get the operator kind later.

That could give something like:

Suggested change
if !expr.span.from_expansion()
&& let Some(suggestion) = self.is_manual_abs_diff_pattern(cx, expr)
{
emit_suggestion(cx, &suggestion);
}
if !expr.span.from_expansion()
&& let Some(if_expr) = If::hir(expr)
&& let Some(r#else) = if_expr.r#else
&& let ExprKind::Binary(op, rhs, lhs) = if_expr.cond.kind
&& let (BinOpKind::Gt | BinOpKind::Ge, a, b) | (BinOpKind::Lt | BinOpKind::Le, b, a) = (op.node, rhs, lhs)
&& self.are_ty_eligible(cx, a, b)
&& Self::is_sub_expr(cx, if_expr.then, a, b)
&& Self::is_sub_expr(cx, r#else, b, a)
{
let a = Sugg::hir(cx, a, "..").maybe_paren();
let b = Sugg::hir(cx, b, "..");
span_lint_and_sugg(
cx,
MANUAL_ABS_DIFF,
expr.span,
"manual absolute difference pattern without using `abs_diff`",
"replace with `abs_diff`",
format!("{a}.abs_diff({b})"),
Applicability::MachineApplicable,
);
}

which is readable, and more inline with the code usually found in Clippy. You can then remove is_manual_abs_diff_pattern(), emit_suggestion(), and the Suggestion and Input types.

I'll comment about are_ty_eligible() in another comment.

Comment on lines 123 to 110
fn is_ty_applicable(&self, cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let expr_ty = cx.typeck_results().expr_ty(expr).peel_refs();
(matches!(expr_ty.kind(), ty::Uint(_)) && self.msrv.meets(cx, msrvs::ABS_DIFF))
|| (is_type_diagnostic_item(cx, expr_ty, sym::Duration) && self.msrv.meets(cx, msrvs::DURATION_ABS_DIFF))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than checking each type individually, you can first make sure that they are identical, then check for their eligibility for the lint:

Suggested change
fn is_ty_applicable(&self, cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let expr_ty = cx.typeck_results().expr_ty(expr).peel_refs();
(matches!(expr_ty.kind(), ty::Uint(_)) && self.msrv.meets(cx, msrvs::ABS_DIFF))
|| (is_type_diagnostic_item(cx, expr_ty, sym::Duration) && self.msrv.meets(cx, msrvs::DURATION_ABS_DIFF))
}
fn are_ty_eligible(&self, cx: &LateContext<'_>, a: &Expr<'_>, b: &Expr<'_>) -> bool {
let a_ty = cx.typeck_results().expr_ty(a).peel_refs();
a_ty == cx.typeck_results().expr_ty(b).peel_refs()
&& ((matches!(a_ty.kind(), ty::Uint(_)) && self.msrv.meets(cx, msrvs::ABS_DIFF))
|| (is_type_diagnostic_item(cx, a_ty, sym::Duration) && self.msrv.meets(cx, msrvs::DURATION_ABS_DIFF)))
}

@yotamofek
Copy link
Contributor Author

@samueltardieu applied all your suggestions, looks much better now, thanks!
I started out from manual_clamp as a "template", so a lot of the complexity was a remnant of that, but yeah - this lint is much simpler, no need for all that verbosity.

@yotamofek
Copy link
Contributor Author

Also got rid of matching against the destructuring of Spanned in is_sub_expr.

@samueltardieu
Copy link
Contributor

I'll have a new look later, thanks for the quick reaction. Can you also check for the presence of comments within the original expression? In this case, I think this is still a good idea to lint, but we should make the suggestion MaybeIncorrect as the user might not want to get rid of the comments they wrote.

@yotamofek
Copy link
Contributor Author

yotamofek commented Mar 27, 2025

Took comments into account, plus fixed the lint for literals (I hope...)

jhpratt added a commit to jhpratt/rust that referenced this pull request Mar 27, 2025
…errors

Use `abs_diff` where applicable

Very small cleanup, dogfooding a [new clippy lint](rust-lang/rust-clippy#14482) I'm trying to add
jhpratt added a commit to jhpratt/rust that referenced this pull request Mar 28, 2025
…errors

Use `abs_diff` where applicable

Very small cleanup, dogfooding a [new clippy lint](rust-lang/rust-clippy#14482) I'm trying to add
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Mar 28, 2025
Rollup merge of rust-lang#139026 - yotamofek:pr/abs-diff, r=compiler-errors

Use `abs_diff` where applicable

Very small cleanup, dogfooding a [new clippy lint](rust-lang/rust-clippy#14482) I'm trying to add
@samueltardieu samueltardieu added the S-blocked Status: marked as blocked ❌ on something else such as an RFC or other implementation work label Mar 28, 2025
@yotamofek
Copy link
Contributor Author

Thanks for the review and thorough explanation, much appreciated!
I'll try to use expr_type_is_certain() in the meantime, expecting some wrong results.

Just a small question about this:

if neither types are certain, you can still emit the lint, but omit the suggestion, and let the user fix it themselves

wouldn't the compiler just error out because of the ambiguous type in that case, meaning the lint won't even run?

@samueltardieu
Copy link
Contributor

Thanks for the review and thorough explanation, much appreciated! I'll try to use expr_type_is_certain() in the meantime, expecting some wrong results.

Just a small question about this:

if neither types are certain, you can still emit the lint, but omit the suggestion, and let the user fix it themselves

wouldn't the compiler just error out because of the ambiguous type in that case, meaning the lint won't even run?

Unless we do the same complete type inference as the compiler, we will not see that some types may be certain. For example in

fn f(_: &mut u32) {}

fn main() {
    let mut b = 3;
    f(&mut b);
    let _ = if 5 < b {
        5 - b
    } else {
        b - 5
    };
}

the compiler knows that b is u32 because of the call to f, but we won't see that and mark both 5 and b as uncertain. And 5.abs_diff(b) won't compile, so we better not suggest it.

@Alexendoo
Copy link
Member

IMO we should still suggest it in the face of likely inference errors, just as MaybeIncorrect. Fixing inference is generally easy to do by hand so having the starting point is a nice time saver

@samueltardieu
Copy link
Contributor

IMO we should still suggest it in the face of likely inference errors, just as MaybeIncorrect. Fixing inference is generally easy to do by hand so having the starting point is a nice time saver

Agreed. I'll let just pass a few days to see if I have time to better inference detection, as I have some reservations in not putting a test with integers when I know it will fail. And:

  • The problem with the current state of type certainty is that it will say that those cases are inferable (they will be false positive, not false negative)
  • MaybeIncorrect suggestions are supposed to compile, so we would have to use Unspecified – and because of the first point we would do that at many places

@Alexendoo
Copy link
Member

MaybeIncorrect is for something that could either change the behaviour of the program or result in a compile error (but doesn't include placeholders). In rust-lang/cargo#13028 it's proposed to split the two cases

New cases of Unspecified shouldn't be added

@samueltardieu
Copy link
Contributor

MaybeIncorrect is for something that could either change the behaviour of the program or result in a compile error (but doesn't include placeholders). In rust-lang/cargo#13028 it's proposed to split the two cases

New cases of Unspecified shouldn't be added

As of today, the definition for MaybeIncorrect explicitly says: "The suggestion may be what the user intended, but it is uncertain. The suggestion should result in valid Rust code if it is applied.".

Also, our testsuite applies MaybeIncorrect suggestions and ensures they compile.

@Alexendoo
Copy link
Member

In clippy as long as it produces valid syntax we use MaybeIncorrect, I don't know what the convention is on the rustc side. Practically speaking until rust-lang/cargo#13028 is resolved there's little difference between the non MachineApplicable variants

Our tests apply every suggestion no matter the applicability, suggestions that don't compile go in a separate file with rustfix disabled

@yotamofek
Copy link
Contributor Author

I'll wait a bit with this PR to see if @samueltardieu can fix the issues with #14492, because that's obviously the best solution to the problem with ambiguous types,
but just wanted to mention that we could maybe return HasPlaceholders suggestions that look something like 2/* _type */.abs_diff(rhs)

@samueltardieu
Copy link
Contributor

I'll wait a bit with this PR to see if @samueltardieu can fix the issues with #14492, because that's obviously the best solution to the problem with ambiguous types

This appears to be a can of worm: every time I add some new uncertainty resolution, it gets too conservative, and when I add some new, I find edge cases. I will continue working on this in the background, but it may take time.

What I propose so that you can go forward is to check if the left hand side is an unsuffixed literal. If it is, and the right hand side is not, swap them. We may get some suggestions that will cause errors, but they should be very rare and easy to fix for the user, and then when the expr type certainty check is better, we may switch to it, as we could in many lints.

Could you do the change, and let's see what the lintcheck run says? You may also want to rebase your commit, because of recent fixes in lintcheck handling in the CI.

we could maybe return HasPlaceholders suggestions that look something like 2/* _type */.abs_diff(rhs)

I don't think this would be beneficial.

@samueltardieu samueltardieu added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-blocked Status: marked as blocked ❌ on something else such as an RFC or other implementation work labels Apr 2, 2025
@samueltardieu
Copy link
Contributor

In parallel, I've started the "final comment period" thread for this lint.

@samueltardieu samueltardieu added the S-final-comment-period Status: final comment period it will be merged unless new objections are raised (~1 week) label Apr 2, 2025
@samueltardieu
Copy link
Contributor

@pitaj suggested in the Zulip thread to also check for (a - b).abs() where a and b are of the same signed primitive integer type. Indeed, .abs_diff() does this on signed types, and has been added with the same MSRV as on unsigned types.

@yotamofek would you be willing to add this as a second commit, so that it can be squashed if appropriate or separated into a future PR if not? If you are not interested in doing this, no problem, we can open a new issue for enhancing this lint.

@yotamofek
Copy link
Contributor Author

What I propose so that you can go forward is to check if the left hand side is an unsuffixed literal. If it is, and the right hand side is not, swap them. We may get some suggestions that will cause errors, but they should be very rare and easy to fix for the user, and then when the expr type certainty check is better, we may switch to it, as we could in many lints.

Could you do the change, and let's see what the lintcheck run says? You may also want to rebase your commit, because of recent fixes in lintcheck handling in the CI.

Sure, sounds good @samueltardieu ! Thanks!!

re: (a - b).abs(), I don't mind implementing in this PR, but I have some qualms about it. I'll explain on Zulip.

@samueltardieu
Copy link
Contributor

What I propose so that you can go forward is to check if the left hand side is an unsuffixed literal. If it is, and the right hand side is not, swap them. We may get some suggestions that will cause errors, but they should be very rare and easy to fix for the user, and then when the expr type certainty check is better, we may switch to it, as we could in many lints.
Could you do the change, and let's see what the lintcheck run says? You may also want to rebase your commit, because of recent fixes in lintcheck handling in the CI.

Sure, sounds good @samueltardieu ! Thanks!!

re: (a - b).abs(), I don't mind implementing in this PR, but I have some qualms about it. I'll explain on Zulip.

Yes, it was a silly suggestion, forget about it.

@yotamofek
Copy link
Contributor Author

@samueltardieu I think this is ready for review, hope I didn't miss anything

@samueltardieu samueltardieu added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Apr 4, 2025
@samueltardieu samueltardieu added this pull request to the merge queue Apr 9, 2025
Merged via the queue into rust-lang:master with commit e463309 Apr 9, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-final-comment-period Status: final comment period it will be merged unless new objections are raised (~1 week)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants