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 3, 2020
1 parent fe535e2 commit e7773b0
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### New features

* [#124](https://github.com/rubocop-hq/rubocop-performance/pull/124): Add new `Performance/Squeeze` cop. ([@fatkodima][])

* [#115](https://github.com/rubocop-hq/rubocop-performance/issues/115): Support `String#sub` and `String#sub!` methods for `Performance/DeletePrefix` and `Performance/DeleteSuffix` cops. ([@fatkodima][])

### Bug fixes
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ Performance/Size:
Enabled: true
VersionAdded: '0.30'

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 @@ -25,6 +25,7 @@
* xref:cops_performance.adoc#performanceregexpmatch[Performance/RegexpMatch]
* xref:cops_performance.adoc#performancereverseeach[Performance/ReverseEach]
* xref:cops_performance.adoc#performancesize[Performance/Size]
* xref:cops_performance.adoc#performancesqueeze[Performance/Squeeze]
* xref:cops_performance.adoc#performancestartwith[Performance/StartWith]
* xref:cops_performance.adoc#performancestringreplacement[Performance/StringReplacement]
* xref:cops_performance.adoc#performancetimesmap[Performance/TimesMap]
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 @@ -1048,6 +1048,40 @@ have been assigned to an array or a hash.

* https://github.com/JuanitoFatas/fast-ruby#arraylength-vs-arraysize-vs-arraycount-code

== 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
4 changes: 4 additions & 0 deletions lib/rubocop/cop/mixin/regexp_metacharacter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def literal_at_end?(regex_str)
regex_str =~ /\A(?:#{Util::LITERAL_REGEX})+(\\z|\$)\z/
end

def repeating_literal?(regex_str)
regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/)
end

def drop_start_metacharacter(regexp_string)
if regexp_string.start_with?('\\A')
regexp_string[2..-1] # drop `\A` anchor
Expand Down
66 changes: 66 additions & 0 deletions lib/rubocop/cop/performance/squeeze.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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
include RegexpMetacharacter

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
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 @@ -24,6 +24,7 @@
require_relative 'performance/regexp_match'
require_relative 'performance/reverse_each'
require_relative 'performance/size'
require_relative 'performance/squeeze'
require_relative 'performance/start_with'
require_relative 'performance/string_replacement'
require_relative 'performance/times_map'
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 e7773b0

Please sign in to comment.