rubocop 1.87.0 → 1.88.1

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 (162) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +78 -72
  3. data/config/obsoletion.yml +21 -1
  4. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -0
  5. data/lib/rubocop/cop/base.rb +17 -2
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +5 -3
  7. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  9. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  10. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  11. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  12. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  13. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  14. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  15. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  16. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  17. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  18. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  19. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  20. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  21. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  22. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  23. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  24. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  25. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  26. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  27. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  28. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  29. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  30. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  31. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  32. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  33. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  34. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  35. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  36. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  37. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  38. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  39. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  40. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  41. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  42. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  43. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  44. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  45. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  46. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  47. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  48. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  49. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  50. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  51. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  52. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  53. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  54. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  55. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  56. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  57. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  58. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  59. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  60. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  61. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  62. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  63. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  64. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  65. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  66. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  67. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  68. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  69. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  70. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +35 -9
  71. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  72. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  73. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  74. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  75. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  76. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  77. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  78. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  79. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  80. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  81. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  82. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  83. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  84. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  85. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  86. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  87. data/lib/rubocop/cop/style/alias.rb +1 -1
  88. data/lib/rubocop/cop/style/and_or.rb +1 -1
  89. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  90. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  91. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  92. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  93. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  94. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  95. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  96. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  97. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  98. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  99. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  100. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  101. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  102. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  103. data/lib/rubocop/cop/style/date_time.rb +2 -2
  104. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  105. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  106. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  107. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  108. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  109. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  110. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  111. data/lib/rubocop/cop/style/env_home.rb +4 -0
  112. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  113. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  114. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  115. data/lib/rubocop/cop/style/file_null.rb +4 -2
  116. data/lib/rubocop/cop/style/file_write.rb +17 -14
  117. data/lib/rubocop/cop/style/format_string.rb +13 -1
  118. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  119. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  120. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  121. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  122. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  123. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  124. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  125. data/lib/rubocop/cop/style/lambda.rb +7 -1
  126. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  127. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  128. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  129. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  130. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  131. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  132. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  133. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  134. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  135. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  136. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  137. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  138. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  139. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  140. data/lib/rubocop/cop/style/parallel_assignment.rb +19 -3
  141. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  142. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  143. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  144. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  145. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  146. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  147. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  148. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  149. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  150. data/lib/rubocop/cop/style/semicolon.rb +20 -5
  151. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  152. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  153. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  154. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  155. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  156. data/lib/rubocop/cop/style/word_array.rb +1 -0
  157. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  158. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  159. data/lib/rubocop/runner.rb +5 -3
  160. data/lib/rubocop/server/core.rb +6 -0
  161. data/lib/rubocop/version.rb +1 -1
  162. metadata +3 -3
@@ -37,7 +37,7 @@ module RuboCop
37
37
  return unless (receiver = node.receiver) && receiver.source == 'IO'
38
38
 
39
39
  argument = node.first_argument
40
- return if argument.respond_to?(:value) && argument.value.strip.start_with?('|')
40
+ return if argument&.str_type? && argument.value.strip.start_with?('|')
41
41
 
42
42
  add_offense(node, message: format(MSG, method_name: node.method_name)) do |corrector|
43
43
  corrector.replace(receiver, 'File')
@@ -25,7 +25,7 @@ module RuboCop
25
25
  # @!method marshal_load(node)
26
26
  def_node_matcher :marshal_load, <<~PATTERN
27
27
  (send (const {nil? cbase} :Marshal) ${:load :restore}
28
- !(send (const {nil? cbase} :Marshal) :dump ...))
28
+ !(send (const {nil? cbase} :Marshal) :dump ...) _?)
29
29
  PATTERN
30
30
 
31
31
  def on_send(node)
@@ -203,13 +203,23 @@ module RuboCop
203
203
  end
204
204
 
205
205
  def range_with_trailing_argument_comment(node)
206
- comment = processed_source.ast_with_comments[node.last_argument].last
206
+ comment = trailing_argument_comment(node)
207
207
  if comment
208
208
  add_range(node.source_range, comment.source_range)
209
209
  else
210
210
  node
211
211
  end
212
212
  end
