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 Jan 21, 2025
1 parent 4e147b0 commit 060e040
Show file tree
Hide file tree
Showing 5 changed files with 467 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][])
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,9 @@ 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
Safe: false
VersionAdded: <<next>>
72 changes: 72 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,72 @@
# 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] }
#
# @safety
# This cop is unsafe for novel definitions of `map` and `collect`
# on non-Enumerable objects that do not respond to `zip`.
# To make your object enumerable, define an `each` method
# as described in https://ruby-doc.org/core/Enumerable.html
class ZipForArrayWrapping < Base
extend AutoCorrector

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

# @!method map_with_array?(node)
def_node_matcher :map_with_array?, <<~PATTERN
{
(block ({send csend} _ RESTRICT_ON_SEND) (args (arg _id)) (array (lvar _id)))
(numblock ({send csend} _ RESTRICT_ON_SEND) 1 (array (lvar _)))
}
PATTERN

def on_send(node)
return unless map_with_array?(node.parent)
return if node.receiver.nil?

register_offense(node)
end
alias on_csend on_send

private

def register_offense(node)
offense_range = offense_range(node.parent)
add_offense(offense_range, message: message(node)) do |corrector|
corrector.replace(offense_range, 'zip')
end
end

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

def offense_range(node)
@offense_range ||= node.children.first.loc.selector.join(node.loc.end)
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 @@ -54,3 +54,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 060e040

Please sign in to comment.