rubocop 1.66.1 → 1.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +55 -6
  4. data/config/internal_affairs.yml +11 -0
  5. data/lib/rubocop/cached_data.rb +12 -4
  6. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -7
  7. data/lib/rubocop/cli/command/execute_runner.rb +1 -1
  8. data/lib/rubocop/cli/command/lsp.rb +2 -2
  9. data/lib/rubocop/cli/command/version.rb +2 -2
  10. data/lib/rubocop/config_loader_resolver.rb +3 -3
  11. data/lib/rubocop/config_validator.rb +2 -1
  12. data/lib/rubocop/cop/autocorrect_logic.rb +22 -2
  13. data/lib/rubocop/cop/base.rb +6 -2
  14. data/lib/rubocop/cop/bundler/gem_version.rb +1 -0
  15. data/lib/rubocop/cop/cop.rb +8 -0
  16. data/lib/rubocop/cop/correctors/alignment_corrector.rb +1 -12
  17. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +1 -1
  18. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +10 -0
  19. data/lib/rubocop/cop/internal_affairs/cop_description.rb +0 -4
  20. data/lib/rubocop/cop/internal_affairs/redundant_message_argument.rb +6 -21
  21. data/lib/rubocop/cop/internal_affairs/redundant_source_range.rb +8 -1
  22. data/lib/rubocop/cop/internal_affairs/useless_message_assertion.rb +0 -5
  23. data/lib/rubocop/cop/internal_affairs.rb +16 -0
  24. data/lib/rubocop/cop/layout/access_modifier_indentation.rb +5 -1
  25. data/lib/rubocop/cop/layout/def_end_alignment.rb +1 -1
  26. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -1
  27. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +8 -0
  28. data/lib/rubocop/cop/layout/indentation_width.rb +4 -5
  29. data/lib/rubocop/cop/layout/leading_comment_space.rb +56 -1
  30. data/lib/rubocop/cop/layout/space_before_brackets.rb +5 -5
  31. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +4 -0
  32. data/lib/rubocop/cop/lint/ambiguous_range.rb +4 -1
  33. data/lib/rubocop/cop/lint/big_decimal_new.rb +4 -7
  34. data/lib/rubocop/cop/lint/boolean_symbol.rb +1 -1
  35. data/lib/rubocop/cop/lint/duplicate_branch.rb +39 -4
  36. data/lib/rubocop/cop/lint/duplicate_set_element.rb +74 -0
  37. data/lib/rubocop/cop/lint/ensure_return.rb +0 -3
  38. data/lib/rubocop/cop/lint/erb_new_arguments.rb +1 -1
  39. data/lib/rubocop/cop/lint/float_comparison.rb +1 -1
  40. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +10 -4
  41. data/lib/rubocop/cop/lint/it_without_arguments_in_block.rb +5 -14
  42. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +25 -2
  43. data/lib/rubocop/cop/lint/non_atomic_file_operation.rb +7 -0
  44. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +5 -6
  45. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -1
  46. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +9 -0
  47. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +107 -41
  48. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -1
  49. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +88 -0
  50. data/lib/rubocop/cop/lint/uri_regexp.rb +25 -7
  51. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +4 -1
  52. data/lib/rubocop/cop/mixin/check_line_breakable.rb +10 -0
  53. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +24 -0
  54. data/lib/rubocop/cop/mixin/frozen_string_literal.rb +3 -1
  55. data/lib/rubocop/cop/mixin/percent_array.rb +1 -1
  56. data/lib/rubocop/cop/mixin/statement_modifier.rb +3 -2
  57. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  58. data/lib/rubocop/cop/naming/inclusive_language.rb +12 -3
  59. data/lib/rubocop/cop/naming/predicate_name.rb +1 -1
  60. data/lib/rubocop/cop/offense.rb +4 -5
  61. data/lib/rubocop/cop/style/access_modifier_declarations.rb +12 -2
  62. data/lib/rubocop/cop/style/accessor_grouping.rb +10 -2
  63. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +79 -0
  64. data/lib/rubocop/cop/style/arguments_forwarding.rb +46 -6
  65. data/lib/rubocop/cop/style/bitwise_predicate.rb +100 -0
  66. data/lib/rubocop/cop/style/block_delimiters.rb +31 -3
  67. data/lib/rubocop/cop/style/collection_compact.rb +10 -10
  68. data/lib/rubocop/cop/style/combinable_defined.rb +115 -0
  69. data/lib/rubocop/cop/style/combinable_loops.rb +7 -0
  70. data/lib/rubocop/cop/style/commented_keyword.rb +7 -1
  71. data/lib/rubocop/cop/style/conditional_assignment.rb +1 -1
  72. data/lib/rubocop/cop/style/data_inheritance.rb +1 -1
  73. data/lib/rubocop/cop/style/endless_method.rb +1 -14
  74. data/lib/rubocop/cop/style/eval_with_location.rb +1 -1
  75. data/lib/rubocop/cop/style/guard_clause.rb +15 -2
  76. data/lib/rubocop/cop/style/hash_each_methods.rb +6 -0
  77. data/lib/rubocop/cop/style/hash_syntax.rb +2 -2
  78. data/lib/rubocop/cop/style/if_inside_else.rb +1 -1
  79. data/lib/rubocop/cop/style/if_with_semicolon.rb +7 -3
  80. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +67 -0
  81. data/lib/rubocop/cop/style/lambda.rb +1 -1
  82. data/lib/rubocop/cop/style/map_into_array.rb +59 -8
  83. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +12 -7
  84. data/lib/rubocop/cop/style/multiline_memoization.rb +1 -1
  85. data/lib/rubocop/cop/style/multiple_comparison.rb +28 -39
  86. data/lib/rubocop/cop/style/nested_modifier.rb +1 -1
  87. data/lib/rubocop/cop/style/nested_parenthesized_calls.rb +1 -1
  88. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -0
  89. data/lib/rubocop/cop/style/operator_method_call.rb +25 -6
  90. data/lib/rubocop/cop/style/redundant_begin.rb +4 -0
  91. data/lib/rubocop/cop/style/redundant_condition.rb +1 -1
  92. data/lib/rubocop/cop/style/redundant_line_continuation.rb +23 -5
  93. data/lib/rubocop/cop/style/redundant_parentheses.rb +9 -11
  94. data/lib/rubocop/cop/style/require_order.rb +1 -1
  95. data/lib/rubocop/cop/style/rescue_modifier.rb +13 -1
  96. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +54 -12
  97. data/lib/rubocop/cop/style/safe_navigation.rb +104 -50
  98. data/lib/rubocop/cop/style/safe_navigation_chain_length.rb +52 -0
  99. data/lib/rubocop/cop/style/select_by_regexp.rb +9 -6
  100. data/lib/rubocop/cop/style/semicolon.rb +1 -1
  101. data/lib/rubocop/cop/style/struct_inheritance.rb +1 -1
  102. data/lib/rubocop/cop/style/ternary_parentheses.rb +26 -5
  103. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  104. data/lib/rubocop/cop/team.rb +8 -1
  105. data/lib/rubocop/cop/util.rb +1 -1
  106. data/lib/rubocop/cop/variable_force/assignment.rb +18 -3
  107. data/lib/rubocop/cop/variable_force/branch.rb +1 -1
  108. data/lib/rubocop/cop/variable_force/variable.rb +5 -1
  109. data/lib/rubocop/cop/variable_force/variable_table.rb +2 -2
  110. data/lib/rubocop/cops_documentation_generator.rb +81 -40
  111. data/lib/rubocop/file_finder.rb +9 -4
  112. data/lib/rubocop/formatter/disabled_config_formatter.rb +1 -1
  113. data/lib/rubocop/lsp/runtime.rb +2 -0
  114. data/lib/rubocop/lsp/server.rb +0 -1
  115. data/lib/rubocop/rspec/expect_offense.rb +1 -0
  116. data/lib/rubocop/runner.rb +17 -6
  117. data/lib/rubocop/server/cache.rb +6 -1
  118. data/lib/rubocop/server/core.rb +1 -0
  119. data/lib/rubocop/target_ruby.rb +13 -13
  120. data/lib/rubocop/version.rb +28 -7
  121. data/lib/rubocop/yaml_duplication_checker.rb +20 -27
  122. data/lib/rubocop.rb +10 -0
  123. metadata +13 -4
