diff --git a/changelog/new_string_bytesize_cop.md b/changelog/new_string_bytesize_cop.md new file mode 100644 index 0000000000..956fa63f69 --- /dev/null +++ b/changelog/new_string_bytesize_cop.md @@ -0,0 +1 @@ +* [#474](https://github.com/rubocop/rubocop-performance/pull/474): Add new `Performance/StringBytesize` cop. ([@viralpraxis][]) diff --git a/config/default.yml b/config/default.yml index da765ab5c4..f1897d96dc 100644 --- a/config/default.yml +++ b/config/default.yml @@ -326,6 +326,12 @@ Performance/StartWith: VersionAdded: '0.36' VersionChanged: '1.10' +Performance/StringBytesize: + Description: "Use `String#bytesize` instead of calculating the size of the bytes array." + SafeAutoCorrect: false + Enabled: 'pending' + VersionAdded: '<>' + Performance/StringIdentifierArgument: Description: 'Use symbol identifier argument instead of string identifier argument.' Enabled: pending diff --git a/lib/rubocop/cop/performance/string_bytesize.rb b/lib/rubocop/cop/performance/string_bytesize.rb new file mode 100644 index 0000000000..17ed614378 --- /dev/null +++ b/lib/rubocop/cop/performance/string_bytesize.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Performance + # Checks for calls to `#bytes` counting method and suggests using `bytesize` instead. + # The `bytesize` method is more efficient and directly returns the size in bytes, + # avoiding the intermediate array allocation that `bytes.size` incurs. + # + # @safety + # This cop is unsafe because it assumes that the receiver + # responds to `#bytesize` method. + # + # @example + # # bad + # string_var.bytes.count + # "foobar".bytes.size + # + # # good + # string_var.bytesize + # "foobar".bytesize + class StringBytesize < Base + extend AutoCorrector + + MSG = 'Use `String#bytesize` instead of calculating the size of the bytes array.' + RESTRICT_ON_SEND = %i[size length count].freeze + + def_node_matcher :string_bytes_method?, <<~MATCHER + (call (call !nil? :bytes) {:size :length :count}) + MATCHER + + def on_send(node) + string_bytes_method?(node) do + range = node.receiver.loc.selector.begin.join(node.source_range.end) + + add_offense(range) do |corrector| + corrector.replace(range, 'bytesize') + end + end + end + alias on_csend on_send + end + end + end +end diff --git a/lib/rubocop/cop/performance_cops.rb b/lib/rubocop/cop/performance_cops.rb index 2a18f26120..e71d4066c7 100644 --- a/lib/rubocop/cop/performance_cops.rb +++ b/lib/rubocop/cop/performance_cops.rb @@ -44,6 +44,7 @@ require_relative 'performance/size' require_relative 'performance/sort_reverse' require_relative 'performance/squeeze' +require_relative 'performance/string_bytesize' require_relative 'performance/start_with' require_relative 'performance/string_identifier_argument' require_relative 'performance/string_include' diff --git a/spec/rubocop/cop/performance/string_bytesize_spec.rb b/spec/rubocop/cop/performance/string_bytesize_spec.rb new file mode 100644 index 0000000000..daa55902b4 --- /dev/null +++ b/spec/rubocop/cop/performance/string_bytesize_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Performance::StringBytesize, :config do + let(:msg) { 'Use `String#bytesize` instead of calculating the size of the bytes array.' } + + it 'registers an offense with `size` method' do + expect_offense(<<~RUBY) + string.bytes.size + ^^^^^^^^^^ #{msg} + RUBY + + expect_correction(<<~RUBY) + string.bytesize + RUBY + end + + it 'registers an offense with `length` method' do + expect_offense(<<~RUBY) + string.bytes.length + ^^^^^^^^^^^^ #{msg} + RUBY + + expect_correction(<<~RUBY) + string.bytesize + RUBY + end + + it 'registers an offense with `count` method' do + expect_offense(<<~RUBY) + string.bytes.count + ^^^^^^^^^^^ #{msg} + RUBY + + expect_correction(<<~RUBY) + string.bytesize + RUBY + end + + it 'registers an offense with string literal' do + expect_offense(<<~RUBY) + "foobar".bytes.count + ^^^^^^^^^^^ #{msg} + RUBY + + expect_correction(<<~RUBY) + "foobar".bytesize + RUBY + end + + it 'registers an offense and autocorrects with safe navigation' do + expect_offense(<<~RUBY) + string&.bytes&.count + ^^^^^^^^^^^^ #{msg} + RUBY + + expect_correction(<<~RUBY) + string&.bytesize + RUBY + end + + it 'registers an offense and autocorrects with partial safe navigation' do + expect_offense(<<~RUBY) + string&.bytes.count + ^^^^^^^^^^^ #{msg} + RUBY + + expect_correction(<<~RUBY) + string&.bytesize + RUBY + end + + it 'does not register an offenses without array size method' do + expect_no_offenses(<<~RUBY) + string.bytes + RUBY + end + + it 'does not register an offenses with `bytes` without explicit receiver' do + expect_no_offenses(<<~RUBY) + bytes.size + RUBY + end +end