Skip to content

Commit a2e54c4

Browse files
authored
Merge pull request #1536 from koic/fix_false_positives_for_rails_find_by_or_assignment_memoization
[Fix #1532] Fix false positives for `Rails/FindByOrAssignmentMemoization`
2 parents b2ccb82 + 972ad23 commit a2e54c4

File tree

3 files changed

+65
-20
lines changed

3 files changed

+65
-20
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#1532](https://github.com/rubocop/rubocop-rails/issues/1532): Fix false positives for `Rails/FindByOrAssignmentMemoization` when assigning a memoization instance variable at `initialize` method. ([@koic][])

lib/rubocop/cop/rails/find_by_or_assignment_memoization.rb

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ module Rails
88
# It is common to see code that attempts to memoize `find_by` result by `||=`,
99
# but `find_by` may return `nil`, in which case it is not memoized as intended.
1010
#
11+
# NOTE: Respecting the object shapes introduced in Ruby 3.2, instance variables used
12+
# for memoization that are initialized at object creation are ignored.
13+
#
1114
# @safety
1215
# This cop is unsafe because detected `find_by` may not be Active Record's method,
1316
# or the code may have a different purpose than memoization.
@@ -56,14 +59,16 @@ class FindByOrAssignmentMemoization < Base
5659

5760
# When a method body contains only memoization, the correction can be more succinct.
5861
def on_def(node)
59-
find_by_or_assignment_memoization(node.body) do |varible_name, find_by|
62+
find_by_or_assignment_memoization(node.body) do |variable_name, find_by|
63+
next if instance_variable_assigned?(variable_name)
64+
6065
add_offense(node.body) do |corrector|
6166
corrector.replace(
6267
node.body,
6368
<<~RUBY.rstrip
64-
return #{varible_name} if defined?(#{varible_name})
69+
return #{variable_name} if defined?(#{variable_name})
6570
66-
#{varible_name} = #{find_by.source}
71+
#{variable_name} = #{find_by.source}
6772
RUBY
6873
)
6974

@@ -74,17 +79,18 @@ def on_def(node)
7479

7580
def on_send(node)
7681
assignment_node = node.parent
77-
find_by_or_assignment_memoization(assignment_node) do |varible_name, find_by|
78-
next if assignment_node.each_ancestor(:if).any?
82+
83+
find_by_or_assignment_memoization(assignment_node) do |variable_name, find_by|
84+
next if assignment_node.each_ancestor(:if).any? || instance_variable_assigned?(variable_name)
7985

8086
add_offense(assignment_node) do |corrector|
8187
corrector.replace(
8288
assignment_node,
8389
<<~RUBY.rstrip
84-
if defined?(#{varible_name})
85-
#{varible_name}
90+
if defined?(#{variable_name})
91+
#{variable_name}
8692
else
87-
#{varible_name} = #{find_by.source}
93+
#{variable_name} = #{find_by.source}
8894
end
8995
RUBY
9096
)
@@ -94,6 +100,18 @@ def on_send(node)
94100

95101
private
96102

103+
def instance_variable_assigned?(instance_variable_name)
104+
initialize_methods.any? do |def_node|
105+
def_node.each_descendant(:ivasgn).any? do |asgn_node|
106+
asgn_node.name == instance_variable_name
107+
end
108+
end
109+
end
110+
111+
def initialize_methods
112+
@initialize_methods ||= processed_source.ast.each_descendant(:def).select { |node| node.method?(:initialize) }
113+
end
114+
97115
def correct_to_regular_method_definition(corrector, node)
98116
range = node.loc.assignment.join(node.body.source_range.begin)
99117

spec/rubocop/cop/rails/find_by_or_assignment_memoization_spec.rb

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,52 @@
1919
end
2020

2121
context 'when using `find_by` with `||=` in a method body' do
22-
it 'registers an offense' do
22+
it 'registers an offense when not assigning the instance variable in the `initialize` method' do
2323
expect_offense(<<~RUBY)
24-
def foo
25-
@current_user ||= User.find_by(id: session[:user_id])
26-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid memoizing `find_by` results with `||=`.
24+
class Foo
25+
def initialize
26+
@not_current_user = nil
27+
end
28+
29+
def current_user
30+
@current_user ||= User.find_by(id: session[:user_id])
31+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid memoizing `find_by` results with `||=`.
32+
end
2733
end
2834
RUBY
2935

3036
expect_correction(<<~RUBY)
31-
def foo
32-
return @current_user if defined?(@current_user)
37+
class Foo
38+
def initialize
39+
@not_current_user = nil
40+
end
41+
42+
def current_user
43+
return @current_user if defined?(@current_user)
3344
3445
@current_user = User.find_by(id: session[:user_id])
46+
end
47+
end
48+
RUBY
49+
end
50+
51+
it 'does not register an offense when assigning the instance variable in the `initialize` method' do
52+
expect_no_offenses(<<~RUBY)
53+
class Foo
54+
def initialize
55+
@current_user = nil
56+
end
57+
58+
def current_user
59+
@current_user ||= User.find_by(id: session[:user_id])
60+
end
3561
end
3662
RUBY
3763
end
3864

39-
it 'registers registers an offense when the method contains other code' do
65+
it 'registers an offense when the method contains other code' do
4066
expect_offense(<<~RUBY)
41-
def foo
67+
def current_user
4268
@current_user ||= User.find_by(id: session[:user_id])
4369
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid memoizing `find_by` results with `||=`.
4470
@@ -47,7 +73,7 @@ def foo
4773
RUBY
4874

4975
expect_correction(<<~RUBY)
50-
def foo
76+
def current_user
5177
if defined?(@current_user)
5278
@current_user
5379
else
@@ -61,12 +87,12 @@ def foo
6187

6288
it 'registers an offense when using endless method definition', :ruby30 do
6389
expect_offense(<<~RUBY)
64-
def foo(arg) = @current_user ||= User.find_by(id: session[:user_id])
65-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid memoizing `find_by` results with `||=`.
90+
def current_user(arg) = @current_user ||= User.find_by(id: session[:user_id])
91+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid memoizing `find_by` results with `||=`.
6692
RUBY
6793

6894
expect_correction(<<~RUBY)
69-
def foo(arg)#{' '}
95+
def current_user(arg)#{' '}
7096
return @current_user if defined?(@current_user)
7197
7298
@current_user = User.find_by(id: session[:user_id])

0 commit comments

Comments
 (0)