rubocop 1.92.0 → 1.92.2

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +1 -0
  3. data/lib/rubocop/config.rb +0 -4
  4. data/lib/rubocop/config_obsoletion.rb +9 -0
  5. data/lib/rubocop/cop/badge.rb +1 -1
  6. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +16 -1
  7. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +2 -2
  9. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +34 -0
  10. data/lib/rubocop/cop/layout/argument_alignment.rb +5 -1
  11. data/lib/rubocop/cop/layout/class_structure.rb +21 -7
  12. data/lib/rubocop/cop/layout/dot_position.rb +25 -15
  13. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +11 -7
  14. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  15. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +9 -3
  16. data/lib/rubocop/cop/layout/end_alignment.rb +5 -15
  17. data/lib/rubocop/cop/layout/extra_spacing.rb +18 -7
  18. data/lib/rubocop/cop/layout/first_argument_indentation.rb +26 -1
  19. data/lib/rubocop/cop/layout/first_array_element_line_break.rb +15 -0
  20. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +11 -5
  21. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +5 -3
  22. data/lib/rubocop/cop/layout/indentation_consistency.rb +22 -5
  23. data/lib/rubocop/cop/layout/line_length.rb +127 -11
  24. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +6 -4
  25. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +16 -5
  26. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -1
  27. data/lib/rubocop/cop/layout/single_line_block_chain.rb +3 -3
  28. data/lib/rubocop/cop/layout/space_around_operators.rb +31 -4
  29. data/lib/rubocop/cop/layout/space_before_block_braces.rb +12 -0
  30. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +1 -1
  31. data/lib/rubocop/cop/layout/space_inside_range_literal.rb +29 -11
  32. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +2 -12
  33. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -1
  34. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +4 -0
  35. data/lib/rubocop/cop/lint/interpolation_check.rb +20 -17
  36. data/lib/rubocop/cop/lint/nested_percent_literal.rb +3 -0
  37. data/lib/rubocop/cop/lint/number_conversion.rb +2 -0
  38. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +22 -1
  39. data/lib/rubocop/cop/lint/percent_string_array.rb +75 -9
  40. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +27 -8
  41. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +11 -4
  42. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +47 -1
  43. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -0
  44. data/lib/rubocop/cop/lint/useless_setter_call.rb +7 -4
  45. data/lib/rubocop/cop/mixin/check_line_breakable.rb +7 -6
  46. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +3 -1
  47. data/lib/rubocop/cop/mixin/comments_help.rb +1 -1
  48. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +36 -0
  49. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +26 -5
  50. data/lib/rubocop/cop/mixin/statement_modifier.rb +17 -3
  51. data/lib/rubocop/cop/mixin/trailing_comma.rb +1 -1
  52. data/lib/rubocop/cop/mixin/visibility_help.rb +2 -1
  53. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  54. data/lib/rubocop/cop/style/access_modifier_declarations.rb +30 -2
  55. data/lib/rubocop/cop/style/and_or.rb +34 -11
  56. data/lib/rubocop/cop/style/arguments_forwarding.rb +28 -2
  57. data/lib/rubocop/cop/style/block_delimiters.rb +6 -0
  58. data/lib/rubocop/cop/style/case_equality.rb +14 -3
  59. data/lib/rubocop/cop/style/class_and_module_children.rb +6 -3
  60. data/lib/rubocop/cop/style/class_methods_definitions.rb +7 -0
  61. data/lib/rubocop/cop/style/collection_querying.rb +79 -46
  62. data/lib/rubocop/cop/style/combinable_loops.rb +4 -0
  63. data/lib/rubocop/cop/style/concat_array_literals.rb +13 -1
  64. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +53 -11
  65. data/lib/rubocop/cop/style/endless_method.rb +38 -6
  66. data/lib/rubocop/cop/style/float_division.rb +16 -1
  67. data/lib/rubocop/cop/style/for.rb +14 -1
  68. data/lib/rubocop/cop/style/guard_clause.rb +22 -21
  69. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +4 -0
  70. data/lib/rubocop/cop/style/hash_lookup_method.rb +10 -0
  71. data/lib/rubocop/cop/style/if_unless_modifier.rb +19 -12
  72. data/lib/rubocop/cop/style/it_block_parameter.rb +63 -2
  73. data/lib/rubocop/cop/style/lambda.rb +16 -1
  74. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +36 -4
  75. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +1 -0
  76. data/lib/rubocop/cop/style/method_def_parentheses.rb +9 -1
  77. data/lib/rubocop/cop/style/mixin_grouping.rb +12 -3
  78. data/lib/rubocop/cop/style/mutable_constant.rb +25 -7
  79. data/lib/rubocop/cop/style/numeric_predicate.rb +7 -0
  80. data/lib/rubocop/cop/style/parentheses_around_condition.rb +19 -1
  81. data/lib/rubocop/cop/style/quoted_symbols.rb +5 -7
  82. data/lib/rubocop/cop/style/raise_args.rb +80 -11
  83. data/lib/rubocop/cop/style/reduce_to_hash.rb +32 -34
  84. data/lib/rubocop/cop/style/redundant_begin.rb +26 -3
  85. data/lib/rubocop/cop/style/redundant_exception.rb +13 -9
  86. data/lib/rubocop/cop/style/redundant_freeze.rb +1 -2
  87. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +24 -1
  88. data/lib/rubocop/cop/style/redundant_self.rb +9 -1
  89. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +28 -0
  90. data/lib/rubocop/cop/style/redundant_sort.rb +38 -4
  91. data/lib/rubocop/cop/style/require_order.rb +16 -8
  92. data/lib/rubocop/cop/style/rescue_modifier.rb +6 -0
  93. data/lib/rubocop/cop/style/send_with_literal_method_name.rb +4 -0
  94. data/lib/rubocop/cop/style/single_line_methods.rb +26 -0
  95. data/lib/rubocop/cop/style/string_concatenation.rb +12 -1
  96. data/lib/rubocop/cop/style/struct_inheritance.rb +6 -0
  97. data/lib/rubocop/cop/style/symbol_proc.rb +1 -1
  98. data/lib/rubocop/lsp/routes.rb +23 -23
  99. data/lib/rubocop/plugin/configuration_integrator.rb +24 -3
  100. data/lib/rubocop/runner.rb +1 -1
  101. data/lib/rubocop/server/cache.rb +4 -4
  102. data/lib/rubocop/target_ruby.rb +21 -9
  103. data/lib/rubocop/version.rb +1 -1
  104. metadata +2 -2
