rubocop 1.84.2 → 1.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +180 -86
  3. data/config/obsoletion.yml +26 -1
  4. data/lib/rubocop/cache_config.rb +1 -1
  5. data/lib/rubocop/cli/command/auto_generate_config.rb +34 -2
  6. data/lib/rubocop/cli/command/list_enabled_cops_for.rb +40 -0
  7. data/lib/rubocop/cli/command/mcp.rb +19 -0
  8. data/lib/rubocop/cli/command/show_cops.rb +2 -2
  9. data/lib/rubocop/cli/command/show_docs_url.rb +4 -8
  10. data/lib/rubocop/cli/command/suggest_extensions.rb +1 -1
  11. data/lib/rubocop/cli.rb +9 -7
  12. data/lib/rubocop/comment_config.rb +12 -15
  13. data/lib/rubocop/config.rb +14 -10
  14. data/lib/rubocop/config_finder.rb +1 -1
  15. data/lib/rubocop/config_loader.rb +17 -2
  16. data/lib/rubocop/config_loader_resolver.rb +13 -4
  17. data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
  18. data/lib/rubocop/config_store.rb +2 -2
  19. data/lib/rubocop/config_validator.rb +1 -1
  20. data/lib/rubocop/cop/autocorrect_logic.rb +2 -1
  21. data/lib/rubocop/cop/base.rb +25 -4
  22. data/lib/rubocop/cop/bundler/gem_comment.rb +2 -2
  23. data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
  24. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +1 -5
  25. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +33 -2
  26. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
  27. data/lib/rubocop/cop/correctors.rb +28 -0
  28. data/lib/rubocop/cop/documentation.rb +2 -3
  29. data/lib/rubocop/cop/exclude_limit.rb +31 -5
  30. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +2 -2
  31. data/lib/rubocop/cop/gemspec/require_mfa.rb +5 -5
  32. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +3 -3
  33. data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
  34. data/lib/rubocop/cop/internal_affairs/location_line_equality_comparison.rb +1 -0
  35. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  36. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  37. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -2
  38. data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
  39. data/lib/rubocop/cop/layout/begin_end_alignment.rb +1 -1
  40. data/lib/rubocop/cop/layout/block_alignment.rb +41 -4
  41. data/lib/rubocop/cop/layout/class_structure.rb +1 -1
  42. data/lib/rubocop/cop/layout/dot_position.rb +1 -1
  43. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +23 -7
  44. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +1 -1
  45. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  46. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
  47. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
  48. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
  49. data/lib/rubocop/cop/layout/end_alignment.rb +8 -5
  50. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +7 -1
  51. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  52. data/lib/rubocop/cop/layout/indentation_width.rb +12 -0
  53. data/lib/rubocop/cop/layout/line_length.rb +5 -3
  54. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
  55. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +1 -1
  56. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +53 -3
  57. data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
  58. data/lib/rubocop/cop/layout/redundant_line_break.rb +3 -1
  59. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  60. data/lib/rubocop/cop/layout/space_around_keyword.rb +3 -1
  61. data/lib/rubocop/cop/layout/space_before_brackets.rb +1 -1
  62. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +1 -0
  63. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  64. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +1 -1
  65. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  66. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  67. data/lib/rubocop/cop/lint/constant_overwritten_in_rescue.rb +1 -1
  68. data/lib/rubocop/cop/lint/constant_reassignment.rb +93 -11
  69. data/lib/rubocop/cop/lint/constant_resolution.rb +6 -6
  70. data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
  71. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  72. data/lib/rubocop/cop/lint/deprecated_constants.rb +2 -8
  73. data/lib/rubocop/cop/lint/duplicate_methods.rb +55 -8
  74. data/lib/rubocop/cop/lint/empty_block.rb +4 -4
  75. data/lib/rubocop/cop/lint/empty_conditional_body.rb +6 -1
  76. data/lib/rubocop/cop/lint/empty_in_pattern.rb +8 -1
  77. data/lib/rubocop/cop/lint/empty_when.rb +8 -1
  78. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  79. data/lib/rubocop/cop/lint/erb_new_arguments.rb +4 -2
  80. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  81. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  82. data/lib/rubocop/cop/lint/interpolation_check.rb +25 -5
  83. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  84. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  85. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  86. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +5 -5
  87. data/lib/rubocop/cop/lint/multiple_comparison.rb +2 -2
  88. data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
  89. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  90. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +4 -2
  91. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  92. data/lib/rubocop/cop/lint/number_conversion.rb +19 -10
  93. data/lib/rubocop/cop/lint/numbered_parameter_assignment.rb +1 -1
  94. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  95. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  96. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +3 -13
  97. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  98. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  99. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  100. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +6 -12
  101. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  102. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +36 -12
  103. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  104. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +10 -3
  105. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  106. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  107. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  108. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  109. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  110. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  111. data/lib/rubocop/cop/lint/require_relative_self_path.rb +7 -5
  112. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  113. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +18 -0
  114. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
  115. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  116. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  117. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  118. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  119. data/lib/rubocop/cop/lint/shadowed_exception.rb +1 -1
  120. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  121. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  122. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  123. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  124. data/lib/rubocop/cop/lint/syntax.rb +25 -1
  125. data/lib/rubocop/cop/lint/to_enum_arguments.rb +28 -1
  126. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  127. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +5 -1
  128. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +4 -2
  129. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
  130. data/lib/rubocop/cop/lint/unreachable_code.rb +2 -2
  131. data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
  132. data/lib/rubocop/cop/lint/unused_method_argument.rb +10 -0
  133. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -14
  134. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +4 -4
  135. data/lib/rubocop/cop/lint/useless_default_value_argument.rb +2 -0
  136. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +8 -4
  137. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  138. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  139. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +35 -9
  140. data/lib/rubocop/cop/lint/void.rb +32 -12
  141. data/lib/rubocop/cop/metrics/block_length.rb +1 -1
  142. data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
  143. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  144. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  145. data/lib/rubocop/cop/metrics/utils/iterating_block.rb +1 -1
  146. data/lib/rubocop/cop/migration/department_name.rb +12 -1
  147. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  148. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +2 -2
  149. data/lib/rubocop/cop/mixin/configurable_max.rb +6 -5
  150. data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
  151. data/lib/rubocop/cop/mixin/hash_transform_method.rb +10 -60
  152. data/lib/rubocop/cop/mixin/project_index_help.rb +48 -0
  153. data/lib/rubocop/cop/mixin.rb +86 -0
  154. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +1 -1
  155. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  156. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +1 -1
  157. data/lib/rubocop/cop/naming/predicate_method.rb +2 -2
  158. data/lib/rubocop/cop/naming/predicate_prefix.rb +1 -1
  159. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  160. data/lib/rubocop/cop/offense.rb +8 -0
  161. data/lib/rubocop/cop/registry.rb +62 -38
  162. data/lib/rubocop/cop/security/eval.rb +15 -2
  163. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  164. data/lib/rubocop/cop/style/access_modifier_declarations.rb +14 -2
  165. data/lib/rubocop/cop/style/accessor_grouping.rb +4 -2
  166. data/lib/rubocop/cop/style/alias.rb +15 -3
  167. data/lib/rubocop/cop/style/and_or.rb +2 -1
  168. data/lib/rubocop/cop/style/arguments_forwarding.rb +25 -7
  169. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  170. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  171. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  172. data/lib/rubocop/cop/style/array_join.rb +4 -2
  173. data/lib/rubocop/cop/style/ascii_comments.rb +6 -3
  174. data/lib/rubocop/cop/style/attr.rb +5 -2
  175. data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
  176. data/lib/rubocop/cop/style/begin_block.rb +3 -1
  177. data/lib/rubocop/cop/style/block_delimiters.rb +37 -31
  178. data/lib/rubocop/cop/style/case_equality.rb +18 -2
  179. data/lib/rubocop/cop/style/character_literal.rb +2 -2
  180. data/lib/rubocop/cop/style/class_and_module_children.rb +18 -2
  181. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  182. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  183. data/lib/rubocop/cop/style/collection_compact.rb +36 -16
  184. data/lib/rubocop/cop/style/colon_method_call.rb +16 -7
  185. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  186. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  187. data/lib/rubocop/cop/style/concat_array_literals.rb +7 -1
  188. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -5
  189. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  190. data/lib/rubocop/cop/style/copyright.rb +22 -11
  191. data/lib/rubocop/cop/style/date_time.rb +4 -4
  192. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  193. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +1 -1
  194. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +6 -1
  195. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  196. data/lib/rubocop/cop/style/each_with_object.rb +2 -0
  197. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  198. data/lib/rubocop/cop/style/empty_class_definition.rb +43 -20
  199. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  200. data/lib/rubocop/cop/style/encoding.rb +7 -1
  201. data/lib/rubocop/cop/style/end_block.rb +3 -1
  202. data/lib/rubocop/cop/style/endless_method.rb +8 -3
  203. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  204. data/lib/rubocop/cop/style/file_open.rb +84 -0
  205. data/lib/rubocop/cop/style/file_write.rb +21 -16
  206. data/lib/rubocop/cop/style/for.rb +3 -0
  207. data/lib/rubocop/cop/style/format_string.rb +4 -3
  208. data/lib/rubocop/cop/style/format_string_token.rb +29 -2
  209. data/lib/rubocop/cop/style/global_vars.rb +5 -2
  210. data/lib/rubocop/cop/style/guard_clause.rb +9 -6
  211. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
  212. data/lib/rubocop/cop/style/hash_conversion.rb +1 -1
  213. data/lib/rubocop/cop/style/hash_lookup_method.rb +19 -7
  214. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  215. data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
  216. data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
  217. data/lib/rubocop/cop/style/if_inside_else.rb +16 -7
  218. data/lib/rubocop/cop/style/if_unless_modifier.rb +15 -4
  219. data/lib/rubocop/cop/style/if_with_semicolon.rb +7 -5
  220. data/lib/rubocop/cop/style/inline_comment.rb +4 -1
  221. data/lib/rubocop/cop/style/ip_addresses.rb +1 -2
  222. data/lib/rubocop/cop/style/magic_comment_format.rb +3 -3
  223. data/lib/rubocop/cop/style/map_join.rb +123 -0
  224. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +5 -3
  225. data/lib/rubocop/cop/style/min_max_comparison.rb +1 -1
  226. data/lib/rubocop/cop/style/module_member_existence_check.rb +7 -14
  227. data/lib/rubocop/cop/style/multiline_if_then.rb +3 -1
  228. data/lib/rubocop/cop/style/mutable_constant.rb +106 -12
  229. data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
  230. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  231. data/lib/rubocop/cop/style/non_nil_check.rb +5 -11
  232. data/lib/rubocop/cop/style/not.rb +2 -0
  233. data/lib/rubocop/cop/style/numeric_literals.rb +3 -2
  234. data/lib/rubocop/cop/style/one_class_per_file.rb +115 -0
  235. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
  236. data/lib/rubocop/cop/style/parallel_assignment.rb +12 -1
  237. data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
  238. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  239. data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
  240. data/lib/rubocop/cop/style/proc.rb +3 -2
  241. data/lib/rubocop/cop/style/raise_args.rb +1 -1
  242. data/lib/rubocop/cop/style/reduce_to_hash.rb +200 -0
  243. data/lib/rubocop/cop/style/redundant_array_constructor.rb +2 -2
  244. data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
  245. data/lib/rubocop/cop/style/redundant_constant_base.rb +5 -5
  246. data/lib/rubocop/cop/style/redundant_each.rb +3 -3
  247. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  248. data/lib/rubocop/cop/style/redundant_format.rb +1 -0
  249. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
  250. data/lib/rubocop/cop/style/redundant_line_continuation.rb +16 -0
  251. data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
  252. data/lib/rubocop/cop/style/redundant_parentheses.rb +25 -22
  253. data/lib/rubocop/cop/style/redundant_percent_q.rb +4 -1
  254. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +2 -2
  255. data/lib/rubocop/cop/style/redundant_return.rb +3 -1
  256. data/lib/rubocop/cop/style/redundant_self.rb +2 -2
  257. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +0 -5
  258. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +114 -0
  259. data/lib/rubocop/cop/style/regexp_literal.rb +31 -2
  260. data/lib/rubocop/cop/style/rescue_modifier.rb +3 -3
  261. data/lib/rubocop/cop/style/safe_navigation.rb +7 -7
  262. data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
  263. data/lib/rubocop/cop/style/select_by_range.rb +197 -0
  264. data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
  265. data/lib/rubocop/cop/style/self_assignment.rb +1 -1
  266. data/lib/rubocop/cop/style/semicolon.rb +18 -1
  267. data/lib/rubocop/cop/style/single_line_block_params.rb +2 -2
  268. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -1
  269. data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
  270. data/lib/rubocop/cop/style/sole_nested_conditional.rb +4 -2
  271. data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
  272. data/lib/rubocop/cop/style/struct_inheritance.rb +13 -0
  273. data/lib/rubocop/cop/style/symbol_proc.rb +7 -6
  274. data/lib/rubocop/cop/style/tally_method.rb +181 -0
  275. data/lib/rubocop/cop/style/top_level_method_definition.rb +2 -2
  276. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  277. data/lib/rubocop/cop/style/trailing_method_end_statement.rb +1 -0
  278. data/lib/rubocop/cop/style/unless_logical_operators.rb +3 -3
  279. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  280. data/lib/rubocop/cop/style/while_until_modifier.rb +16 -0
  281. data/lib/rubocop/cop/style/word_array.rb +1 -0
  282. data/lib/rubocop/cop/style/yoda_condition.rb +1 -1
  283. data/lib/rubocop/cop/style/yoda_expression.rb +1 -1
  284. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  285. data/lib/rubocop/cop/team.rb +86 -35
  286. data/lib/rubocop/cop/variable_force/branch.rb +2 -2
  287. data/lib/rubocop/directive_comment.rb +2 -1
  288. data/lib/rubocop/file_patterns.rb +9 -1
  289. data/lib/rubocop/formatter/disabled_config_formatter.rb +19 -9
  290. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  291. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  292. data/lib/rubocop/formatter/simple_text_formatter.rb +0 -2
  293. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  294. data/lib/rubocop/formatter.rb +22 -21
  295. data/lib/rubocop/lsp/diagnostic.rb +1 -0
  296. data/lib/rubocop/lsp/routes.rb +10 -3
  297. data/lib/rubocop/lsp/runtime.rb +1 -2
  298. data/lib/rubocop/mcp/server.rb +200 -0
  299. data/lib/rubocop/options.rb +35 -4
  300. data/lib/rubocop/path_util.rb +14 -2
  301. data/lib/rubocop/plugin/loader.rb +1 -1
  302. data/lib/rubocop/project_index_loader.rb +66 -0
  303. data/lib/rubocop/result_cache.rb +22 -10
  304. data/lib/rubocop/rspec/cop_helper.rb +8 -0
  305. data/lib/rubocop/rspec/shared_contexts.rb +32 -2
  306. data/lib/rubocop/runner.rb +124 -53
  307. data/lib/rubocop/server/cache.rb +5 -7
  308. data/lib/rubocop/server/core.rb +8 -0
  309. data/lib/rubocop/target_finder.rb +14 -7
  310. data/lib/rubocop/target_ruby.rb +18 -12
  311. data/lib/rubocop/version.rb +21 -3
  312. data/lib/rubocop.rb +22 -96
  313. metadata +27 -5
