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

Upgrade equatable_if_let to style #7777

Closed
wants to merge 1 commit into from
Closed

Conversation

HKalbasi
Copy link
Member

@HKalbasi HKalbasi commented Oct 6, 2021

Continue of #7762

Fix #1716

changelog: upgrade equatable_if_let to style and add pedantic lint equatable_matches

After reading some more about StructuralPartialEq I got that my previous understanding of it was completely wrong and it should be checked recursively. So now it can detect which pattern supports equality and that false positive is fixed.

Also I added a separate lint for matches! based on this comment. Lint for if let is style and lint for matches! is pedantic, but probably both of them need discussion.

@rust-highfive
Copy link

r? @Manishearth

(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 Oct 6, 2021
@flip1995
Copy link
Member

flip1995 commented Oct 6, 2021

Not sure if this should be a warn-by-default lint. Doing something like

if let Some(42) = some_very.long().expr.chain[1..4].that_is().hard_to.read() { ... }

Is pretty common in Rust, especially for Options and Results and therefore not really harder to read IMO. I even prefer this version over

if some_very.long().expr.chain[1..4].that_is().hard_to.read() == Some(42) { ... }

because I can see at the start of the line what the expected value is, before reading the whole expression. I would even make this argument for other types than Option or Result.

So I rather tend towards pedantic here. But maybe that's just me.

@HKalbasi
Copy link
Member Author

HKalbasi commented Oct 6, 2021

What about

if Some(42) == some_very.long().expr.chain[1..4].that_is().hard_to.read() { ... }

in this case? And also what do you think about

if let 42 = some_very.long().expr.chain[1..4].that_is().hard_to.read() { ... }

I'm agree that in the long case it doesn't make much difference. In these cases bottleneck of readablity is somewhere else.

Also there is a good amount of data in the dogfooding fixes of previous PR which we can see for deciding.

By the way, I don't now why CI is unhappy. It pass tests on my local system.

@flip1995
Copy link
Member

flip1995 commented Oct 6, 2021

What about

if Some(42) == some_very.long().expr.chain[1..4].that_is().hard_to.read() { ... }

in this case? And also what do you think about

if let 42 = some_very.long().expr.chain[1..4].that_is().hard_to.read() { ... }

The first would go against the "yoda condition" argument in the lint documentation. It might be better, but the difference is 2 characters. Maybe it's just that I'm more used to seeing if let Some(_) ... and that's why this looks more natural to me. I just feel like this lint is pretty pedantic.

The second example looks weird to me, because you usually don't pattern match on just integers.

Looking thorugh the dogfood fixes, I especially liked the changes from if let Ordering::Greater = _ to if _ == Ordering::Greater and the like. I think only applying this lint on unit-variants and primitives can be warn-by-default. I don't really like it for more complex matches. pattern matching is such a core concept in Rust, that if let E::ABC(4, 2) = expr just feels more natural to me than if E::ABC(4, 2) == expr.

Matching on a unit-variant is more like comparing (==) values (like integers), but matching on more complex things is more like pattern matching to me (if let).

@Manishearth
Copy link
Member

Going to r? @flip1995 on this one since he's already posted a bit. I agree with him on this though.

@rust-highfive rust-highfive assigned flip1995 and unassigned Manishearth Oct 6, 2021
@HKalbasi
Copy link
Member Author

HKalbasi commented Oct 6, 2021

I see two things here:

  • Pattern before expression (Yoda condition) is more readable in some cases
  • Some values are pattern-ish, so equality is less readable in those cases

Generally, writing value first and pattern after reads better (When nine hundred years old you reach), but since in your example subject isn't readable itself, it doesn't improve anything. And yes, since we see if let everyday, it is not very strong argument but I think even now people prefer thing == Some(42) to Some(42) == thing (probably this is the reason some people write if matches! even though it is more verbose).

About the second point, I agree that there are some values that are more like patterns, but not all non unit variants. I feel Constant::Int(1) is definitely value, Some(42) is somehow value and Err(Error::Variant) feels more like pattern. So we should draw a line between pattern like and value like values and split it into two lints? Or just make it pedantic? I can't draw a good line because my feelings are not consistent but unit variant line seems better than nothing.

cc authors of issues @adamchalmers @clarfonthey if they have more to say.

@camsteffen
Copy link
Contributor

I like the idea of style for unit-variants.

I have a theory that this may be a minor perf issue, if only for unoptimized builds. Using == requires two instances of a type where pattern matching is just reading (the discriminant of) a single instance.

enum MyEnum {
    LittleGuy,
    BigGuy([u8; 100]),
}

x == MyEnum::LittleGuy may be less optimal than if let MyEnum::LittleGuy = x in unoptimized builds. I think this could be reason enough to only lint (by default) on enums with all unit-like variants. And also reason enough to make anything else restriction.

@HKalbasi
Copy link
Member Author

HKalbasi commented Oct 6, 2021

Code generated by #[derive(PartialEq)] for MyEnum is:

#[automatically_derived]
#[allow(unused_qualifications)]
impl ::core::cmp::PartialEq for MyEnum {
    #[inline]
    fn eq(&self, other: &MyEnum) -> bool {
        {
            let __self_vi = ::core::intrinsics::discriminant_value(&*self);
            let __arg_1_vi = ::core::intrinsics::discriminant_value(&*other);
            if true && __self_vi == __arg_1_vi {
                match (&*self, &*other) {
                    (&MyEnum::BigGuy(ref __self_0),
                     &MyEnum::BigGuy(ref __arg_1_0)) =>
                    (*__self_0) == (*__arg_1_0),
                    _ => true,
                }
            } else { false }
        }
    }
    #[inline]
    fn ne(&self, other: &MyEnum) -> bool {
        {
            let __self_vi = ::core::intrinsics::discriminant_value(&*self);
            let __arg_1_vi = ::core::intrinsics::discriminant_value(&*other);
            if true && __self_vi == __arg_1_vi {
                match (&*self, &*other) {
                    (&MyEnum::BigGuy(ref __self_0),
                     &MyEnum::BigGuy(ref __arg_1_0)) =>
                    (*__self_0) != (*__arg_1_0),
                    _ => false,
                }
            } else { true }
        }
    }
}

So if your point is it would compare 100 byte in debug builds, it doesn't seems so.

@camsteffen
Copy link
Contributor

No my point is that it adds another 100 byte variable to the stack.

But I also think the non-unit cases may be too controversial for pedantic purely from a readability/style standpoint.

@HKalbasi
Copy link
Member Author

HKalbasi commented Oct 6, 2021

hmm yes it seems pattern matching has better performance without optimization.

So I will change both of them to restriction.

@flip1995
Copy link
Member

flip1995 commented Oct 7, 2021

I don't really see a blocker of a perf impact on unoptimized builds, as long as there is no perf impact for optimzed builds. So if the only argument for not putting the unit-variant-only version into style, then I would say put it into style anyway. That is, if we really want to split up the lint on the unit-variant boundary. If we want to keep everything in one lint, I don't have a strong opinion about pedantic vs restriction.

@flip1995
Copy link
Member

flip1995 commented Oct 7, 2021

By the way, I don't now why CI is unhappy. It pass tests on my local system.

Forgot to reply to this yesterday: If you run the tests with cargo test --features internal-lints, you'll also see the test fails locally.

To run the same tests that are ran in CI, use cargo test --features internal-lints,deny-warnings,metadata-collector-lint

@HKalbasi
Copy link
Member Author

HKalbasi commented Oct 7, 2021

split up the lint on the unit-variant boundary

Or maybe a sensitivity configuration with some levels like "primitive", "unit-variant" and "all", with "unit-variant" as default?

@camsteffen
Copy link
Contributor

I don't really see a blocker of a perf impact on unoptimized builds

Well maybe my "unoptimized" point is kinda moot - that is actually a common tradeoff for abstractions in Rust.

Also I added a separate lint for matches!

Just noticed this. Don't forget to write a "changelog:".

Or maybe a sensitivity configuration with some levels like "primitive", "unit-variant" and "all", with "unit-variant" as default?

I guess that makes sense considering that it affects multiple lints? I'd be okay with that and pedantic.

@flip1995
Copy link
Member

flip1995 commented Oct 7, 2021

Or maybe a sensitivity configuration with some levels like "primitive", "unit-variant" and "all", with "unit-variant" as default?

I guess that makes sense considering that it affects multiple lints? I'd be okay with that and pedantic.

Yes, I agree. A configuration for those lints is probably a good idea. The question is what the default should be. unit-variant (which I would just call unit)?

@camsteffen
Copy link
Contributor

The question is what the default should be. unit-variant (which I would just call unit)?

I think that's a good default. I'm a little unsure about just "unit". equatable_patterns: unit (if that's a good config name?) makes me think ()...but I don't know...maybe simple or unit-like? I don't think there's a formalized name for it.

@flip1995
Copy link
Member

flip1995 commented Oct 8, 2021

Ah right, that can be confusing. I think primitive, simple and all would be the best names.

@HKalbasi
Copy link
Member Author

I added a configuration.

Just noticed this. Don't forget to write a "changelog:".

There is a changelog, but it needs update after we decide about style and pedantic.

@bors
Copy link
Collaborator

bors commented Oct 13, 2021

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

@HKalbasi HKalbasi force-pushed the master branch 3 times, most recently from bfcc766 to c614402 Compare October 14, 2021 14:27
@bors
Copy link
Collaborator

bors commented Oct 29, 2021

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

@HKalbasi HKalbasi force-pushed the master branch 2 times, most recently from 298c8ed to 21dce73 Compare October 30, 2021 15:51
@flip1995
Copy link
Member

flip1995 commented Nov 2, 2021

Oh wow, this discussion is already 3 weeks old. Sorry for the late reply!

I would say, keep the configuration, but put this lint into pedantic anyway. I just read through the discussion again and noticed that most arguments for putting this as warn-by-default started with "some people". And I also have the feeling that this is really opinionated and not clear that if x == Ordering::Greater is better style than if let Ordering::Greater = x.

@@ -278,7 +278,7 @@ pub fn eq_item_kind(l: &ItemKind, r: &ItemKind) -> bool {
},
(Trait(box TraitKind(la, lu, lg, lb, li)), Trait(box TraitKind(ra, ru, rg, rb, ri))) => {
la == ra
&& matches!(lu, Unsafe::No) == matches!(ru, Unsafe::No)
&& matches!(lu, Unsafe::Yes(_)) == matches!(ru, Unsafe::Yes(_))
Copy link
Member

Choose a reason for hiding this comment

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

Wait, all the conditions are reversed in this file?

Copy link
Member Author

Choose a reason for hiding this comment

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

I at first changed it to lu == ru which was wrong (I assumed there is Unsafe::No and Unsafe::Yes), and this style better shows why it is wrong so next person won't try lu == ru.

@@ -0,0 +1,136 @@
// run-rustfix
Copy link
Member

Choose a reason for hiding this comment

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

This seems to be a 1:1 copy of the original test file. But this file should rather test that the lint also works for the All variant and not so much that it still works for all other cases (one or two test cases are fine for that, but copying everything doesn't make much sense).

pub exp: &'tcx Expr<'tcx>,
}

impl MatchesExpn<'tcx> {
Copy link
Member

Choose a reason for hiding this comment

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

Ah, we don't really want to keep doing this: #7843

Is this only required to generate the suggestion? If so, I'd prefer to just have a non-auto-applicable suggestion in the matches case with Applicability::HasPlaceholder

Copy link
Member Author

Choose a reason for hiding this comment

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

Parameters of matches! macro are needed to detect if this pattern is equatable.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, that's unfortunate. We really have to figure out how to do this without relying on the exact expansion of the macro...

clippy_lints/src/utils/conf.rs Outdated Show resolved Hide resolved
@HKalbasi
Copy link
Member Author

HKalbasi commented Nov 3, 2021

I made it pedantic, but style with sensitivity = primitive is also an option. I think it is clear that if let "foo" = bar is bad style.

@flip1995
Copy link
Member

flip1995 commented Nov 3, 2021

I made it pedantic, but style with sensitivity = primitive is also an option. I think it is clear that if let "foo" = bar is bad style.

I agree. I think the Simple config value is the better default though. So for me the question is, if style+primitive or pedantic+simple is the better option.

@bors
Copy link
Collaborator

bors commented Nov 5, 2021

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

@camsteffen
Copy link
Contributor

Yet another option is to make a if_let_primitive lint for style. I think I'd like that option best. My second pick would be simple+pedantic. The biggest downside IMO is with the primitive+style option because a lot of users won't discover the config or care to change it.

Also I would put "aggregate primitives" in the "simple" category. Don't know if that has been considered.

@HKalbasi
Copy link
Member Author

HKalbasi commented Nov 8, 2021

I don't know how many people use pedantic lints, but primitive+style would be better for default users than simple+pedantic. if_let_primitive (and possibly matches_primitive) are duplicate lints to me, but they will solve the problem of noise/visibility.

Also I would put "aggregate primitives" in the "simple" category. Don't know if that has been considered.

There are currently in "all".

@blyxyas
Copy link
Member

blyxyas commented Oct 6, 2023

Closing this because the original issue (#1716) was resolved by #9368

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.

Lint if lets that could be better worded as equality
7 participants