Skip to content

Commit 5431a42

Browse files
authored
Merge pull request #145 from github/cop-no-dyn-obj-send
Add GitHub/AvoidObjectSendWithDynamicMethod cop
2 parents 4046c80 + bdbf52f commit 5431a42

File tree

7 files changed

+167
-2
lines changed

7 files changed

+167
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
## v0.21.0
6+
7+
- Added new GitHub/AvoidObjectSendWithDynamicMethod cop to discourage use of methods like Object#send
8+
59
## v0.20.0
610

711
- Updated minimum dependencies for "rubocop" (`>= 1.37`), "rubocop-performance" (`>= 1.15`), and "rubocop-rails", (`>= 2.17`).

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
rubocop-github (0.20.0)
4+
rubocop-github (0.21.0)
55
rubocop (>= 1.37)
66
rubocop-performance (>= 1.15)
77
rubocop-rails (>= 2.17)

config/default.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ Gemspec/RequiredRubyVersion:
4444
Gemspec/RubyVersionGlobalsUsage:
4545
Enabled: false
4646

47+
GitHub/AvoidObjectSendWithDynamicMethod:
48+
Enabled: true
49+
4750
GitHub/InsecureHashAlgorithm:
4851
Enabled: true
4952

lib/rubocop-github.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66

77
RuboCop::GitHub::Inject.default_defaults!
88

9+
require "rubocop/cop/github/avoid_object_send_with_dynamic_method"
910
require "rubocop/cop/github/insecure_hash_algorithm"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require "rubocop"
4+
5+
module RuboCop
6+
module Cop
7+
module GitHub
8+
# Public: A Rubocop to discourage using methods like Object#send that allow you to dynamically call other
9+
# methods on a Ruby object, when the method being called is itself completely dynamic. Instead, explicitly call
10+
# methods by name.
11+
#
12+
# Examples:
13+
#
14+
# # bad
15+
# foo.send(some_variable)
16+
#
17+
# # good
18+
# case some_variable
19+
# when "bar"
20+
# foo.bar
21+
# else
22+
# foo.baz
23+
# end
24+
#
25+
# # fine
26+
# foo.send(:bar)
27+
# foo.public_send("some_method")
28+
# foo.__send__("some_#{variable}_method")
29+
class AvoidObjectSendWithDynamicMethod < Base
30+
MESSAGE_TEMPLATE = "Avoid using Object#%s with a dynamic method name."
31+
SEND_METHODS = %i(send public_send __send__).freeze
32+
CONSTANT_TYPES = %i(sym str const).freeze
33+
34+
def on_send(node)
35+
return unless send_method?(node)
36+
return if method_being_sent_is_constrained?(node)
37+
add_offense(source_range_for_method_call(node), message: MESSAGE_TEMPLATE % node.method_name)
38+
end
39+
40+
private
41+
42+
def send_method?(node)
43+
SEND_METHODS.include?(node.method_name)
44+
end
45+
46+
def method_being_sent_is_constrained?(node)
47+
method_name_being_sent_is_constant?(node) || method_name_being_sent_is_dynamic_string_with_constants?(node)
48+
end
49+
50+
def method_name_being_sent_is_constant?(node)
51+
method_being_sent = node.arguments.first
52+
# e.g., `worker.send(:perform)` or `base.send("extend", Foo)`
53+
CONSTANT_TYPES.include?(method_being_sent.type)
54+
end
55+
56+
def method_name_being_sent_is_dynamic_string_with_constants?(node)
57+
method_being_sent = node.arguments.first
58+
return false unless method_being_sent.type == :dstr
59+
60+
# e.g., `foo.send("can_#{action}?")`
61+
method_being_sent.child_nodes.any? { |child_node| CONSTANT_TYPES.include?(child_node.type) }
62+
end
63+
64+
def source_range_for_method_call(node)
65+
begin_pos =
66+
if node.receiver # e.g., for `foo.send(:bar)`, `foo` is the receiver
67+
node.receiver.source_range.end_pos
68+
else # e.g., `send(:bar)`
69+
node.source_range.begin_pos
70+
end
71+
end_pos = node.loc.selector.end_pos
72+
Parser::Source::Range.new(processed_source.buffer, begin_pos, end_pos)
73+
end
74+
end
75+
end
76+
end
77+
end

rubocop-github.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Gem::Specification.new do |s|
44
s.name = "rubocop-github"
5-
s.version = "0.20.0"
5+
s.version = "0.21.0"
66
s.summary = "RuboCop GitHub"
77
s.description = "Code style checking for GitHub Ruby repositories "
88
s.homepage = "https://github.com/github/rubocop-github"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "./cop_test"
4+
require "minitest/autorun"
5+
require "rubocop/cop/github/avoid_object_send_with_dynamic_method"
6+
7+
class TestAvoidObjectSendWithDynamicMethod < CopTest
8+
def cop_class
9+
RuboCop::Cop::GitHub::AvoidObjectSendWithDynamicMethod
10+
end
11+
12+
def test_offended_by_send_call
13+
offenses = investigate cop, <<-RUBY
14+
def my_method(foo)
15+
foo.send(@some_ivar)
16+
end
17+
RUBY
18+
assert_equal 1, offenses.size
19+
assert_equal "Avoid using Object#send with a dynamic method name.", offenses.first.message
20+
end
21+
22+
def test_offended_by_public_send_call
23+
offenses = investigate cop, <<-RUBY
24+
foo.public_send(bar)
25+
RUBY
26+
assert_equal 1, offenses.size
27+
assert_equal "Avoid using Object#public_send with a dynamic method name.", offenses.first.message
28+
end
29+
30+
def test_offended_by_call_to___send__
31+
offenses = investigate cop, <<-RUBY
32+
foo.__send__(bar)
33+
RUBY
34+
assert_equal 1, offenses.size
35+
assert_equal "Avoid using Object#__send__ with a dynamic method name.", offenses.first.message
36+
end
37+
38+
def test_offended_by_send_calls_without_receiver
39+
offenses = investigate cop, <<-RUBY
40+
send(some_method)
41+
public_send(@some_ivar)
42+
__send__(a_variable, "foo", "bar")
43+
RUBY
44+
assert_equal 3, offenses.size
45+
assert_equal "Avoid using Object#send with a dynamic method name.", offenses[0].message
46+
assert_equal "Avoid using Object#public_send with a dynamic method name.", offenses[1].message
47+
assert_equal "Avoid using Object#__send__ with a dynamic method name.", offenses[2].message
48+
end
49+
50+
def test_unoffended_by_other_method_calls
51+
offenses = investigate cop, <<-RUBY
52+
foo.bar(arg1, arg2)
53+
case @some_ivar
54+
when :foo
55+
baz.foo
56+
when :bar
57+
baz.bar
58+
end
59+
puts "public_send" if send?
60+
RUBY
61+
assert_equal 0, offenses.size
62+
end
63+
64+
def test_unoffended_by_send_calls_to_dynamic_methods_that_include_hardcoded_strings
65+
offenses = investigate cop, <<-'RUBY'
66+
foo.send("can_#{action}?")
67+
foo.public_send("make_#{SOME_CONSTANT}")
68+
RUBY
69+
assert_equal 0, offenses.size
70+
end
71+
72+
def test_unoffended_by_send_calls_without_dynamic_methods
73+
offenses = investigate cop, <<-RUBY
74+
base.send :extend, ClassMethods
75+
foo.public_send(:bar)
76+
foo.__send__("bar", arg1, arg2)
77+
RUBY
78+
assert_equal 0, offenses.size
79+
end
80+
end

0 commit comments

Comments
 (0)