213
+
214
+ # For a single-line declaration the parser associates the trailing
215
+ # comment with the first argument, not `last_argument`, so look through
216
+ # all arguments for a comment that trails the whole node.
217
+ def trailing_argument_comment(node)
218
+ comments = node.arguments.filter_map do |argument|
219
+ processed_source.ast_with_comments[argument].last
220
+ end
221
+ comments.find { |comment| comment.source_range.begin_pos >= node.source_range.end_pos }
222
+ end
213
223
  end
214
224
  end
215
225
  end
@@ -113,7 +113,7 @@ module RuboCop
113
113
  return :lexical
114
114
  when :def, :defs
115
115
  return :dynamic
116
- when :block
116
+ when :block, :numblock, :itblock
117
117
  return :instance_eval if parent.method?(:instance_eval)
118
118
 
119
119
  return :dynamic
@@ -71,7 +71,7 @@ module RuboCop
71
71
  node.each_child_node do |expr|
72
72
  if expr.send_type?
73
73
  correct_send(expr, corrector)
74
- elsif expr.return_type? || expr.assignment?
74
+ elsif expr.type?(:return, :next, :break, :yield) || expr.assignment?
75
75
  correct_other(expr, corrector)
76
76
  end
77
77
  end
@@ -40,8 +40,9 @@ module RuboCop
40
40
 
41
41
  node = innermost_braces_node(node)
42
42
  return if node.parent && brace_method?(node.parent)
43
+ return if compound_assignment_target?(node)
43
44
 
44
- preferred = (value.zero? ? 'first' : 'last')
45
+ preferred = preferred_name(value)
45
46
  offense_range = find_offense_range(node)
46
47
 
47
48
  add_offense(offense_range, message: format(MSG, preferred: preferred)) do |corrector|
@@ -53,6 +54,10 @@ module RuboCop
53
54
 
54
55
  private
55
56
 
57
+ def preferred_name(value)
58
+ value.zero? ? 'first' : 'last'
59
+ end
60
+
56
61
  def preferred_value(node, value)
57
62
  value = ".#{value}" unless node.loc.dot
58
63
  value
@@ -74,6 +79,12 @@ module RuboCop
74
79
  def brace_method?(node)
75
80
  node.send_type? && (node.method?(:[]) || node.method?(:[]=))
76
81
  end
82
+
83
+ # `arr[0] += 1` etc. would autocorrect to `arr.first += 1`, which calls the
84
+ # nonexistent `first=`/`last=` setter and raises `NoMethodError`.
85
+ def compound_assignment_target?(node)
86
+ node.parent&.type?(:op_asgn, :or_asgn, :and_asgn) && node.parent.children.first == node
87
+ end
77
88
  end
78
89
  end
79
90
  end
@@ -145,6 +145,10 @@ module RuboCop
145
145
 
146
146
  dot = dot_node.loc.dot.source
147
147
  bang = straight?(method_name) ? '' : '!'
148
+ # `a&.intersection(b)&.none?` returns `nil` when `a` is `nil`, but the negated
149
+ # rewrite `!a&.intersect?(b)` returns `true` there, flipping the result.
150
+ return if bang == '!' && dot == '&.'
151
+
148
152
  replacement = "#{bang}#{receiver.source}#{dot}intersect?(#{argument.source})"
149
153
 
150
154
  register_offense(node, replacement)
@@ -29,6 +29,9 @@ module RuboCop
29
29
  def on_send(node)
30
30
  array, element = single_element(node)
31
31
  return unless array
32
+ # `[*foo]` is not a single element: the splat can expand to any number of
33
+ # elements, so `intersect?([*foo])` is not equivalent to `include?(*foo)`.
34
+ return if element.splat_type?
32
35
 
