diff --git a/changelog/new_ArrayConcatLiteral.md b/changelog/new_ArrayConcatLiteral.md new file mode 100644 index 000000000..b04daa08a --- /dev/null +++ b/changelog/new_ArrayConcatLiteral.md @@ -0,0 +1 @@ +* [#434](https://github.com/rubocop/rubocop-performance/issues/434): Add `ArrayConcatLiteral` cop, which replaces `array.concat([1, 2, 3])` with `array.push(1, 2, 3)`. ([@amomchilov][]) diff --git a/config/default.yml b/config/default.yml index 68eae785e..86b0d55ab 100644 --- a/config/default.yml +++ b/config/default.yml @@ -11,6 +11,13 @@ Performance/AncestorsInclude: Safe: false VersionAdded: '1.7' +Performance/ArrayConcatLiteral: + Description: 'Use `array.push(1, 2, 3)` instead of `array.concat([1, 2, 3])`.' + Reference: 'https://github.com/rubocop/rubocop-performance/issues/434' + Enabled: true + Safe: false + VersionAdded: '<>' + Performance/ArrayPushSingle: Description: 'Use `array << x` instead of `array.push(x)`.' Reference: 'https://github.com/rubocop/rubocop-performance/issues/431' diff --git a/lib/rubocop/cop/performance/array_concat_literal.rb b/lib/rubocop/cop/performance/array_concat_literal.rb new file mode 100755 index 000000000..2a10acccb --- /dev/null +++ b/lib/rubocop/cop/performance/array_concat_literal.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Performance + # Identifies places where concatinating a literal to an array + # can be replaced by pushing its elements directly. + # + # @safety + # This cop is unsafe because not all objects that respond to `#concat` also respond to `#push` + # + # @example + # # bad + # array.concat([1, 2, 3]) + # + # # good + # array.push(1, 2, 3) + # + # # good + # array.concat(not_a_literal) + # + class ArrayConcatLiteral < Base + extend AutoCorrector + + MSG = 'Use `push` instead of concatinating a literal.' + + RESTRICT_ON_SEND = %i[concat].freeze + + def_node_matcher :concat_call?, <<~PATTERN + (call $_receiver :concat $(array ...)) + PATTERN + + def on_send(node) + concat_call?(node) do |receiver, elements| + add_offense(node) do |corrector| + corrector.replace(node, "#{receiver.source}.push(#{remove_brackets(elements)})") + end + end + end + + def on_csend(node) + concat_call?(node) do |receiver, elements| + add_offense(node) do |corrector| + corrector.replace(node, "#{receiver.source}&.push(#{remove_brackets(elements)})") + end + end + end + + private + + # Copied from `Lint/RedundantSplatExpansion` + # TODO: Expose this as a helper API from the base RuboCop gem + PERCENT_W = '%w' + PERCENT_CAPITAL_W = '%W' + PERCENT_I = '%i' + PERCENT_CAPITAL_I = '%I' + + def remove_brackets(array) + array_start = array.loc.begin.source + elements = *array + elements = elements.map(&:source) + + if array_start.start_with?(PERCENT_W) + "'#{elements.join("', '")}'" + elsif array_start.start_with?(PERCENT_CAPITAL_W) + %("#{elements.join('", "')}") + elsif array_start.start_with?(PERCENT_I) + ":#{elements.join(', :')}" + elsif array_start.start_with?(PERCENT_CAPITAL_I) + %(:"#{elements.join('", :"')}") + else + elements.join(', ') + end + end + end + end + end +end diff --git a/lib/rubocop/cop/performance_cops.rb b/lib/rubocop/cop/performance_cops.rb index b21673df0..5e3363cd1 100644 --- a/lib/rubocop/cop/performance_cops.rb +++ b/lib/rubocop/cop/performance_cops.rb @@ -4,6 +4,7 @@ require_relative 'mixin/sort_block' require_relative 'performance/ancestors_include' +require_relative 'performance/array_concat_literal' require_relative 'performance/array_push_single' require_relative 'performance/array_semi_infinite_range_slice' require_relative 'performance/big_decimal_with_numeric_argument' diff --git a/spec/rubocop/cop/performance/array_concat_literal_spec.rb b/spec/rubocop/cop/performance/array_concat_literal_spec.rb new file mode 100644 index 000000000..a1a7a51e2 --- /dev/null +++ b/spec/rubocop/cop/performance/array_concat_literal_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Performance::ArrayConcatLiteral, :config do + it 'registers an offense and corrects when using `concat` with array literal' do + expect_offense(<<~RUBY) + array.concat([1, 2, 3]) + ^^^^^^^^^^^^^^^^^^^^^^^ Use `push` instead of concatinating a literal. + RUBY + + expect_correction(<<~RUBY) + array.push(1, 2, 3) + RUBY + end + + it 'registers an offense and corrects when using `concat` with array literal and safe navigation operator' do + expect_offense(<<~RUBY) + array&.concat([1, 2, 3]) + ^^^^^^^^^^^^^^^^^^^^^^^^ Use `push` instead of concatinating a literal. + RUBY + + expect_correction(<<~RUBY) + array&.push(1, 2, 3) + RUBY + end + + describe 'replacing `concat` with `push`' do + it 'returns the same result' do + expect([1, 2, 3].concat(4, 5, 6)).to eq([1, 2, 3].push(4, 5, 6)) + end + + it 'has the same side-effect' do + expected = [1, 2, 3] + expected.push(4, 5, 6) + + actual = [1, 2, 3] + actual.push(4, 5, 6) + + expect(actual).to eq(expected) + end + end +end