@@ -69,6 +69,8 @@ module RuboCop
69
69
  extend AutoCorrector
70
70
 
71
71
  MSG = 'Redundant line continuation.'
72
+ LINE_CONTINUATION = "\\\n"
73
+ LINE_CONTINUATION_PATTERN = /(\\\n)/.freeze
72
74
  ALLOWED_STRING_TOKENS = %i[tSTRING tSTRING_CONTENT].freeze
73
75
  ARGUMENT_TYPES = %i[
74
76
  kFALSE kNIL kSELF kTRUE tCONSTANT tCVAR tFLOAT tGVAR tIDENTIFIER tINTEGER tIVAR
@@ -79,7 +81,7 @@ module RuboCop
79
81
  def on_new_investigation
80
82
  return unless processed_source.ast
81
83
 
82
- each_match_range(processed_source.ast.source_range, /(\\\n)/) do |range|
84
+ each_match_range(processed_source.ast.source_range, LINE_CONTINUATION_PATTERN) do |range|
83
85
  next if require_line_continuation?(range)
84
86
  next unless redundant_line_continuation?(range)
85
87
 
@@ -87,6 +89,8 @@ module RuboCop
87
89
  corrector.remove_leading(range, 1)
88
90
  end
89
91
  end
92
+
93
+ inspect_eof_line_continuation
90
94
  end
91
95
 
92
96
  private
@@ -122,13 +126,27 @@ module RuboCop
122
126
  end
123
127
 
124
128
  def redundant_line_continuation?(range)
125
- return true unless (node = find_node_for_line(range.line))
129
+ return true unless (node = find_node_for_line(range.last_line))
126
130
  return false if argument_newline?(node)
127
131
 
128
- source = node.parent ? node.parent.source : node.source
132
+ source = node.source
133
+ while (node = node.parent)
134
+ source = node.source
135
+ end
129
136
  parse(source.gsub("\\\n", "\n")).valid_syntax?
130
137
  end
131
138
 
139
+ def inspect_eof_line_continuation
140
+ return unless processed_source.raw_source.end_with?(LINE_CONTINUATION)
141
+
142
+ rindex = processed_source.raw_source.rindex(LINE_CONTINUATION)
143
+ line_continuation_range = range_between(rindex, rindex + 1)
144
+
145
+ add_offense(line_continuation_range) do |corrector|
146
+ corrector.remove_trailing(line_continuation_range, 1)
147
+ end
148
+ end
149
+
132
150
  def inside_string_literal?(range, token)
133
151
  ALLOWED_STRING_TOKENS.include?(token.type) && token.pos.overlaps?(range)
134
152
  end
@@ -160,9 +178,9 @@ module RuboCop
160
178
  end
161
179
  # rubocop:enable Metrics/AbcSize
162
180
 
163
- def find_node_for_line(line)
181
+ def find_node_for_line(last_line)
164
182
  processed_source.ast.each_node do |node|
165
- return node if same_line?(node, line)
183
+ return node if node.respond_to?(:expression) && node.expression&.last_line == last_line
166
184
  end
167
185
  end
168
186
 
@@ -31,9 +31,6 @@ module RuboCop
31
31
  # @!method allowed_pin_operator?(node)
32
32
  def_node_matcher :allowed_pin_operator?, '^(pin (begin !{lvar ivar cvar gvar}))'
33
33
 
34
- # @!method arg_in_call_with_block?(node)
35
- def_node_matcher :arg_in_call_with_block?, '^^(block (send _ _ equal?(%0) ...) ...)'
36
-
37
34
  def on_begin(node)
38
35
  return if !parentheses?(node) || parens_allowed?(node) || ignore_syntax?(node)
39
36
 
@@ -59,7 +56,6 @@ module RuboCop
59
56
 
60
57
  def allowed_expression?(node)
61
58
  allowed_ancestor?(node) ||
62
- allowed_method_call?(node) ||
63
59
  allowed_multiple_expression?(node) ||
64
60
  allowed_ternary?(node) ||
65
61
  node.parent&.range_type?
@@ -70,11 +66,6 @@ module RuboCop
70
66
  keyword_ancestor?(node) && parens_required?(node)
71
67
  end
72
68
 
73
- def allowed_method_call?(node)
74
- # Don't flag `method (arg) { }`
75
- arg_in_call_with_block?(node) && !parentheses?(node.parent)
76
- end
77
-
78
69
  def allowed_multiple_expression?(node)
79
70
  return false if node.children.one?
80
71
 
@@ -160,7 +151,7 @@ module RuboCop
160
151
  return if node.semantic_operator? && begin_node.parent
161
152
  return if node.multiline? && allow_in_multiline_conditions?
162
153
  return if ALLOWED_NODE_TYPES.include?(begin_node.parent&.type)
163
- return if begin_node.parent&.if_type? && begin_node.parent&.ternary?
154
+ return if begin_node.parent&.if_type? && begin_node.parent.ternary?
164
155
 
165
156
  'a logical expression'
166
157
  elsif node.respond_to?(:comparison_method?) && node.comparison_method?
@@ -185,7 +176,8 @@ module RuboCop
185
176
  return check_unary(begin_node, node) if node.unary_operation?
186
177
 
187
178
  return unless method_call_with_redundant_parentheses?(node)
188
- return if call_chain_starts_with_int?(begin_node, node)
179
+ return if call_chain_starts_with_int?(begin_node, node) ||
180
+ do_end_block_in_method_chain?(begin_node, node)
189
181
 
190
182
  offense(begin_node, 'a method call')
191
183
  end
@@ -285,6 +277,12 @@ module RuboCop
285
277
  recv&.int_type? && (parent = begin_node.parent) &&
286
278
  parent.send_type? && (parent.method?(:-@) || parent.method?(:+@))
287
279
  end
280
+
281
+ def do_end_block_in_method_chain?(begin_node, node)
282
+ return false unless (block = node.each_descendant(:block, :numblock).first)
283
+
284
+ block.keywords? && begin_node.each_ancestor(:send, :csend).any?
285
+ end
288
286
  end
289
287
  end
290
288
  end
@@ -103,7 +103,7 @@ module RuboCop
103
103
  next unless sibling.is_a?(AST::Node)
104
104
 
105
105
  sibling = sibling_node(sibling)
106
- break unless sibling&.send_type? && sibling&.method?(node.method_name)
106
+ break unless sibling&.send_type? && sibling.method?(node.method_name)
107
107
  break unless sibling.arguments? && !sibling.receiver
108
108
  break unless in_same_section?(sibling, node)
109
109
  break unless node.first_argument.str_type? && sibling.first_argument.str_type?
@@ -75,7 +75,7 @@ module RuboCop
75
75
 
76
76
  corrector.remove(range_between(operation.source_range.end_pos, node.source_range.end_pos))
77
77
  corrector.insert_before(operation, "begin\n#{node_indentation}")
78
- corrector.insert_after(operation, <<~RESCUE_CLAUSE.chop)
78
+ corrector.insert_after(heredoc_end(operation) || operation, <<~RESCUE_CLAUSE.chop)
79
79
 
80
80
  #{node_offset}rescue
81
81
  #{node_indentation}#{rescue_args.source}
@@ -92,6 +92,18 @@ module RuboCop
92
92
  end
93
93
  [node_indentation, node_offset]
94
94
  end
95
+
96
+ def heredoc_end(node)
97
+ return unless node.call_type?
98
+
99
+ heredoc = node.arguments.reverse.find do |argument|
100
+ argument.respond_to?(:heredoc?) && argument.heredoc?
101
+ end
102
+
103
+ return unless heredoc
104
+
105
+ heredoc.loc.heredoc_end
106
+ end
95
107
  end
96
108
  end
97
109
  end
@@ -3,7 +3,8 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks if `return` or `return nil` is used in predicate method definitions.
6
+ # Checks for predicate method definitions that return `nil`.
7
+ # A predicate method should only return a boolean value.
7
8
  #
8
9
  # @safety
9
10
  # Autocorrection is marked as unsafe because the change of the return value
@@ -31,6 +32,24 @@ module RuboCop
31
32
  # do_something?
32
33
  # end
33
34
  #
35
+ # # bad
36
+ # def foo?
37
+ # if condition
38
+ # nil
39
+ # else
40
+ # true
41
+ # end
42
+ # end
43
+ #
44
+ # # good
45
+ # def foo?
46
+ # if condition
47
+ # false
48
+ # else
49
+ # true
50
+ # end
51
+ # end
52
+ #
34
53
  # @example AllowedMethods: ['foo?']
35
54
  # # good
36
55
  # def foo?
@@ -64,24 +83,25 @@ module RuboCop
64
83
  return if allowed_method?(node.method_name) || matches_allowed_pattern?(node.method_name)
65
84
  return unless (body = node.body)
66
85
 
67
- body.each_descendant(:return) do |return_node|
68
- register_offense(return_node, 'return false') if return_nil?(return_node)
69
- end
70
-
71
- return unless (nil_node = nil_node_at_the_end_of_method_body(body))
86
+ body.each_descendant(:return) { |return_node| handle_return(return_node) }
72
87
 
73
- register_offense(nil_node, 'false')
88
+ handle_implicit_return_values(body)
74
89
  end
75
90
  alias on_defs on_def
76
91
 
77
92
  private
78
93
 
79
- def nil_node_at_the_end_of_method_body(body)
80
- return body if body.nil_type?
81
- return unless body.begin_type?
82
- return unless (last_child = body.children.last)
94
+ def last_node_of_type(node, type)
95
+ return unless node
96
+ return node if node_type?(node, type)
97
+ return unless node.begin_type?
98
+ return unless (last_child = node.children.last)
99
+
100
+ last_child if last_child.is_a?(AST::Node) && node_type?(last_child, type)
101
+ end
83
102
 
84
- last_child if last_child.is_a?(AST::Node) && last_child.nil_type?
103
+ def node_type?(node, type)
104
+ node.type == type.to_sym
85
105
  end
86
106
 
87
107
  def register_offense(offense_node, replacement)
@@ -89,6 +109,28 @@ module RuboCop
89
109
  corrector.replace(offense_node, replacement)
90
110
  end
91
111
  end
112
+
113
+ def handle_implicit_return_values(node)
114
+ handle_if(last_node_of_type(node, :if))
115
+ handle_nil(last_node_of_type(node, :nil))
116
+ end
117
+
118
+ def handle_return(return_node)
119
+ register_offense(return_node, 'return false') if return_nil?(return_node)
120
+ end
121
+
122
+ def handle_nil(nil_node)
123
+ return unless nil_node
124
+
125
+ register_offense(nil_node, 'false')
126
+ end
127
+
128
+ def handle_if(if_node)
129
+ return unless if_node
130
+
131
+ handle_implicit_return_values(if_node.if_branch)
132
+ handle_implicit_return_values(if_node.else_branch)
133
+ end
92
134
  end
93
135
  end
94
136
  end
@@ -21,6 +21,11 @@ module RuboCop
21
21
  # We have limited the cop to not register an offense for method chains
22
22
  # that exceed this option's value.
23
23
  #
24
+ # NOTE: This cop will recognize offenses but not autocorrect code when the
25
+ # right hand side (RHS) of the `&&` statement is an `||` statement
26
+ # (eg. `foo && (foo.bar? || foo.baz?)`). It can be corrected
27
+ # manually by removing the `foo &&` and adding `&.` to each `foo` on the RHS.
28
+ #
24
29
  # @safety
25
30
  # Autocorrection is unsafe because if a value is `false`, the resulting
26
31
  # code will have different behavior or raise an error.
@@ -81,7 +86,7 @@ module RuboCop
81
86
  # foo.baz = bar if foo
82
87
  # foo.baz + bar if foo
83
88
  # foo.bar > 2 if foo
84
- class SafeNavigation < Base
89
+ class SafeNavigation < Base # rubocop:disable Metrics/ClassLength
85
90
  include NilMethods
86
91
  include RangeHelp
87
92
  extend AutoCorrector
@@ -121,50 +126,122 @@ module RuboCop
121
126
  }
