Skip to content

Commit adb7cee

Browse files
koicbbatsov
authored andcommitted
[Fix #9816] Refine Lint/SafeNavigationConsistency
Fixes #9816. As highlighted in user feedback in #9816, the current implementation excessively requires the use of the safe navigation operator. This PR refines `Lint/SafeNavigationConsistency` cop to check that the safe navigation operator is applied consistently and without excess or deficiency.
1 parent 7d6797c commit adb7cee

17 files changed

+245
-118
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#9816](https://github.com/rubocop/rubocop/issues/9816): Refine `Lint/SafeNavigationConsistency` cop to check that the safe navigation operator is applied consistently and without excess or deficiency. ([@koic][])

config/default.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,9 +2334,9 @@ Lint/SafeNavigationChain:
23342334

23352335
Lint/SafeNavigationConsistency:
23362336
Description: >-
2337-
Check to make sure that if safe navigation is used for a method
2338-
call in an `&&` or `||` condition that safe navigation is used
2339-
for all method calls on that same object.
2337+
Check to make sure that if safe navigation is used in an `&&` or `||` condition,
2338+
consistent and appropriate safe navigation, without excess or deficiency,
2339+
is used for all method calls on the same object.
23402340
Enabled: true
23412341
VersionAdded: '0.55'
23422342
VersionChanged: '0.77'

lib/rubocop/cop/correctors/parentheses_corrector.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def correct(corrector, node)
2222
private
2323

2424
def ternary_condition?(node)
25-
node.parent&.if_type? && node.parent&.ternary?
25+
node.parent&.if_type? && node.parent.ternary?
2626
end
2727

2828
def next_char_is_question_mark?(node)

lib/rubocop/cop/layout/empty_line_after_guard_clause.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def next_sibling_parent_empty_or_else?(node)
135135

136136
parent = next_sibling.parent
137137

138-
parent&.if_type? && parent&.else?
138+
parent&.if_type? && parent.else?
139139
end
140140

141141
def next_sibling_empty_or_guard_clause?(node)

lib/rubocop/cop/lint/boolean_symbol.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def on_sym(node)
3636
return unless boolean_symbol?(node)
3737

3838
parent = node.parent
39-
return if parent&.array_type? && parent&.percent_literal?(:symbol)
39+
return if parent&.array_type? && parent.percent_literal?(:symbol)
4040

4141
add_offense(node, message: format(MSG, boolean: node.value)) do |corrector|
4242
autocorrect(corrector, node)

lib/rubocop/cop/lint/literal_in_interpolation.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def space_literal?(node)
173173

174174
def ends_heredoc_line?(node)
175175
grandparent = node.parent.parent
176-
return false unless grandparent&.dstr_type? && grandparent&.heredoc?
176+
return false unless grandparent&.dstr_type? && grandparent.heredoc?
177177

178178
line = processed_source.lines[node.last_line - 1]
179179
line.size == node.loc.last_column + 1
@@ -184,7 +184,7 @@ def in_array_percent_literal?(node)
184184
return false unless parent.dstr_type? || parent.dsym_type?
185185

186186
grandparent = parent.parent
187-
grandparent&.array_type? && grandparent&.percent_literal?
187+
grandparent&.array_type? && grandparent.percent_literal?
188188
end
189189
end
190190
end

lib/rubocop/cop/lint/safe_navigation_consistency.rb

Lines changed: 105 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,87 +3,152 @@
33
module RuboCop
44
module Cop
55
module Lint
6-
# Check to make sure that if safe navigation is used for a method
7-
# call in an `&&` or `||` condition that safe navigation is used for all
8-
# method calls on that same object.
6+
# Check to make sure that if safe navigation is used in an `&&` or `||` condition,
7+
# consistent and appropriate safe navigation, without excess or deficiency,
8+
# is used for all method calls on the same object.
99
#
1010
# @example
1111
# # bad
12-
# foo&.bar && foo.baz
12+
# foo&.bar && foo&.baz
1313
#
14-
# # bad
15-
# foo.bar || foo&.baz
14+
# # good
15+
# foo&.bar && foo.baz
1616
#
1717
# # bad
18-
# foo&.bar && (foobar.baz || foo.baz)
18+
# foo.bar && foo&.baz
1919
#
2020
# # good
2121
# foo.bar && foo.baz
2222
#
23+
# # bad
24+
# foo&.bar || foo.baz
25+
#
2326
# # good
2427
# foo&.bar || foo&.baz
2528
#
29+
# # bad
30+
# foo.bar || foo&.baz
31+
#
2632
# # good
33+
# foo.bar || foo.baz
34+
#
35+
# # bad
2736
# foo&.bar && (foobar.baz || foo&.baz)
2837
#
38+
# # good
39+
# foo&.bar && (foobar.baz || foo.baz)
40+
#
2941
class SafeNavigationConsistency < Base
30-
include IgnoredNode
3142
include NilMethods
3243
extend AutoCorrector
3344

34-
MSG = 'Ensure that safe navigation is used consistently inside of `&&` and `||`.'
45+
USE_DOT_MSG = 'Use `.` instead of unnecessary `&.`.'
46+
USE_SAFE_NAVIGATION_MSG = 'Use `&.` for consistency with safe navigation.'
47+
48+
def on_and(node)
49+
all_operands = collect_operands(node, [])
50+
operand_groups = all_operands.group_by { |operand| receiver_name_as_key(operand, +'') }
51+
52+
operand_groups.each_value do |grouped_operands|
53+
next unless (dot_op, begin_of_rest_operands = find_consistent_parts(grouped_operands))
3554

36-
def on_csend(node)
37-
return unless node.parent&.operator_keyword?
55+
rest_operands = grouped_operands[begin_of_rest_operands..]
56+
rest_operands.each do |operand|
57+
next if already_appropriate_call?(operand, dot_op)
3858

39-
check(node)
59+
register_offense(operand, dot_op)
60+
end
61+
end
4062
end
63+
alias on_or on_and
4164

42-
def check(node)
43-
ancestor = top_conditional_ancestor(node)
44-
conditions = ancestor.conditions
45-
safe_nav_receiver = node.receiver
65+
private
4666

47-
method_calls = conditions.select(&:send_type?)
48-
unsafe_method_calls = unsafe_method_calls(method_calls, safe_nav_receiver)
67+
def collect_operands(node, operand_nodes)
68+
operand_nodes(node.lhs, operand_nodes)
69+
operand_nodes(node.rhs, operand_nodes)
4970

50-
unsafe_method_calls.each do |unsafe_method_call|
51-
location = location(node, unsafe_method_call)
71+
operand_nodes
72+
end
5273

53-
add_offense(location) { |corrector| autocorrect(corrector, unsafe_method_call) }
74+
def receiver_name_as_key(method, fully_receivers)
75+
if method.parent.call_type?
76+
receiver(method.parent, fully_receivers)
77+
else
78+
fully_receivers << method.receiver&.source.to_s
79+
end
80+
end
5481

55-
ignore_node(unsafe_method_call)
82+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
83+
def find_consistent_parts(grouped_operands)
84+
csend_in_and, csend_in_or, send_in_and, send_in_or = most_left_indices(grouped_operands)
85+
86+
if csend_in_and
87+
['.', (send_in_and ? [send_in_and, csend_in_and].min : csend_in_and) + 1]
88+
elsif send_in_or && csend_in_or
89+
send_in_or < csend_in_or ? ['.', send_in_or + 1] : ['&.', csend_in_or + 1]
90+
elsif send_in_and && csend_in_or && send_in_and < csend_in_or
91+
['.', csend_in_or]
5692
end
5793
end
94+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
5895

59-
private
96+
def already_appropriate_call?(operand, dot_op)
97+
(operand.safe_navigation? && dot_op == '&.') || (operand.dot? && dot_op == '.')
98+
end
99+
100+
def register_offense(operand, dot_operator)
101+
offense_range = operand.operator_method? ? operand : operand.loc.dot
102+
message = dot_operator == '.' ? USE_DOT_MSG : USE_SAFE_NAVIGATION_MSG
60103

61-
def autocorrect(corrector, node)
62-
return unless node.dot?
104+
add_offense(offense_range, message: message) do |corrector|
105+
next if operand.operator_method?
63106

64-
corrector.insert_before(node.loc.dot, '&')
107+
corrector.replace(operand.loc.dot, dot_operator)
108+
end
65109
end
66110

67-
def location(node, unsafe_method_call)
68-
node.source_range.join(unsafe_method_call.source_range)
111+
def operand_nodes(operand, operand_nodes)
112+
if operand.operator_keyword?
113+
collect_operands(operand, operand_nodes)
114+
elsif operand.call_type?
115+
operand_nodes << operand
116+
end
69117
end
70118

71-
def top_conditional_ancestor(node)
72-
parent = node.parent
73-
unless parent&.operator_keyword? ||
74-
(parent&.begin_type? && parent.parent&.operator_keyword?)
75-
return node
119+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
120+
def most_left_indices(grouped_operands)
121+
indices = { csend_in_and: nil, csend_in_or: nil, send_in_and: nil, send_in_or: nil }
122+
123+
grouped_operands.each_with_index do |operand, index|
124+
indices[:csend_in_and] ||= index if operand_in_and?(operand) && operand.csend_type?
125+
indices[:csend_in_or] ||= index if operand_in_or?(operand) && operand.csend_type?
126+
indices[:send_in_and] ||= index if operand_in_and?(operand) && !nilable?(operand)
127+
indices[:send_in_or] ||= index if operand_in_or?(operand) && !nilable?(operand)
76128
end
77129

78-
top_conditional_ancestor(parent)
130+
indices.values
79131
end
132+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
80133

81-
def unsafe_method_calls(method_calls, safe_nav_receiver)
82-
method_calls.select do |method_call|
83-
safe_nav_receiver == method_call.receiver &&
84-
!nil_methods.include?(method_call.method_name) &&
85-
!ignored_node?(method_call)
86-
end
134+
def operand_in_and?(node)
135+
return true if node.parent.and_type?
136+
137+
parent = node.parent.parent while node.parent.begin_type?
138+
139+
parent&.and_type?
140+
end
141+
142+
def operand_in_or?(node)
143+
return true if node.parent.or_type?
144+
145+
parent = node.parent.parent while node.parent.begin_type?
146+
147+
parent&.or_type?
148+
end
149+
150+
def nilable?(node)
151+
node.csend_type? || nil_methods.include?(node.method_name)
87152
end
88153
end
89154
end

lib/rubocop/cop/lint/symbol_conversion.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def in_alias?(node)
141141
end
142142

143143
def in_percent_literal_array?(node)
144-
node.parent&.array_type? && node.parent&.percent_literal?
144+
node.parent&.array_type? && node.parent.percent_literal?
145145
end
146146

147147
def correct_hash_key(node)

lib/rubocop/cop/mixin/percent_array.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def invalid_percent_array_context?(node)
1515
parent = node.parent
1616

1717
parent&.send_type? && parent.arguments.include?(node) &&
18-
!parent.parenthesized? && parent&.block_literal?
18+
!parent.parenthesized? && parent.block_literal?
1919
end
2020

2121
# Override to determine values that are invalid in a percent array

lib/rubocop/cop/style/conditional_assignment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def expand_elsif(node, elsif_branches = [])
7373
elsif_branches << node.if_branch
7474

7575
else_branch = node.else_branch
76-
if else_branch&.if_type? && else_branch&.elsif?
76+
if else_branch&.if_type? && else_branch.elsif?
7777
expand_elsif(else_branch, elsif_branches)
7878
else
7979
elsif_branches << else_branch

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