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

Modulo arithmetic #4867

Merged
merged 3 commits into from
Dec 28, 2019
Merged

Modulo arithmetic #4867

merged 3 commits into from
Dec 28, 2019

Conversation

mgr-inz-rafal
Copy link
Contributor

changelog: Added modulo_arithmetic lint

@mgr-inz-rafal
Copy link
Contributor Author

mgr-inz-rafal commented Nov 30, 2019

Hi!
Recently I stumbled upon a problem of using modulo operator on negative values. I've been exchanging data between Python and C++ and it took me some time to find out about that inconsistency and fix a bug.

I thought it would be good to add such lint in order to:

  1. Share the knowledge about differences in the handling of negative values with % operator
  2. Let Rust developers easily find such places in code
  3. Get acquainted with how to create your own lints :)

@llogiq
Copy link
Contributor

llogiq commented Nov 30, 2019

Hi! Do you want to lint a) modulo on a negative const value or b) modulo on signed integers?

@mgr-inz-rafal
Copy link
Contributor Author

Well, I guess to lint on both.

My first idea was to simply lint all modulo arithmetic, but from your question, I see that it can indeed be done in a more sophisticated way, i.e. no lint on positive const and/or unsigned integers. I'll see what I can do about it.

@flip1995 flip1995 added the S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) label Dec 1, 2019
@llogiq
Copy link
Contributor

llogiq commented Dec 1, 2019

An easy thing to do would be linting on ExprKind::Binary with an op of BinaryOp::Rem where the left-hand side operand's type (which you can get with cx.tcx.expr_ty(_)) is TyKind::Int(_) (any signed integer type).

@mgr-inz-rafal
Copy link
Contributor Author

mgr-inz-rafal commented Dec 2, 2019

I had issues with cx.tcx.expr_ty(_) and TyKind::Int(_), maybe due to some changes in API. But by looking at the implementation of other lints I came up with the solution using cx.tables.expr_ty and is_integral() and alike functions. Implementation is now pushed to the branch.

BTW: I see the build failing on the last commit with only changes in comments introduced. This is probably not related to this PR.

Additionally, I would like this lint not to warn on const values when both sides are either positive or negative, such as (123+5)%(-100+200). Any hints on how I should approach that will be greatly appreciated.

Two more things:

  1. I addition to ExprKind::Binary I also check on ExprKind::AssignOp
  2. I also check for floating point numbers, since apparently they may cause problems as well, for example -18.3 % 6.7

@llogiq
Copy link
Contributor

llogiq commented Dec 2, 2019

There's a rustup in the queue, which means there's likely breakage and you may have to rebase after we merge it.

@flip1995
Copy link
Member

flip1995 commented Dec 3, 2019

Additionally, I would like this lint not to warn on const values when both sides are either positive or negative

You can use functions from the clippy_lints/src/const.rs module. I think you want to use the constant(..) function to evaluate if an expression is a constant and what its value is.

I had issues with cx.tcx.expr_ty(_) and TyKind::Int(_)

This code should work:

use rustc::ty;

if let ty::Int(_) = cx.tcx.expr_ty(..).kind {
    // lint
}

@mgr-inz-rafal
Copy link
Contributor Author

Thanks for you suggestions. I modified the code not to lint on consts that give predictable results (both operand of the same sign). This applies to both signed integral and floating point types.

@bors
Copy link
Contributor

bors commented Dec 8, 2019

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

@mgr-inz-rafal
Copy link
Contributor Author

After rebase and fine-tuning the build is finally green :)

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 now. Some nitpicking.

clippy_lints/src/modulo_arithmetic.rs Outdated Show resolved Hide resolved
clippy_lints/src/modulo_arithmetic.rs Outdated Show resolved Hide resolved
clippy_lints/src/modulo_arithmetic.rs Outdated Show resolved Hide resolved
clippy_lints/src/modulo_arithmetic.rs Outdated Show resolved Hide resolved
clippy_lints/src/modulo_arithmetic.rs Outdated Show resolved Hide resolved
clippy_lints/src/modulo_arithmetic.rs Outdated Show resolved Hide resolved
"you are using modulo operator on constants with different signs: `{} % {}`", lhs_negative.1.unwrap(), rhs_negative.1.unwrap()
),
expr.span,
"double check for expected result especially when interoperating with different languages",
Copy link
Contributor