@@ -49,7 +49,7 @@ module RuboCop
49
49
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
50
50
 
51
51
  def self.autocorrect_incompatible_with
52
- [Style::MethodCallWithArgsParentheses, Style::ReturnNilInPredicateMethodDefinition]
52
+ [Style::MethodCallWithArgsParentheses, Style::ReturnNil, Style::ReturnNilInPredicateMethodDefinition]
53
53
  end
54
54
 
55
55
  def on_and(node)
@@ -72,13 +72,7 @@ module RuboCop
72
72
 
73
73
  message = message(node)
74
74
  add_offense(node.loc.operator, message: message) do |corrector|
75
- node.each_child_node do |expr|
76
- if expr.send_type?
77
- correct_send(expr, corrector)
78
- elsif expr.type?(:return, :next, :break, :yield) || expr.assignment?
79
- correct_other(expr, corrector)
80
- end
81
- end
75
+ node.each_child_node { |expr| correct_operand(expr, corrector) }
82
76
 
83
77
  corrector.replace(node.loc.operator, node.alternate_operator)
84
78
 
@@ -86,6 +80,22 @@ module RuboCop
86
80
  end
87
81
  end
88
82
 
83
+ def correct_operand(expr, corrector)
84
+ expr = expr.send_node if expr.any_block_type?
85
+
86
+ if expr.type?(:call, :super)
87
+ correct_send(expr, corrector)
88
+ elsif wrap_in_parentheses?(expr)
89
+ correct_other(expr, corrector)
90
+ end
91
+ end
92
+
93
+ # These bind looser than `&&` and `||`, or can't be their operands at all.
94
+ def wrap_in_parentheses?(node)
95
+ node.type?(:return, :next, :break, :yield, :any_match_pattern, :range, :defined?) ||
96
+ node.assignment? || (node.if_type? && node.ternary?)
97
+ end
98
+
89
99
  def on_conditionals(node)
90
100
  node.condition.each_node(*AST::Node::OPERATOR_KEYWORDS) do |operator|
91
101
  process_logical_operator(operator)
@@ -101,6 +111,7 @@ module RuboCop
101
111
  return correct_setter(node, corrector) if node.setter_method?
102
112
  return correct_other(node, corrector) if node.comparison_method?
103
113
 
