Add AvoidObjectSendWithDynamicMethod cop

This commit is contained in:
Sarah Vessels 2024-01-03 13:26:32 -06:00
Родитель 4046c80032
Коммит 5ab0dadc00
2 изменённых файлов: 156 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,76 @@
# frozen_string_literal: true
require "rubocop"
module RuboCop
module Cop
module GitHub
# Public: A Rubocop to discourage using methods like Object#send that allow you to dynamically call other
# methods on a Ruby object, when the method being called is itself completely dynamic. Instead, explicitly call
# methods by name.
#
# Examples:
#
# # bad
# foo.send(some_variable)
#
# # good
# case some_variable
# when "bar"
# foo.bar
# else
# foo.baz
# end
#
# # fine
# foo.send(:bar)
# foo.public_send("some_method")
# foo.__send__("some_#{variable}_method")
class AvoidObjectSendWithDynamicMethod < Base
MESSAGE_TEMPLATE = "Avoid using Object#%s with a dynamic method name."
SEND_METHODS = %i(send public_send __send__).freeze
CONSTANT_TYPES = %i(sym str const).freeze
def on_send(node)
return unless send_method?(node)
return if method_being_sent_is_constrained?(node)
add_offense(source_range_for_method_call(node), message: MESSAGE_TEMPLATE % node.method_name)
end
private
def send_method?(node)
SEND_METHODS.include?(node.method_name)
end
def method_being_sent_is_constrained?(node)
method_name_being_sent_is_constant?(node) || method_name_being_sent_is_dynamic_string_with_constants?(node)
end
def method_name_being_sent_is_constant?(node)
method_being_sent = node.arguments.first
# e.g., `worker.send(:perform)` or `base.send("extend", Foo)`
CONSTANT_TYPES.include?(method_being_sent.type)
end
def method_name_being_sent_is_dynamic_string_with_constants?(node)
method_being_sent = node.arguments.first
return false unless method_being_sent.type == :dstr
# e.g., `foo.send("can_#{action}?")`
method_being_sent.child_nodes.any? { |child_node| CONSTANT_TYPES.include?(child_node.type) }
end
def source_range_for_method_call(node)
begin_pos = if node.receiver # e.g., for `foo.send(:bar)`, `foo` is the receiver
node.receiver.source_range.end_pos
else # e.g., `send(:bar)`
node.source_range.begin_pos
end
end_pos = node.loc.selector.end_pos
Parser::Source::Range.new(processed_source.buffer, begin_pos, end_pos)
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,80 @@
# frozen_string_literal: true
require_relative "./cop_test"
require "minitest/autorun"
require "rubocop/cop/github/avoid_object_send_with_dynamic_method"
class TestAvoidObjectSendWithDynamicMethod < CopTest
def cop_class
RuboCop::Cop::GitHub::AvoidObjectSendWithDynamicMethod
end
def test_offended_by_send_call
offenses = investigate cop, <<-RUBY
def my_method(foo)
foo.send(@some_ivar)
end
RUBY
assert_equal 1, offenses.size
assert_equal "Avoid using Object#send with a dynamic method name.", offenses.first.message
end
def test_offended_by_public_send_call
offenses = investigate cop, <<-RUBY
foo.public_send(bar)
RUBY
assert_equal 1, offenses.size
assert_equal "Avoid using Object#public_send with a dynamic method name.", offenses.first.message
end
def test_offended_by_call_to___send__
offenses = investigate cop, <<-RUBY
foo.__send__(bar)
RUBY
assert_equal 1, offenses.size
assert_equal "Avoid using Object#__send__ with a dynamic method name.", offenses.first.message
end
def test_offended_by_send_calls_without_receiver
offenses = investigate cop, <<-RUBY
send(some_method)
public_send(@some_ivar)
__send__(a_variable, "foo", "bar")
RUBY
assert_equal 3, offenses.size
assert_equal "Avoid using Object#send with a dynamic method name.", offenses[0].message
assert_equal "Avoid using Object#public_send with a dynamic method name.", offenses[1].message
assert_equal "Avoid using Object#__send__ with a dynamic method name.", offenses[2].message
end
def test_unoffended_by_other_method_calls
offenses = investigate cop, <<-RUBY
foo.bar(arg1, arg2)
case @some_ivar
when :foo
baz.foo
when :bar
baz.bar
end
puts "public_send" if send?
RUBY
assert_equal 0, offenses.size
end
def test_unoffended_by_send_calls_to_dynamic_methods_that_include_hardcoded_strings
offenses = investigate cop, <<-'RUBY'
foo.send("can_#{action}?")
foo.public_send("make_#{SOME_CONSTANT}")
RUBY
assert_equal 0, offenses.size
end
def test_unoffended_by_send_calls_without_dynamic_methods
offenses = investigate cop, <<-RUBY
base.send :extend, ClassMethods
foo.public_send(:bar)
foo.__send__("bar", arg1, arg2)
RUBY
assert_equal 0, offenses.size
end
end