Skip to content

Commit

Permalink
[Fix rubocop#431] prefer << over push
Browse files Browse the repository at this point in the history
  • Loading branch information
amomchilov committed Jan 12, 2024
1 parent 3ba5d91 commit 58e20b8
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 4 deletions.
1 change: 1 addition & 0 deletions changelog/new_ArrayPushSingle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#431](https://github.com/rubocop/rubocop-performance/issues/431): Add `ArrayPushSingle` cop, which replaces `array.push(x)` with `array << x`. ([@amomchilov][])
7 changes: 7 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ Performance/AncestorsInclude:
Safe: false
VersionAdded: '1.7'

Performance/ArrayPushSingle:
Description: 'Use `array << x` instead of `array.push(x)`.'
Reference: 'https://github.com/fastruby/fast-ruby#ancestorsinclude-vs--code'
Enabled: true
Safe: false
VersionAdded: '<<next>>'

Performance/ArraySemiInfiniteRangeSlice:
Description: 'Identifies places where slicing arrays with semi-infinite ranges can be replaced by `Array#take` and `Array#drop`.'
# This cop was created due to a mistake in microbenchmark.
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop-performance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
RuboCop::Cop::Lint::UnusedMethodArgument.singleton_class.prepend(
Module.new do
def autocorrect_incompatible_with
super.push(RuboCop::Cop::Performance::BlockGivenWithExplicitBlock)
super << RuboCop::Cop::Performance::BlockGivenWithExplicitBlock
end
end
)
56 changes: 56 additions & 0 deletions lib/rubocop/cop/performance/array_push_single.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# Identifies places where pushing a single element to an array
# can be replaced by `Array#<<`.
#
# @safety
# This cop is unsafe because not all objects that respond to `#push` also respond to `#<<`
#
# @example
# # bad
# array.push(1)
#
# # good
# array << 1
#
# # good
# array.push(1, 2, 3) # `<<` only works for one element
#
class ArrayPushSingle < Base
extend AutoCorrector

MSG = 'Use `<<` instead of `%<current>s`.'

PUSH_METHODS = Set[:push, :append].freeze
RESTRICT_ON_SEND = PUSH_METHODS

def_node_matcher :push_call?, <<~PATTERN
(call $_receiver $%PUSH_METHODS $!(splat _))
PATTERN

def on_send(node)
push_call?(node) do |receiver, method_name, element|
message = format(MSG, current: method_name)

add_offense(node, message: message) do |corrector|
corrector.replace(node, "#{receiver.source} << #{element.source}")
end
end
end

def on_csend(node)
push_call?(node) do |receiver, method_name, element|
message = format(MSG, current: method_name)

add_offense(node, message: message) do |corrector|
corrector.replace(node, "#{receiver.source}&.<< #{element.source}")
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def replacement(regexp_node)
stack = []
chars = regexp_content.chars.each_with_object([]) do |char, strings|
if stack.empty? && char == '\\'
stack.push(char)
stack.push(char) # rubocop:disable Performance/ArrayPushSingle
else
strings << "#{stack.pop}#{char}"
end
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop/cop/performance_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative 'mixin/sort_block'

require_relative 'performance/ancestors_include'
require_relative 'performance/array_push_single'
require_relative 'performance/array_semi_infinite_range_slice'
require_relative 'performance/big_decimal_with_numeric_argument'
require_relative 'performance/bind_call'
Expand Down
4 changes: 2 additions & 2 deletions spec/project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@
supported_key = RuboCop::Cop::Util.to_supported_styles(style_name)
valid = config[name][supported_key]
unless valid
errors.push("#{supported_key} is missing for #{name}")
errors << "#{supported_key} is missing for #{name}"
next
end
next if valid.include?(style)

errors.push("invalid #{style_name} '#{style}' for #{name} found")
errors << "invalid #{style_name} '#{style}' for #{name} found"
end
end

Expand Down
58 changes: 58 additions & 0 deletions spec/rubocop/cop/performance/array_push_single_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Performance::ArrayPushSingle, :config do
it 'registers an offense and corrects when using `push` with a single element' do
expect_offense(<<~RUBY)
array.push(element)
^^^^^^^^^^^^^^^^^^^ Use `<<` instead of `push`.
RUBY

expect_correction(<<~RUBY)
array << element
RUBY
end

it 'registers an offense and corrects when using `push` with a single element and safe navigation operator' do
expect_offense(<<~RUBY)
array&.push(element)
^^^^^^^^^^^^^^^^^^^^ Use `<<` instead of `push`.
RUBY

# gross. TODO: make a configuration option?
expect_correction(<<~RUBY)
array&.<< element
RUBY
end

it 'does not register an offense when using `push` with multiple elements' do
expect_no_offenses(<<~RUBY)
array.push(1, 2, 3)
RUBY
end

it 'does not register an offense when using `push` with splatted elements' do
expect_no_offenses(<<~RUBY)
array.push(*elements)
RUBY
end

# rubocop:disable Performance/ArrayPushSingle
describe 'replacing `push` with `<<`' do
subject(:array) { [1, 2, 3] }

it 'returns the same result' do
expect([1, 2, 3].push(4)).to eq([1, 2, 3] << 4)
end

it 'has the same side-effect' do
a = [1, 2, 3]
a << 4

b = [1, 2, 3]
b << 4

expect(a).to eq(b)
end
end
# rubocop:enable Performance/ArrayPushSingle
end

0 comments on commit 58e20b8

Please sign in to comment.