114
+ correct_block_receiver(node, corrector)
104
115
  return unless correctable_send?(node)
105
116
 
106
117
  whitespace_before_arg_range = whitespace_before_arg(node)
@@ -120,7 +131,8 @@ module RuboCop
120
131
  # however, 'not x' also parses as (send x :!)
121
132
  def correct_not(node, receiver, corrector)
122
133
  if node.prefix_bang?
123
- return unless receiver.send_type?
134
+ receiver = receiver.send_node if receiver.any_block_type?
135
+ return unless receiver.type?(:call, :super)
124
136
 
125
137
  correct_send(receiver, corrector)
126
138
  elsif node.prefix_not?
@@ -145,12 +157,23 @@ module RuboCop
145
157
  end
146
158
  end
147
159
 
160
+ # A method chained onto a `do` block, as in `bar baz do ... end.qux`,
161
+ # still needs parentheses around the arguments of the call that takes the block.
162
+ def correct_block_receiver(node, corrector)
163
+ receiver = node.receiver
164
+ receiver = receiver.receiver while receiver&.call_type?
165
+ correct_operand(receiver, corrector) if receiver&.any_block_type?
166
+ end
167
+
148
168
  def correctable_send?(node)
149
- !node.parenthesized? && node.arguments? && !node.method?(:[])
169
+ # Operator syntax such as `a + b` or `a[b]` already binds tighter than `&&` and `||`.
170
+ return false if node.operator_method? && !node.loc.dot
171
+
172
+ !node.parenthesized? && node.arguments?
150
173
  end
151
174
 
152
175
  def whitespace_before_arg(node)
153
- begin_paren = node.loc.selector.end_pos
176
+ begin_paren = node.selector.end_pos
154
177
  end_paren = begin_paren
155
178
  # Increment position of parenthesis, unless message is a predicate
156
179
  # method followed by a non-whitespace char (e.g. is_a?String).
@@ -169,7 +169,7 @@ module RuboCop
169
169
 
170
170
  return if send_classifications.empty?
171
171
 
172
- if only_forwards_all?(send_classifications)
172
+ if forward_all?(send_nodes, send_classifications)
173
173
  add_forward_all_offenses(node, send_classifications, forwardable_args)
174
174
  elsif target_ruby_version >= 3.2
175
175
  add_post_ruby_32_offenses(node, send_classifications, forwardable_args)
@@ -192,11 +192,33 @@ module RuboCop
192
192
  [restarg_node, kwrestarg_node, blockarg_node]
193
193
  end
194
194
 
195
+ def forward_all?(send_nodes, send_classifications)
196
+ only_forwards_all?(send_classifications) &&
197
+ !anonymous_forwarding_elsewhere?(send_nodes, send_classifications)
198
+ end
199
+
195
200
  def only_forwards_all?(send_classifications)
196
201
  all_classifications = %i[all all_anonymous].freeze
197
202
  send_classifications.all? { |_, c, _, _| all_classifications.include?(c) }
198
203
  end
199
204
 
205
+ def anonymous_forwarding_elsewhere?(send_nodes, send_classifications)
206
+ classified = send_classifications.map(&:first)
207
+
208
+ send_nodes.any? do |send_node|
209
+ classified.none? { |node| node.equal?(send_node) } &&
210
+ forwards_anonymous_args?(send_node)
211
+ end
212
+ end
213
+
214
+ def forwards_anonymous_args?(send_node)
215
+ send_node.arguments.any? do |arg|
216
+ next arg.children.any?(&:forwarded_kwrestarg_type?) if arg.hash_type?
217
+
218
+ arg.forwarded_restarg_type?
219
+ end
220
+ end
221
+
200
222
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
201
223
  def add_forward_all_offenses(node, send_classifications, forwardable_args)
202
224
  rest_arg, kwrest_arg, block_arg = *forwardable_args
@@ -376,12 +398,16 @@ module RuboCop
376
398
 
377
399
  def add_parens_if_missing(node, corrector)
378
400
  return if parentheses?(node)
379
- return if node.send_type? && node.method?(:[])
401
+ return if node.call_type? && operator_or_setter_call?(node)
380
402
  return unless (@parenthesized_nodes ||= Set.new.compare_by_identity).add?(node)
381
403
 
382
404
  add_parentheses(node, corrector)
383
405
  end
384
406
 
407
+ def operator_or_setter_call?(node)
408
+ node.setter_method? || (node.operator_method? && !node.loc.dot)
409
+ end
410
+
385
411
  # Classifies send nodes for possible rest/kwrest/all (including block) forwarding.
386
412
  class SendNodeClassifier
387
413
  extend NodePattern::Macros
@@ -306,6 +306,8 @@ module RuboCop
306
306
  end
307
307
 
308
308
  def move_comment_before_block(corrector, comment, block_node, closing_brace)
309
+ return if directive_comment?(comment)
310
+
309
311
  range = block_node.chained? ? end_of_chain(block_node.parent).source_range : closing_brace
310
312
 
311
313
  # It is possible that there is code between the block and the comment
@@ -319,6 +321,10 @@ module RuboCop
319
321
  corrector.insert_before(block_node, "#{comment.text}\n")
320
322
  end
321
323
 
324
+ def directive_comment?(comment)
325
+ comment.text.match?(DirectiveComment::DIRECTIVE_COMMENT_REGEXP)
326
+ end
327
+
322
328
  def source_range_before_comment(range, comment)
323
329
  range = range.end.join(comment.source_range.begin)
324
330
 
@@ -7,7 +7,7 @@ module RuboCop
7
7
  # The `===` operator has different behavior depending on the
8
8
  # receiver and its use outside of `case`/`when` is confusing.
9
9
  # Prefer more explicit alternatives like `is_a?`, `include?`,
10
- # or `match?`.
10
+ # `cover?`, or `match?`.
11
11
  #
12
12
  # If `AllowOnConstant` option is enabled, the cop will ignore violations when the receiver of
13
13
  # the case equality operator is a constant.
@@ -22,9 +22,11 @@ module RuboCop
22
22
  # @example
23
23
  # # bad
24
24
  # (1..100) === 7
25
+ # ('a'..'z') === char
25
26
  #
26
27
  # # good
27
28
  # (1..100).include?(7)
29
+ # ('a'..'z').cover?(char)
28
30
  #
29
31
  # @example AllowOnConstant: false (default)
30
32
  # # bad
@@ -90,9 +92,18 @@ module RuboCop
90
92
  end
91
93
 
92
94
  def begin_replacement(lhs, rhs)
93
- return unless lhs.children.first&.range_type?
95
+ range = lhs.children.first
96
+ return unless range&.range_type?
94
97
 
95
- "#{lhs.source}.include?(#{rhs.source})"
98
+ method = cover_semantics?(range) ? 'cover?' : 'include?'
99
+
100
+ "#{lhs.source}.#{method}(#{rhs.source})"
101
+ end
102
+
103
+ # `Range#===` behaves like `Range#cover?` since Ruby 2.7 (since Ruby 2.6 for ranges
104
+ # that aren't string ranges), and `Range#include?` only matches that for numeric ranges.
105
+ def cover_semantics?(range)
106
+ target_ruby_version >= 2.7 && !range.children.compact.all?(&:numeric_type?)
96
107
  end
97
108
 
98
109
  def const_replacement(lhs, rhs)
@@ -65,10 +65,11 @@ module RuboCop
65
65
  NESTED_MSG = 'Use nested module/class definitions instead of compact style.'
66
66
  COMPACT_MSG = 'Use compact module/class definition instead of nested style.'
67
67
 
68
- # `Style/StructInheritance` turns a class definition into a constant assignment in
69
- # the same pass, which the compacted `class` keyword would then precede.
68
+ # `Style/StructInheritance` and `Style/DataInheritance` turn a class definition into
69
+ # a constant assignment in the same pass, which the compacted `class` keyword would
70
+ # then precede.
70
71
  def self.autocorrect_incompatible_with
71
- [Style::StructInheritance]
72
+ [Style::DataInheritance, Style::StructInheritance]
72
73
  end
73
74
 
74
75
  def on_class(node)
@@ -152,7 +153,9 @@ module RuboCop
152
153
  # making autocorrection ping-pong between the two forms.
153
154
  return if style_for_kind(node.body.type) == :nested
154
155
  return unless compactible_namespace?(node)
156
+ return if node.each_ancestor(:class, :module).any? { |ancestor| ignored_node?(ancestor) }
155
157
 
158
+ ignore_node(node)
156
159
  compact_node(corrector, node)
157
160
  remove_end(corrector, node.body)
158
161
  unindent(corrector, node)
@@ -75,7 +75,10 @@ module RuboCop
75
75
  return unless def_nodes(node).all? { |def_node| no_code_after?(def_node.source_range) }
76
76
 