@@ -46,7 +46,7 @@ module RuboCop
46
46
  message = format_message_from(processed_source)
47
47
 
48
48
  add_offense(comment, message: message) do
49
- autocorrect if autocorrect_requested?
49
+ autocorrect if autocorrect?
50
50
  end
51
51
  end
52
52
 
@@ -57,6 +57,10 @@ module RuboCop
57
57
  end
58
58
 
59
59
  def executable?(processed_source)
60
+ # Virtual sources (LSP buffers, programmatic `ProcessedSource`) have no file on
61
+ # disk to stat or `chmod`, so treat them as executable to skip the offense.
62
+ return true unless File.exist?(processed_source.file_path)
63
+
60
64
  # Returns true if stat is executable or if the operating system
61
65
  # doesn't distinguish executable files from nonexecutable files.
62
66
  # See at: https://github.com/ruby/ruby/blob/ruby_2_4/file.c#L5362
@@ -90,12 +90,35 @@ module RuboCop
90
90
  def on_or_asgn(node)
91
91
  return if allow_rbs_inline_annotation? && rbs_inline_annotation?(node.lhs)
92
92
 
93
- add_offense(node) if rhs_matches_lhs?(node.rhs, node.lhs)
93
+ add_offense(node) if or_and_asgn_self_assignment?(node.lhs, node.rhs)
94
94
  end
95
95
  alias on_and_asgn on_or_asgn
96
96
 
97
97
  private
98
98
 
99
+ def or_and_asgn_self_assignment?(lhs, rhs)
100
+ case lhs.type
101
+ when :casgn
102
+ rhs.const_type? && lhs.namespace == rhs.namespace && lhs.short_name == rhs.short_name
103
+ when :send, :csend
104
+ reader_self_assignment?(lhs, rhs)
105
+ else
106
+ rhs_matches_lhs?(rhs, lhs)
107
+ end
108
+ end
109
+
110
+ # Compares two reader calls (attribute `foo.bar` or key `hash['foo']`).
111
+ def reader_self_assignment?(lhs, rhs)
112
+ return false unless rhs.type == lhs.type
113
+ return false unless lhs.method?(rhs.method_name)
114
+ return false unless lhs.receiver == rhs.receiver
115
+ return false unless lhs.arguments == rhs.arguments
116
+
117
+ # `hash[foo] ||= hash[foo]` is intentionally allowed because a method-call key may
118
+ # return different results on each call.
119
+ lhs.arguments.none?(&:call_type?)
120
+ end
121
+
99
122
  def multiple_self_assignment?(node)
100
123
  lhs = node.lhs
101
124
  rhs = node.rhs
@@ -45,7 +45,7 @@ module RuboCop
45
45
  # @!method send_with_mixin_argument?(node)
46
46
  def_node_matcher :send_with_mixin_argument?, <<~PATTERN
47
47
  (send
48
- (const _ _) {:#{SEND_METHODS.join(' :')}}
48
+ {nil? self (const _ _)} {:#{SEND_METHODS.join(' :')}}
49
49
  ({sym str} $#mixin_method?)
50
50
  $(const _ _)+)
51
51
  PATTERN
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Lint
6
- # Checks for a rescued exception that get shadowed by a
6
+ # Checks for a rescued exception that gets shadowed by a
7
7
  # less specific exception being rescued before a more specific
8
8
  # exception is rescued.
9
9
  #
@@ -75,6 +75,8 @@ module RuboCop
75
75
  end
76
76
 
77
77
  def same_conditions_node_different_branch?(variable, outer_local_variable)
78
+ return true if different_case_in_branch?(variable, outer_local_variable)
79
+
78
80
  variable_node = variable_node(variable)
79
81
  return false unless node_or_its_ascendant_conditional?(variable_node)
80
82
 
@@ -88,6 +90,18 @@ module RuboCop
88
90
  variable_node == outer_local_variable_node.else_branch
89
91
  end
90
92
 
93
+ # `case ... in` binds variables in the pattern itself, so a block argument in one
94
+ # `in` branch does not shadow a pattern variable from a different `in` branch of the
95
+ # same `case` (the branches are mutually exclusive).
96
+ def different_case_in_branch?(variable, outer_local_variable)
97
+ inner_branch = variable.scope.node.each_ancestor(:in_pattern).first
98
+ outer_branch = outer_local_variable.declaration_node.each_ancestor(:in_pattern).first
99
+
100
+ return false unless inner_branch && outer_branch
101
+
102
+ inner_branch != outer_branch && inner_branch.parent == outer_branch.parent
103
+ end
104
+
91
105
  def variable_node(variable)
92
106
  parent_node = variable.scope.node.parent
93
107
 
@@ -56,7 +56,9 @@ module RuboCop
56
56
  {array hash (send (const {nil? cbase} {:Array :Hash}) :new)}
57
57
  !#capacity_keyword_argument?
58
58
  ])
59
- (send (const {nil? cbase} :Hash) :new hash #capacity_keyword_argument?)
59
+ (send (const {nil? cbase} :Hash) :new
60
+ {array hash (send (const {nil? cbase} {:Array :Hash}) :new)}
61
+ #capacity_keyword_argument?)
60
62
  }
61
63
  PATTERN
62
64
 
@@ -88,6 +88,10 @@ module RuboCop
88
88
  return unless (method = numeric_constructor_rescue_nil(node))
89
89
  end
90
90
 
91
+ # `Integer(arg, exception: false)` already suppresses the conversion error, so there
92
+ # is nothing unintentionally swallowed; adding another `exception: false` is wrong.
93
+ return if exception_keyword_argument?(method)
94
+
91
95
  arguments = method.arguments.map(&:source) << 'exception: false'
92
96
  prefer = "#{method.method_name}(#{arguments.join(', ')})"
93
97
  prefer = "#{method.receiver.source}#{method.loc.dot.source}#{prefer}" if method.receiver
@@ -100,6 +104,14 @@ module RuboCop
100
104
 
101
105
  private
102
106
 
107
+ def exception_keyword_argument?(method)
108
+ method.arguments.any? do |argument|
109
+ argument.hash_type? && argument.pairs.any? do |pair|
110
+ pair.key.sym_type? && pair.key.value == :exception
111
+ end
112
+ end
113
+ end
114
+
103
115
  def expected_exception_classes_only?(exception_classes)
104
116
  return true unless (arguments = exception_classes.first)
105
117
 
@@ -77,11 +77,28 @@ module RuboCop
77
77
 
78
78
  def on_send(node)
79
79
  return unless node.receiver
80
+ return unless (correction = symbol_conversion_correction(node.receiver))
80
81
 
81
- if node.receiver.type?(:str, :sym)
82
- register_offense(node, correction: node.receiver.value.to_sym.inspect)
83
- elsif node.receiver.dstr_type?
84
- register_offense(node, correction: ":\"#{node.receiver.value.to_sym}\"")
82
+ register_offense(node, correction: correction)
83
+ end
84
+
85
+ def symbol_conversion_correction(receiver)
86
+ if receiver.type?(:str, :sym)
87
+ receiver.value.to_sym.inspect
88
+ elsif receiver.dstr_type? && !receiver.heredoc?
89
+ dstr_correction(receiver)
90
+ end
91
+ end
92
+
93
+ # Reuse the already-escaped inner source for a plain `"..."` string so embedded
94
+ # quotes stay escaped. Percent literals (`%{}`, `%Q{}`, ...) and adjacent string
95
+ # concatenation have multi-character or no delimiters, so slicing the source would
96
+ # corrupt them; fall back to the node's value there.
97
+ def dstr_correction(receiver)
98
+ if receiver.loc.begin&.source == '"'
99
+ ":\"#{receiver.source[1..-2]}\""
100
+ else
101
+ ":\"#{receiver.value.to_sym}\""
85
102
  end
86
103
  end
87
104
 
@@ -26,7 +26,31 @@ module RuboCop
26
26
  "#{diagnostic.message}\n(Using Ruby #{ruby_version} parser; " \
27
27
  'configure using `TargetRubyVersion` parameter, under `AllCops`)'
28
28
  end
29
- add_offense(diagnostic.location, message: message, severity: diagnostic.level)
29
+ location = diagnostic_location(diagnostic.location)
30
+ add_offense(location, message: message, severity: diagnostic.level)
31
+ end
32
+
33
+ # Expand zero-length diagnostic ranges so that editors and formatters
34
+ # can display them. This typically occurs when the parser reports
35
+ # `unexpected token $end` at EOF.
36
+ def diagnostic_location(location)
37
+ return location if location.size.positive?
38
+
39
+ source_buffer = location.source_buffer
40
+ if location.end_pos < source_buffer.source.size
41
+ location.resize(1)
42
+ elsif location.begin_pos.positive?
43
+ location.adjust(begin_pos: -1)
44
+ else
45
+ location
46
+ end
47
+ end
48
+
49
+ # Override to skip multiline_ranges check which requires AST.
50
+ # Syntax errors mean the AST is nil, so we go directly to
51
+ # the EOL comment insertion path.
52
+ def disable_offense(offense_range)
53
+ disable_offense_with_eol_or_surround_comment(offense_range)
30
54
  end
31
55
 
32
56
  def add_offense_from_error(error)
@@ -61,7 +61,7 @@ module RuboCop
61
61
  def arguments_match?(arguments, def_node)
62
62
  index = 0
63
63
 
64
- def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
64
+ all_present = def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
65
65
  send_arg = arguments[index]
66
66
  case def_arg.type
67
67
  when :arg, :restarg, :optarg
@@ -70,6 +70,33 @@ module RuboCop
70
70
 
71
71
  send_arg && argument_match?(send_arg, def_arg)
72
72
  end
73
+
74
+ all_present && !extra_positional_arguments?(arguments, def_node)
75
+ end
76
+
77
+ # The enumerator re-invokes the method with these arguments, so passing more
78
+ # positional arguments than the method accepts raises `ArgumentError` at runtime.
79
+ def extra_positional_arguments?(arguments, def_node)
80
+ return false if variadic_parameters?(def_node) || expandable_arguments?(arguments)
81
+
82
+ positional_arguments(arguments).size > positional_parameters(def_node).size
83
+ end
84
+
85
+ def variadic_parameters?(def_node)
86
+ def_node.arguments.any? { |arg| arg.type?(:restarg, :forward_arg) }
87
+ end
88
+
89
+ # A splat or argument forwarding on the call side can expand to any arity.
90
+ def expandable_arguments?(arguments)
91
+ arguments.any? { |arg| arg.type?(:splat, :forwarded_args, :forwarded_restarg) }
92
+ end
93
+
94
+ def positional_arguments(arguments)
95
+ arguments.reject { |arg| arg.type?(:hash, :kwsplat, :block_pass, :forwarded_kwrestarg) }
96
+ end
97
+
98
+ def positional_parameters(def_node)
99
+ def_node.arguments.select { |arg| arg.type?(:arg, :optarg) }
73
100
  end
74
101
 
75
102
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
@@ -40,7 +40,7 @@ module RuboCop
40
40
  # top-level return node's ancestors should not be of block, def, or
41
41
  # defs type.
42
42
  def top_level_return?(return_node)
43
- return_node.each_ancestor(:block, :any_def).none?
43
+ return_node.each_ancestor(:any_block, :any_def).none?
44
44
  end
45
45
  end
46
46
  end
@@ -32,9 +32,13 @@ module RuboCop
32
32
  include RangeHelp
33
33
 
34
34
  MSG = 'Avoid leaving a trailing comma in attribute declarations.'
35
+ RESTRICT_ON_SEND = %i[attr_reader attr_writer attr_accessor attr].freeze
35
36
 
36
37
  def on_send(node)
37
- return unless node.attribute_accessor? && node.last_argument.def_type?
38
+ return unless node.attribute_accessor? && node.last_argument.any_def_type?
39
+ # A lone `def` argument (e.g. `attr_reader def foo; end`) has no preceding
40
+ # attribute, so there is no trailing comma to flag.
41
+ return unless node.arguments.size > 1
38
42
 
39
43
  trailing_comma = trailing_comma_range(node)
40
44
 
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Lint
6
- # Checks for Regexpes (both literals and via `Regexp.new` / `Regexp.compile`)
6
+ # Checks for Regexps (both literals and via `Regexp.new` / `Regexp.compile`)
7
7
  # that contain unescaped `]` characters.
8
8
  #
9
9
  # It emulates the following Ruby warning:
@@ -68,7 +68,9 @@ module RuboCop
68
68
 
69
69
  expr.text.scan(/(?<!\\)\]/) do
70
70
  pos = Regexp.last_match.begin(0)
71
- next if pos.zero? # if the unescaped bracket is the first character, Ruby does not warn
71
+ # If the unescaped bracket is the first character of the regexp, Ruby does not warn.
72
+ # `pos` is relative to the sub-expression, so add its start offset (`expr.ts`).
73
+ next if (expr.ts + pos).zero?
72
74
 
73
75
  location = range_at_index(node, expr.ts, pos)
74
76
 
@@ -120,6 +120,7 @@ module RuboCop
120
120
  check_return_values(node)
121
121
  end
122
122
  alias on_numblock on_block
123
+ alias on_itblock on_block
123
124
 
124
125
  private
125
126
 
@@ -4,8 +4,8 @@ module RuboCop
4
4
  module Cop
5
5
  module Lint
6
6
  # Checks for unreachable code.
7
- # The check are based on the presence of flow of control
8
- # statement in non-final position in `begin` (implicit) blocks.
7
+ # The check is based on the presence of flow-of-control
8
+ # statements in non-final position in `begin` (implicit) blocks.
9
9
  #
10
10
  # @example
11
11
  #
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for unreachable `in` pattern branches in `case...in` statements.
7
+ #
8
+ # An `in` branch is unreachable when a previous branch uses an unguarded
9
+ # catch-all pattern that matches any value unconditionally. Any `in` branches
10
+ # (and `else`) that follow such a catch-all are dead code.
11
+ #
12
+ # A catch-all pattern is one of:
13
+ #
14
+ # * A bare variable capture (`in x`)
15
+ # * An underscore (`in _`)
16
+ # * A pattern alias where the left side is a catch-all (`in _ => y`)
17
+ # * An alternation pattern where at least one alternative is a catch-all
18
+ # (`in _ | Integer`)
19
+ #
20
+ # NOTE: A catch-all pattern with a guard clause (e.g., `in _ if condition`)
21
+ # does NOT make subsequent branches unreachable because the guard might
22
+ # not be satisfied.
23
+ #
24
+ # @example
25
+ #
26
+ # # bad
27
+ # case value
28
+ # in Integer
29
+ # handle_integer
30
+ # in x
31
+ # handle_other
32
+ # in String
33
+ # handle_string
34
+ # else
35
+ # handle_else
36
+ # end
37
+ #
38
+ # # good
39
+ # case value
40
+ # in Integer
41
+ # handle_integer
42
+ # in String
43
+ # handle_string
44
+ # in x
45
+ # handle_other
46
+ # end
47
+ #
48
+ # # bad - else is unreachable after catch-all
49
+ # case value
50
+ # in Integer
51
+ # handle_integer
52
+ # in _
53
+ # handle_other
54
+ # else
55
+ # handle_else
56
+ # end
57
+ #
58
+ # # good - guard clause means catch-all might not match
59
+ # case value
60
+ # in x if x.positive?
61
+ # handle_positive
62
+ # in Integer
63
+ # handle_integer
64
+ # else
65
+ # handle_other
66
+ # end
67
+ #
68
+ class UnreachablePatternBranch < Base
69
+ extend TargetRubyVersion
70
+
71
+ MSG = 'Unreachable `in` pattern branch detected.'
72
+ MSG_ELSE = 'Unreachable `else` branch detected.'
73
+
74
+ minimum_target_ruby_version 2.7
75
+
76
+ def on_case_match(case_node)
77
+ catch_all_found = false
78
+
79
+ case_node.in_pattern_branches.each do |in_pattern_node|
80
+ if catch_all_found
81
+ add_offense(in_pattern_node)
82
+ next
83
+ end
84
+
85
+ pattern = in_pattern_node.pattern
86
+ guard = in_pattern_node.children[1]
87
+
88
+ catch_all_found = true if catch_all_pattern?(pattern) && guard.nil?
89
+ end
90
+
91
+ return unless catch_all_found && case_node.else?
92
+
93
+ add_offense(case_node.loc.else, message: MSG_ELSE)
94
+ end
95
+
96
+ private
97
+
98
+ def catch_all_pattern?(pattern)
99
+ case pattern.type
100
+ when :match_var
101
+ true
102
+ when :match_as, :begin
103
+ catch_all_pattern?(pattern.children[0])
104
+ when :match_alt
105
+ pattern.children.any? { |child| catch_all_pattern?(child) }
106
+ else
107
+ false
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -95,10 +95,20 @@ module RuboCop
95
95
  return unless variable.method_argument?
96
96
  return if variable.keyword_argument? && cop_config['AllowUnusedKeywordArguments']
97
97
  return if ignored_method?(variable.scope.node.body)
98
+ return if block_argument_with_yield?(variable)
98
99
 
99
100
  super
100
101
  end
101
102
 
103
+ def block_argument_with_yield?(variable)
104
+ return false unless variable.declaration_node.blockarg_type?
105
+
106
+ method_body = variable.scope.node.body
107
+ return false unless method_body
108
+
109
+ method_body.yield_type? || method_body.each_descendant(:yield).any?
110
+ end
111
+
102
112
  def ignored_method?(body)
103
113
  (cop_config['IgnoreEmptyMethods'] && body.nil?) ||
104
114
  (cop_config['IgnoreNotImplementedMethods'] && not_implemented?(body))
@@ -40,8 +40,6 @@ module RuboCop
40
40
  class UselessAssignment < Base
41
41
  extend AutoCorrector
42
42
 
43
- include RangeHelp
44
-
45
43
  MSG = 'Useless assignment to variable - `%<variable>s`.'
46
44
 
47
45
  def self.joining_forces
@@ -69,11 +67,7 @@ module RuboCop
69
67
  range = offense_range(assignment)
70
68
 
71
69
  add_offense(range, message: message) do |corrector|
72
- # In cases like `x = 1, y = 2`, where removing a variable would cause a syntax error,
73
- # and where changing `x ||= 1` to `x = 1` would cause `NameError`,
74
- # the autocorrect will be skipped, even if the variable is unused.
75
- next if sequential_assignment?(assignment_node) ||
76
- assignment_node.parent&.or_asgn_type?
70
+ next if uncorrectable_assignment?(assignment_node)
77
71
 
78
72
  autocorrect(corrector, assignment)
79
73
  end
@@ -81,6 +75,15 @@ module RuboCop
81
75
  ignore_node(assignment_node) if chained_assignment?(assignment_node)
82
76
  end
83
77
 
78
+ # Autocorrect is skipped when removing a variable would cause a syntax error
79
+ # (`x = 1, y = 2`), or where rewriting `x ||= 1`/`x &&= 1` to `x = 1` would raise
80
+ # `NameError` because the variable is not declared before the operator assignment.
81
+ def uncorrectable_assignment?(assignment_node)
82
+ sequential_assignment?(assignment_node) ||
83
+ assignment_node.parent&.or_asgn_type? ||
84
+ assignment_node.parent&.and_asgn_type?
85
+ end
86
+
84
87
  def ignored_assignment?(variable, assignment_node, assignment)
85
88
  assignment.used? || part_of_ignored_node?(assignment_node) ||
86
89
  variable_in_loop_condition?(assignment_node, variable)
@@ -189,12 +192,9 @@ module RuboCop
189
192
  # rubocop:enable Metrics/AbcSize
190
193
 
191
194
  def remove_exception_assignment_part(corrector, node)
192
- corrector.remove(
193
- range_between(
194
- (node.parent.children.first&.source_range || node.parent.location.keyword).end_pos,
195
- node.source_range.end_pos
196
- )
197
- )
195
+ range = node.parent.children.first&.source_range || node.parent.location.keyword
196
+
197
+ corrector.remove(range.end.join(node.source_range.end))
198
198
  end
199
199
 
200
200
  def rename_variable_with_underscore(corrector, node)
@@ -213,7 +213,7 @@ module RuboCop
213
213
  end
214
214
 
215
215
  def remove_local_variable_assignment_part(corrector, node)
216
- corrector.replace(node, node.expression.source)
216
+ corrector.remove(node.loc.name.begin.join(node.expression.source_range.begin))
217
217
  end
218
218
 
219
219
  def variable_in_loop_condition?(assignment_node, variable)
@@ -48,12 +48,12 @@ module RuboCop
48
48
 
49
49
  def after_private_modifier?(left_siblings)
50
50
  access_modifier_candidates = left_siblings.compact.select do |left_sibling|
51
- left_sibling.respond_to?(:send_type?) && left_sibling.send_type?
51
+ left_sibling.respond_to?(:bare_access_modifier?) && left_sibling.bare_access_modifier?
52
52
  end
53
53
 
54
- access_modifier_candidates.any? do |candidate|
55
- candidate.command?(:private) && candidate.arguments.none?
56
- end
54
+ return false if access_modifier_candidates.empty?
55
+
56
+ access_modifier_candidates.last.command?(:private)
57
57
  end
58
58
 
59
59
  def private_constantize?(right_siblings, const_value)
@@ -67,6 +67,8 @@ module RuboCop
67
67
  PATTERN
68
68
 
69
69
  def on_send(node)
70
+ return unless node.receiver
71
+
70
72
  unless (prev_arg_node, default_value_node = default_value_argument_and_block(node.parent))
71
73
  return
72
74
  end
@@ -6,7 +6,7 @@ module RuboCop
6
6
  # Looks for `ruby2_keywords` calls for methods that do not need it.
7
7
  #
8
8
  # `ruby2_keywords` should only be called on methods that accept an argument splat
9
- # (`\*args`) but do not explicit keyword arguments (`k:` or `k: true`) or
9
+ # (`\*args`) but do not have explicit keyword arguments (`k:` or `k: true`) or
10
10
  # a keyword splat (`**kwargs`).
11
11
  #
12
12
  # @example
@@ -107,11 +107,15 @@ module RuboCop
107
107
  end
108
108
 
109
109
  def find_method_definition(node, method_name)
110
- node.each_ancestor.lazy.map do |ancestor|
111
- ancestor.each_child_node(:def, :any_block).find do |child|
110
+ node.each_ancestor do |ancestor|
111
+ found = ancestor.each_child_node(:def, :any_block).find do |child|
112
112
  method_definition(child, method_name)
113
113
  end
114
- end.find(&:itself)
114
+ return found if found
115
+ # A method defined in an outer lexical scope does not define this scope's method,
116
+ # so stop searching once a class/module boundary is crossed without a match.
117
+ return nil if ancestor.type?(:class, :module, :sclass)
118
+ end
115
119
  end
116
120
 
117
121
  # `ruby2_keywords` is only allowed if there's a `restarg` and no keyword arguments
@@ -107,7 +107,10 @@ module RuboCop
107
107
  end
108
108
 
109
109
  def process_multiple_assignment(masgn_node)
110
- masgn_node.assignments.each_with_index do |lhs_node, index|
110
+ # Iterate the top-level destructuring slots so each maps to the right-hand side
111
+ # element at the same position. Using the flattened `assignments` would misalign
112
+ # the index when a slot is itself a nested destructuring (e.g. `(a, b), c = x, y`).
113
+ masgn_node.lhs.children.each_with_index do |lhs_node, index|
111
114
  next unless ASSIGNMENT_TYPES.include?(lhs_node.type)
112
115
 
113
116
  if masgn_node.rhs.array_type? && (rhs_node = masgn_node.rhs.children[index])
@@ -83,7 +83,7 @@ module RuboCop
83
83
 
84
84
  def autocorrect_block(corrector, node)
85
85
  block_arg = block_arg(node)
86
- return if block_reassigns_arg?(node, block_arg)
86
+ return unless reducible_to_body?(node, block_arg)
87
87
 
88
88
  source = node.body.source
89
89
  source.gsub!(/\b#{block_arg}\b/, '0') if block_arg
@@ -91,6 +91,27 @@ module RuboCop
91
91
  corrector.replace(node, fix_indentation(source, node.loc.column...node.body.loc.column))
92
92
  end
93
93
 
94
+ def reducible_to_body?(node, block_arg)
95
+ # A block with multiple arguments can't be reduced to its body (the extra arguments
96
+ # would become undefined references), and `next`/`break`/`redo` bound to the block
97
+ # become orphaned (a syntax error) once the block is removed.
98
+ return false if node.arguments.size > 1 || orphans_loop_control_keyword?(node)
99
+
100
+ # A lone non-simple argument (destructuring `|(a, b)|` or a splat `|*a|`) can't be
101
+ # substituted either, so reducing to the body would leave it referencing an
102
+ # undefined variable.
103
+ return false if node.arguments.one? && block_arg.nil?
104
+
105
+ !block_reassigns_arg?(node, block_arg)
106
+ end
107
+
108
+ def orphans_loop_control_keyword?(node)
109
+ node.body&.each_node(:next, :break, :redo)&.any? do |control|
110
+ inner = control.each_ancestor.take_while { |ancestor| !ancestor.equal?(node) }
111
+ inner.none? { |ancestor| ancestor.type?(:any_block, :while, :until, :for) }
112
+ end
113
+ end
114
+
94
115
  def fix_indentation(source, range)
95
116
  # Cleanup indentation in a multiline block
96
117
  source_lines = source.split("\n")