Skip to content

Commit

Permalink
Merge pull request #105 from koic/add_new_delete_prefix_and_delete_su…
Browse files Browse the repository at this point in the history
…ffix_cops

Add new `Performance/DeletePrefix` and `Performance/DeleteSuffix` cops
  • Loading branch information
koic authored May 10, 2020
2 parents c4d4496 + 78047b0 commit 0f14f80
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New features

* [#77](https://github.com/rubocop-hq/rubocop-performance/issues/77): Add new `Performance/BindCall` cop. ([@koic][])
* [#105](https://github.com/rubocop-hq/rubocop-performance/pull/105): Add new `Performance/DeletePrefix` and `Performance/DeleteSuffix` cops. ([@koic][])

### Changes

Expand Down
10 changes: 10 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ Performance/Count:
VersionAdded: '0.31'
VersionChanged: '1.5'

Performance/DeletePrefix:
Description: 'Use `delete_prefix` instead of `gsub`.'
Enabled: true
VersionAdded: '1.6'

Performance/DeleteSuffix:
Description: 'Use `delete_suffix` instead of `gsub`.'
Enabled: true
VersionAdded: '1.6'

Performance/Detect:
Description: >-
Use `detect` instead of `select.first`, `find_all.first`,
Expand Down
88 changes: 88 additions & 0 deletions lib/rubocop/cop/performance/delete_prefix.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# In Ruby 2.5, `String#delete_prefix` has been added.
#
# This cop identifies places where `gsub(/\Aprefix/, '')`
# can be replaced by `delete_prefix('prefix')`.
#
# The `delete_prefix('prefix')` method is faster than
# `gsub(/\Aprefix/, '')`.
#
# @example
#
# # bad
# str.gsub(/\Aprefix/, '')
# str.gsub!(/\Aprefix/, '')
# str.gsub(/^prefix/, '')
# str.gsub!(/^prefix/, '')
#
# # good
# str.delete_prefix('prefix')
# str.delete_prefix!('prefix')
#
class DeletePrefix < Cop
extend TargetRubyVersion

minimum_target_ruby_version 2.5

MSG = 'Use `%<prefer>s` instead of `%<current>s`.'

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

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

def on_send(node)
gsub_method?(node) do |_, bad_method, _, replace_string|
return unless replace_string.blank?

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)
gsub_method?(node) do |receiver, bad_method, regexp_str, _|
lambda do |corrector|
good_method = PREFERRED_METHODS[bad_method]

regexp_str = if regexp_str.start_with?('\\A')
regexp_str[2..-1] # drop `\A` anchor
else
regexp_str[1..-1] # drop `^` anchor
end
regexp_str = interpret_string_escapes(regexp_str)
string_literal = to_string_literal(regexp_str)

new_code = "#{receiver.source}.#{good_method}(#{string_literal})"

corrector.replace(node, new_code)
end
end
end

private

def literal_at_start?(regex_str)
# is this regexp 'literal' in the sense of only matching literal
# chars, rather than using metachars like `.` and `*` and so on?
# also, is it anchored at the start of the string?
# (tricky: \s, \d, and so on are metacharacters, but other characters
# escaped with a slash are just literals. LITERAL_REGEX takes all
# that into account.)
regex_str =~ /\A(\\A|\^)(?:#{LITERAL_REGEX})+\z/
end
end
end
end
end
88 changes: 88 additions & 0 deletions lib/rubocop/cop/performance/delete_suffix.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# In Ruby 2.5, `String#delete_suffix` has been added.
#
# This cop identifies places where `gsub(/suffix\z/, '')`
# can be replaced by `delete_suffix('suffix')`.
#
# The `delete_suffix('suffix')` method is faster than
# `gsub(/suffix\z/, '')`.
#
# @example
#
# # bad
# str.gsub(/suffix\z/, '')
# str.gsub!(/suffix\z/, '')
# str.gsub(/suffix$/, '')
# str.gsub!(/suffix$/, '')
#
# # good
# str.delete_suffix('suffix')
# str.delete_suffix!('suffix')
#
class DeleteSuffix < Cop
extend TargetRubyVersion

minimum_target_ruby_version 2.5

MSG = 'Use `%<prefer>s` instead of `%<current>s`.'

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

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

def on_send(node)
gsub_method?(node) do |_, bad_method, _, replace_string|
return unless replace_string.blank?

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)
gsub_method?(node) do |receiver, bad_method, regexp_str, _|
lambda do |corrector|
good_method = PREFERRED_METHODS[bad_method]

regexp_str = if regexp_str.end_with?('\\z')
regexp_str.chomp('\z') # drop `\z` anchor
else
regexp_str.chop # drop `$` anchor
end
regexp_str = interpret_string_escapes(regexp_str)
string_literal = to_string_literal(regexp_str)

new_code = "#{receiver.source}.#{good_method}(#{string_literal})"

corrector.replace(node, new_code)
end
end
end

private

def literal_at_end?(regex_str)
# is this regexp 'literal' in the sense of only matching literal
# chars, rather than using metachars like `.` and `*` and so on?
# also, is it anchored at the start of the string?
# (tricky: \s, \d, and so on are metacharacters, but other characters
# escaped with a slash are just literals. LITERAL_REGEX takes all
# that into account.)
regex_str =~ /\A(?:#{LITERAL_REGEX})+(\\z|\$)\z/
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/rubocop/cop/performance_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
require_relative 'performance/casecmp'
require_relative 'performance/compare_with_block'
require_relative 'performance/count'
require_relative 'performance/delete_prefix'
require_relative 'performance/delete_suffix'
require_relative 'performance/detect'
require_relative 'performance/double_start_end_with'
require_relative 'performance/end_with'
Expand Down
2 changes: 2 additions & 0 deletions manual/cops.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* [Performance/ChainArrayAllocation](cops_performance.md#performancechainarrayallocation)
* [Performance/CompareWithBlock](cops_performance.md#performancecomparewithblock)
* [Performance/Count](cops_performance.md#performancecount)
* [Performance/DeletePrefix](cops_performance.md#performancedeleteprefix)
* [Performance/DeleteSuffix](cops_performance.md#performancedeletesuffix)
* [Performance/Detect](cops_performance.md#performancedetect)
* [Performance/DoubleStartEndWith](cops_performance.md#performancedoublestartendwith)
* [Performance/EndWith](cops_performance.md#performanceendwith)
Expand Down
56 changes: 56 additions & 0 deletions manual/cops_performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,62 @@ Model.select('field AS field_one').count
Model.select(:value).count
```

## Performance/DeletePrefix

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Enabled | Yes | Yes | 1.6 | -

In Ruby 2.5, `String#delete_prefix` has been added.

This cop identifies places where `gsub(/\Aprefix/, '')`
can be replaced by `delete_prefix('prefix')`.

The `delete_prefix('prefix')` method is faster than
`gsub(/\Aprefix/, '')`.

### Examples

```ruby
# bad
str.gsub(/\Aprefix/, '')
str.gsub!(/\Aprefix/, '')
str.gsub(/^prefix/, '')
str.gsub!(/^prefix/, '')

# good
str.delete_prefix('prefix')
str.delete_prefix!('prefix')
```

## Performance/DeleteSuffix

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Enabled | Yes | Yes | 1.6 | -

In Ruby 2.5, `String#delete_suffix` has been added.

This cop identifies places where `gsub(/suffix\z/, '')`
can be replaced by `delete_suffix('suffix')`.

The `delete_suffix('suffix')` method is faster than
`gsub(/suffix\z/, '')`.

### Examples

```ruby
# bad
str.gsub(/suffix\z/, '')
str.gsub!(/suffix\z/, '')
str.gsub(/suffix$/, '')
str.gsub!(/suffix$/, '')

# good
str.delete_suffix('suffix')
str.delete_suffix!('suffix')
```

## Performance/Detect

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
Expand Down
Loading

0 comments on commit 0f14f80

Please sign in to comment.