77
77
  add_offense(node, message: MSG_SCLASS) do |corrector|
78
+ next if inside_corrected_sclass?(node)
79
+
78
80
  autocorrect_sclass(node, corrector)
81
+ ignore_node(node)
79
82
  end
80
83
  end
81
84
 
@@ -120,6 +123,10 @@ module RuboCop
120
123
  trailing.empty? || trailing.start_with?('#')
121
124
  end
122
125
 
126
+ def inside_corrected_sclass?(node)
127
+ node.each_ancestor(:sclass).any? { |ancestor| ignored_node?(ancestor) }
128
+ end
129
+
123
130
  def autocorrect_sclass(node, corrector)
124
131
  rewritten_defs = []
125
132
 
@@ -15,31 +15,24 @@ module RuboCop
15
15
  # would yield false positives. For example, `String` implements `length`
16
16
  # and `size`, but it doesn't include `Enumerable`.
17
17
  #
18
+ # When `count` is used without a block, it measures the collection's
19
+ # size, so the offense is corrected to `empty?` or `!empty?`. The
20
+ # predicate methods `any?`, `none?` and `one?` test element truthiness
21
+ # instead and would return different results for collections with falsey
22
+ # elements (e.g. `[false].count.zero?` is `false`, but `[false].none?`
23
+ # is `true`). For the same reason, `count == 1` and `count > 1` without
24
+ # a block are not flagged.
25
+ #
18
26
  # @safety
19
27
  # The cop is unsafe because receiver might not include `Enumerable`, or
20
28
  # it has nonstandard implementation of `count` or any replacement
21
29
  # methods.
22
30
  #
23
- # It's also unsafe because for collections with falsey values, expressions
24
- # with `count` without a block return a different result than methods `any?`,
25
- # `none?` and `one?`:
26
- #
27
- # [source,ruby]
28
- # ----
29
- # [nil, false].count.positive?
30
- # [nil].count == 1
31
- # # => true
32
- #
33
- # [nil, false].any?
34
- # [nil].one?
35
- # # => false
36
- #
37
- # [nil].count == 0
38
- # # => false
39
- #
40
- # [nil].none?
41
- # # => true
42
- # ----
31
+ # Blockless `count` is corrected to `empty?`, which `Enumerable` doesn't
32
+ # define. `Range`, `Enumerator` (including lazy ones), `Struct`, `StringIO`
33
+ # and any class that just includes `Enumerable` respond to `count` but not
34
+ # to `empty?`, so `(1...1).count.zero?` is `true` while `(1...1).empty?`
35
+ # raises `NoMethodError`.
43
36
  #
44
37
  # Autocorrection is unsafe when replacement methods don't iterate over
45
38
  # every element in collection and the given block runs side effects:
@@ -60,12 +53,14 @@ module RuboCop
60
53
  # x.count > 0
61
54
  # x.count != 0
62
55
  #
56
+ # # good
57
+ # !x.empty?
58
+ #
59
+ # # bad
63
60
  # x.count(&:foo?).positive?
64
61
  # x.count { |item| item.foo? }.positive?
65
62
  #
66
63
  # # good
67
- # x.any?
68
- #
69
64
  # x.any?(&:foo?)
70
65
  # x.any? { |item| item.foo? }
71
66
  #
@@ -74,24 +69,33 @@ module RuboCop
74
69
  # x.count == 0
75
70
  #
76
71
  # # good
77
- # x.none?
72
+ # x.empty?
78
73
  #
79
74
  # # bad
80
- # x.count == 1
81
- # x.one?
75
+ # x.count(&:foo?).zero?
76
+ # x.count(&:foo?) == 0
77
+ #
78
+ # # good
79
+ # x.none?(&:foo?)
80
+ #
81
+ # # bad
82
+ # x.count(&:foo?) == 1
83
+ #
84
+ # # good
85
+ # x.one?(&:foo?)
82
86
  #
83
87
  # @example AllCops:ActiveSupportExtensionsEnabled: false (default)
84
88
  #
85
89
  # # good
86
- # x.count > 1
90
+ # x.count(&:foo?) > 1
87
91
  #
88
92
  # @example AllCops:ActiveSupportExtensionsEnabled: true
89
93
  #
90
94
  # # bad
91
- # x.count > 1
95
+ # x.count(&:foo?) > 1
92
96
  #
93
97
  # # good
94
- # x.many?
98
+ # x.many?(&:foo?)
95
99
  #
