diff --git a/CHANGELOG.md b/CHANGELOG.md index 098a64cc6b..841975b7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#173](https://github.com/rubocop-hq/rubocop-performance/pull/173): Add new `Performance/BlockGivenWithExplicitBlock` cop. ([@fatkodima][]) * [#136](https://github.com/rubocop-hq/rubocop-performance/issues/136): Add new `Performance/MethodObjectAsBlock` cop. ([@fatkodima][]) * [#151](https://github.com/rubocop-hq/rubocop-performance/issues/151): Add new `Performance/ConstantRegexp` cop. ([@fatkodima][]) +* [#175](https://github.com/rubocop-hq/rubocop-performance/pull/175): Add new `Performance/ArraySemiInfiniteRangeSlice` cop. ([@fatkodima][]) ### Changes diff --git a/config/default.yml b/config/default.yml index a26df65302..95be3aed29 100644 --- a/config/default.yml +++ b/config/default.yml @@ -7,6 +7,11 @@ Performance/AncestorsInclude: Safe: false VersionAdded: '1.7' +Performance/ArraySemiInfiniteRangeSlice: + Description: 'Identifies places where slicing arrays with semi-infinite ranges can be replaced by `Array#take` and `Array#drop`.' + Enabled: pending + VersionAdded: '1.9' + Performance/BigDecimalWithNumericArgument: Description: 'Convert numeric argument to string before passing to BigDecimal.' Enabled: 'pending' diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 0d2821d879..3010af4f68 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -13,6 +13,7 @@ Performance cops optimization analysis for your projects. === Department xref:cops_performance.adoc[Performance] * xref:cops_performance.adoc#performanceancestorsinclude[Performance/AncestorsInclude] +* xref:cops_performance.adoc#performancearraysemiinfiniterangeslice[Performance/ArraySemiInfiniteRangeSlice] * xref:cops_performance.adoc#performancebigdecimalwithnumericargument[Performance/BigDecimalWithNumericArgument] * xref:cops_performance.adoc#performancebindcall[Performance/BindCall] * xref:cops_performance.adoc#performanceblockgivenwithexplicitblock[Performance/BlockGivenWithExplicitBlock] diff --git a/docs/modules/ROOT/pages/cops_performance.adoc b/docs/modules/ROOT/pages/cops_performance.adoc index 41286aa2c7..abaffb5525 100644 --- a/docs/modules/ROOT/pages/cops_performance.adoc +++ b/docs/modules/ROOT/pages/cops_performance.adoc @@ -30,6 +30,42 @@ A <= B * https://github.com/JuanitoFatas/fast-ruby#ancestorsinclude-vs--code +== Performance/ArraySemiInfiniteRangeSlice + +NOTE: Required Ruby version: 2.7 + +|=== +| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged + +| Pending +| Yes +| Yes +| 1.9 +| - +|=== + +This cop identifies places where slicing arrays with semi-infinite ranges +can be replaced by `Array#take` and `Array#drop`. + +=== Examples + +[source,ruby] +---- +# bad +# array[..2] +# array[...2] +# array[2..] +# array[2...] +# array.slice(..2) + +# good +array.take(3) +array.take(2) +array.drop(2) +array.drop(2) +array.take(3) +---- + == Performance/BigDecimalWithNumericArgument |=== diff --git a/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb b/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb new file mode 100644 index 0000000000..dffe677c55 --- /dev/null +++ b/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Performance + # This cop identifies places where slicing arrays with semi-infinite ranges + # can be replaced by `Array#take` and `Array#drop`. + # + # @example + # # bad + # # array[..2] + # # array[...2] + # # array[2..] + # # array[2...] + # # array.slice(..2) + # + # # good + # array.take(3) + # array.take(2) + # array.drop(2) + # array.drop(2) + # array.take(3) + # + class ArraySemiInfiniteRangeSlice < Base + include RangeHelp + extend AutoCorrector + extend TargetRubyVersion + + minimum_target_ruby_version 2.7 + + MSG = 'Use `%s` instead of `%s` with semi-infinite range.' + + SLICE_METHODS = Set[:[], :slice].freeze + RESTRICT_ON_SEND = SLICE_METHODS + + def_node_matcher :endless_range_slice?, <<~PATTERN + (send $_ $%SLICE_METHODS $#endless_range?) + PATTERN + + def_node_matcher :endless_range?, <<~PATTERN + { + ({irange erange} nil? (int positive?)) + ({irange erange} (int positive?) nil?) + } + PATTERN + + def on_send(node) + endless_range_slice?(node) do |receiver, method_name, range_node| + prefer = range_node.begin ? :drop : :take + message = format(MSG, prefer: prefer, current: method_name) + + add_offense(node, message: message) do |corrector| + corrector.replace(node, correction(receiver, range_node)) + end + end + end + + private + + def correction(receiver, range_node) + method_call = if range_node.begin + "drop(#{range_node.begin.value})" + elsif range_node.irange_type? + "take(#{range_node.end.value + 1})" + else + "take(#{range_node.end.value})" + end + + "#{receiver.source}.#{method_call}" + end + end + end + end +end diff --git a/lib/rubocop/cop/performance_cops.rb b/lib/rubocop/cop/performance_cops.rb index 7f056d8237..9a37fac7ed 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_semi_infinite_range_slice' require_relative 'performance/big_decimal_with_numeric_argument' require_relative 'performance/bind_call' require_relative 'performance/block_given_with_explicit_block' diff --git a/spec/rubocop/cop/performance/array_semi_infinite_range_slice_spec.rb b/spec/rubocop/cop/performance/array_semi_infinite_range_slice_spec.rb new file mode 100644 index 0000000000..b50d52cc1b --- /dev/null +++ b/spec/rubocop/cop/performance/array_semi_infinite_range_slice_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Performance::ArraySemiInfiniteRangeSlice, :config do + subject(:cop) { described_class.new(config) } + + context 'TargetRubyVersion >= 2.7', :ruby27 do + it 'registers an offense and corrects when using `[]` with beginless range' do + expect_offense(<<~RUBY) + array[..2] + ^^^^^^^^^^ Use `take` instead of `[]` with semi-infinite range. + array[...2] + ^^^^^^^^^^^ Use `take` instead of `[]` with semi-infinite range. + RUBY + + expect_correction(<<~RUBY) + array.take(3) + array.take(2) + RUBY + end + + it 'registers an offense and corrects when using `[]` with endless range' do + expect_offense(<<~RUBY) + array[2..] + ^^^^^^^^^^ Use `drop` instead of `[]` with semi-infinite range. + array[2...] + ^^^^^^^^^^^ Use `drop` instead of `[]` with semi-infinite range. + RUBY + + expect_correction(<<~RUBY) + array.drop(2) + array.drop(2) + RUBY + end + + it 'registers an offense and corrects when using `slice` with semi-infinite ranges' do + expect_offense(<<~RUBY) + array.slice(2..) + ^^^^^^^^^^^^^^^^ Use `drop` instead of `slice` with semi-infinite range. + array.slice(..2) + ^^^^^^^^^^^^^^^^ Use `take` instead of `slice` with semi-infinite range. + RUBY + + expect_correction(<<~RUBY) + array.drop(2) + array.take(3) + RUBY + end + + it 'does not register an offense when using `[]` with full range' do + expect_no_offenses(<<~RUBY) + array[0..2] + RUBY + end + + it 'does not register an offense when using `[]` with semi-infinite range with non literal' do + expect_no_offenses(<<~RUBY) + array[..index] + array[index..] + RUBY + end + + it 'does not register an offense when using `[]` with semi-infinite range with negative int' do + expect_no_offenses(<<~RUBY) + array[..-2] + array[-2..] + RUBY + end + end +end