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
Conversation
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 |
There was a problem hiding this comment.
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 馃榿
I like the cop, but I find the name confusing. Perhaps something like |
There was a problem hiding this 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
@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 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. |
@bbatsov I agree it's not the best name. |
681673e
to
0294d53
Compare
Lint/ReduceAccumulator
copLint/UnmodifiedReduceAccumulator
cop
0294d53
to
d4509a0
Compare
@bbatsov @marcandre I updated for your feedback so far. I've also renamed the cop to |
I like the new name and I'm fine with merging the cop in its current state, unless @marcandre has some objections. |
There was a problem hiding this 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.
# # ignored as the return value cannot be determined | ||
# enum.reduce do |acc, el| | ||
# x + y | ||
# end |
There was a problem hiding this comment.
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
# %w(a b c).reduce({}) do |acc, letter| | ||
# acc[letter] = true | ||
# end |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
- We can remove this behaviour (I think this isn't ideal, because I do think this is still a good check)
- We can put it behind a conf variable that is not enabled by default
- 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
There was a problem hiding this comment.
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
)
There was a problem hiding this comment.
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 馃槗 ).
# (1..4).reduce(0) do |acc, el| | ||
# el * 2 | ||
# end |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 馃槄
There was a problem hiding this comment.
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. 馃榿
(%RuboCop::AST::Node::VARIABLES $_) | ||
(%RuboCop::AST::Node::EQUALS_ASSIGNMENTS $_ ...) | ||
(send (%RuboCop::AST::Node::VARIABLES $_) :<< ...) | ||
(send nil? $_) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 $_)})) |
There was a problem hiding this comment.
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 {}
)
@marcandre I opened #8949 to continue this. |
Adds a cop to check for
reduce
orinject
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):
reduce { |acc, el| acc[el] += 1 }
), it'll always register an offense because that will likely cause exceptions to be thrown.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:
[Fix #issue-number]
(if the related issue exists).master
(if not - rebase it).bundle exec rake default
. It executes all tests and RuboCop for itself, and generates the documentation.