122
127
  PATTERN
123
128
 
129
+ # @!method and_with_rhs_or?(node)
130
+ def_node_matcher :and_with_rhs_or?, '(and _ {or (begin or)})'
131
+
124
132
  # @!method not_nil_check?(node)
125
133
  def_node_matcher :not_nil_check?, '(send (send $_ :nil?) :!)'
126
134
 
135
+ # @!method and_inside_begin?(node)
136
+ def_node_matcher :and_inside_begin?, '`(begin and ...)'
137
+
138
+ # @!method strip_begin(node)
139
+ def_node_matcher :strip_begin, '{ (begin $!begin) $!(begin) }'
140
+
127
141
  def on_if(node)
128
142
  return if allowed_if_condition?(node)
129
143
 
130
- check_node(node)
144
+ checked_variable, receiver, method_chain, _method = extract_parts_from_if(node)
145
+ return unless offending_node?(node, checked_variable, method_chain, receiver)
146
+
147
+ body = extract_if_body(node)
148
+ method_call = receiver.parent
149
+
150
+ removal_ranges = [begin_range(node, body), end_range(node, body)]
151
+
152
+ report_offense(node, method_chain, method_call, *removal_ranges) do |corrector|
153
+ corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation?
154
+ end
131
155
  end
132
156
 
133
- def on_and(node)
134
- check_node(node)
157
+ def on_and(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
158
+ collect_and_clauses(node).each do |(lhs, lhs_operator_range), (rhs, _rhs_operator_range)|
159
+ lhs_not_nil_check = not_nil_check?(lhs)
160
+ lhs_receiver = lhs_not_nil_check || lhs
161
+ rhs_receiver = find_matching_receiver_invocation(strip_begin(rhs), lhs_receiver)
162
+
163
+ next if !cop_config['ConvertCodeThatCanStartToReturnNil'] && lhs_not_nil_check
164
+ next unless offending_node?(node, lhs_receiver, rhs, rhs_receiver)
165
+
166
+ # Since we are evaluating every clause in potentially a complex chain of `and` nodes,
167
+ # we need to ensure that there isn't an object check happening
168
+ lhs_method_chain = find_method_chain(lhs_receiver)
169
+ next unless lhs_method_chain == lhs_receiver || lhs_not_nil_check
170
+
171
+ report_offense(
172
+ node,
173
+ rhs, rhs_receiver,
174
+ range_with_surrounding_space(range: lhs.source_range, side: :right),
175
+ range_with_surrounding_space(range: lhs_operator_range, side: :right),
176
+ offense_range: range_between(lhs.source_range.begin_pos, rhs.source_range.end_pos)
177
+ )
178
+ end
179
+ end
180
+
181
+ def report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node)
182
+ add_offense(offense_range) do |corrector|
183
+ # If the RHS is an `or` we cannot safely autocorrect because in order to remove
184
+ # the non-nil check we need to add safe-navs to all clauses where the receiver is used
185
+ next if and_with_rhs_or?(node)
186
+
187
+ removal_ranges.each { |range| corrector.remove(range) }
188
+ yield corrector if block_given?
189
+
190
+ handle_comments(corrector, node, rhs)
191
+
192
+ add_safe_nav_to_all_methods_in_chain(corrector, rhs_receiver, rhs)
193
+ end
135
194
  end
