Skip to content

Commit 84c131f

Browse files
authored
Merge pull request #1461 from vlad-pisanov/vp_presence_1
Add support for chained method calls in `Rails/Presence`
2 parents 289d8c7 + a010b64 commit 84c131f

File tree

4 files changed

+129
-18
lines changed

4 files changed

+129
-18
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#932](https://github.com/rubocop/rubocop-rails/issues/932): Add support for chained method calls in `Rails/Presence`. ([@vlad-pisanov][])

config/default.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ Rails/Presence:
844844
Description: 'Checks code that can be written more easily using `Object#presence` defined by Active Support.'
845845
Enabled: true
846846
VersionAdded: '0.52'
847+
VersionChanged: '<<next>>'
847848

848849
Rails/Present:
849850
Description: 'Enforces use of `present?`.'

lib/rubocop/cop/rails/presence.rb

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ module Rails
3737
#
3838
# # good
3939
# a.presence || b
40+
#
41+
# @example
42+
# # bad
43+
# a.present? ? a.foo : nil
44+
#
45+
# # bad
46+
# !a.present? ? nil : a.foo
47+
#
48+
# # bad
49+
# a.blank? ? nil : a.foo
50+
#
51+
# # bad
52+
# !a.blank? ? a.foo : nil
53+
#
54+
# # good
55+
# a.presence&.foo
4056
class Presence < Base
4157
include RangeHelp
4258
extend AutoCorrector
@@ -46,29 +62,29 @@ class Presence < Base
4662
def_node_matcher :redundant_receiver_and_other, <<~PATTERN
4763
{
4864
(if
49-
(send $_recv :present?)
50-
_recv
65+
{(send $_recv :blank?) (send (send $_recv :present?) :!)}
5166
$!begin
67+
_recv
5268
)
5369
(if
54-
(send $_recv :blank?)
55-
$!begin
70+
{(send $_recv :present?) (send (send $_recv :blank?) :!)}
5671
_recv
72+
$!begin
5773
)
5874
}
5975
PATTERN
6076

61-
def_node_matcher :redundant_negative_receiver_and_other, <<~PATTERN
77+
def_node_matcher :redundant_receiver_and_chain, <<~PATTERN
6278
{
6379
(if
64-
(send (send $_recv :present?) :!)
65-
$!begin
66-
_recv
80+
{(send $_recv :blank?) (send (send $_recv :present?) :!)}
81+
{nil? nil_type?}
82+
$(send _recv ...)
6783
)
6884
(if
69-
(send (send $_recv :blank?) :!)
70-
_recv
71-
$!begin
85+
{(send $_recv :present?) (send (send $_recv :blank?) :!)}
86+
$(send _recv ...)
87+
{nil? nil_type?}
7288
)
7389
}
7490
PATTERN
@@ -82,18 +98,26 @@ def on_if(node)
8298
register_offense(node, receiver, other)
8399
end
84100

85-
redundant_negative_receiver_and_other(node) do |receiver, other|
86-
return if ignore_other_node?(other) || receiver.nil?
101+
redundant_receiver_and_chain(node) do |receiver, chain|
102+
return if ignore_chain_node?(chain) || receiver.nil?
87103

88-
register_offense(node, receiver, other)
104+
register_chain_offense(node, receiver, chain)
89105
end
90106
end
91107

92108
private
93109

94110
def register_offense(node, receiver, other)
95-
add_offense(node, message: message(node, receiver, other)) do |corrector|
96-
corrector.replace(node, replacement(receiver, other, node.left_sibling))
111+
replacement = replacement(receiver, other, node.left_sibling)
112+
add_offense(node, message: message(node, replacement)) do |corrector|
113+
corrector.replace(node, replacement)
114+
end
115+
end
116+
117+
def register_chain_offense(node, receiver, chain)
118+
replacement = chain_replacement(receiver, chain, node.left_sibling)
119+
add_offense(node, message: message(node, replacement)) do |corrector|
120+
corrector.replace(node, replacement)
97121
end
98122
end
99123

