Skip to content

Commit

Permalink
Add new Performance/Squeeze cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Jun 8, 2020
1 parent 0591cfd commit f1eb268
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### New features

* [#124](https://github.com/rubocop-hq/rubocop-performance/pull/124): Add new `Performance/Squeeze` cop. ([@fatkodima][])
* [#129](https://github.com/rubocop-hq/rubocop-performance/pull/129): Add new `Performance/BigDecimalWithNumericArgument` cop. ([@fatkodima][])
* [#130](https://github.com/rubocop-hq/rubocop-performance/pull/130): Add new `Performance/SortReverse` cop. ([@fatkodima][])
* [#81](https://github.com/rubocop-hq/rubocop-performance/issues/81): Add new `Performance/StringInclude` cop. ([@fatkodima][])
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ Performance/SortReverse:
Enabled: true
VersionAdded: '1.7'

Performance/Squeeze:
Description: "Use `squeeze('a')` instead of `gsub(/a+/, 'a')`."
Reference: 'https://github.com/JuanitoFatas/fast-ruby#remove-extra-spaces-or-other-contiguous-characters-code'
Enabled: true
VersionAdded: '1.7'

Performance/StartWith:
Description: 'Use `start_with?` instead of a regex match anchored to the beginning of a string.'
Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringmatch-vs-stringstart_withstringend_with-code-start-code-end'
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* xref:cops_performance.adoc#performancereverseeach[Performance/ReverseEach]
* xref:cops_performance.adoc#performancesize[Performance/Size]
* xref:cops_performance.adoc#performancesortreverse[Performance/SortReverse]
* xref:cops_performance.adoc#performancesqueeze[Performance/Squeeze]
* xref:cops_performance.adoc#performancestartwith[Performance/StartWith]
* xref:cops_performance.adoc#performancestringinclude[Performance/StringInclude]
* xref:cops_performance.adoc#performancestringreplacement[Performance/StringReplacement]
Expand Down
34 changes: 34 additions & 0 deletions docs/modules/ROOT/pages/cops_performance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,40 @@ array.sort { |a, b| b <=> a }
array.sort.reverse
----

== Performance/Squeeze

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Enabled
| Yes
| Yes
| 1.7
| -
|===

This cop identifies places where `gsub(/a+/, 'a')` and `gsub!(/a+/, 'a')`
can be replaced by `squeeze('a')` and `squeeze!('a')`.

The `squeeze('a')` method is faster than `gsub(/a+/, 'a')`.

=== Examples

[source,ruby]
----
# bad
str.gsub(/a+/, 'a')
str.gsub!(/a+/, 'a')
# good
str.squeeze('a')
str.squeeze!('a')
----

=== References

* https://github.com/JuanitoFatas/fast-ruby#remove-extra-spaces-or-other-contiguous-characters-code

== Performance/StartWith

|===
Expand Down
70 changes: 70 additions & 0 deletions lib/rubocop/cop/performance/squeeze.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# This cop identifies places where `gsub(/a+/, 'a')` and `gsub!(/a+/, 'a')`
# can be replaced by `squeeze('a')` and `squeeze!('a')`.
#
# The `squeeze('a')` method is faster than `gsub(/a+/, 'a')`.
#
# @example
#
# # bad
# str.gsub(/a+/, 'a')
# str.gsub!(/a+/, 'a')
#
# # good
# str.squeeze('a')
# str.squeeze!('a')
#
class Squeeze < Cop
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'

PREFERRED_METHODS = {
gsub: :squeeze,
gsub!: :squeeze!
}.freeze

def_node_matcher :squeeze_candidate?, <<~PATTERN
(send
$!nil? ${:gsub :gsub!}
(regexp
(str $#repeating_literal?)
(regopt))
(str $_))
PATTERN

def on_send(node)
squeeze_candidate?(node) do |_, bad_method, regexp_str, replace_str|
regexp_str = regexp_str[0..-2] # delete '+' from the end
regexp_str = interpret_string_escapes(regexp_str)
return unless replace_str == regexp_str

good_method = PREFERRED_METHODS[bad_method]
message = format(MSG, current: bad_method, prefer: good_method)
add_offense(node, location: :selector, message: message)
end
end

def autocorrect(node)
squeeze_candidate?(node) do |receiver, bad_method, _regexp_str, replace_str|
lambda do |corrector|
good_method = PREFERRED_METHODS[bad_method]
string_literal = to_string_literal(replace_str)

new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
corrector.replace(node.source_range, new_code)
end
end
end

private

def repeating_literal?(regex_str)
regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/)
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 @@ -27,6 +27,7 @@
require_relative 'performance/reverse_each'
require_relative 'performance/size'
require_relative 'performance/sort_reverse'
require_relative 'performance/squeeze'
require_relative 'performance/start_with'
require_relative 'performance/string_include'
require_relative 'performance/string_replacement'
Expand Down
45 changes: 45 additions & 0 deletions spec/rubocop/cop/performance/squeeze_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Performance::Squeeze do
subject(:cop) { described_class.new }

it "registers an offense and corrects when using `#gsub(/a+/, 'a')`" do
expect_offense(<<~RUBY)
str.gsub(/a+/, 'a')
^^^^ Use `squeeze` instead of `gsub`.
RUBY

expect_correction(<<~RUBY)
str.squeeze('a')
RUBY
end

it "registers an offense and corrects when using `#gsub!(/a+/, 'a')`" do
expect_offense(<<~RUBY)
str.gsub!(/a+/, 'a')
^^^^^ Use `squeeze!` instead of `gsub!`.
RUBY

expect_correction(<<~RUBY)
str.squeeze!('a')
RUBY
end

it 'does not register an offense when using `#squeeze`' do
expect_no_offenses(<<~RUBY)
str.squeeze('a')
RUBY
end

it 'does not register an offense when using `#squeeze!`' do
expect_no_offenses(<<~RUBY)
str.squeeze!('a')
RUBY
end

it 'does not register an offense when replacement does not match pattern' do
expect_no_offenses(<<~RUBY)
str.gsub(/a+/, 'b')
RUBY
end
end

0 comments on commit f1eb268

Please sign in to comment.