Skip to content

Commit

Permalink
Add new Performance/UnfreezeString cop
Browse files Browse the repository at this point in the history
In Ruby 2.3 or later, `String#+@` is available.
This method unfreezes a string.

```ruby
str = 'foo'.freeze
p str.frozen?    # => true
p (+str).frozen? # => false
```

`String#dup` works similarly, but `+@` is faster than `dup`.
See. https://gist.github.com/k0kubun/e3da77cae2c132badd386c96f2de5768

This cop recommends to use `+@` instead of `dup`.
  • Loading branch information
pocke authored and bbatsov committed Jul 10, 2017
1 parent 584b025 commit d87cea2
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@
* Add new `Style/HeredocDelimiterCase` cop. ([@drenmi][])
* [#2943](https://github.com/bbatsov/rubocop/pull/2943): Add new `Lint/RescueWithoutErrorClass` cop. ([@drenmi][])
* [#4568](https://github.com/bbatsov/rubocop/pull/4568): Fix autocorrection for `Style/TrailingUnderscoreVariable`. ([@smakagon][])
* [#4586](https://github.com/bbatsov/rubocop/pull/4586): Add new `Performance/UnfreezeString` cop. ([@pocke][])

### Bug fixes

Expand Down
4 changes: 4 additions & 0 deletions config/enabled.yml
Expand Up @@ -1583,6 +1583,10 @@ Performance/TimesMap:
Description: 'Checks for .times.map calls.'
Enabled: true

Performance/UnfreezeString:
Description: 'Use unary plus to get an unfrozen string literal.'
Enabled: true

#################### Rails #################################

Rails/ActionFilter:
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -322,6 +322,7 @@
require 'rubocop/cop/performance/start_with'
require 'rubocop/cop/performance/string_replacement'
require 'rubocop/cop/performance/times_map'
require 'rubocop/cop/performance/unfreeze_string'

require 'rubocop/cop/style/alias'
require 'rubocop/cop/style/and_or'
Expand Down
50 changes: 50 additions & 0 deletions lib/rubocop/cop/performance/unfreeze_string.rb
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# In Ruby 2.3 or later, use unary plus operator to unfreeze a string
# literal instead of `String#dup` and `String.new`.
# Unary plus operator is faster than `String#dup`.
#
# Note: `String.new` (without operator) is not exactly the same as `+''`.
# These differ in encoding. `String.new.encoding` is always `ASCII-8BIT`.
# However, `(+'').encoding` is the same as script encoding(e.g. `UTF-8`).
# So, if you expect `ASCII-8BIT` encoding, disable this cop.
#
# @example
# # bad
# ''.dup
# "something".dup
# String.new
# String.new('')
# String.new('something')
#
# # good
# +'something'
# +''
class UnfreezeString < Cop
extend TargetRubyVersion

minimum_target_ruby_version 2.3

MSG = 'Use unary plus to get an unfrozen string literal.'.freeze

def_node_matcher :dup_string?, <<-PATTERN
(send {str dstr} :dup)
PATTERN

def_node_matcher :string_new?, <<-PATTERN
{
(send (const nil :String) :new {str dstr})
(send (const nil :String) :new)
}
PATTERN

def on_send(node)
add_offense(node) if dup_string?(node) || string_new?(node)
end
end
end
end
end
1 change: 1 addition & 0 deletions manual/cops.md
Expand Up @@ -281,6 +281,7 @@ In the following section you find all available cops:
* [Performance/StartWith](cops_performance.md#performancestartwith)
* [Performance/StringReplacement](cops_performance.md#performancestringreplacement)
* [Performance/TimesMap](cops_performance.md#performancetimesmap)
* [Performance/UnfreezeString](cops_performance.md#performanceunfreezestring)

#### Department [Rails](cops_rails.md)

Expand Down
30 changes: 30 additions & 0 deletions manual/cops_performance.md
Expand Up @@ -750,3 +750,33 @@ Array.new(9) do |i|
i.to_s
end
```

## Performance/UnfreezeString

Enabled by default | Supports autocorrection
--- | ---
Enabled | No

In Ruby 2.3 or later, use unary plus operator to unfreeze a string
literal instead of `String#dup` and `String.new`.
Unary plus operator is faster than `String#dup`.

Note: `String.new` (without operator) is not exactly the same as `+''`.
These differ in encoding. `String.new.encoding` is always `ASCII-8BIT`.
However, `(+'').encoding` is the same as script encoding(e.g. `UTF-8`).
So, if you expect `ASCII-8BIT` encoding, disable this cop.

### Example

```ruby
# bad
''.dup
"something".dup
String.new
String.new('')
String.new('something')

# good
+'something'
+''
```
78 changes: 78 additions & 0 deletions spec/rubocop/cop/performance/unfreeze_string_spec.rb
@@ -0,0 +1,78 @@
# frozen_string_literal: true

describe RuboCop::Cop::Performance::UnfreezeString, :config do
subject(:cop) { described_class.new(config) }

context 'TargetRubyVersion >= 2.3', :ruby23 do
it 'registers an offense for an empty string with `.dup`' do
expect_offense(<<-RUBY.strip_indent)
"".dup
^^^^^^ Use unary plus to get an unfrozen string literal.
RUBY
end

it 'registers an offense for a string with `.dup`' do
expect_offense(<<-RUBY.strip_indent)
"foo".dup
^^^^^^^^^ Use unary plus to get an unfrozen string literal.
RUBY
end

it 'registers an offense for a heredoc with `.dup`' do
expect_offense(<<-RUBY.strip_indent)
<<TEXT.dup
^^^^^^^^^^ Use unary plus to get an unfrozen string literal.
foo
bar
TEXT
RUBY
end

it 'registers an offense for a string that contains a string' \
'interpolation with `.dup`' do
expect_offense(<<-'RUBY'.strip_indent)
"foo#{bar}baz".dup
^^^^^^^^^^^^^^^^^^ Use unary plus to get an unfrozen string literal.
RUBY
end

it 'registers an offense for `String.new`' do
expect_offense(<<-RUBY.strip_indent)
String.new
^^^^^^^^^^ Use unary plus to get an unfrozen string literal.
RUBY
end

it 'registers an offense for `String.new` with an empty string' do
expect_offense(<<-RUBY.strip_indent)
String.new('')
^^^^^^^^^^^^^^ Use unary plus to get an unfrozen string literal.
RUBY
end

it 'registers an offense for `String.new` with a string' do
expect_offense(<<-RUBY.strip_indent)
String.new('foo')
^^^^^^^^^^^^^^^^^ Use unary plus to get an unfrozen string literal.
RUBY
end

it 'accepts an empty string with unary plus operator' do
expect_no_offenses(<<-RUBY.strip_indent)
+""
RUBY
end

it 'accepts a string with unary plus operator' do
expect_no_offenses(<<-RUBY.strip_indent)
+"foobar"
RUBY
end

it 'accepts `String.new` with capacity option' do
expect_no_offenses(<<-RUBY.strip_indent)
String.new(capacity: 100)
RUBY
end
end
end

0 comments on commit d87cea2

Please sign in to comment.