Skip to content

Commit

Permalink
[Fix #12842] Add new Style/StaticSend cop
Browse files Browse the repository at this point in the history
Fixes #12842.

## Summary

Detects the use of the `public_send` method with a static method name argument.
Since the `send` method can be used to call private methods, by default,
only the `public_send` method is detected.

```ruby
# bad
obj.public_send(:static_name)
obj.public_send('static_name')

# good
obj.static_name
```

## Safety

This cop is not safe because it can incorrectly detect based on the receiver.
Additionally, when `AllowSend` is set to `true`, it cannot determine whether
the `send` method being detected is calling a private method.

## `AllowSend` option

This cop has `AllowSend` option.

### AllowSend: true (default)

```ruby
# good
obj.send(:static_name)
obj.send('static_name')
obj.__send__(:static_name)
obj.__send__('static_name')
```

### AllowSend: false

```ruby
# bad
obj.send(:static_name)
obj.send('static_name')
obj.__send__(:static_name)
obj.__send__('static_name')

# good
obj.static_name
```
  • Loading branch information
koic committed Apr 16, 2024
1 parent 5c4cd37 commit 09071bc
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_add_new_style_static_send_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#12842](https://github.com/rubocop/rubocop/issues/12842): Add new `Style/StaticSend` cop. ([@koic][])
7 changes: 7 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5332,6 +5332,13 @@ Style/StaticClass:
Safe: false
VersionAdded: '1.3'

Style/StaticSend:
Description: 'Detects the use of the `public_send` method with a static method name argument.'
Enabled: pending
Safe: false
AllowSend: true
VersionAdded: '<<next>>'

Style/StderrPuts:
Description: 'Use `warn` instead of `$stderr.puts`.'
StyleGuide: '#warn'
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@
require_relative 'rubocop/cop/style/slicing_with_range'
require_relative 'rubocop/cop/style/special_global_vars'
require_relative 'rubocop/cop/style/stabby_lambda_parentheses'
require_relative 'rubocop/cop/style/static_send'
require_relative 'rubocop/cop/style/stderr_puts'
require_relative 'rubocop/cop/style/string_chars'
require_relative 'rubocop/cop/style/string_concatenation'
Expand Down
83 changes: 83 additions & 0 deletions lib/rubocop/cop/style/static_send.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Style
# Detects the use of the `public_send` method with a static method name argument.
# Since the `send` method can be used to call private methods, by default,
# only the `public_send` method is detected.
#
# @safety
# This cop is not safe because it can incorrectly detect based on the receiver.
# Additionally, when `AllowSend` is set to `true`, it cannot determine whether
# the `send` method being detected is calling a private method.
#
# @example
# # bad
# obj.public_send(:static_name)
# obj.public_send('static_name')
#
# # good
# obj.static_name
#
# @example AllowSend: true (default)
# # good
# obj.send(:static_name)
# obj.send('static_name')
# obj.__send__(:static_name)
# obj.__send__('static_name')
#
# @example AllowSend: false
# # bad
# obj.send(:static_name)
# obj.send('static_name')
# obj.__send__(:static_name)
# obj.__send__('static_name')
#
# # good
# obj.static_name
#
class StaticSend < Base
extend AutoCorrector

MSG = 'Use `%<method_name>s` method call directly instead.'
RESTRICT_ON_SEND = %i[public_send send __send__].freeze
LITERALS = %i[sym str].freeze

# rubocop:disable Metrics/AbcSize
def on_send(node)
return if allow_send? && !node.method?(:public_send)
return unless (first_argument = node.first_argument)
return unless LITERALS.include?(first_argument.type)

offense_range = offense_range(node)
method_name = first_argument.value

add_offense(offense_range, message: format(MSG, method_name: method_name)) do |corrector|
if node.arguments.one?
corrector.replace(offense_range, method_name)
else
corrector.replace(node.loc.selector, method_name)
corrector.remove(removal_argument_range(first_argument, node.arguments[1]))
end
end
end
# rubocop:enable Metrics/AbcSize

private

def allow_send?
!!cop_config['AllowSend']
end

def offense_range(node)
node.loc.selector.join(node.source_range.end)
end

def removal_argument_range(first_argument, second_argument)
first_argument.source_range.begin.join(second_argument.source_range.begin)
end
end
end
end
end
130 changes: 130 additions & 0 deletions spec/rubocop/cop/style/static_send_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Style::StaticSend, :config do
it 'registers an offense when using `public_send` with symbol literal argument' do
expect_offense(<<~RUBY)
obj.public_send(:foo)
^^^^^^^^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
obj.foo
RUBY
end

it 'registers an offense when using `public_send` with symbol literal argument and some arguments with parentheses' do
expect_offense(<<~RUBY)
obj.public_send(:foo, bar, 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
obj.foo(bar, 42)
RUBY
end

it 'registers an offense when using `public_send` with symbol literal argument and some arguments without parentheses' do
expect_offense(<<~RUBY)
obj.public_send :foo, bar, 42
^^^^^^^^^^^^^^^^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
obj.foo bar, 42
RUBY
end

it 'registers an offense when using `public_send` with symbol literal argument without receiver' do
expect_offense(<<~RUBY)
public_send(:foo)
^^^^^^^^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
foo
RUBY
end

it 'registers an offense when using `public_send` with string literal argument' do
expect_offense(<<~RUBY)
obj.public_send('foo')
^^^^^^^^^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
obj.foo
RUBY
end

it 'does not register an offense when using `public_send` with variable argument' do
expect_no_offenses(<<~RUBY)
obj.public_send(variable)
RUBY
end

it 'does not register an offense when using `public_send` with interpolated string argument' do
expect_no_offenses(<<~'RUBY')
obj.public_send("#{interpolated}string")
RUBY
end

it 'does not register an offense when using `public_send` with integer literal argument' do
expect_no_offenses(<<~RUBY)
obj.public_send(42)
RUBY
end

it 'does not register an offense when using `public_send` with no arguments' do
expect_no_offenses(<<~RUBY)
obj.public_send
RUBY
end

it 'does not register an offense when using method call without `public_send`' do
expect_no_offenses(<<~RUBY)
obj.foo
RUBY
end

context 'when `AllowSend: true`' do
let(:cop_config) { { 'AllowSend' => true } }

it 'does not register an offense when using `send` with symbol literal argumen' do
expect_no_offenses(<<~RUBY)
obj.send(:foo)
RUBY
end

it 'does not register an offense when using `__send__` with symbol literal argument' do
expect_no_offenses(<<~RUBY)
obj.__send__(:foo)
RUBY
end
end

context 'when `AllowSend: false`' do
let(:cop_config) { { 'AllowSend' => false } }

it 'registers an offense when using `send` with symbol literal argument' do
expect_offense(<<~RUBY)
obj.send(:foo)
^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
obj.foo
RUBY
end

it 'registers an offense when using `__send__` with symbol literal argument' do
expect_offense(<<~RUBY)
obj.__send__(:foo)
^^^^^^^^^^^^^^ Use `foo` method call directly instead.
RUBY

expect_correction(<<~RUBY)
obj.foo
RUBY
end
end
end

0 comments on commit 09071bc

Please sign in to comment.