136
195
 
137
196
  private
138
197
 
139
- def check_node(node)
140
- checked_variable, receiver, method_chain, method = extract_parts(node)
141
- return if receiver != checked_variable || receiver.nil?
142
- return if use_var_only_in_unless_modifier?(node, checked_variable)
143
- return if chain_length(method_chain, method) > max_chain_length
144
- return if unsafe_method_used?(method_chain, method)
145
- return if method_chain.method?(:empty?)
198
+ def find_method_chain(node)
199
+ return node unless node&.parent&.call_type?
146
200
 
147
- add_offense(node) { |corrector| autocorrect(corrector, node) }
201
+ find_method_chain(node.parent)
148
202
  end
149
203
 
150
- def use_var_only_in_unless_modifier?(node, variable)
151
- node.if_type? && node.unless? && !method_called?(variable)
204
+ def collect_and_clauses(node)
205
+ # Collect the lhs, operator and rhs of all `and` nodes
206
+ # `and` nodes can be nested and can contain `begin` nodes
207
+ # This gives us a source-ordered list of clauses that is then used to look
208
+ # for matching receivers as well as operator locations for offense and corrections
209
+ node.each_descendant(:and)
210
+ .inject(and_parts(node)) { |nodes, and_node| concat_nodes(nodes, and_node) }
211
+ .sort_by { |a| a.is_a?(RuboCop::AST::Node) ? a.source_range.begin_pos : a.begin_pos }
212
+ .each_slice(2)
213
+ .each_cons(2)
152
214
  end
153
215
 
154
- def autocorrect(corrector, node)
155
- body = extract_body(node)
156
- method_call = method_call(node)
216
+ def concat_nodes(nodes, and_node)
217
+ return nodes if and_node.each_ancestor(:block).any?
157
218
 
158
- corrector.remove(begin_range(node, body))
159
- corrector.remove(end_range(node, body))
160
- corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation?
161
- handle_comments(corrector, node, method_call)
219
+ nodes.concat(and_parts(and_node))
220
+ end
162
221
 
163
- add_safe_nav_to_all_methods_in_chain(corrector, method_call, body)
222
+ def and_parts(node)
223
+ parts = [node.loc.operator]
224
+ parts << node.rhs unless and_inside_begin?(node.rhs)
225
+ parts << node.lhs unless node.lhs.and_type? || and_inside_begin?(node.lhs)
226
+ parts
164
227
  end
165
228
 
166
- def extract_body(node)
167
- if node.if_type? && node.ternary?
229
+ def offending_node?(node, lhs_receiver, rhs, rhs_receiver) # rubocop:disable Metrics/CyclomaticComplexity
230
+ return false if lhs_receiver != rhs_receiver || rhs_receiver.nil?
231
+ return false if use_var_only_in_unless_modifier?(node, lhs_receiver)
232
+ return false if chain_length(rhs, rhs_receiver) > max_chain_length
233
+ return false if unsafe_method_used?(rhs, rhs_receiver.parent)
234
+ return false if rhs.send_type? && rhs.method?(:empty?)
235
+
236
+ true
237
+ end
238
+
239
+ def use_var_only_in_unless_modifier?(node, variable)
240
+ node.if_type? && node.unless? && !method_called?(variable)
241
+ end
242
+
243
+ def extract_if_body(node)
244
+ if node.ternary?
168
245
  node.branches.find { |branch| !branch.nil_type? }
169
246
  else
170
247
  node.node_parts[1]
@@ -201,20 +278,6 @@ module RuboCop
201
278
  node.else? || node.elsif?
202
279
  end
203
280
 
204
- def method_call(node)
205
- _checked_variable, matching_receiver, = extract_parts(node)
206
- matching_receiver.parent
207
- end
208
-
209
- def extract_parts(node)
210
- case node.type
211
- when :if
212
- extract_parts_from_if(node)
213
- when :and
214
- extract_parts_from_and(node)
215
- end
216
- end
217
-
218
281
  def extract_parts_from_if(node)
219
282
  variable, receiver =
