diff --git a/CHANGELOG.md b/CHANGELOG.md index 08647f45..9fa7ddc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## v0.21.0 + +- Added new GitHub/AvoidObjectSendWithDynamicMethod cop to discourage use of methods like Object#send + ## v0.20.0 - Updated minimum dependencies for "rubocop" (`>= 1.37`), "rubocop-performance" (`>= 1.15`), and "rubocop-rails", (`>= 2.17`). diff --git a/Gemfile.lock b/Gemfile.lock index 411b6ac6..0d748cfe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rubocop-github (0.20.0) + rubocop-github (0.21.0) rubocop (>= 1.37) rubocop-performance (>= 1.15) rubocop-rails (>= 2.17) diff --git a/config/default.yml b/config/default.yml index 0ea967a6..5a319885 100644 --- a/config/default.yml +++ b/config/default.yml @@ -44,6 +44,9 @@ Gemspec/RequiredRubyVersion: Gemspec/RubyVersionGlobalsUsage: Enabled: false +GitHub/AvoidObjectSendWithDynamicMethod: + Enabled: true + GitHub/InsecureHashAlgorithm: Enabled: true diff --git a/lib/rubocop-github.rb b/lib/rubocop-github.rb index 17c07e77..65bc8dc8 100644 --- a/lib/rubocop-github.rb +++ b/lib/rubocop-github.rb @@ -6,4 +6,5 @@ RuboCop::GitHub::Inject.default_defaults! +require "rubocop/cop/github/avoid_object_send_with_dynamic_method" require "rubocop/cop/github/insecure_hash_algorithm" diff --git a/lib/rubocop/cop/github/avoid_object_send_with_dynamic_method.rb b/lib/rubocop/cop/github/avoid_object_send_with_dynamic_method.rb new file mode 100644 index 00000000..e5e7677d --- /dev/null +++ b/lib/rubocop/cop/github/avoid_object_send_with_dynamic_method.rb @@ -0,0 +1,77 @@ +# 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 diff --git a/rubocop-github.gemspec b/rubocop-github.gemspec index 00242746..ed3a5816 100644 --- a/rubocop-github.gemspec +++ b/rubocop-github.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = "rubocop-github" - s.version = "0.20.0" + s.version = "0.21.0" s.summary = "RuboCop GitHub" s.description = "Code style checking for GitHub Ruby repositories " s.homepage = "https://github.com/github/rubocop-github" diff --git a/test/test_avoid_object_send_with_dynamic_method.rb b/test/test_avoid_object_send_with_dynamic_method.rb new file mode 100644 index 00000000..68d529fb --- /dev/null +++ b/test/test_avoid_object_send_with_dynamic_method.rb @@ -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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy