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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix #8859] Add new Lint/UnmodifiedReduceAccumulator cop #8916

Merged
merged 1 commit into from Oct 25, 2020

Conversation

dvandersluis
Copy link
Member

@dvandersluis dvandersluis commented Oct 21, 2020

Adds a cop to check for reduce or inject blocks that do not modify the accumulator. Closes #8859.

I tried to do this in the safest way, and avoid false positives as much as possible. The cop takes a look at all the return values from the block and makes some judgement on them (in order):

  • If the block returns the accumulator indexed (ie. reduce { |acc, el| acc[el] += 1 }), it'll always register an offense because that will likely cause exceptions to be thrown.
  • If the block returns the accumulator, or an expression containing it, in any branch, it is considered valid and no offenses will be registered.
  • If the block returns an expression containing the element and another value (or just other values), it is considered valid because we cannot determine for sure what that value is.
  • Otherwise, returns containing the element are registered as offenses.

I think I covered a lot of edge cases (running this on rubocop itself helped there), but please let me know if I've missed something, I'd like to try to avoid having a new cop immediately have a bunch of bug reports 馃槄

PS: I'm not 100% sold on the name and went back and forth for a while. Any suggests are appreciated.


Before submitting the PR make sure the following are checked:

  • Wrote good commit messages.
  • Commit message starts with [Fix #issue-number] (if the related issue exists).
  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Added an entry to the Changelog if the new code introduces user-observable changes. See changelog entry format.
  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • Run bundle exec rake default. It executes all tests and RuboCop for itself, and generates the documentation.

config/default.yml Outdated Show resolved Hide resolved
Comment on lines +63 to +86
def_node_search :expression_values, <<~PATTERN
`{
(%RuboCop::AST::Node::VARIABLES $_)
(%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_ ...)
(send (%RuboCop::AST::Node::VARIABLES $_) :<< ...)
(send nil? $_)
(dstr (begin {(send nil? $_) (%RuboCop::AST::Node::VARIABLES $_)}))
(%RuboCop::AST::Node::SHORTHAND_ASSIGNMENTS (%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_) ...)
}
PATTERN
Copy link
Member Author

Choose a reason for hiding this comment

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

@marcandre I feel like you probably have a simpler way to do this 馃榿

@bbatsov
Copy link
Collaborator

bbatsov commented Oct 22, 2020

I like the cop, but I find the name confusing. Perhaps something like UnusedReduceAccumulator? I'm totally open to better suggestions myself. :D

Copy link
Contributor

@marcandre marcandre left a comment

Choose a reason for hiding this comment

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

I started a review, but I think this cop is too ambitious and it's in general not possible to do what it wants to do.
I fear that the risk of false positive is too high. Unless I'm mistaken, this would be a false positive, and it would be very difficult to distinguish:

enum.inject do |acc, elem|
  x = [*acc, elem]
  x << 42 if foo
  x
end

lib/rubocop/cop/lint/reduce_accumulator.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/lint/reduce_accumulator.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/lint/reduce_accumulator.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/lint/reduce_accumulator.rb Outdated Show resolved Hide resolved
@dvandersluis
Copy link
Member Author

@marcandre thanks for the feedback, your example actually won't trip the cop (I can add it as a test if you'd like, I have a whole section of does not register an offense if the return value cannot be determined tests already here).

I missed it in the description, but the cop only flags return values where the element is the only variable, for this exact reason. I didn't want the cop to make any assumptions here so if it's not clearly just returning the element, it does not register.

@dvandersluis
Copy link
Member Author

I like the cop, but I find the name confusing. Perhaps something like UnusedReduceAccumulator? I'm totally open to better suggestions myself. :D

@bbatsov I agree it's not the best name. UnmodifiedReduceAccumulator?

@dvandersluis dvandersluis changed the title [Fix #8859] Add new Lint/ReduceAccumulator cop [Fix #8859] Add new Lint/UnmodifiedReduceAccumulator cop Oct 22, 2020
@dvandersluis
Copy link
Member Author

@bbatsov @marcandre I updated for your feedback so far. I've also renamed the cop to UnmodifiedReduceAccumulator for now, still happy to rename further if a better name comes up. I've also added more examples to the documentation.

@bbatsov
Copy link
Collaborator

bbatsov commented Oct 23, 2020

I like the new name and I'm fine with merging the cop in its current state, unless @marcandre has some objections.

@bbatsov bbatsov merged commit 69a0284 into rubocop:master Oct 25, 2020
Copy link
Contributor

@marcandre marcandre left a comment

Choose a reason for hiding this comment

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

Sorry I didn't have the time to reply earlier, and I do have objections.

It checks for two scenarios, both of which can yield false positives for perfectly valid code (in the review)

I think this should either be reverted (at least until it is revised, but again I think it is not possible to do so), or this cop should remain disabled by default.

Comment on lines +51 to +54
# # ignored as the return value cannot be determined
# enum.reduce do |acc, el|
# x + y
# end
Copy link
Contributor

Choose a reason for hiding this comment

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

Bad example, as it could be determined that this is an error. x = foo(acc, el); bar(x) would be a better example

Comment on lines +30 to +32
# %w(a b c).reduce({}) do |acc, letter|
# acc[letter] = true
# end
Copy link
Contributor

Choose a reason for hiding this comment

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

Issue is in the word may.
There are very valid uses:

def chain(*values)
  values.inject({value: nil}) do |acc, elem|
    node = {value: elem, next: acc}
    acc[:prev] = node
  end
end

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 concede that that is a valid use that will be registered by the cop, but I'd argue that this is going to be much less prevalent than mistakenly forgetting to return the accumulator when setting a hash key.

I see a few options:

  1. We can remove this behaviour (I think this isn't ideal, because I do think this is still a good check)
  2. We can put it behind a conf variable that is not enabled by default
  3. We can accept the false positives (if this was my code I'd just # rubocop:disable it, but maybe that's suboptimal).

What do we think? /cc @bbatsov

Copy link
Contributor

Choose a reason for hiding this comment

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

forgetting to return the accumulator when setting a hash key.

BTW this is a double error, as inject probably shouldn't be used in the first place in that case (see Style/EachWithObject)

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely true, but in this case Style/EachWithObject doesn't register (because the accumulator is not being returned). We definitely have other places where a piece of code is chain corrected by multiple cops so I don't see this as being an issue (except for it not being autocorrectable here I guess 馃槗 ).

Comment on lines +25 to +27
# (1..4).reduce(0) do |acc, el|
# el * 2
# end
Copy link
Contributor

Choose a reason for hiding this comment

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

Bad example, acc is not used at all. There are already cops that deal with this, and Ruby too.

Even if acc was used, this could still lead to false negatives:

def nest(*values)
  values.inject([]) do |prev, elem|
    elem = [elem] unless elem.is_a?(Array)
    elem << prev
    elem
  end
end

Copy link
Member Author

Choose a reason for hiding this comment

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

Bad example, acc is not used at all. There are already cops that deal with this, and Ruby too.

Do you mean Lint/UnusedBlockArgument here? While that's true it doesn't really convey the same thing that this cop does. (I don't see any other offenses for the example, so I think it's valid).

I think you meant false positives (false negatives are inherent here on purpose), but you're right that there is a class of false positives here that I did not consider. I'm looking at this now, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any other offenses for the example, so I think it's valid

specs only check for the current cop, you can't rely on that.

I think you meant false positives

Yes, sorry 馃槄

Copy link
Member Author

Choose a reason for hiding this comment

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

@marcandre no I meant I ran all of rubocop on the example code haha, it raised Lint/UnusedBlockArgument and this cop. 馃榿

lib/rubocop/cop/lint/reduce_accumulator.rb Show resolved Hide resolved
(%RuboCop::AST::Node::VARIABLES $_)
(%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_ ...)
(send (%RuboCop::AST::Node::VARIABLES $_) :<< ...)
(send nil? $_)
Copy link
Contributor

Choose a reason for hiding this comment

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

This mistakes the name of a method with the name of a variable

Copy link
Member Author

Choose a reason for hiding this comment

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

This is actually needed, because I'm looking for identifiers in general, not specifically variables. This is valid code and should not be flagged since we can't determine what method is from static analysis:

(1..4).reduce(0) do |acc, el|
  method + el
end

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused as to why there's nil? then. For example self.method + el, or el.method?

Copy link
Contributor

Choose a reason for hiding this comment

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

(BTW, please do not "resolve conversation" when there's no agreement)

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right there shouldn't be!

Copy link
Member Author

Choose a reason for hiding this comment

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

(BTW, please do not "resolve conversation" when there's no agreement)

Sorry about that!

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right there shouldn't be!

Ok, if you remove nil?, then el and self.el are treated the same way, even though they are not related?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call, I've fixed that now too. (btw I've been taking all your notes and applying them locally for the purpose of a new fix PR)

(%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_ ...)
(send (%RuboCop::AST::Node::VARIABLES $_) :<< ...)
(send nil? $_)
(dstr (begin {(send nil? $_) (%RuboCop::AST::Node::VARIABLES $_)}))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here (first part of {})

@dvandersluis
Copy link
Member Author

@marcandre I opened #8949 to continue this.

@dvandersluis dvandersluis deleted the lint/reduce-return-accum branch January 18, 2021 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants