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)
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