From 896ba88176f5f73c1e4454891726c2369fba0007 Mon Sep 17 00:00:00 2001 From: viralpraxis Date: Tue, 22 Oct 2024 09:40:15 +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 | 49 +++++++++++ lib/rubocop/cop/performance_cops.rb | 1 + .../cop/performance/string_bytesize_spec.rb | 83 +++++++++++++++++++ 5 files changed, 140 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..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..ebecc836b4 --- /dev/null +++ b/lib/rubocop/cop/performance/string_bytesize.rb @@ -0,0 +1,49 @@ +# 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 + add_offense(node) do |corrector| + corrector.replace(node, replacement(node)) + end + end + end + alias on_csend on_send + + def replacement(node) + receiver = node.receiver + + "#{receiver.receiver.source}#{receiver.loc.dot.source}bytesize" + end + 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..e3a341af78 --- /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