33
36
  add_offense(
34
37
  node.source_range.with(begin_pos: node.loc.selector.begin_pos)
@@ -389,9 +389,23 @@ module RuboCop
389
389
 
390
390
  def require_do_end?(node)
391
391
  return false if node.braces? || node.multiline?
392
- return false unless (resbody = node.each_descendant(:resbody).first)
393
392
 
394
- resbody.children.first&.array_type?
393
+ body = node.body
394
+ return false unless body
395
+ # `ensure` and a block-level `rescue` are illegal inside `{ }`; only a
396
+ # bare modifier rescue (`expr rescue expr`) can be written with braces.
397
+ return true if body.ensure_type?
398
+ return false unless body.rescue_type?
399
+
400
+ !modifier_rescue?(body)
401
+ end
402
+
403
+ def modifier_rescue?(rescue_node)
404
+ return false if rescue_node.body.nil? || rescue_node.else_branch
405
+ return false unless rescue_node.resbody_branches.one?
406
+
407
+ resbody = rescue_node.resbody_branches.first
408
+ resbody.exceptions.empty? && resbody.exception_variable.nil?
395
409
  end
396
410
 
397
411
  def braces_required_method?(method_name)
@@ -96,13 +96,25 @@ module RuboCop
96
96
  end
97
97
 
98
98
  def const_replacement(lhs, rhs)
99
- "#{rhs.source}.is_a?(#{lhs.source})"
99
+ "#{parenthesize_if_needed(rhs)}.is_a?(#{lhs.source})"
100
100
  end
101
101
 
102
102
  def send_replacement(lhs, rhs)
103
103
  return unless self_class?(lhs)
104
104
 
105
- "#{rhs.source}.is_a?(#{lhs.source})"
105
+ "#{parenthesize_if_needed(rhs)}.is_a?(#{lhs.source})"
106
+ end
107
+
108
+ # `Array === a + b` must become `(a + b).is_a?(Array)`, not
109
+ # `a + b.is_a?(Array)` (which parses as `a + (b.is_a?(Array))`).
110
+ def parenthesize_if_needed(node)
111
+ requires_parentheses?(node) ? "(#{node.source})" : node.source
112
+ end
113
+
114
+ def requires_parentheses?(node)
115
+ return true if node.type?(:and, :or, :if, :range) || node.assignment?
116
+
117
+ node.send_type? && (node.operator_method? || node.unary_operation?)
106
118
  end
107
119
  end
108
120
  end
@@ -90,25 +90,33 @@ module RuboCop
90
90
  private
91
91
 
92
92
  def class_name(class_node, node)
93
- if class_name_method?(node.children.first.method_name)
94
- if (receiver = class_node.receiver) && class_name_method?(class_node.method_name)
95
- return receiver.source
96
- end
93
+ unless class_name_method?(node.children.first.method_name)
94
+ # `var.class == 'Foo'` compares a `Class` to a `String` (always false) and
95
+ # has no valid `instance_of?` rewrite, so don't suggest one.
96
+ return if class_node.str_type?
97
97
 
98
- if class_node.str_type?
99
- value = trim_string_quotes(class_node)
100
- value.prepend('::') if require_cbase?(class_node)
101
- return value
102
- elsif unable_to_determine_type?(class_node)
103
- # When a variable or return value of a method is used, it returns nil
104
- # because the type is not known and cannot be suggested.
105
- return
106
- end
98
+ return class_node.source
99
+ end
100
+
101
+ if (receiver = class_node.receiver) && class_name_method?(class_node.method_name)
102
+ return receiver.source
107
103
  end
108
104
 
105
+ return string_class_name(class_node) if class_node.str_type?
106
+ # When a variable or return value of a method is used, the type is not known
107
+ # and cannot be suggested.
108
+ return if unable_to_determine_type?(class_node)
109
+
109
110
  class_node.source
110
111
  end
111
112
 
113
+ def string_class_name(class_node)
114
+ value = trim_string_quotes(class_node)
115
+ # Avoid `::::Foo` when the name is already fully qualified.
116
+ value.prepend('::') if require_cbase?(class_node) && !value.start_with?('::')
117
+ value
118
+ end
119
+
112
120
  def class_name_method?(method_name)
113
121
  CLASS_NAME_METHODS.include?(method_name)
114
122
  end
@@ -140,15 +140,21 @@ module RuboCop
140
140
 
141
141
  def extract_def_from_sclass(def_node, sclass_node)
142
142
  range = source_range_with_comment(def_node)
143
- source = range.source.sub!(
144
- "def #{def_node.method_name}",
145
- "def self.#{def_node.method_name}"
146
- )
147
-
143
+ source = prefix_def_with_self(range, def_node)
148
144
  source = source.gsub(/^ {#{indentation_diff(def_node, sclass_node)}}/, '')
149
145
  [range, source.chomp]
150
146
  end
151
147
 
148
+ # Splice in `self.` at the actual `def` keyword rather than substituting the
149
+ # first textual `def <name>`, which may appear inside a preceding comment.
150
+ def prefix_def_with_self(range, def_node)
151
+ keyword_offset = def_node.loc.keyword.begin_pos - range.begin_pos
152
+ name_end_offset = def_node.loc.name.end_pos - range.begin_pos
153
+ source = range.source.dup
154
+ source[keyword_offset...name_end_offset] = "def self.#{def_node.method_name}"
155
+ source
156
+ end
157
+
152
158
  def indentation_diff(node1, node2)
153
159
  node1.loc.column - node2.loc.column
154
160
  end
@@ -24,10 +24,9 @@ module RuboCop
24
24
 
25
25
  MSG = 'Do not use `::` for method calls.'
26
26
 
27
- # @!method java_type_node?(node)
28
- def_node_matcher :java_type_node?, <<~PATTERN
29
- (send
30
- (const nil? :Java) _)
27
+ # @!method java_root?(node)
28
+ def_node_matcher :java_root?, <<~PATTERN
29
+ (const nil? :Java)
31
30
  PATTERN
32
31
 
33
32
  def self.autocorrect_incompatible_with
@@ -37,11 +36,19 @@ module RuboCop
37
36
  def on_send(node)
38
37
  return unless node.receiver && node.double_colon?
39
38
  return if node.camel_case_method?
40
- # ignore Java interop code like Java::int
41
- return if java_type_node?(node)
39
+ # ignore Java interop code like `Java::int` or `Java::com::method`
40
+ return if java_interop?(node)
42
41
 
43
42
  add_offense(node.loc.dot) { |corrector| corrector.replace(node.loc.dot, '.') }
44
43
  end
44
+
45
+ private
46
+
47
+ def java_interop?(node)
48
+ receiver = node.receiver
49
+ receiver = receiver.receiver while receiver.respond_to?(:receiver) && receiver.receiver
50
+ java_root?(receiver)
51
+ end
45
52
  end
46
53
  end
47
54
  end
@@ -85,8 +85,13 @@ module RuboCop
85
85
  def on_for(node)
86
86
  return unless node.parent&.begin_type?
87
87
  return unless same_collection_looping_for?(node, node.left_sibling)
88
+ return unless node.body && node.left_sibling.body
88
89
 
89
90
  add_offense(node) do |corrector|
91
+ # Combining loops with different iteration variables would leave the second
92
+ # body referencing an undefined variable, so only autocorrect when they match.
93
+ next unless node.variable == node.left_sibling.variable
94
+
90
95
  combine_with_left_sibling(corrector, node)
91
96
  end
92
97
  end
@@ -90,7 +90,7 @@ module RuboCop
90
90
  max = if_body.source
91
91
  end
92
92
 
93
- prefer = "#{else_body_source}.clamp(#{min}, #{max})"
93
+ prefer = "#{parenthesize_if_needed(else_body)}.clamp(#{min}, #{max})"
94
94
 
95
95
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
96
96
  autocorrect(corrector, node, prefer)
@@ -119,6 +119,17 @@ module RuboCop
119
119
 
120
120
  (lhs.source == else_body && op == :<) || (rhs.source == else_body && op == :>)
121
121
  end
122
+
123
+ # `a + b` must become `(a + b).clamp(low, high)`, not `a + b.clamp(low, high)`
124
+ # (which parses as `a + (b.clamp(low, high))`).
125
+ def parenthesize_if_needed(node)
126
+ if node.type?(:and, :or, :if, :range) || node.assignment? ||
127
+ (node.send_type? && (node.operator_method? || node.unary_operation?))
128
+ "(#{node.source})"
129
+ else
130
+ node.source
131
+ end
132
+ end
122
133
  end
123
134
  end
124
135
  end
@@ -55,6 +55,10 @@ module RuboCop
55
55
  next unless prefer
56
56
 
57
57
  corrector.replace(offense, prefer)
58
+ elsif node.arguments.any? { |argument| argument.children.empty? }
59
+ # In-place bracket removal would leave dangling commas (e.g.
60
+ # `concat([], [b])` -> `push(, b)`), so rebuild the call instead.
61
+ corrector.replace(offense, preferred_method(node))
58
62
  else
59
63
  corrector.replace(node.loc.selector, 'push')
60
64
  node.arguments.each do |argument|
@@ -75,7 +79,7 @@ module RuboCop
75
79
 
76
80
  def preferred_method(node)
77
81
  new_arguments =
78
- node.arguments.map do |arg|
82
+ node.arguments.flat_map do |arg|
79
83
  if arg.percent_literal?
80
84
  arg.children.map { |child| child.value.inspect }
81
85
  else
@@ -283,7 +283,10 @@ module RuboCop
283
283
 
284
284
  _condition, *branches, else_branch = *assignment
285
285
 
286
- return unless else_branch
286
+ # Use the node accessor rather than the raw destructured branch: for
287
+ # `x = unless cond; body; end` (no `else`) the parser puts `body` in the
288
+ # else slot, but `else_branch` correctly reports there is no `else` clause.
289
+ return unless assignment.else_branch
287
290
  return if allowed_single_line?([*branches, else_branch])
288
291
 
289
292
  add_offense(node, message: ASSIGN_TO_CONDITION_MSG) do |corrector|
@@ -657,6 +660,8 @@ module RuboCop
657
660
  remove_whitespace_in_branches(corrector, branch, condition, column)
658
661
 
659
662
  parent_keyword = branch.parent.loc.keyword
663
+ return if same_line?(parent_keyword, condition)
664
+
660
665
  corrector.remove_preceding(parent_keyword, parent_keyword.column - column)
661
666
  end
662
667
  end
@@ -89,7 +89,10 @@ module RuboCop
89
89
 
90
90
  arguments = arguments.first.children.first.to_a if arguments.first&.splat_type?
91
91
  constant_values = arguments.map do |argument|
92
- argument.value.to_sym if argument.respond_to?(:value)
92
+ # `respond_to?(:value)` is too broad: `int`/`float` nodes respond to it
93
+ # but their value is a `Numeric`, which has no `to_sym` (e.g.
94
+ # `private_constant 42`). Only symbol/string arguments are real names.
95
+ argument.value.to_sym if argument.type?(:sym, :str)
93
96
  end
94
97
 
95
98
  constant_values.include?(node.name)
@@ -61,6 +61,10 @@ module RuboCop
61
61
 
62
62
  def correct_parent(parent, corrector)
63
63
  if parent.block_type?
64
+ # Convert a brace block to `do`, so the class's own `end` closes it
65
+ # once the closing delimiter is removed; otherwise a dangling `{` is
66
+ # left behind, producing invalid Ruby.
67
+ corrector.replace(parent.loc.begin, 'do') if parent.braces?
64
68
  corrector.remove(range_with_surrounding_space(parent.loc.end, newlines: false))
65
69
  elsif (class_node = parent.parent).body.nil?
66
70
  corrector.remove(range_for_empty_class_body(class_node, parent))
@@ -59,12 +59,12 @@ module RuboCop
59
59
 
60
60
  # @!method historic_date?(node)
61
61
  def_node_matcher :historic_date?, <<~PATTERN
62
- (send _ _ _ (const (const {nil? (cbase)} :Date) _))
62
+ (call _ _ _ (const (const {nil? (cbase)} :Date) _))
63
63
  PATTERN
64
64
 
65
65
  # @!method to_datetime?(node)
66
66
  def_node_matcher :to_datetime?, <<~PATTERN
67
- (call _ :to_datetime)
67
+ (call !nil? :to_datetime)
68
68
  PATTERN
69
69
 
70
70
  def on_send(node)
@@ -79,6 +79,11 @@ module RuboCop
79
79
  corrector.replace(range, replacement)
80
80
 
81
81
  comments_in_range(node).reverse_each do |comment|
82
+ # Only relocate comments that the replacement destroys. A trailing
83
+ # comment after the chain survives in place, so moving it would
84
+ # duplicate it (and splitting the line drops the indentation).
85
+ next if comment.source_range.begin_pos >= range.end_pos
86
+
82
87
  corrector.insert_before(node, "#{comment.source}\n")
83
88
  end
84
89
  end
@@ -35,6 +35,10 @@ module RuboCop
35
35
  PATTERN
36
36
 
37
37
  def on_send(node)
38
+ # A trailing block (e.g. `Dir.each_child(path).none? { ... }`) changes
39
+ # the meaning and is not equivalent to `Dir.empty?`.
40
+ return if node.block_literal?
41
+
38
42
  offensive?(node) do |const_node, arg_node|
39
43
  replacement = "#{bang(node)}#{const_node.source}.empty?(#{arg_node.source})"
40
44
  add_offense(node, message: format(MSG, replacement: replacement)) do |corrector|
@@ -40,7 +40,7 @@ module RuboCop
40
40
  extend AutoCorrector
41
41
 
42
42
  MSG = 'Do not use empty `case` condition, instead use an `if` expression.'
43
- NOT_SUPPORTED_PARENT_TYPES = %i[return break next send csend].freeze
43
+ NOT_SUPPORTED_PARENT_TYPES = %i[return break next send csend yield super].freeze
44
44
 
45
45
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
46
46
  def on_case(case_node)
@@ -90,7 +90,17 @@ module RuboCop
90
90
  range = range_between(conditions.first.source_range.begin_pos,
91
91
  conditions.last.source_range.end_pos)
92
92
 
93
- corrector.replace(range, conditions.map(&:source).join(' || '))
93
+ corrector.replace(range, conditions.map { |c| parenthesize_condition(c) }.join(' || '))
94
+ end
95
+ end
96
+
97
+ # A condition that binds looser than `||` (e.g. a ternary, range, or
98
+ # assignment) must be parenthesized so the joined `||` keeps its meaning.
99
+ def parenthesize_condition(condition)
100
+ if condition.assignment? || condition.type?(:if, :and, :or, :range)
101
+ "(#{condition.source})"
102
+ else
103
+ condition.source
94
104
  end
95
105
  end
96
106
 
@@ -101,12 +101,19 @@ module RuboCop
101
101
 
102
102
  def autocorrect_class_new(corrector, node, class_new_node)
103
103
  indent = ' ' * node.loc.column
104
- class_name = node.name
104
+ class_name = constant_name(node)
105
105
  parent_class_name = class_new_node.first_argument.source
106
106
 
107
107
  corrector.replace(node, "class #{class_name} < #{parent_class_name}\n#{indent}end")
108
108
  end
109
109
 
110
+ # Preserve any namespace on the assigned constant (e.g. `Foo::Bar`),
111
+ # which `node.name` drops.
112
+ def constant_name(node)
113
+ namespace = node.namespace
114
+ namespace ? "#{namespace.source}::#{node.name}" : node.name
115
+ end
116
+
110
117
  def autocorrect_class_definition(corrector, node)
111
118
  class_name = node.identifier.source
112
119
  parent_class_name = node.parent_class.source
@@ -42,6 +42,10 @@ module RuboCop
42
42
  MSG = 'Use an empty string literal instead of heredoc.'
43
43
 
44
44
  def on_heredoc(node)
45
+ # A backtick heredoc (`<<~`CMD``) executes a command, so it cannot be
46
+ # replaced with an empty string literal.
47
+ return if node.xstr_type?
48
+
45
49
  heredoc_body = node.loc.heredoc_body
46
50
 
47
51
  return unless heredoc_body.source.empty?
@@ -44,13 +44,18 @@ module RuboCop
44
44
  def_node_matcher :str_node, '(send (const {nil? cbase} :String) :new)'
45
45
 
46
46
  # @!method array_with_block(node)
47
- def_node_matcher :array_with_block, '(block (send (const {nil? cbase} :Array) :new) args _)'
47
+ def_node_matcher :array_with_block, <<~PATTERN
48
+ {
49
+ (block (send (const {nil? cbase} :Array) :new) args _)
50
+ ({numblock itblock} (send (const {nil? cbase} :Array) :new) ...)
51
+ }
52
+ PATTERN
48
53
 
49
54
  # @!method hash_with_block(node)
50
55
  def_node_matcher :hash_with_block, <<~PATTERN
51
56
  {
52
57
  (block (send (const {nil? cbase} :Hash) :new) args _)
53
- (numblock (send (const {nil? cbase} :Hash) :new) ...)
58
+ ({numblock itblock} (send (const {nil? cbase} :Hash) :new) ...)
54
59
  }
55
60
  PATTERN
56
61
 
@@ -45,36 +45,46 @@ 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
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
49
48
  def on_interpolation(node)
50
49
  node.each_child_node(:if) do |child_node|
51
50
  if style == :trailing_conditional
52
- if empty_if_outcome?(child_node)
53
- ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
54
- end
55
-
56
- if empty_else_outcome?(child_node)
57
- ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
58
- end
51
+ trailing_conditional_correction(child_node)
59
52
  elsif style == :ternary
60
- next unless child_node.modifier_form?
61
-
62
- ternary_component = if child_node.unless?
63
- "'' : #{child_node.if_branch.source}"
64
- else
65
- "#{child_node.if_branch.source} : ''"
66
- end
67
-
68
- add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
69
- corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
70
- end
53
+ ternary_correction(node, child_node)
71
54
  end
72
55
  end
73
56
  end
74
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
75
57
 
76
58
  private
77
59
 
60
+ def trailing_conditional_correction(child_node)
61
+ # A modifier `if`/`unless` is already a trailing conditional and has
62
+ # no `else` branch, so the ternary-to-trailing rewrite does not apply.
63
+ return if child_node.modifier_form?
64
+
65
+ if empty_if_outcome?(child_node)
66
+ ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
67
+ end
68
+
69
+ return unless empty_else_outcome?(child_node)
70
+
71
+ ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
72
+ end
73
+
74
+ def ternary_correction(node, child_node)
75
+ return unless child_node.modifier_form?
76
+
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
+ add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
84
+ corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
85
+ end
86
+ end
87
+
78
88
  def empty_if_outcome?(node)
79
89
  empty_branch_outcome?(node.if_branch)
80
90
  end
@@ -45,6 +45,10 @@ module RuboCop
45
45
  def on_send(node)
46
46
  return unless env_home?(node)
47
47
  return if node.arguments.count == 2 && !node.arguments[1].nil_type?
48
+ # `ENV.fetch('HOME') { default }` supplies a fallback, just like
49
+ # `ENV.fetch('HOME', default)`. `Dir.home` ignores the block, so
50
+ # converting would silently drop it.
51
+ return if node.block_node
48
52
 
49
53
  add_offense(node) do |corrector|
50
54
  corrector.replace(node, 'Dir.home')
@@ -34,7 +34,7 @@ module RuboCop
34
34
  even_odd_candidate?(node) do |base_number, method, arg|
35
35
  replacement_method = replacement_method(arg, method)
36
36
  add_offense(node, message: format(MSG, method: replacement_method)) do |corrector|
37
- correction = "#{base_number.source}.#{replacement_method}?"
37
+ correction = "#{receiver_source(base_number)}.#{replacement_method}?"
38
38
  corrector.replace(node, correction)
39
39
  end
40
40
  end
@@ -42,6 +42,16 @@ module RuboCop
42
42
 
43
43
  private
44
44
 
45
+ def receiver_source(node)
46
+ # A binary or unary operator receiver (e.g. `a * b`, `-a`) binds looser
47
+ # than the appended method call, so it must be wrapped in parentheses.
48
+ if node.send_type? && node.operator_method? && !node.method?(:[])
49
+ "(#{node.source})"
50
+ else
51
+ node.source
52
+ end
53
+ end
54
+
45
55
  def replacement_method(arg, method)
46
56
  case arg
47
57
  when 0
@@ -43,7 +43,8 @@ module RuboCop
43
43
  return unless (parsed_regexp = parse_regexp(regexp))
44
44
  return unless exact_match_pattern?(parsed_regexp)
45
45
 
46
- prefer = "#{receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
46
+ string = escape_single_quotes(parsed_regexp[1].text)
47
+ prefer = "#{receiver.source} #{new_method(node)} '#{string}'"
47
48
 
48
49
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
49
50
  corrector.replace(node, prefer)
@@ -53,6 +54,12 @@ module RuboCop
53
54
 
54
55
  private
55
56
 
57
+ # Escape characters that are special inside a single-quoted string so the
58
+ # generated literal (e.g. for `/\Afoo'bar\z/`) stays valid Ruby.
59
+ def escape_single_quotes(text)
60
+ text.gsub(/['\\]/) { |char| "\\#{char}" }
61
+ end
62
+
56
63
  def exact_match_pattern?(parsed_regexp)
57
64
  tokens = parsed_regexp.map(&:token)
58
65
  return false unless tokens[0] == :bos && tokens[1] == :literal && tokens[2] == :eos