96
100
  class CollectionQuerying < Base
97
101
  include RangeHelp
@@ -102,13 +106,21 @@ module RuboCop
102
106
  RESTRICT_ON_SEND = %i[positive? > != zero? ==].freeze
103
107
 
104
108
  REPLACEMENTS = {
105
- [:positive?, nil] => :any?,
106
- [:>, 0] => :any?,
107
- [:!=, 0] => :any?,
108
- [:zero?, nil] => :none?,
109
- [:==, 0] => :none?,
110
- [:==, 1] => :one?,
111
- [:>, 1] => :many?
109
+ [:positive?, nil] => 'any?',
110
+ [:>, 0] => 'any?',
111
+ [:!=, 0] => 'any?',
112
+ [:zero?, nil] => 'none?',
113
+ [:==, 0] => 'none?',
114
+ [:==, 1] => 'one?',
115
+ [:>, 1] => 'many?'
116
+ }.freeze
117
+
118
+ NO_BLOCK_REPLACEMENTS = {
119
+ [:positive?, nil] => '!empty?',
120
+ [:>, 0] => '!empty?',
121
+ [:!=, 0] => '!empty?',
122
+ [:zero?, nil] => 'empty?',
123
+ [:==, 0] => 'empty?'
112
124
  }.freeze
113
125
 
114
126
  # @!method count_predicate(node)
@@ -132,28 +144,49 @@ module RuboCop
132
144
  def on_send(node)
133
145
  return unless (count_node = count_predicate(node))
134
146
 
135
- replacement_method = replacement_method(node)
147
+ replacement = replacement_method(node, count_node)
136
148
 
137
- return unless replacement_supported?(replacement_method)
149
+ return unless replacement_supported?(node, replacement)
138
150
 
139
151
  offense_range = count_node.loc.selector.join(node.source_range.end)
140
- add_offense(offense_range,
141
- message: format(MSG, prefer: replacement_method)) do |corrector|
142
- corrector.replace(count_node.loc.selector, replacement_method)
143
- corrector.remove(removal_range(node))
152
+ add_offense(offense_range, message: format(MSG, prefer: replacement)) do |corrector|
153
+ autocorrect(corrector, node, count_node, replacement)
144
154
  end
145
155
  end
146
156
 
147
157
  private
148
158
 
149
- def replacement_method(node)
150
- REPLACEMENTS.fetch([node.method_name, node.first_argument&.value])
159
+ def replacement_method(node, count_node)
160
+ key = [node.method_name, node.first_argument&.value]
161
+ return REPLACEMENTS.fetch(key) if block_given_to_count?(count_node)
162
+
163
+ NO_BLOCK_REPLACEMENTS[key]
164
+ end
165
+
166
+ def block_given_to_count?(count_node)
167
+ count_node.parent&.any_block_type? || count_node.last_argument&.block_pass_type?
168
+ end
169
+
170
+ def replacement_supported?(node, replacement)
171
+ return false unless replacement
172
+ return false if replacement == 'many?' && !active_support_extensions_enabled?
173
+
174
+ !(replacement.start_with?('!') && receiver_of_call?(node))
151
175
  end
152
176
 
153
- def replacement_supported?(method_name)
154
- return true if active_support_extensions_enabled?
177
+ def receiver_of_call?(node)
178
+ node.parent&.call_type? && node.parent.receiver == node && node.parent.loc.dot
179
+ end
180
+
181
+ def autocorrect(corrector, node, count_node, replacement)
182
+ if replacement.start_with?('!')
183
+ corrector.insert_before(count_node, '!')
184
+ corrector.replace(count_node.loc.selector, 'empty?')
185
+ else
186
+ corrector.replace(count_node.loc.selector, replacement)
187
+ end
155
188
 
156
- method_name != :many?
189
+ corrector.remove(removal_range(node))
157
190
  end
158
191
 
159
192
  def removal_range(node)
@@ -65,6 +65,10 @@ module RuboCop
65
65
 
66
66
  MSG = 'Combine this loop with the previous loop.'
67
67
 
68
+ def self.autocorrect_incompatible_with
69
+ [Layout::BlockEndNewline, Layout::RedundantLineBreak]
70
+ end
71
+
68
72
  def on_block(node)
69
73
  return unless node.parent&.begin_type?
70
74
  return unless collection_looping_method?(node)
@@ -118,7 +118,8 @@ module RuboCop
118
118
  # A comment or a heredoc body there can't be moved out of the way.