Choose a reason for hiding this comment

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

What are your thoughts on also suggesting usage of rem_euclid or checked_rem_euclid when the signs are different? Sometime that's what the user intended when performing modulo arithmetic on signed integers and they may not be aware that this method exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I myself was not aware that such functions exist :)
Let me check how they relate to the content of this PR.

Copy link
Contributor Author

@mgr-inz-rafal mgr-inz-rafal Dec 18, 2019

Choose a reason for hiding this comment

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

OK, so I see that wrapping_rem_euclid() and checked_rem_euclid() yield results that are consistent with what we can see in Python.

I will add info about these methods in the lint note later today.

Thanks!

BTW: Do I see correctly that these functions are available for integral types only?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup these methods are only for integer types so we would only want to suggest these when operating on integers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relevant changes are now pushed to the branch.

declare_clippy_lint! {
/// **What it does:** Checks for modulo arithemtic.
///
/// **Why is this bad?** The results of moudlo (%) operation migth differ
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey you have some typos here: modulo and might.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, thanks! Fixed in 6084c53

@mgr-inz-rafal
Copy link
Contributor Author

To be honest, I don't quite like the fragment below (too much repetition) and I would like to merge the match arms. What would be the best way to do it?

        Some((Constant::F32(f), _)) => {
            return Some(OperandInfo {
                string_representation: Some(format!("{:.3}", f)),
                is_negative: f < 0.0,
                is_integral: false,
            });
        },
        Some((Constant::F64(f), _)) => {
            return Some(OperandInfo {
                string_representation: Some(format!("{:.3}", f)),
                is_negative: f < 0.0,
                is_integral: false,
            });
        },

@mgr-inz-rafal
Copy link
Contributor Author

Regarding my previous comment, I reduced the code duplication by extracting the method, see 56f507b

@bors
Copy link
Contributor

bors commented Dec 22, 2019

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

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.

Looks great now. Only formatting and using another lint function.

Comment on lines 101 to 102
lhs_operand.string_representation.unwrap(),
rhs_operand.string_representation.unwrap()
Copy link
Member

Choose a reason for hiding this comment

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

NIT: 1 indentation level too much

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reduced by extracting helper functions: check_const_operands() and check_non_const_operands()

expr.span,
&format!(
"double check for expected result especially when interoperating with different languages{}",
if lhs_operand.is_integral {" or consider using `rem_euclid` or similar function"} else {""}
Copy link
Member

Choose a reason for hiding this comment

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

You should use the function

span_lint_and_then(
    ..,
    |db| {
         db.note("double check for expected result especially when interoperating with different languages");
         if lhs_operand.is_integral {
             db.note("or consider using `rem_euclid` or a similar function");
         }
    },
);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, now using span_lint_and_then() and db.note() functions

@mgr-inz-rafal
Copy link
Contributor Author

Looks like I polluted the PR with rebase... I'll try to clean this up, but if that doesn't work I'll close the PR and create a fresh one with only the necessary changes.

@flip1995
Copy link
Member

If you want, I can fix it for you real quick. Or you can do it with:

# assuming your on the modulo_arithmetic branch
$ git fetch upstream  # assuming upstream is the remote of this repo
$ git rebase upstream/master  # you may have to fix some merge conflicts:
$ git mergetool
$ git rebase --continue  # repeat until no more merge conflicts
$ git diff mgr-inz-rafal/modulo_arithmetic  # should be empty
$ git push --force-with-lease

@mgr-inz-rafal
Copy link
Contributor Author

@flip1995 I'll be more than grateful if you can fix it if it's not too much hassle.

I'm far from being a git wizard and copy&pasting your commands without a full understanding doesn't sound like a great idea.

@flip1995
Copy link
Member

copy&pasting your commands without a full understanding doesn't sound like a great idea.

Yeah most of the time it isn't.

Fixed it :)

@krishna-veerareddy
Copy link
Contributor

Hey since you have a huge number of commits consider squashing them into a single commit. Not sure what the squash policy of this repository is though so maybe one of the maintainers could chip in.

@@ -26,7 +26,6 @@ fn main() {
1i16 % -2i16;
-1i32 % 2i32;
1i32 % -2i32;
-1i64 % 2i64;
Copy link
Contributor

@krishna-veerareddy krishna-veerareddy Dec 22, 2019

Choose a reason for hiding this comment

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

Please don't remove test cases to satisfy the linter. You can move the test cases into separate files like so: modulo_arithmetic_integer.rs and modulo_arithmetic_float.rs to keep the file sizes within bounds. Having the modulo_arithmetic prefix or any other prefix in your test case file name ensures that both files are tested by running TESTNAME=modulo_arithmetic cargo uitest.

Copy link
Contributor Author

@mgr-inz-rafal mgr-inz-rafal Dec 22, 2019

Choose a reason for hiding this comment

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

Thanks for the hint, didn't know it works that way. I will reintroduce the removed tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 8d3efd6

@mgr-inz-rafal
Copy link
Contributor Author

Hey since you have a huge number of commits consider squashing them into a single commit. Not sure what the squash policy of this repository is though so maybe one of the maintainers could chip in.

This PR turned out to require many more commits than I initially anticipated. Even though my personal preference is to keep the commits small, let's wait for the maintainers' decision indeed.

@flip1995
Copy link
Member

Thanks, LGTM now. We prefer to have less commits. I usually make 2 commits for fixing something: 1 for the fix and 1 for tests. For new lints I usually use 3-5 commits. 1 for the implementation, 1 for tests, 1 for running commands for updating lints, formatting, ... and a few additional commits to fix something if necessary.

It would be great if you could squash your commits down to under 10 approximately.

@mgr-inz-rafal
Copy link
Contributor Author

mgr-inz-rafal commented Dec 23, 2019

It would be great if you could squash your commits down to under 10 approximately.

Due to my previous "messy" operation, there are currently duplicated commits in the PR, for example:
74cc7e1
ba537da
Like part of the history is duplicated...

How would you recommend progressing with the squash operation in such a scenario? My first idea was just to squash the duplicated commits respectively and create squashed duplicates (which I will further squash into single commit) , but maybe there is a better way...

@flip1995
Copy link
Member

I'd recommend to just squash everything in one commit, then git reset HEAD~1 and create new file based commits (lint module, tests, everything else)

@bors
Copy link
Contributor

bors commented Dec 24, 2019

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

@mgr-inz-rafal
Copy link
Contributor Author

Code rebased, commits squashed and cleaned-up.

@phansch
Copy link
Member

phansch commented Dec 27, 2019

@bors r=flip1995

@bors
Copy link
Contributor

bors commented Dec 27, 2019

📌 Commit 5346954 has been approved by flip1995

@bors
Copy link
Contributor

bors commented Dec 27, 2019

⌛ Testing commit 5346954 with merge 53ae6ad...

bors added a commit that referenced this pull request Dec 27, 2019
Modulo arithmetic

changelog: Added modulo_arithmetic lint
@bors
Copy link
Contributor

bors commented Dec 27, 2019

💔 Test failed - checks-travis

@flip1995
Copy link
Member

@bors retry

@bors
Copy link
Contributor

bors commented Dec 28, 2019

⌛ Testing commit 5346954 with merge f64c088...

bors added a commit that referenced this pull request Dec 28, 2019
Modulo arithmetic

changelog: Added modulo_arithmetic lint
@bors
Copy link
Contributor

bors commented Dec 28, 2019

💔 Test failed - checks-travis

@flip1995
Copy link
Member

You have to fix the warnings regarding Expr -> Expr<'_>

@mgr-inz-rafal
Copy link
Contributor Author

For some reason, these warnings were reluctant to surface on my local build, but I finally managed to reproduce (and fix) them on the clean repo checkout.

@flip1995
Copy link
Member

You probably had to update to the latest rustc master.

Thanks! @bors r+

@bors
Copy link
Contributor

bors commented Dec 28, 2019

📌 Commit a208906 has been approved by flip1995

@bors
Copy link
Contributor

bors commented Dec 28, 2019

⌛ Testing commit a208906 with merge 8e493c6...

bors added a commit that referenced this pull request Dec 28, 2019
Modulo arithmetic

changelog: Added modulo_arithmetic lint
@bors
Copy link
Contributor

bors commented Dec 28, 2019

☀️ Test successful - checks-travis, status-appveyor
Approved by: flip1995
Pushing 8e493c6 to master...

@bors bors merged commit a208906 into rust-lang:master Dec 28, 2019
@mgr-inz-rafal
Copy link
Contributor Author

Thank you, I've learned a lot here ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants