rubocop 1.87.0 → 1.88.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 (193) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +80 -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 +30 -10
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +5 -3
  7. data/lib/rubocop/cop/commissioner.rb +13 -11
  8. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  9. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  10. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  11. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  12. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  13. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  14. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  15. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  16. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  17. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  18. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  19. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  20. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  21. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  22. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  23. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  24. data/lib/rubocop/cop/layout/line_length.rb +10 -5
  25. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +6 -1
  26. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  27. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  28. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  29. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  30. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  31. data/lib/rubocop/cop/lint/debugger.rb +13 -2
  32. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  33. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  34. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  35. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  36. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  37. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  38. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  39. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  40. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  41. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  42. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  43. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  44. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  45. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  46. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +10 -0
  47. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  48. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  49. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  50. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  51. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  52. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  53. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  54. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  55. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  56. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  57. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  58. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  59. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  60. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  61. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  62. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  63. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  64. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  65. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  66. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  67. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  68. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  69. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  70. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  71. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  72. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  73. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  74. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  75. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  76. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  77. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +35 -9
  78. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  79. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  80. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  81. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  82. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  83. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  84. data/lib/rubocop/cop/lint/void.rb +2 -2
  85. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  86. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  87. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  88. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  89. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  90. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  91. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  92. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  93. data/lib/rubocop/cop/mixin/statement_modifier.rb +1 -1
  94. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  95. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  96. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  97. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  98. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  99. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  100. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  101. data/lib/rubocop/cop/style/alias.rb +1 -1
  102. data/lib/rubocop/cop/style/and_or.rb +1 -1
  103. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  104. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  105. data/lib/rubocop/cop/style/array_intersect.rb +8 -4
  106. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  107. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  108. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  109. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  110. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  111. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  112. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  113. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  114. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  115. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  116. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  117. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  118. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  119. data/lib/rubocop/cop/style/date_time.rb +2 -2
  120. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  121. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  122. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  123. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  124. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  125. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  126. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  127. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  128. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  129. data/lib/rubocop/cop/style/env_home.rb +4 -0
  130. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  131. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  132. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  133. data/lib/rubocop/cop/style/file_null.rb +4 -2
  134. data/lib/rubocop/cop/style/file_write.rb +17 -14
  135. data/lib/rubocop/cop/style/format_string.rb +13 -1
  136. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  137. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  138. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  139. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  140. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  141. data/lib/rubocop/cop/style/if_unless_modifier.rb +5 -5
  142. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  143. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  144. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  145. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  146. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  147. data/lib/rubocop/cop/style/lambda.rb +7 -1
  148. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  149. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  150. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  151. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
  152. data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
  153. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  154. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +10 -7
  155. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  156. data/lib/rubocop/cop/style/module_function.rb +4 -2
  157. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  158. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  159. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  160. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  161. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  162. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  163. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  164. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  165. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  166. data/lib/rubocop/cop/style/parallel_assignment.rb +19 -3
  167. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  168. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  169. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  170. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  171. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  172. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  173. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  174. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  175. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  176. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  177. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  178. data/lib/rubocop/cop/style/semicolon.rb +20 -5
  179. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  180. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  181. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  182. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  183. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  184. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  185. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  186. data/lib/rubocop/cop/style/word_array.rb +1 -0
  187. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  188. data/lib/rubocop/cop/team.rb +18 -4
  189. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  190. data/lib/rubocop/runner.rb +55 -13
  191. data/lib/rubocop/server/core.rb +6 -0
  192. data/lib/rubocop/version.rb +1 -1
  193. metadata +3 -3
@@ -6,6 +6,14 @@ module RuboCop
6
6
  # Checks whether some constant value isn't a
7
7
  # mutable literal (e.g. array or hash).
8
8
  #
9
+ # When the `Recursive` option is enabled, mutable literals nested inside
10
+ # arrays and hashes are also frozen, so an offense on the outermost
11
+ # unfrozen literal will autocorrect every nested mutable literal as well.
12
+ # When the outer literal already has `.freeze` appended, the cop descends
13
+ # into it and reports each outermost unfrozen literal underneath. The
14
+ # option is disabled by default to preserve existing behavior; opt in to
15
+ # get strict nested freezing.
16
+ #
9
17
  # Strict mode can be used to freeze all constants, rather than
10
18
  # just literals.
11
19
  # Strict mode is considered an experimental feature. It has not been
@@ -49,6 +57,17 @@ module RuboCop
49
57
  # CONST = Something.new
50
58
  #
51
59
  #
60
+ # @example Recursive: false (default)
61
+ # # good - only the outer container needs to be frozen
62
+ # CONST = [{ a: [], b: 'foo' }].freeze
63
+ #
64
+ # @example Recursive: true
65
+ # # bad - nested mutable literals must be frozen too
66
+ # CONST = [{ a: [], b: 'foo' }].freeze
67
+ #
68
+ # # good
69
+ # CONST = [{ a: [].freeze, b: 'foo'.freeze }.freeze].freeze
70
+ #
52
71
  # @example EnforcedStyle: strict
53
72
  # # bad
54
73
  # CONST = Something.new
@@ -138,10 +157,30 @@ module RuboCop
138
157
  private
139
158
 
140
159
  def on_assignment(value)
141
- if style == :strict
142
- strict_check(value)
160
+ nodes = mutable_nodes(value) do |node|
161
+ if style == :strict
162
+ strict_check(node)
163
+ else
164
+ literal_check(node)
165
+ end
166
+ end
167
+
168
+ nodes.each do |node|
169
+ add_offense(node) { |corrector| autocorrect(corrector, node) }
170
+ end
171
+ end
172
+
173
+ def mutable_nodes(value, &block)
174
+ if recursive? && explicitly_frozen_literal?(value)
175
+ literal_children(value.receiver).flat_map { |c| mutable_nodes(c, &block) }
143
176
  else
144
- check(value)
177
+ node_offending = yield(value)
178
+
179
+ if node_offending
180
+ [value]
181
+ else
182
+ []
183
+ end
145
184
  end
146
185
  end
147
186
 
@@ -151,18 +190,20 @@ module RuboCop
151
190
  return if frozen_string_literal?(value)
152
191
  return if shareable_constant_value?(value)
153
192
 
154
- add_offense(value) { |corrector| autocorrect(corrector, value) }
193
+ true
155
194
  end
156
195
 
157
- def check(value)
158
- range_enclosed_in_parentheses = range_enclosed_in_parentheses?(value)
159
- return unless mutable_literal?(value) ||
160
- (target_ruby_version <= 2.7 && range_enclosed_in_parentheses)
161
-
196
+ def literal_check(value)
197
+ return unless mutable_or_unfrozen_range?(value)
162
198
  return if frozen_string_literal?(value)
163
199
  return if shareable_constant_value?(value)
164
200
 
165
- add_offense(value) { |corrector| autocorrect(corrector, value) }
201
+ true
202
+ end
203
+
204
+ def mutable_or_unfrozen_range?(value)
205
+ mutable_literal?(value) ||
206
+ (target_ruby_version <= 2.7 && range_enclosed_in_parentheses?(value))
166
207
  end
167
208
 
168
209
  def autocorrect(corrector, node)
@@ -171,13 +212,66 @@ module RuboCop
171
212
  splat_value = splat_value(node)
172
213
  if splat_value
173
214
  correct_splat_expansion(corrector, expr, splat_value)
174
- elsif node.array_type? && !node.bracketed?
215
+ corrector.insert_after(expr, '.freeze')
216
+ return
217
+ end
218
+
219
+ if node.array_type? && !node.bracketed?
175
220
  corrector.wrap(expr, '[', ']')
176
221
  elsif requires_parentheses?(node)
177
222
  corrector.wrap(expr, '(', ')')
178
223
  end
179
224
 
180
225
  corrector.insert_after(expr, '.freeze')
226
+
227
+ freeze_nested_literals(corrector, node) if recursive?
228
+ end
229
+
230
+ # Recursively freezes every nested mutable literal inside an array or
231
+ # hash literal. Already-frozen subtrees are not re-frozen, but their
232
+ # children are still inspected for unfrozen literals deeper down.
233
+ def freeze_nested_literals(corrector, node)
234
+ literal_children(node).each do |child|
235
+ if explicitly_frozen_literal?(child)
236
+ freeze_nested_literals(corrector, child.receiver)
237
+ elsif freezable_nested_literal?(child)
238
+ autocorrect(corrector, child)
239
+ end
240
+ end
241
+ end
242
+
243
+ def freezable_nested_literal?(node)
244
+ return false if frozen_string_literal?(node)
245
+ return false if shareable_constant_value?(node)
246
+
247
+ mutable_literal?(node)
248
+ end
249
+
250
+ # Returns the child literals of an array or hash node that may
251
+ # themselves need freezing. For hashes, both keys and values are
252
+ # included. Percent-literal arrays (e.g. `%w(a b)`) are skipped because
253
+ # `.freeze` cannot be appended to their contents.
254
+ def literal_children(node)
255
+ case node.type
256
+ when :array
257
+ return [] if node.percent_literal?
258
+
259
+ node.children
260
+ when :hash
261
+ node.children.flat_map { |child| child.pair_type? ? child.children : [] }
262
+ else
263
+ []
264
+ end
265
+ end
266
+
267
+ def explicitly_frozen_literal?(node)
268
+ return false unless node.send_type? && node.method?(:freeze)
269
+
270
+ node.receiver && mutable_literal?(node.receiver)
271
+ end
272
+
273
+ def recursive?
274
+ cop_config.fetch('Recursive', false)
181
275
  end
182
276
 
183
277
  def mutable_literal?(value)
@@ -6,6 +6,11 @@ module RuboCop
6
6
  # Checks for lambdas and procs that always return nil,
7
7
  # which can be replaced with an empty lambda or proc instead.
8
8
  #
9
+ # NOTE: A `proc` that returns nil via an explicit `return` is allowed,
10
+ # because in a `proc` `return` exits the enclosing method, so removing it
11
+ # would change behavior. A lambda is still reported, since there `return`
12
+ # only exits the lambda itself.
13
+ #
9
14
  # @example
10
15
  # # bad
11
16
  # -> { nil }
@@ -46,6 +51,9 @@ module RuboCop
46
51
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
47
52
  return unless node.lambda_or_proc?
48
53
  return unless nil_return?(node.body)
54
+ # `return` inside a non-lambda proc returns from the enclosing method,
55
+ # so dropping it changes behavior; only a lambda can omit it safely.
56
+ return if node.body.return_type? && !node.lambda?
49
57
 
50
58
  message = format(MSG, type: node.lambda? ? 'lambda' : 'proc')
51
59
  add_offense(node, message: message) do |corrector|
@@ -92,7 +92,7 @@ module RuboCop
92
92
  return unless numeric
93
93
 
94
94
  return if allowed_method_name?(node.method_name) ||
95
- node.each_ancestor(:send, :block).any? do |ancestor|
95
+ node.each_ancestor(:send, :any_block).any? do |ancestor|
96
96
  allowed_method_name?(ancestor.method_name)
97
97
  end
98
98
 
@@ -61,7 +61,7 @@ module RuboCop
61
61
  def custom_class_or_module_definition?(node)
62
62
  parent = node.parent
63
63
 
64
- parent.type?(:class, :module) && node.left_siblings.empty?
64
+ parent&.type?(:class, :module) && node.left_siblings.empty?
65
65
  end
66
66
  end
67
67
  end
@@ -46,7 +46,7 @@ module RuboCop
46
46
  end
47
47
 
48
48
  def super_used?(node)
49
- node.parent.each_node(:zsuper).any?
49
+ node.parent.each_node(:zsuper, :super).any?
50
50
  end
51
51
  end
52
52
  end
@@ -27,6 +27,7 @@ module RuboCop
27
27
  def on_def(node)
28
28
  each_misplaced_optional_arg(node.arguments) { |argument| add_offense(argument) }
29
29
  end
30
+ alias on_defs on_def
30
31
 
31
32
  private
32
33
 
@@ -38,7 +38,7 @@ module RuboCop
38
38
  rhs_elements = Array(rhs).compact # edge case for one constant
39
39
 
40
40
  return if allowed_lhs?(node.assignments) || allowed_rhs?(rhs) ||
41
- allowed_masign?(node.assignments, rhs_elements)
41
+ allowed_masign?(node.assignments, rhs_elements) || contains_heredoc?(rhs)
42
42
 
43
43
  range = node.source_range.begin.join(rhs.source_range.end)
44
44
 
@@ -77,6 +77,13 @@ module RuboCop
77
77
  !node.array_type? || elements.any?(&:splat_type?)
78
78
  end
79
79
 
80
+ # Autocorrection splits the assignment into single assignments on
81
+ # consecutive lines, which would put following assignments into the
82
+ # heredoc body unless the heredoc bodies were moved along.
83
+ def contains_heredoc?(node)
84
+ node.each_descendant(:any_str).any?(&:heredoc?)
85
+ end
86
+
80
87
  def assignment_corrector(node, rhs, order)
81
88
  if node.parent&.rescue_type?
82
89
  _assignment, modifier = *node.parent
@@ -226,14 +233,23 @@ module RuboCop
226
233
  def source(node, loc)
227
234
  # __FILE__ is treated as a StrNode but has no begin
228
235
  if node.str_type? && loc.respond_to?(:begin) && loc.begin.nil?
229
- "'#{node.source}'"
236
+ # `%w` elements have no per-element delimiter, so the value must be
237
+ # quoted and escaped to stay valid (e.g. `%w(it's)` -> `'it\'s'`).
238
+ quote(node.value)
230
239
  elsif node.sym_type? && !node.loc?(:begin)
231
- ":#{node.source}"
240
+ # `%i` elements have no per-element delimiter, so a symbol that needs
241
+ # quoting must be emitted as `:"..."` (e.g. `%i(foo-bar)` -> `:"foo-bar"`),
242
+ # otherwise `:foo-bar` would parse as `:foo.-(bar)`.
243
+ node.value.inspect
232
244
  else
233
245
  node.source
234
246
  end
235
247
  end
236
248
 
249
+ def quote(string)
250
+ "'#{string.gsub(/[\\']/) { |char| "\\#{char}" }}'"
251
+ end
252
+
237
253
  def extract_sources(node)
238
254
  node.children.map(&:source)
239
255
  end
@@ -101,6 +101,8 @@ module RuboCop
101
101
  def string_source(node)
102
102
  if node.is_a?(String)
103
103
  node.scrub
104
+ elsif node.is_a?(Symbol)
105
+ node.to_s
104
106
  elsif node.respond_to?(:type) && node.type?(:str, :sym)
105
107
  node.source
106
108
  end
@@ -5,7 +5,7 @@ module RuboCop
5
5
  module Style
6
6
  # Looks for uses of Perl-style regexp match
7
7
  # backreferences and their English versions like
8
- # $1, $2, $&, &+, $MATCH, $PREMATCH, etc.
8
+ # $1, $2, $&, $MATCH, $PREMATCH, etc.
9
9
  #
10
10
  # @example
11
11
  # # bad
@@ -69,6 +69,10 @@ module RuboCop
69
69
  # @return [String, nil]
70
70
  def preferred_expression_to(node)
71
71
  first = node.to_a.first
72
+ # NOTE: `$+` / `$LAST_PAREN_MATCH` is deliberately not converted. It
73
+ # refers to the last group that actually matched, which has no concise
74
+ # `Regexp.last_match` equivalent (`Regexp.last_match(-1)` is the last
75
+ # group in the pattern, which may be `nil`).
72
76
  case first
73
77
  when ::Integer
74
78
  "Regexp.last_match(#{first})"
@@ -78,8 +82,6 @@ module RuboCop
78
82
  'Regexp.last_match.pre_match'
79
83
  when :$', :$POSTMATCH
80
84
  'Regexp.last_match.post_match'
81
- when :$+, :$LAST_PAREN_MATCH
82
- 'Regexp.last_match(-1)'
83
85
  end
84
86
  end
85
87
 
@@ -26,7 +26,9 @@ module RuboCop
26
26
  def on_send(node)
27
27
  return unless (first_argument = node.first_argument)
28
28
  return unless (index = first_argument.source.index(CURRENT_DIRECTORY_PREFIX))
29
- return unless (redundant_length = redundant_path_length(first_argument.str_content))
29
+
30
+ content = leading_path_content(first_argument)
31
+ return unless (redundant_length = redundant_path_length(content))
30
32
 
31
33
  begin_pos = first_argument.source_range.begin.begin_pos + index
32
34
  end_pos = begin_pos + redundant_length
@@ -39,6 +41,16 @@ module RuboCop
39
41
 
40
42
  private
41
43
 
44
+ # The literal text at the start of the path, which is the whole string
45
+ # for a plain string and the leading literal segment for an interpolated
46
+ # one (`nil` when it starts with interpolation).
47
+ def leading_path_content(node)
48
+ return node.str_content if node.str_type?
49
+ return unless node.dstr_type? && (first = node.children.first)&.str_type?
50
+
51
+ first.str_content
52
+ end
53
+
42
54
  def redundant_path_length(path)
43
55
  return unless (match = path&.match(REDUNDANT_CURRENT_DIRECTORY_PREFIX))
44
56
 
@@ -38,6 +38,10 @@ module RuboCop
38
38
 
39
39
  def fix_exploded(node)
40
40
  exploded?(node) do |command, message|
41
+ # `raise RuntimeError, nil` uses the class name as the message, so
42
+ # rewriting it to `raise nil.to_s` (an empty message) would change it.
43
+ next if message.nil_type?
44
+
41
45
  add_offense(node, message: MSG_1) do |corrector|
42
46
  corrector.replace(node, replaced_exploded(node, command, message))
43
47
  end
@@ -56,6 +60,8 @@ module RuboCop
56
60
 
57
61
  def fix_compact(node)
58
62
  compact?(node) do |new_call, message|
63
+ next if message.nil_type?
64
+
59
65
  add_offense(node, message: MSG_2) do |corrector|
60
66
  corrector.replace(new_call, replaced_compact(message))
61
67
  end
@@ -62,7 +62,7 @@ module RuboCop
62
62
  def_node_matcher :select_predicate?, <<~PATTERN