119
119
  return unless closing_ranges.all? { |range| range.source.match?(/\A[\s,]*\]\z/) }
120
120
 
121
- corrector.replace(node.loc.selector, 'push')
121
+ replace_selector(corrector, node)
122
+
122
123
  node.arguments.zip(closing_ranges) do |argument, closing_range|
123
124
  corrector.remove(argument.loc.begin)
124
125
  corrector.remove(closing_range)
@@ -133,6 +134,17 @@ module RuboCop
133
134
 
134
135
  range_between(argument.children.last.source_range.end_pos, argument.loc.end.end_pos)
135
136
  end
137
+
138
+ def replace_selector(corrector, node)
139
+ if node.parenthesized? || node.single_line?
140
+ corrector.replace(node.loc.selector, 'push')
141
+ else
142
+ range = node.loc.selector.join(node.first_argument.source_range.begin)
143
+
144
+ corrector.replace(range, 'push(')
145
+ corrector.insert_after(node.last_argument.loc.end, ')')
146
+ end
147
+ end
136
148
  end
137
149
  end
138
150
  end
@@ -45,6 +45,8 @@ module RuboCop
45
45
  MSG_TRAILING_CONDITIONAL = 'Do not use trailing conditionals in string interpolation.'
46
46
  MSG_TERNARY = 'Do not return empty strings in string interpolation.'
47
47
 
48
+ INTERPOLATED_TYPES = %i[dstr dsym regexp xstr].freeze
49
+
48
50
  def on_interpolation(node)
49
51
  node.each_child_node(:if) do |child_node|
50
52
  if style == :trailing_conditional
@@ -63,28 +65,54 @@ module RuboCop
63
65
  return if child_node.modifier_form?
64
66
 
65
67
  if empty_if_outcome?(child_node)
66
- ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
68
+ ternary_style_autocorrect(child_node, child_node.else_branch, 'unless')
67
69
  end
68
70
 
69
71
  return unless empty_else_outcome?(child_node)
70
72
 
71
- ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
73
+ ternary_style_autocorrect(child_node, child_node.if_branch, 'if')
72
74
  end
73
75
 
74
76
  def ternary_correction(node, child_node)
75
77
  return unless child_node.modifier_form?
76
78
 
77
- ternary_component = if child_node.unless?
78
- "'' : #{child_node.if_branch.source}"
79
- else
80
- "#{child_node.if_branch.source} : ''"
81
- end
82
-
83
79
  add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
84
- corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
80
+ next if nested_trailing_conditional?(child_node)
81
+
82
+ autocorrect_to_ternary(corrector, child_node)
83
+ end
84
+ end
85
+
86
+ def nested_trailing_conditional?(if_node)
87
+ if_node.each_descendant(:if).any? do |node|
88
+ node.modifier_form? && node.parent.parent&.type?(*INTERPOLATED_TYPES)
85
89
  end
86
90
  end
87
91
 
92
+ def autocorrect_to_ternary(corrector, if_node)
93
+ condition = if_node.condition.source
94
+ branch = ternary_branch_source(if_node.if_branch)
95
+ outcomes = if_node.unless? ? "'' : #{branch}" : "#{branch} : ''"
96
+
97
+ corrector.replace(if_node, "#{condition} ? #{outcomes}")
98
+ end
99
+
100
+ def ternary_branch_source(branch)
101
+ return branch.source unless command_call?(branch)
102
+
103
+ arguments = branch.first_argument.source_range.join(branch.last_argument.source_range)
104
+ method = branch.source_range.begin.join(arguments.begin).source.rstrip
105
+
106
+ "#{method}(#{arguments.source})"
107
+ end
108
+
109
+ def command_call?(node)
110
+ return false unless node.type?(:call, :super, :yield) && node.arguments?
111
+ return false if node.call_type? && (node.operator_method? || node.setter_method?)
112
+
113
+ !node.parenthesized?
114
+ end
115
+
88
116
  def empty_if_outcome?(node)
89
117
  empty_branch_outcome?(node.if_branch)
90
118
  end
@@ -99,9 +127,23 @@ module RuboCop
99
127
  branch.nil_type? || (branch.str_type? && branch.value.empty?)
100
128
  end
101
129
 
102
- def ternary_style_autocorrect(node, outcome, condition)
130
+ def ternary_style_autocorrect(node, outcome, keyword)
103
131
  add_offense(node, message: MSG_TERNARY) do |corrector|
