From d3637be8a7a036c9e84be09f3e68039adca9a0fa Mon Sep 17 00:00:00 2001 From: viralpraxis Date: Wed, 30 Oct 2024 21:03:29 +0300 Subject: [PATCH] [Fix #475] Add new `Performance/StringBytesize` cop --- changelog/new_string_bytesize_cop.md | 1 + config/default.yml | 6 ++ .../cop/performance/string_bytesize.rb | 45 ++++++++++ lib/rubocop/cop/performance_cops.rb | 1 + .../cop/performance/string_bytesize_spec.rb | 89 +++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 changelog/new_string_bytesize_cop.md create mode 100644 lib/rubocop/cop/performance/string_bytesize.rb create mode 100644 spec/rubocop/cop/performance/string_bytesize_spec.rb 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..6492c2a7ba 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." + Safe: 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..29ffaae3d8 --- /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? int} :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..29f3b4116d --- /dev/null +++ b/spec/rubocop/cop/performance/string_bytesize_spec.rb @@ -0,0 +1,89 @@ +# 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 offense without array size method' do + expect_no_offenses(<<~RUBY) + string.bytes + RUBY + end + + it 'does not register an offense with `bytes` without explicit receiver' do + expect_no_offenses(<<~RUBY) + bytes.size + RUBY + end + + it 'does not register an offense when the receiver is of type `int`' do + expect_no_offenses(<<~RUBY) + 3.bytes.size + RUBY + end +end