Skip to content

Commit 8e0d453

Browse files
committed
Make ReadWriteAttribute cop aware of shadowing methods
1 parent 377bf82 commit 8e0d453

File tree

3 files changed

+68
-0
lines changed

3 files changed

+68
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#585](https://github.com/rubocop/rubocop-rails/pull/585): Make `Rails/ReadWriteAttribute` cop aware of shadowing methods. ([@drenmi][])

lib/rubocop/cop/rails/read_write_attribute.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ module Rails
2323
# # good
2424
# x = self[:attr]
2525
# self[:attr] = val
26+
#
27+
# When called from within a method with the same name as the attribute,
28+
# `read_attribute` and `write_attribute` must be used to prevent an
29+
# infinite loop:
30+
#
31+
# @example
32+
#
33+
# # good
34+
# def foo
35+
# bar || read_attribute(:foo)
36+
# end
2637
class ReadWriteAttribute < Base
2738
extend AutoCorrector
2839

@@ -38,6 +49,7 @@ class ReadWriteAttribute < Base
3849

3950
def on_send(node)
4051
return unless read_write_attribute?(node)
52+
return if within_shadowing_method?(node)
4153

4254
add_offense(node.loc.selector, message: message(node)) do |corrector|
4355
case node.method_name
@@ -53,6 +65,15 @@ def on_send(node)
5365

5466
private
5567

68+
def within_shadowing_method?(node)
69+
node.each_ancestor(:def).any? do |enclosing_method|
70+
shadowing_method_name = node.first_argument.value.to_s
71+
shadowing_method_name << '=' if node.method?(:write_attribute)
72+
73+
enclosing_method.method_name.to_s == shadowing_method_name
74+
end
75+
end
76+
5677
def message(node)
5778
if node.method?(:read_attribute)
5879
format(MSG, prefer: 'self[:attr]', current: 'read_attribute(:attr)')

spec/rubocop/cop/rails/read_write_attribute_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@
2424
RUBY
2525
end
2626

27+
it 'does not register an offense when called from a method with the same name' do
28+
expect_no_offenses(<<~RUBY)
29+
def foo
30+
bar || read_attribute(:foo)
31+
end
32+
RUBY
33+
end
34+
35+
it 'registers an offense when called from a method with a different name' do
36+
expect_offense(<<~RUBY)
37+
def foo
38+
bar || read_attribute(:baz)
39+
^^^^^^^^^^^^^^ Prefer `self[:attr]` over `read_attribute(:attr)`.
40+
end
41+
RUBY
42+
43+
expect_correction(<<~RUBY)
44+
def foo
45+
bar || self[:baz]
46+
end
47+
RUBY
48+
end
49+
2750
it 'autocorrects without parentheses' do
2851
expect_offense(<<~RUBY)
2952
res = read_attribute 'test'
@@ -106,6 +129,29 @@
106129
RUBY
107130
end
108131

132+
it 'does not register an offense when called from a method with the same name' do
133+
expect_no_offenses(<<~RUBY)
134+
def foo=(value)
135+
bar(value) || write_attribute(:foo, "baz")
136+
end
137+
RUBY
138+
end
139+
140+
it 'registers an offense when called from a method with a different name' do
141+
expect_offense(<<~RUBY)
142+
def foo=(value)
143+
bar(value) || write_attribute(:baz, "baz")
144+
^^^^^^^^^^^^^^^ Prefer `self[:attr] = val` over `write_attribute(:attr, val)`.
145+
end
146+
RUBY
147+
148+
expect_correction(<<~RUBY)
149+
def foo=(value)
150+
bar(value) || self[:baz] = "baz"
151+
end
152+
RUBY
153+
end
154+
109155
it 'corrects assignment with chained methods' do
110156
expect_offense(<<~RUBY)
111157
write_attribute(:attr, 'test_' + postfix)

0 commit comments

Comments
 (0)