104
- corrector.replace(node, "#{outcome} #{condition} #{node.condition.source}")
132
+ next if empty_string_conditional_in_condition?(node)
133
+
134
+ head = node.source_range.begin.join(outcome.source_range.begin)
135
+ tail = outcome.source_range.end.join(node.source_range.end)
136
+
137
+ corrector.remove(head)
138
+ corrector.replace(tail, " #{keyword} #{node.condition.source}")
139
+ end
140
+ end
141
+
142
+ def empty_string_conditional_in_condition?(if_node)
143
+ if_node.condition.each_descendant(:if).any? do |node|
144
+ next false if node.modifier_form? || !node.parent.parent&.type?(*INTERPOLATED_TYPES)
145
+
146
+ empty_if_outcome?(node) || empty_else_outcome?(node)
105
147
  end
106
148
  end
107
149
  end
@@ -147,8 +147,21 @@ module RuboCop
147
147
 
148
148
  # `Style/MethodDefParentheses` adds the same parentheses a parameter list needs in
149
149
  # an endless definition, so letting both correct in the same pass would double them.
150
+ # `Style/GuardClause` turns a single `if` body into several statements,
151
+ # which an endless definition cannot hold.
152
+ # `Style/RaiseArgs` would drop the parentheses of a `raise` that becomes
153
+ # the body in the same pass, which a method passed as an argument needs.
154
+ # `Style/IfUnlessModifier` and `Style/WhileUntilModifier` would turn the body into
155
+ # a modifier form, which then applies to the whole endless definition.
150
156
  def self.autocorrect_incompatible_with
151
- [Style::MethodCallWithArgsParentheses, Style::MethodDefParentheses]
157
+ [
158
+ Style::GuardClause,
159
+ Style::IfUnlessModifier,
160
+ Style::MethodCallWithArgsParentheses,
161
+ Style::MethodDefParentheses,
162
+ Style::RaiseArgs,
163
+ Style::WhileUntilModifier
164
+ ]
152
165
  end
153
166
 
154
167
  def on_def(node)
@@ -185,9 +198,7 @@ module RuboCop
185
198
  add_offense(node, message: MSG_MULTI_LINE) do |corrector|
186
199
  correct_to_multiline(corrector, node)
187
200
  end
188
- elsif !node.endless? && can_be_made_endless?(node) && single_line_when_made_endless?(node)
189
- return if too_long_when_made_endless?(node)
190
-
201
+ elsif convertible_to_endless?(node) && single_line_when_made_endless?(node)
191
202
  add_offense(node, message: MSG_REQUIRE_SINGLE) do |corrector|
192
203
  correct_to_endless(corrector, node)
193
204
  end
@@ -195,14 +206,18 @@ module RuboCop
195
206
  end
196
207
 
197
208
  def handle_require_always_style(node)
198
- return if node.endless? || !can_be_made_endless?(node)
199
- return if too_long_when_made_endless?(node)
209
+ return unless convertible_to_endless?(node)
200
210
 
201
211
  add_offense(node, message: MSG_REQUIRE_ALWAYS) do |corrector|
202
212
  correct_to_endless(corrector, node)
203
213
  end
204
214
  end
205
215
 
216
+ def convertible_to_endless?(node)
217
+ !node.endless? && can_be_made_endless?(node) &&
218
+ !too_long_when_made_endless?(node) && !ambiguous_when_made_endless?(node)
219
+ end
220
+
206
221
  def handle_disallow_style(node)
207
222
  return unless node.endless?
208
223
 
@@ -293,6 +308,23 @@ module RuboCop
293
308
  !endless_replacement(node).include?("\n")
294
309
  end
295
310
 
311
+ def ambiguous_when_made_endless?(node)
312
+ (operation = ambiguous_operation(node)) && ambiguous_enclosing_operation?(operation)
313
+ end
314
+
315
+ # @!method ambiguous_operation(node)
316
+ def_node_matcher :ambiguous_operation, <<~PATTERN
317
+ ^${
318
+ (if _ <any_def _>)
319
+ ({and or} any_def _)
320
+ ({while until} _ any_def)
321
+ }
322
+ PATTERN
323
+
324
+ def ambiguous_enclosing_operation?(operation)
325
+ operation.operator_keyword? || operation.modifier_form?
326
+ end
327
+
296
328
  def too_long_when_made_endless?(node)
297
329
  return false unless config.cop_enabled?('Layout/LineLength')
298
330