@@ -105,8 +129,12 @@ def ignore_other_node?(node)
105129
node&.type?(:if, :rescue, :while)
106130
end
107131

108-
def message(node, receiver, other)
109-
prefer = replacement(receiver, other, node.left_sibling).gsub(/^\s*|\n/, '')
132+
def ignore_chain_node?(node)
133+
node.method?('[]') || node.arithmetic_operation?
134+
end
135+
136+
def message(node, replacement)
137+
prefer = replacement.gsub(/^\s*|\n/, '')
110138
current = current(node).gsub(/^\s*|\n/, '')
111139
format(MSG, prefer: prefer, current: current)
112140
end
@@ -146,6 +174,12 @@ def build_source_for_or_method(other)
146174
def method_range(node)
147175
range_between(node.source_range.begin_pos, node.first_argument.source_range.begin_pos - 1)
148176
end
177+
178+
def chain_replacement(receiver, chain, left_sibling)
179+
replaced = "#{receiver.source}.presence&.#{chain.method_name}"
180+
replaced += "(#{chain.arguments.map(&:source).join(', ')})" if chain.arguments?
181+
left_sibling ? "(#{replaced})" : replaced
182+
end
149183
end
150184
end
151185
end

spec/rubocop/cop/rails/presence_spec.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,81 @@
233233
RUBY
234234
end
235235

236+
context 'when a method is called on the receiver' do
237+
it 'registers an offense and corrects when `a.present? ? a.foo : nil' do
238+
expect_offense(<<~RUBY)
239+
a.present? ? a.foo : nil
240+
^^^^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.present? ? a.foo : nil`.
241+
RUBY
242+
243+
expect_correction(<<~RUBY)
244+
a.presence&.foo
245+
RUBY
246+
end
247+
248+
it 'registers an offense and corrects when `a.blank? ? nil : a.foo' do
249+
expect_offense(<<~RUBY)
250+
a.blank? ? nil : a.foo
251+
^^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.blank? ? nil : a.foo`.
252+
RUBY
253+
254+
expect_correction(<<~RUBY)
255+
a.presence&.foo
256+
RUBY
257+
end
258+
259+
it 'registers an offense and corrects when `a.foo if a.present?`' do
260+
expect_offense(<<~RUBY)
261+
a.foo if a.present?
262+
^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.foo if a.present?`.
263+
RUBY
264+
265+
expect_correction(<<~RUBY)
266+
a.presence&.foo
267+
RUBY
268+
end
269+
270+
it 'registers an offense and corrects when `a.foo unless a.blank?`' do
271+
expect_offense(<<~RUBY)
272+
a.foo unless a.blank?
273+
^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.foo unless a.blank?`.
274+
RUBY
275+
276+
expect_correction(<<~RUBY)
277+
a.presence&.foo
278+
RUBY
279+
end
280+
281+
it 'registers an offense and corrects when chained method takes parameters' do
282+
expect_offense(<<~RUBY)
283+
a.present? ? a.foo(42, key: :value) : nil
284+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo(42, key: :value)` instead of `a.present? ? a.foo(42, key: :value) : nil`.
285+
RUBY
286+
287+
expect_correction(<<~RUBY)
288+
a.presence&.foo(42, key: :value)
289+
RUBY
290+
end
291+
292+
it 'does not register an offense when chained method is `[]`' do
293+
expect_no_offenses(<<~RUBY)
294+
a.present? ? a[1] : nil
295+
RUBY
296+
end
297+
298+
it 'does not register an offense when chained method is an arithmetic operation' do
299+
expect_no_offenses(<<~RUBY)
300+
a.present? ? a + 42 : nil
301+
RUBY
302+
end
303+
304+
it 'does not register an offense when multiple methods are chained' do
305+
expect_no_offenses(<<~RUBY)
306+
a.present? ? a.foo.bar : nil
307+
RUBY
308+
end
309+
end
310+
236311
context 'when multiline ternary can be replaced' do
237312
it 'registers an offense and corrects' do
238313
expect_offense(<<~RUBY)

0 commit comments

Comments
 (0)