220
283
  if node.ternary?
@@ -230,16 +293,6 @@ module RuboCop
230
293
  [checked_variable, matching_receiver, receiver, method]
231
294
  end
232
295
 
233
- def extract_parts_from_and(node)
234
- checked_variable, rhs = *node
235
- if cop_config['ConvertCodeThatCanStartToReturnNil']
236
- checked_variable = not_nil_check?(checked_variable) || checked_variable
237
- end
238
-
239
- checked_variable, matching_receiver, method = extract_common_parts(rhs, checked_variable)
240
- [checked_variable, matching_receiver, rhs, method]
241
- end
242
-
243
296
  def extract_common_parts(method_chain, checked_variable)
244
297
  matching_receiver = find_matching_receiver_invocation(method_chain, checked_variable)
245
298
 
@@ -249,7 +302,7 @@ module RuboCop
249
302
  end
250
303
 
251
304
  def find_matching_receiver_invocation(method_chain, checked_variable)
252
- return nil unless method_chain
305
+ return nil unless method_chain.respond_to?(:receiver)
253
306
 
254
307
  receiver = method_chain.receiver
255
308
 
@@ -259,7 +312,7 @@ module RuboCop
259
312
  end
260
313
 
261
314
  def chain_length(method_chain, method)
262
- method.each_ancestor(:send).inject(1) do |total, ancestor|
315
+ method.each_ancestor(:send).inject(0) do |total, ancestor|
263
316
  break total + 1 if ancestor == method_chain
264
317
 
265
318
  total + 1
@@ -310,6 +363,7 @@ module RuboCop
310
363
  start_method.each_ancestor do |ancestor|
311
364
  break unless %i[send block].include?(ancestor.type)
312
365
  next unless ancestor.send_type?
366
+ next if ancestor.safe_navigation?
313
367
 
314
368
  corrector.insert_before(ancestor.loc.dot, '&')
315
369
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Enforces safe navigation chains length to not exceed the configured maximum.
7
+ # The longer the chain is, the harder it becomes to track what on it could be
8
+ # returning `nil`.
9
+ #
10
+ # There is a potential interplay with `Style/SafeNavigation` - if both are enabled
11
+ # and their settings are "incompatible", one of the cops will complain about what
12
+ # the other proposes.
13
+ #
14
+ # E.g. if `Style/SafeNavigation` is configured with `MaxChainLength: 2` (default)
15
+ # and this cop is configured with `Max: 1`, then for `foo.bar.baz if foo` the former
16
+ # will suggest `foo&.bar&.baz`, which is an offense for the latter.
17
+ #
18
+ # @example Max: 2 (default)
19
+ # # bad
20
+ # user&.address&.zip&.upcase
21
+ #
22
+ # # good
23
+ # user&.address&.zip
24
+ # user.address.zip if user
25
+ #
26
+ class SafeNavigationChainLength < Base
27
+ MSG = 'Avoid safe navigation chains longer than %<max>d calls.'
28
+
29
+ def on_csend(node)
30
+ safe_navigation_chains = safe_navigation_chains(node)
31
+ return if safe_navigation_chains.size < max
32
+
33
+ add_offense(safe_navigation_chains.last, message: format(MSG, max: max))
34
+ end
35
+
36
+ private
37
+
38
+ def safe_navigation_chains(node)
39
+ node.each_ancestor.with_object([]) do |parent, chains|
40
+ break chains unless parent.csend_type?
41
+
42
+ chains << parent
43
+ end
44
+ end
45
+
46
+ def max
47
+ cop_config['Max'] || 2
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -27,7 +27,7 @@ module RuboCop
27
27
  # so the correction may not be actually equivalent.
28
28
  #
29
29
  # @example
30
- # # bad (select or find_all)
30
+ # # bad (select, filter, or find_all)
31
31
  # array.select { |x| x.match? /regexp/ }
32
32
  # array.select { |x| /regexp/.match?(x) }
33
33
  # array.select { |x| x =~ /regexp/ }
@@ -47,9 +47,11 @@ module RuboCop
47
47
  include RangeHelp
48
48
 
49
49
  MSG = 'Prefer `%<replacement>s` to `%<original_method>s` with a regexp match.'
50
- RESTRICT_ON_SEND = %i[select find_all reject].freeze
51
- REPLACEMENTS = { select: 'grep', find_all: 'grep', reject: 'grep_v' }.freeze
52
- OPPOSITE_REPLACEMENTS = { select: 'grep_v', find_all: 'grep_v', reject: 'grep' }.freeze
50
+ RESTRICT_ON_SEND = %i[select filter find_all reject].freeze
51
+ REPLACEMENTS = { select: 'grep', filter: 'grep', find_all: 'grep', reject: 'grep_v' }.freeze
52
+ OPPOSITE_REPLACEMENTS = {
53
+ select: 'grep_v', filter: 'grep_v', find_all: 'grep_v', reject: 'grep'
54
+ }.freeze
53
55
  REGEXP_METHODS = %i[match? =~ !~].to_set.freeze
