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

Add new ArrayPushSingle cop to replace a.push(x) with a << x #433

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/rubocop/rubocop-performance/issues/431'
Enabled: true
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be true, false or pending?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cop generator should output pending. On the next major release all pending cops are enabled in bulk according to the docs.

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}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are some edge cases that this won't handle correctly e.g.:

# original
[].push(1 && 2)  # result is [2]
# corrected
[] << 1 && 2     # result is 2

# original
[].push 1 << 2   # result is [4]
# corrected
[] << 1 << 2     # result is [1,2]

# original
[].push(1) + [].push(2)  # result is [1,2]
# corrected
[] << 1 + [] << 2
TypeError: Array can't be coerced into Integer

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm does Rubocop have any utilities for detecting the precedence of a subexpr, relative to the other parts of the expression it's in?

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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping push is nicer here, to match pop below

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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably add a test for append as well

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?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the best way to handle this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just ignore it. &.<< looks horrible

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

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