Skip to content

Add support for chained method calls in Rails/Presence #1461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/change_presence_to_support_chained_calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#932](https://github.com/rubocop/rubocop-rails/issues/932): Add support for chained method calls in `Rails/Presence`. ([@vlad-pisanov][])
1 change: 1 addition & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ Rails/Presence:
Description: 'Checks code that can be written more easily using `Object#presence` defined by Active Support.'
Enabled: true
VersionAdded: '0.52'
VersionChanged: '<<next>>'

Rails/Present:
Description: 'Enforces use of `present?`.'
Expand Down
70 changes: 52 additions & 18 deletions lib/rubocop/cop/rails/presence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ module Rails
#
# # good
# a.presence || b
#
# @example
# # bad
# a.present? ? a.foo : nil
#
# # bad
# !a.present? ? nil : a.foo
#
# # bad
# a.blank? ? nil : a.foo
#
# # bad
# !a.blank? ? a.foo : nil
#
# # good
# a.presence&.foo
class Presence < Base
include RangeHelp
extend AutoCorrector
Expand All @@ -46,29 +62,29 @@ class Presence < Base
def_node_matcher :redundant_receiver_and_other, <<~PATTERN
{
(if
(send $_recv :present?)
_recv
{(send $_recv :blank?) (send (send $_recv :present?) :!)}
$!begin
_recv
)
(if
(send $_recv :blank?)
$!begin
{(send $_recv :present?) (send (send $_recv :blank?) :!)}
_recv
$!begin
)
}
PATTERN

def_node_matcher :redundant_negative_receiver_and_other, <<~PATTERN
def_node_matcher :redundant_receiver_and_chain, <<~PATTERN
{
(if
(send (send $_recv :present?) :!)
$!begin
_recv
{(send $_recv :blank?) (send (send $_recv :present?) :!)}
{nil? nil_type?}
$(send _recv ...)
)
(if
(send (send $_recv :blank?) :!)
_recv
$!begin
{(send $_recv :present?) (send (send $_recv :blank?) :!)}
$(send _recv ...)
{nil? nil_type?}
)
}
PATTERN
Expand All @@ -82,18 +98,26 @@ def on_if(node)
register_offense(node, receiver, other)
end

redundant_negative_receiver_and_other(node) do |receiver, other|
return if ignore_other_node?(other) || receiver.nil?
redundant_receiver_and_chain(node) do |receiver, chain|
return if ignore_chain_node?(chain) || receiver.nil?

register_offense(node, receiver, other)
register_chain_offense(node, receiver, chain)
end
end

private

def register_offense(node, receiver, other)
add_offense(node, message: message(node, receiver, other)) do |corrector|
corrector.replace(node, replacement(receiver, other, node.left_sibling))
replacement = replacement(receiver, other, node.left_sibling)
add_offense(node, message: message(node, replacement)) do |corrector|
corrector.replace(node, replacement)
end
end

def register_chain_offense(node, receiver, chain)
replacement = chain_replacement(receiver, chain, node.left_sibling)
add_offense(node, message: message(node, replacement)) do |corrector|
corrector.replace(node, replacement)
end
end

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

def message(node, receiver, other)
prefer = replacement(receiver, other, node.left_sibling).gsub(/^\s*|\n/, '')
def ignore_chain_node?(node)
node.method?('[]') || node.arithmetic_operation?
end

def message(node, replacement)
prefer = replacement.gsub(/^\s*|\n/, '')
current = current(node).gsub(/^\s*|\n/, '')
format(MSG, prefer: prefer, current: current)
end
Expand Down Expand Up @@ -146,6 +174,12 @@ def build_source_for_or_method(other)
def method_range(node)
range_between(node.source_range.begin_pos, node.first_argument.source_range.begin_pos - 1)
end

def chain_replacement(receiver, chain, left_sibling)
replaced = "#{receiver.source}.presence&.#{chain.method_name}"
replaced += "(#{chain.arguments.map(&:source).join(', ')})" if chain.arguments?
left_sibling ? "(#{replaced})" : replaced
end
end
end
end
Expand Down
75 changes: 75 additions & 0 deletions spec/rubocop/cop/rails/presence_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,81 @@
RUBY
end

context 'when a method is called on the receiver' do
it 'registers an offense and corrects when `a.present? ? a.foo : nil' do
expect_offense(<<~RUBY)
a.present? ? a.foo : nil
^^^^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.present? ? a.foo : nil`.
RUBY

expect_correction(<<~RUBY)
a.presence&.foo
RUBY
end

it 'registers an offense and corrects when `a.blank? ? nil : a.foo' do
expect_offense(<<~RUBY)
a.blank? ? nil : a.foo
^^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.blank? ? nil : a.foo`.
RUBY

expect_correction(<<~RUBY)
a.presence&.foo
RUBY
end

it 'registers an offense and corrects when `a.foo if a.present?`' do
expect_offense(<<~RUBY)
a.foo if a.present?
^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.foo if a.present?`.
RUBY

expect_correction(<<~RUBY)
a.presence&.foo
RUBY
end

it 'registers an offense and corrects when `a.foo unless a.blank?`' do
expect_offense(<<~RUBY)
a.foo unless a.blank?
^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo` instead of `a.foo unless a.blank?`.
RUBY

expect_correction(<<~RUBY)
a.presence&.foo
RUBY
end

it 'registers an offense and corrects when chained method takes parameters' do
expect_offense(<<~RUBY)
a.present? ? a.foo(42, key: :value) : nil
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `a.presence&.foo(42, key: :value)` instead of `a.present? ? a.foo(42, key: :value) : nil`.
RUBY

expect_correction(<<~RUBY)
a.presence&.foo(42, key: :value)
RUBY
end

it 'does not register an offense when chained method is `[]`' do
expect_no_offenses(<<~RUBY)
a.present? ? a[1] : nil
Copy link
Contributor Author

@vlad-pisanov vlad-pisanov Mar 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be autocorrected, but looks wonky: a.presence&.[](1)

same with arithmetic operators, and multiple method calls

RUBY
end

it 'does not register an offense when chained method is an arithmetic operation' do
expect_no_offenses(<<~RUBY)
a.present? ? a + 42 : nil
RUBY
end

it 'does not register an offense when multiple methods are chained' do
expect_no_offenses(<<~RUBY)
a.present? ? a.foo.bar : nil
RUBY
end
end

context 'when multiline ternary can be replaced' do
it 'registers an offense and corrects' do
expect_offense(<<~RUBY)
Expand Down
Loading