Skip to content

Commit

Permalink
Add new ZipForArrayWrapping cop
Browse files Browse the repository at this point in the history
Add new `Performance/ZipForArrayWrapping` cop that checks patterns like `.map { |id| [id] }` or `.map { [_1] }` and can safely replace them with `.zip`

This is a Performance Cop for the more efficient way to generate an Array of Arrays.

 * Performs 40-90% faster than `.map` to iteratively wrap array contents.
 * Performs 5 - 55% faster on ranges, depending on size.
  • Loading branch information
corsonknowles committed Nov 10, 2024
1 parent 4c4cacd commit b06bfaf
Show file tree
Hide file tree
Showing 5 changed files with 395 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_merge_pull_request_462_from.md
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/ZipForArrayWrapping` cop that checks patterns like `.map { |id| [id] }` or `.map { [_1] }` and can safely replace them with `.zip`. ([@corsonknowles][])
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,8 @@ Performance/UriDefaultParser:
Description: 'Use `URI::DEFAULT_PARSER` instead of `URI::Parser.new`.'
Enabled: true
VersionAdded: '0.50'

Performance/ZipForArrayWrapping:
Description: 'Checks for `.map { |id| [id] }` and suggests replacing it with `.zip`.'
Enabled: pending
VersionAdded: <<next>>
81 changes: 81 additions & 0 deletions lib/rubocop/cop/performance/zip_for_array_wrapping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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] }
#
# # bad
# [1, 2, 3].map { [_1] }
#
# # 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 ZipForArrayWrapping < Base
include RangeHelp
extend AutoCorrector

MSG = 'Use `zip` instead of `%<original_code>s`.'
RESTRICT_ON_SEND = Set.new(%i[map collect]).freeze

# Matches regular block form `.map { |e| [e] }`
# @!method map_with_array?(node)
def_node_matcher :map_with_array?, <<~PATTERN
(block
(send _ RESTRICT_ON_SEND)
(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)
return unless (parent = node.parent)
return unless map_with_array?(parent) || map_with_array_numblock?(parent)
return unless (receiver = node.receiver&.source)

register_offense(parent, receiver, node)
end

private

def register_offense(parent, receiver, node)
add_offense(offense_range(parent), message: message(node)) do |corrector|
autocorrect(parent, receiver, corrector)
end
end

def message(node)
format(MSG, original_code: offense_range(node).source.lines.first.chomp)
end

def autocorrect(parent, receiver, corrector)
corrector.replace(parent, "#{receiver}.zip")
end

def offense_range(node)
@offense_range ||= range_between(node.children.first.loc.selector.begin_pos, node.loc.end.end_pos)
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/performance_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@
require_relative 'performance/unfreeze_string'
require_relative 'performance/uri_default_parser'
require_relative 'performance/chain_array_allocation'
require_relative 'performance/zip_for_array_wrapping'
Loading

0 comments on commit b06bfaf

Please sign in to comment.