63
63
  (call
64
64
  {
65
- (block $(call _ {:select :filter :find_all}) ...)
65
+ (any_block $(call _ {:select :filter :find_all}) ...)
66
66
  $(call _ {:select :filter :find_all} block_pass_type?)
67
67
  }
68
68
  ${:#{RESTRICT_ON_SEND.join(' :')}})
@@ -89,6 +89,8 @@ module RuboCop
89
89
 
90
90
  def on_send(node)
91
91
  format_without_additional_args?(node) do |value|
92
+ next if string_with_format_sequence?(value)
93
+
92
94
  replacement = escape_control_chars(value.source)
93
95
 
94
96
  add_offense(node, message: message(node, replacement)) do |corrector|
@@ -102,12 +104,31 @@ module RuboCop
102
104
 
103
105
  private
104
106
 
107
+ # A single-argument `format` whose string still contains a format sequence is
108
+ # not redundant: `format('%s')` raises at runtime, and `format('%%')` returns
109
+ # `'%'`, so replacing it with the literal would change behavior.
110
+ def string_with_format_sequence?(node)
111
+ string = static_string_value(node)
112
+ return false unless string
113
+
114
+ RuboCop::Cop::Utils::FormatString.new(string).format_sequences.any?
115
+ end
116
+
117
+ def static_string_value(node)
118
+ if node.str_type?
119
+ node.value
120
+ elsif node.dstr_type?
121
+ node.children.select(&:str_type?).map(&:value).join
122
+ end
123
+ end
124
+
105
125
  def message(node, prefer)
106
126
  format(MSG, prefer: prefer, method_name: node.method_name)
107
127
  end
108
128
 
109
129
  def detect_unnecessary_fields(node)
110
130
  return unless node.first_argument&.str_type?
131
+ return if node.first_argument.heredoc?
111
132
 
112
133
  string = node.first_argument.value
113
134
  arguments = node.arguments[1..]
@@ -245,6 +266,14 @@ module RuboCop
245
266
  def argument_value(argument)
246
267
  argument = argument.children.first if argument.begin_type?
247
268
 
269
+ # `nil` formats to an empty string (`format('%s', nil) == ''`), not the
270
+ # literal `'nil'` that `argument.source` would return.
271
+ return if argument.nil_type?
272
+
273
+ typed_argument_value(argument)
274
+ end
275
+
276
+ def typed_argument_value(argument)
248
277
  if argument.dsym_type?
249
278
  dsym_value(argument)
250
279
  elsif argument.hash_type?
@@ -48,8 +48,13 @@ module RuboCop
48
48
  def need_heredoc_delimiter_quotes?(node)
49
49
  heredoc_delimiter = node.source.delete(heredoc_type(node))
50
50
  return true unless heredoc_delimiter.start_with?("'", '"')
51
+ return true if node.loc.heredoc_end.source.strip.match?(/\W/)
51
52
 
52
- node.loc.heredoc_end.source.strip.match?(/\W/) ||
53
+ # A double-quoted delimiter interpolates exactly like an unquoted one,
54
+ # so its quotes are always redundant. A single-quoted delimiter is
55
+ # required when the body contains interpolation or escapes that would
56
+ # otherwise be evaluated.
57
+ heredoc_delimiter.start_with?("'") &&
53
58
  node.loc.heredoc_body.source.match?(STRING_INTERPOLATION_OR_ESCAPED_CHARACTER_PATTERN)
54
59
  end
55
60
  end
@@ -165,11 +165,11 @@ module RuboCop
165
165
  end
166
166
 
167
167
  def inspect_end_of_ruby_code_line_continuation
168
- last_line = processed_source.lines[processed_source.ast.last_line - 1]
168
+ last_line_number = processed_source.ast.last_line
169
+ last_line = processed_source.lines[last_line_number - 1]
169
170
  return unless code_ends_with_continuation?(last_line)
170
171
 
171
- last_column = last_line.length
172
- line_continuation_range = range_between(last_column - 1, last_column)
172
+ line_continuation_range = trailing_line_continuation_range(last_line_number)
173
173
 
174
174
  add_offense(line_continuation_range) do |corrector|
175
175
  corrector.remove_trailing(line_continuation_range, 1)
@@ -182,6 +182,14 @@ module RuboCop
182
182
  last_line.end_with?(LINE_CONTINUATION)
183
183
  end
184
184
 
185
+ # The backslash is the last character of the line; locate it by the line's
186
+ # position in the buffer rather than treating the column as an absolute offset
187
+ # (which corrupts an earlier line in multi-line files).
188
+ def trailing_line_continuation_range(line_number)
189
+ line_range = processed_source.buffer.line_range(line_number)
190
+ range_between(line_range.end_pos - 1, line_range.end_pos)
191
+ end
192
+
185
193
  def inside_string_literal?(range, token)
186
194
  ALLOWED_STRING_TOKENS.include?(token.type) && token.pos.overlaps?(range)
187
195
  end
@@ -65,7 +65,7 @@ module RuboCop
65
65
  # different versions of Ruby so that e.g. /\i/ != /i/
66
66
  return true if /[[:alnum:]]/.match?(char)
67
67
  return true if ALLOWED_ALWAYS_ESCAPES.include?(char) || delimiter?(node, char)
68
- return true if requires_escape_to_avoid_interpolation?(node.source[index], char)
68
+ return true if requires_escape_to_avoid_interpolation?(node, index, char)
69
69
 
70
70
  if within_character_class
71
71
  ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES.include?(char) &&
@@ -97,10 +97,14 @@ module RuboCop
97
97
  delimiters.include?(char)
98
98
  end
99
99
 
100
- def requires_escape_to_avoid_interpolation?(char_before_escape, escaped_char)
100
+ def requires_escape_to_avoid_interpolation?(node, index, escaped_char)
101
101
  # Preserve escapes after '#' that would otherwise trigger interpolation:
102
- # '#@ivar', '#@@cvar', and '#$gvar'.
103
- char_before_escape == '#' && INTERPOLATION_SIGILS.include?(escaped_char)
102
+ # '#@ivar', '#@@cvar', and '#$gvar'. `index` is relative to the regexp
103
+ # contents, so index into those rather than `node.source` (which also
104
+ # includes the opening delimiter, e.g. `%r{`).
105
+ return false unless index.positive? && INTERPOLATION_SIGILS.include?(escaped_char)
106
+
107
+ contents_range(node).source[index - 1] == '#'
104
108
  end
105
109
 
106
110
  def each_escape(node)
@@ -100,6 +100,15 @@ module RuboCop
100
100
  add_lhs_to_local_variables_scopes(node.rhs, node.lhs)
101
101
  end
102
102
 
103
+ # Register the exception variable of `rescue => e` so that `self.e` in the
104
+ # body is not treated as redundant (it disambiguates the local variable).
105
+ def on_resbody(node)
106
+ exception_variable = node.exception_variable
107
+ return unless exception_variable&.lvasgn_type?
108
+
109
+ @local_variables_scopes[node] << exception_variable.name
110
+ end
111
+
103
112
  def on_in_pattern(node)
104
113
  add_match_var_scopes(node)
105
114
  end
@@ -100,12 +100,31 @@ module RuboCop
100
100
  end
101
101
 
102
102
  def range(redundant_keyword_init)
103
- if redundant_keyword_init.parent.left_siblings.last.is_a?(AST::Node)
104
- beginning_of_range = redundant_keyword_init.parent.left_siblings.last.source_range.end
103
+ if redundant_keyword_init.parent.pairs.all? { |pair| keyword_init?(pair) }
104
+ # The hash holds only `keyword_init` pairs, so it is emptied; anchor the
105
+ # removal before the hash to also drop the comma that precedes it.
106
+ range_emptying_hash(redundant_keyword_init)
107
+ else
108
+ # Other pairs remain, so just drop this pair and one adjacent comma.
109
+ range_within_hash(redundant_keyword_init)
110
+ end
111
+ end
112
+
113
+ def range_within_hash(pair)
114
+ if (left = pair.left_sibling)
115
+ left.source_range.end.join(pair.source_range.end)
116
+ elsif (right = pair.right_sibling)
117
+ pair.source_range.begin.join(right.source_range.begin)
118
+ else
119
+ pair.source_range
120
+ end
121
+ end
105
122
 
106
- beginning_of_range.join(redundant_keyword_init.source_range.end)
123
+ def range_emptying_hash(pair)
124
+ if (preceding = pair.parent.left_sibling).is_a?(AST::Node)
125
+ preceding.source_range.end.join(pair.source_range.end)
107
126
  else
108
- redundant_keyword_init
127
+ pair.source_range
109
128
  end
110
129
  end
111
130
  end
@@ -128,7 +128,7 @@ module RuboCop
128
128
 
129
129
  add_offense(range) do |corrector|
130
130
  if after_expression
131
- corrector.replace(range, "\n")
131
+ replace_semicolon_with_line_break(corrector, range)
132
132
  else
133
133
  # Prevents becoming one range instance with subsequent line when endless range
134
134
  # without parentheses.
@@ -148,6 +148,21 @@ module RuboCop
148
148
  end
149
149
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
150
150
 
151
+ def replace_semicolon_with_line_break(corrector, range)
152
+ # Replacing the semicolon with a newline would move the rest of the
153
+ # line into the body of a heredoc opened earlier on that line.
154
+ return if heredoc_opened_before_semicolon?(range)
155
+
156
+ corrector.replace(range, "\n")
157
+ end
158
+
159
+ def heredoc_opened_before_semicolon?(semicolon_range)
160
+ processed_source.ast.each_descendant(:any_str).select(&:heredoc?).any? do |heredoc|
161
+ heredoc.first_line == semicolon_range.line &&
162
+ heredoc.source_range.end_pos <= semicolon_range.begin_pos
163
+ end
164
+ end
165
+
151
166
  def expressions_per_line(exprs)
152
167
  # create a map matching lines to the number of expressions on them
153
168
  exprs_lines = exprs.map(&:last_line)
@@ -155,10 +170,10 @@ module RuboCop
155
170
  end
156
171
 
157
172
  def find_semicolon_positions(line)
158
- # Scan for all the semicolons on the line
159
- semicolons = processed_source[line - 1].enum_for(:scan, ';')
160
- semicolons.each do
161
- yield Regexp.last_match.begin(0)
173
+ # Scan for all the semicolon tokens on the line. Iterating tokens rather
174
+ # than the raw source skips `;` characters inside string/regexp literals.
175
+ processed_source.tokens.each do |token|
176
+ yield token.column if token.line == line && token.semicolon?
162
177
  end
163
178
  end
164
179
 
@@ -44,11 +44,12 @@ module RuboCop
44
44
  add_offense(node) do |corrector|
45
45
  corrector.insert_after(do_line(node), "\n")
46
46
 
47
- node_body = node.body
48
-
49
- if node_body.respond_to?(:heredoc?) && node_body.heredoc?
47
+ if (heredoc = trailing_heredoc(node.body))
48
+ # The heredoc body extends past the `end` on the source, so the
49
+ # `end` has to be moved after it rather than before, which would
50
+ # otherwise move it into the heredoc body and break the syntax.
50
51
  corrector.remove(node.loc.end)
51
- corrector.insert_after(node_body.loc.heredoc_end, "\nend")
52
+ corrector.insert_after(heredoc.loc.heredoc_end, "\nend")
52
53
  else
53
54
  corrector.insert_before(node.loc.end, "\n")
54
55
  end
@@ -60,6 +61,18 @@ module RuboCop
60
61
 
61
62
  private
62
63
 
64
+ # Returns the heredoc opened on the block's line whose body extends the
65
+ # furthest down, whether it is the block body itself or nested within it.
66
+ def trailing_heredoc(node_body)
67
+ return unless node_body
68
+
69
+ heredocs = [node_body, *node_body.each_descendant].select do |node|
70
+ node.respond_to?(:heredoc?) && node.heredoc?
71
+ end
72
+
73
+ heredocs.max_by { |heredoc| heredoc.loc.heredoc_end.line }
74
+ end
75
+
63
76
  def do_line(node)
64
77
  if node.type?(:numblock, :itblock) ||
65
78
  node.arguments.children.empty? || node.send_node.lambda_literal?
@@ -41,6 +41,7 @@ module RuboCop
41
41
 
42
42
  def on_pair(node)
43
43
  return unless string_hash_key?(node)
44
+ return if node.key.heredoc?
44
45
 
45
46
  key_content = node.key.str_content
46
47
  return unless key_content.valid_encoding?
@@ -41,7 +41,9 @@ module RuboCop
41
41
  return unless struct_constructor?(node.parent_class)
42
42
 
43
43
  add_offense(node.parent_class) do |corrector|
44
- corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
44
+ corrector.remove(
45
+ range_with_surrounding_space(node.loc.keyword, side: :right, newlines: false)
46
+ )
45
47
  corrector.replace(node.loc.operator, '=')
46
48
 
47
49
  correct_parent(node.parent_class, corrector)