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

Mark Performance/Sum auto-correction as unsafe and extend documentation #270

Merged
merged 1 commit into from Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/change_mark_performancesum_autocorrection_as.md
@@ -0,0 +1 @@
* [#270](https://github.com/rubocop/rubocop-performance/pull/270): Mark `Performance/Sum` auto-correction as unsafe and extend documentation. ([@leoarnold][])
2 changes: 2 additions & 0 deletions config/default.yml
Expand Up @@ -330,9 +330,11 @@ Performance/StringReplacement:

Performance/Sum:
Description: 'Use `sum` instead of a custom array summation.'
SafeAutoCorrect: false
Reference: 'https://blog.bigbinary.com/2016/11/02/ruby-2-4-introduces-enumerable-sum.html'
Enabled: 'pending'
VersionAdded: '1.8'
leoarnold marked this conversation as resolved.
Show resolved Hide resolved
VersionChanged: '1.13'
OnlySumOrWithInitialValue: false

Performance/TimesMap:
Expand Down
50 changes: 31 additions & 19 deletions docs/modules/ROOT/pages/cops_performance.adoc
Expand Up @@ -2013,37 +2013,49 @@ This cop identifies places where `gsub` can be replaced by

| Pending
| Yes
| Yes
| Yes (Unsafe)
| 1.8
| -
| 1.13
|===

This cop identifies places where custom code finding the sum of elements
in some Enumerable object can be replaced by `Enumerable#sum` method.

This cop can change auto-correction scope depending on the value of
`SafeAutoCorrect`.
Its auto-correction is marked as safe by default (`SafeAutoCorrect: true`)
to prevent `TypeError` in auto-correced code when initial value is not
specified as shown below:
=== Safety

Auto-corrections are unproblematic wherever an initial value is provided explicitly:

[source,ruby]
----
['a', 'b'].sum # => (String can't be coerced into Integer)
----
[1, 2, 3].reduce(4, :+) # => 10
[1, 2, 3].sum(4) # => 10

Therefore if initial value is not specified, unsafe auto-corrected will not occur.
[].reduce(4, :+) # => 4
[].sum(4) # => 4
----

If you always want to enable auto-correction, you can set `SafeAutoCorrect: false`.
This also holds true for non-numeric types which implement a `:+` method:

[source,yaml]
[source,ruby]
----
['l', 'o'].reduce('Hel', :+) # => "Hello"
['l', 'o'].sum('Hel') # => "Hello"
----
Performance/Sum:
SafeAutoCorrect: false

When no initial value is provided though, `Enumerable#reduce` will pick the first enumerated value
as initial value and successively add all following values to it, whereas
`Enumerable#sum` will set an initial value of `0` (`Integer`) which can lead to a `TypeError`:

[source,ruby]
----
[].reduce(:+) # => nil
[1, 2, 3].reduce(:+) # => 6
['H', 'e', 'l', 'l', 'o'].reduce(:+) # => "Hello"

Please note that the auto-correction command line option will be changed from
`rubocop -a` to `rubocop -A`, which includes unsafe auto-correction.
[].sum # => 0
[1, 2, 3].sum # => 6
['H', 'e', 'l', 'l', 'o'].sum # => in `+': String can't be coerced into Integer (TypeError)
----

=== Examples

Expand All @@ -2052,9 +2064,9 @@ Please note that the auto-correction command line option will be changed from
[source,ruby]
----
# bad
[1, 2, 3].inject(:+) # These bad cases with no initial value are unsafe and
[1, 2, 3].inject(&:+) # will not be auto-correced by default. If you want to
[1, 2, 3].reduce { |acc, elem| acc + elem } # auto-corrected, you can set `SafeAutoCorrect: false`.
[1, 2, 3].inject(:+) # Auto-corrections for cases without initial value are unsafe
[1, 2, 3].inject(&:+) # and will only be performed when using the `-A` option.
[1, 2, 3].reduce { |acc, elem| acc + elem } # They can be prohibited completely using `SafeAutoCorrect: true`.
[1, 2, 3].reduce(10, :+)
[1, 2, 3].map { |elem| elem ** 2 }.sum
[1, 2, 3].collect(&:count).sum(10)
Expand Down
53 changes: 32 additions & 21 deletions lib/rubocop/cop/performance/sum.rb
Expand Up @@ -6,35 +6,46 @@ module Performance
# This cop identifies places where custom code finding the sum of elements
# in some Enumerable object can be replaced by `Enumerable#sum` method.
#
# This cop can change auto-correction scope depending on the value of
# `SafeAutoCorrect`.
# Its auto-correction is marked as safe by default (`SafeAutoCorrect: true`)
# to prevent `TypeError` in auto-correced code when initial value is not
# specified as shown below:
# @safety
# Auto-corrections are unproblematic wherever an initial value is provided explicitly:
#
# [source,ruby]
# ----
# ['a', 'b'].sum # => (String can't be coerced into Integer)
# ----
# [source,ruby]
# ----
# [1, 2, 3].reduce(4, :+) # => 10
# [1, 2, 3].sum(4) # => 10
#
# Therefore if initial value is not specified, unsafe auto-corrected will not occur.
# [].reduce(4, :+) # => 4
# [].sum(4) # => 4
# ----
#
# If you always want to enable auto-correction, you can set `SafeAutoCorrect: false`.
# This also holds true for non-numeric types which implement a `:+` method:
#
# [source,yaml]
# ----
# Performance/Sum:
# SafeAutoCorrect: false
# ----
# [source,ruby]
# ----
# ['l', 'o'].reduce('Hel', :+) # => "Hello"
# ['l', 'o'].sum('Hel') # => "Hello"
# ----
#
# Please note that the auto-correction command line option will be changed from
# `rubocop -a` to `rubocop -A`, which includes unsafe auto-correction.
# When no initial value is provided though, `Enumerable#reduce` will pick the first enumerated value
# as initial value and successively add all following values to it, whereas
# `Enumerable#sum` will set an initial value of `0` (`Integer`) which can lead to a `TypeError`:
#
# [source,ruby]
# ----
# [].reduce(:+) # => nil
# [1, 2, 3].reduce(:+) # => 6
# ['H', 'e', 'l', 'l', 'o'].reduce(:+) # => "Hello"
#
# [].sum # => 0
# [1, 2, 3].sum # => 6
# ['H', 'e', 'l', 'l', 'o'].sum # => in `+': String can't be coerced into Integer (TypeError)
# ----
#
# @example OnlySumOrWithInitialValue: false (default)
# # bad
# [1, 2, 3].inject(:+) # These bad cases with no initial value are unsafe and
# [1, 2, 3].inject(&:+) # will not be auto-correced by default. If you want to
# [1, 2, 3].reduce { |acc, elem| acc + elem } # auto-corrected, you can set `SafeAutoCorrect: false`.
# [1, 2, 3].inject(:+) # Auto-corrections for cases without initial value are unsafe
# [1, 2, 3].inject(&:+) # and will only be performed when using the `-A` option.
# [1, 2, 3].reduce { |acc, elem| acc + elem } # They can be prohibited completely using `SafeAutoCorrect: true`.
# [1, 2, 3].reduce(10, :+)
# [1, 2, 3].map { |elem| elem ** 2 }.sum
# [1, 2, 3].collect(&:count).sum(10)
Expand Down
72 changes: 30 additions & 42 deletions spec/rubocop/cop/performance/sum_spec.rb
Expand Up @@ -232,43 +232,6 @@
end
end

context 'when `SafeAutoCorrect: false' do
let(:cop_config) { { 'SafeAutoCorrect' => false } }

it 'autocorrects `:+` when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
array.#{method}(:+)
^{method}^^^^ Use `sum` instead of `#{method}(:+)`, unless calling `#{method}(:+)` on an empty array.
RUBY

expect_correction(<<~RUBY)
array.sum
RUBY
end

it 'autocorrects `&:+` when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
array.#{method}(&:+)
^{method}^^^^^ Use `sum` instead of `#{method}(&:+)`, unless calling `#{method}(&:+)` on an empty array.
RUBY

expect_correction(<<~RUBY)
array.sum
RUBY
end

it 'autocorrects `:+` without brackets when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
array.#{method} :+
^{method}^^^ Use `sum` instead of `#{method}(:+)`, unless calling `#{method}(:+)` on an empty array.
RUBY

expect_correction(<<~RUBY)
array.sum
RUBY
end
end

it "registers an offense and corrects when using `array.#{method}(0, &:+)`" do
expect_offense(<<~RUBY, method: method)
array.#{method}(0, &:+)
Expand All @@ -280,23 +243,48 @@
RUBY
end

it 'does not autocorrect `&:+` when initial value is not provided' do
it 'autocorrects `&:+` when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
array.#{method}(&:+)
^{method}^^^^^ Use `sum` instead of `#{method}(&:+)`, unless calling `#{method}(&:+)` on an empty array.
RUBY

expect_no_corrections
expect_correction(<<~RUBY)
array.sum
RUBY
end

it 'autocorrects `:+` when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
array.#{method}(:+)
^{method}^^^^ Use `sum` instead of `#{method}(:+)`, unless calling `#{method}(:+)` on an empty array.
RUBY

expect_correction(<<~RUBY)
array.sum
RUBY
end

# ideally it would autocorrect to `[1, 2, 3].sum`
it 'registers an offense but does not autocorrect on array literals' do
it 'autocorrects `:+` without brackets when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
array.#{method} :+
^{method}^^^ Use `sum` instead of `#{method}(:+)`, unless calling `#{method}(:+)` on an empty array.
RUBY

expect_correction(<<~RUBY)
array.sum
RUBY
end

it 'autocorrects `:+` on array literals when initial value is not provided' do
expect_offense(<<~RUBY, method: method)
[1, 2, 3].#{method}(:+)
^{method}^^^^ Use `sum` instead of `#{method}(:+)`.
RUBY

expect_no_corrections
expect_correction(<<~RUBY)
[1, 2, 3].sum
RUBY
end

it 'does not register an offense when the array is empty' do
Expand Down