|
3 | 3 | module RuboCop
|
4 | 4 | module Cop
|
5 | 5 | 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. |
9 | 9 | #
|
10 | 10 | # @example
|
11 | 11 | # # bad
|
12 |
| - # foo&.bar && foo.baz |
| 12 | + # foo&.bar && foo&.baz |
13 | 13 | #
|
14 |
| - # # bad |
15 |
| - # foo.bar || foo&.baz |
| 14 | + # # good |
| 15 | + # foo&.bar && foo.baz |
16 | 16 | #
|
17 | 17 | # # bad
|
18 |
| - # foo&.bar && (foobar.baz || foo.baz) |
| 18 | + # foo.bar && foo&.baz |
19 | 19 | #
|
20 | 20 | # # good
|
21 | 21 | # foo.bar && foo.baz
|
22 | 22 | #
|
| 23 | + # # bad |
| 24 | + # foo&.bar || foo.baz |
| 25 | + # |
23 | 26 | # # good
|
24 | 27 | # foo&.bar || foo&.baz
|
25 | 28 | #
|
| 29 | + # # bad |
| 30 | + # foo.bar || foo&.baz |
| 31 | + # |
26 | 32 | # # good
|
| 33 | + # foo.bar || foo.baz |
| 34 | + # |
| 35 | + # # bad |
27 | 36 | # foo&.bar && (foobar.baz || foo&.baz)
|
28 | 37 | #
|
| 38 | + # # good |
| 39 | + # foo&.bar && (foobar.baz || foo.baz) |
| 40 | + # |
29 | 41 | class SafeNavigationConsistency < Base
|
30 |
| - include IgnoredNode |
31 | 42 | include NilMethods
|
32 | 43 | extend AutoCorrector
|
33 | 44 |
|
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)) |
35 | 54 |
|
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) |
38 | 58 |
|
39 |
| - check(node) |
| 59 | + register_offense(operand, dot_op) |
| 60 | + end |
| 61 | + end |
40 | 62 | end
|
| 63 | + alias on_or on_and |
41 | 64 |
|
42 |
| - def check(node) |
43 |
| - ancestor = top_conditional_ancestor(node) |
44 |
| - conditions = ancestor.conditions |
45 |
| - safe_nav_receiver = node.receiver |
| 65 | + private |
46 | 66 |
|
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) |
49 | 70 |
|
50 |
| - unsafe_method_calls.each do |unsafe_method_call| |
51 |
| - location = location(node, unsafe_method_call) |
| 71 | + operand_nodes |
| 72 | + end |
52 | 73 |
|
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 |
54 | 81 |
|
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] |
56 | 92 | end
|
57 | 93 | end
|
| 94 | + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity |
58 | 95 |
|
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 |
60 | 103 |
|
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? |
63 | 106 |
|
64 |
| - corrector.insert_before(node.loc.dot, '&') |
| 107 | + corrector.replace(operand.loc.dot, dot_operator) |
| 108 | + end |
65 | 109 | end
|
66 | 110 |
|
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 |
69 | 117 | end
|
70 | 118 |
|
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) |
76 | 128 | end
|
77 | 129 |
|
78 |
| - top_conditional_ancestor(parent) |
| 130 | + indices.values |
79 | 131 | end
|
| 132 | + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity |
80 | 133 |
|
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) |
87 | 152 | end
|
88 | 153 | end
|
89 | 154 | end
|
|
0 commit comments