-
-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Performance Cop for the more efficient way to generate an Array of Arrays. * Performs 40-90% faster than mapping to iteratively wrap array contents. * Performs 5 - 55% faster on ranges, depending on size.
- Loading branch information
1 parent
4c4cacd
commit 93c36b6
Showing
5 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* [#462](https://github.com/rubocop/rubocop-performance/pull/462): Add new `Performance/UseZipToWrapArrayContents` cop that checks patterns like `.map { |id| [id] }` or `.map { [_1] }` and can replace them with `.zip`. ([@corsonknowles][]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
lib/rubocop/cop/performance/use_zip_to_wrap_array_contents.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module Performance | ||
# Checks for `.map { |id| [id] }` and suggests replacing it with `.zip`. | ||
# | ||
# @example | ||
# # bad | ||
# [1, 2, 3].map { |id| [id] } | ||
# | ||
# # good | ||
# [1, 2, 3].zip | ||
# | ||
# @example | ||
# # good (no offense) | ||
# [1, 2, 3].map { |id| id } | ||
# | ||
# @example | ||
# # good (no offense) | ||
# [1, 2, 3].map { |id| [id, id] } | ||
class UseZipToWrapArrayContents < Base | ||
include RangeHelp | ||
extend AutoCorrector | ||
|
||
MSG = 'Use `.zip` instead of `.map { |id| [id] }`.' | ||
RESTRICT_ON_SEND = %i[map].freeze | ||
|
||
# Matches regular block form `.map { |e| [e] }` | ||
# @!method map_with_array?(node) | ||
def_node_matcher :map_with_array?, <<~PATTERN | ||
(block | ||
(send _ :map) | ||
(args (arg _id)) | ||
(array (lvar _id))) | ||
PATTERN | ||
|
||
# Matches numblock form `.map { [_1] }` | ||
# @!method map_with_array_numblock?(node) | ||
def_node_matcher :map_with_array_numblock?, <<~PATTERN | ||
(numblock | ||
(send _ :map) | ||
1 | ||
(array (lvar _)) | ||
) | ||
PATTERN | ||
|
||
def on_send(node) | ||
parent = node.parent | ||
|
||
register_offense(parent, node) if map_with_array?(parent) || map_with_array_numblock?(parent) | ||
end | ||
|
||
private | ||
|
||
def register_offense(parent, node) | ||
add_offense(offense_range(parent)) do |corrector| | ||
autocorrect(parent, node, corrector) | ||
end | ||
end | ||
|
||
def autocorrect(parent, node, corrector) | ||
receiver = node.receiver.source | ||
corrector.replace(parent, "#{receiver}.zip") | ||
end | ||
|
||
def offense_range(node) | ||
range_between(node.children.first.loc.selector.begin_pos, node.loc.end.end_pos) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
264 changes: 264 additions & 0 deletions
264
spec/rubocop/cop/performance/use_zip_to_wrap_array_contents_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe RuboCop::Cop::Performance::UseZipToWrapArrayContents, :config do | ||
context 'when using map with array literal' do | ||
it 'registers an offense and corrects to use zip with no arguments' do | ||
expect_offense(<<~RUBY) | ||
[1, 2, 3].map { |id| [id] } | ||
^^^^^^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
[1, 2, 3].zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with a short iterator name' do | ||
it 'registers an offense and corrects to use zip with no arguments' do | ||
expect_offense(<<~RUBY) | ||
[1, 2, 3].map { |e| [e] } | ||
^^^^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
[1, 2, 3].zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map on a range with another iterator name' do | ||
it 'registers an offense and corrects the code' do | ||
expect_offense(<<~RUBY) | ||
(1..3).map { |x| [x] } | ||
^^^^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
(1..3).zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map in a do end block' do | ||
it 'registers an offense' do | ||
expect_offense(<<~RUBY) | ||
(a..b).map do |m| [m] end | ||
^^^^^^^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
(a..b).zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map in a chain' do | ||
it 'registers an offense' do | ||
expect_offense(<<~RUBY) | ||
[nil, tuple].flatten.map { |e| [e] }.call | ||
^^^^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
[nil, tuple].flatten.zip.call | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when the map block does not contain an array literal' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| id } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using collect' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].collect { |id| [id] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using select with an array literal' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].select { |id| [id] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with an array literal containing multiple elements' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| [id, id] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with doubly wrapped arrays' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| [[id]] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with addition' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| id + 1 } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with array addition' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| [id] + [id] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with indexing into an array' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| [id][id] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling map with no arguments' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling map with an empty block' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map {} | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling filter_map' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].filter_map {|id| [id]} | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling flat_map' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].flat_map {|id| [id]} | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using Array.wrap' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |id| Array.wrap(id) } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with a numerical argment reference' do | ||
it 'registers an offense' do | ||
expect_offense(<<~RUBY) | ||
[1, 2, 3].map { [_1] } | ||
^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
[1, 2, 3].zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map with a numerical argment reference in a chain' do | ||
it 'registers an offense' do | ||
expect_offense(<<~RUBY) | ||
[1, 2].sum.map { [_1] }.flatten | ||
^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
[1, 2].sum.zip.flatten | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map on a range with a numeric arguement reference' do | ||
it 'registers an offense and corrects the code' do | ||
expect_offense(<<~RUBY) | ||
(1..3).map { [_1] } | ||
^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
(1..3).zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when using map in a do end block with a numeric argument reference' do | ||
it 'registers an offense' do | ||
expect_offense(<<~RUBY) | ||
(a..b).map do [_1] end | ||
^^^^^^^^^^^^^^^ Use `.zip` instead of `.map { |id| [id] }`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
(a..b).zip | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling filter_map with a numblock' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].filter_map { [_1] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling map, adding, and wrapping, with a numblock' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { [_1 + 1] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling double wrapping with a numblock' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { [[_1]] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling map with an unused iterator' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { |e| [1] } | ||
RUBY | ||
end | ||
end | ||
|
||
context 'when calling map with a static block that always returns the same value' do | ||
it 'does not register an offense' do | ||
expect_no_offenses(<<~RUBY) | ||
[1, 2, 3].map { [id] } | ||
RUBY | ||
end | ||
end | ||
end |