54
56
 
55
57
  # @!method regexp_match?(node)
@@ -84,8 +86,9 @@ module RuboCop
84
86
  }
85
87
  PATTERN
86
88
 
87
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
89
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
88
90
  def on_send(node)
91
+ return if target_ruby_version < 2.6 && node.method?(:filter)
89
92
  return unless (block_node = node.block_node)
90
93
  return if block_node.body&.begin_type?
91
94
  return if receiver_allowed?(block_node.receiver)
@@ -99,7 +102,7 @@ module RuboCop
99
102
 
100
103
  register_offense(node, block_node, regexp, replacement)
101
104
  end
102
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
103
106
  alias on_csend on_send
104
107
 
105
108
  private
@@ -141,7 +141,7 @@ module RuboCop
141
141
 
142
142
  def expressions_per_line(exprs)
143
143
  # create a map matching lines to the number of expressions on them
144
- exprs_lines = exprs.map(&:first_line)
144
+ exprs_lines = exprs.map(&:last_line)
145
145
  exprs_lines.group_by(&:itself)
146
146
  end
147
147
 
@@ -33,7 +33,7 @@ module RuboCop
33
33
  def on_class(node)
34
34
  return unless struct_constructor?(node.parent_class)
35
35
 
36
- add_offense(node.parent_class.source_range) do |corrector|
36
+ add_offense(node.parent_class) do |corrector|
37
37
  corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
38
38
  corrector.replace(node.loc.operator, '=')
39
39
 
@@ -75,7 +75,7 @@ module RuboCop
75
75
 
76
76
  message = message(node)
77
77
 
78
- add_offense(node.source_range, message: message) do |corrector|
78
+ add_offense(node, message: message) do |corrector|
79
79
  autocorrect(corrector, node)
80
80
  end
81
81
  end
@@ -171,9 +171,7 @@ module RuboCop
171
171
  end
172
172
 
173
173
  def unsafe_autocorrect?(condition)
174
- condition.children.any? do |child|
175
- unparenthesized_method_call?(child) || below_ternary_precedence?(child)
176
- end
174
+ condition.children.any? { |child| below_ternary_precedence?(child) }
177
175
  end
178
176
 
179
177
  def unparenthesized_method_call?(child)
@@ -192,7 +190,7 @@ module RuboCop
192
190
  # @!method method_name(node)
193
191
  def_node_matcher :method_name, <<~PATTERN
194
192
  {($:defined? _ ...)
195
- (send {_ nil?} $_ _ ...)}
193
+ (call {_ nil?} $_ _ ...)}
196
194
  PATTERN
197
195
 
198
196
  def correct_parenthesized(corrector, condition)
@@ -203,16 +201,39 @@ module RuboCop
203
201
  # If we remove the parentheses, we need to add a space or we'll
204
202
  # generate invalid code.
205
203
  corrector.insert_after(condition.loc.end, ' ') unless whitespace_after?(condition)
204
+
205
+ if (send_node = condition.child_nodes.last) && node_args_need_parens?(send_node)
206
+ parenthesize_condition_arguments(corrector, send_node)
207
+ end
206
208
  end
207
209
 
208
210
  def correct_unparenthesized(corrector, condition)
209
211
  corrector.wrap(condition, '(', ')')
210
212
  end
211
213
 
214
+ def parenthesize_condition_arguments(corrector, send_node)
215
+ range_start = send_node.defined_type? ? send_node.loc.keyword : send_node.loc.selector
216
+ opening_range = range_start.end.join(send_node.first_argument.source_range.begin)
217
+
218
+ corrector.replace(opening_range, '(')
219
+ corrector.insert_after(send_node.last_argument, ')')
220
+ end
221
+
212
222
  def whitespace_after?(node)
213
223
  last_token = processed_source.last_token_of(node)
214
224
  last_token.space_after?
215
225
  end
226
+
227
+ def node_args_need_parens?(send_node)
228
+ return false unless node_with_args?(send_node)
229
+ return false if send_node.arguments.none? || send_node.parenthesized?
230
+
231
+ send_node.dot? || send_node.safe_navigation? || unparenthesized_method_call?(send_node)
232
+ end
233
+
234
+ def node_with_args?(node)
235
+ node.call_type? || node.defined_type?
236
+ end
216
237
  end
217
238
  end
218
239
  end