rubocop 1.90.0 → 1.92.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 (303) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +241 -11
  4. data/config/obsoletion.yml +9 -0
  5. data/lib/rubocop/cached_data.rb +47 -7
  6. data/lib/rubocop/changed_files.rb +134 -0
  7. data/lib/rubocop/cli/command/cop_names.rb +57 -0
  8. data/lib/rubocop/cli/command/execute_runner.rb +23 -3
  9. data/lib/rubocop/cli/command/explain.rb +34 -0
  10. data/lib/rubocop/cli/command/show_cops.rb +6 -0
  11. data/lib/rubocop/cli/command/show_docs_url.rb +6 -4
  12. data/lib/rubocop/cli.rb +11 -4
  13. data/lib/rubocop/comment_config/directive_range.rb +3 -1
  14. data/lib/rubocop/comment_config/disable_next.rb +49 -3
  15. data/lib/rubocop/comment_config/push_pop.rb +55 -0
  16. data/lib/rubocop/comment_config.rb +32 -53
  17. data/lib/rubocop/config.rb +10 -0
  18. data/lib/rubocop/config_loader.rb +18 -7
  19. data/lib/rubocop/config_loader_resolver.rb +79 -12
  20. data/lib/rubocop/config_obsoletion/cop_rule.rb +2 -1
  21. data/lib/rubocop/config_obsoletion/removed_cop.rb +9 -1
  22. data/lib/rubocop/config_obsoletion.rb +1 -1
  23. data/lib/rubocop/config_validator.rb +29 -3
  24. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  25. data/lib/rubocop/cop/badge.rb +1 -1
  26. data/lib/rubocop/cop/base.rb +57 -24
  27. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  28. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  29. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  30. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  31. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  32. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +22 -7
  33. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  34. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +23 -2
  35. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  36. data/lib/rubocop/cop/correctors.rb +2 -2
  37. data/lib/rubocop/cop/documentation.rb +1 -1
  38. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  39. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  40. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  41. data/lib/rubocop/cop/generator.rb +1 -1
  42. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  43. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  44. data/lib/rubocop/cop/internal_affairs/on_send_without_on_csend.rb +1 -1
  45. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  46. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  47. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -0
  48. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  49. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  50. data/lib/rubocop/cop/layout/class_structure.rb +9 -1
  51. data/lib/rubocop/cop/layout/comment_indentation.rb +8 -5
  52. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  53. data/lib/rubocop/cop/layout/else_alignment.rb +1 -0
  54. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +14 -5
  55. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  56. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  57. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  58. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +1 -1
  59. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  60. data/lib/rubocop/cop/layout/end_alignment.rb +16 -10
  61. data/lib/rubocop/cop/layout/extra_spacing.rb +4 -0
  62. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -0
  63. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +17 -4
  64. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +5 -3
  65. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  66. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  67. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +11 -0
  68. data/lib/rubocop/cop/layout/hash_alignment.rb +15 -10
  69. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +2 -0
  70. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  71. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  72. data/lib/rubocop/cop/layout/indentation_width.rb +4 -4
  73. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  74. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  75. data/lib/rubocop/cop/layout/line_length.rb +71 -19
  76. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  77. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  78. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +7 -0
  79. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  80. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +14 -0
  81. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  82. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  83. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +15 -2
  84. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  85. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  86. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +10 -4
  87. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  88. data/lib/rubocop/cop/layout/space_around_operators.rb +5 -1
  89. data/lib/rubocop/cop/layout/space_before_block_braces.rb +4 -1
  90. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  91. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +31 -8
  92. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  93. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  94. data/lib/rubocop/cop/lint/argument_mismatch.rb +14 -4
  95. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -0
  96. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +88 -14
  97. data/lib/rubocop/cop/lint/duplicate_methods.rb +1 -1
  98. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  99. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  100. data/lib/rubocop/cop/lint/literal_as_condition.rb +23 -2
  101. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  102. data/lib/rubocop/cop/lint/loop.rb +24 -2
  103. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +245 -0
  104. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +19 -4
  105. data/lib/rubocop/cop/lint/nested_method_definition.rb +1 -1
  106. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +1 -1
  107. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  108. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +17 -1
  109. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +42 -17
  110. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  111. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  112. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  113. data/lib/rubocop/cop/lint/void.rb +8 -1
  114. data/lib/rubocop/cop/lint.rb +1 -0
  115. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  116. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  117. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  118. data/lib/rubocop/cop/migration/department_name.rb +1 -1
  119. data/lib/rubocop/cop/mixin/alignment.rb +5 -1
  120. data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -1
  121. data/lib/rubocop/cop/mixin/check_line_breakable.rb +17 -14
  122. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +5 -1
  123. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  124. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  125. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +69 -0
  126. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  127. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +26 -2
  128. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +31 -3
  129. data/lib/rubocop/cop/mixin/hash_transform_method.rb +1 -1
  130. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  131. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  132. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  133. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  134. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  135. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +29 -3
  136. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  137. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  138. data/lib/rubocop/cop/mixin/statement_modifier.rb +22 -2
  139. data/lib/rubocop/cop/mixin.rb +4 -2
  140. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  141. data/lib/rubocop/cop/naming/block_forwarding.rb +4 -1
  142. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  143. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  144. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  145. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  146. data/lib/rubocop/cop/offense.rb +59 -3
  147. data/lib/rubocop/cop/registry.rb +10 -2
  148. data/lib/rubocop/cop/security/json_load.rb +16 -4
  149. data/lib/rubocop/cop/severity.rb +12 -0
  150. data/lib/rubocop/cop/style/access_modifier_declarations.rb +10 -2
  151. data/lib/rubocop/cop/style/alias.rb +18 -5
  152. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  153. data/lib/rubocop/cop/style/and_or.rb +4 -0
  154. data/lib/rubocop/cop/style/arguments_forwarding.rb +6 -3
  155. data/lib/rubocop/cop/style/attr.rb +3 -4
  156. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  157. data/lib/rubocop/cop/style/block_delimiters.rb +29 -3
  158. data/lib/rubocop/cop/style/case_like_if.rb +9 -6
  159. data/lib/rubocop/cop/style/class_and_module_children.rb +54 -16
  160. data/lib/rubocop/cop/style/class_methods_definitions.rb +8 -0
  161. data/lib/rubocop/cop/style/combinable_loops.rb +85 -11
  162. data/lib/rubocop/cop/style/concat_array_literals.rb +46 -7
  163. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  164. data/lib/rubocop/cop/style/constant_visibility.rb +6 -2
  165. data/lib/rubocop/cop/style/data_inheritance.rb +14 -0
  166. data/lib/rubocop/cop/style/directive_scope.rb +154 -21
  167. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +118 -18
  168. data/lib/rubocop/cop/style/documentation_method.rb +1 -9
  169. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  170. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  171. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  172. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  173. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  174. data/lib/rubocop/cop/style/empty_method.rb +15 -3
  175. data/lib/rubocop/cop/style/endless_method.rb +91 -11
  176. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  177. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  178. data/lib/rubocop/cop/style/file_null.rb +2 -2
  179. data/lib/rubocop/cop/style/float_division.rb +28 -14
  180. data/lib/rubocop/cop/style/for.rb +44 -1
  181. data/lib/rubocop/cop/style/format_string.rb +28 -6
  182. data/lib/rubocop/cop/style/format_string_token.rb +1 -1
  183. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  184. data/lib/rubocop/cop/style/guard_clause.rb +13 -1
  185. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  186. data/lib/rubocop/cop/style/hash_lookup_method.rb +35 -9
  187. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  188. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  189. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  190. data/lib/rubocop/cop/style/if_unless_modifier.rb +19 -20
  191. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  192. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  193. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  194. data/lib/rubocop/cop/style/it_block_parameter.rb +32 -3
  195. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  196. data/lib/rubocop/cop/style/lambda.rb +18 -0
  197. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  198. data/lib/rubocop/cop/style/magic_comment_format.rb +1 -1
  199. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  200. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +45 -10
  201. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +25 -2
  202. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  203. data/lib/rubocop/cop/style/method_def_parentheses.rb +29 -3
  204. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  205. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  206. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  207. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  208. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  209. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  210. data/lib/rubocop/cop/style/mutable_constant.rb +28 -1
  211. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  212. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  213. data/lib/rubocop/cop/style/nested_modifier.rb +36 -12
  214. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  215. data/lib/rubocop/cop/style/next.rb +24 -10
  216. data/lib/rubocop/cop/style/nil_comparison.rb +48 -8
  217. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  218. data/lib/rubocop/cop/style/non_nil_check.rb +18 -2
  219. data/lib/rubocop/cop/style/not.rb +10 -3
  220. data/lib/rubocop/cop/style/numeric_literal_prefix.rb +11 -1
  221. data/lib/rubocop/cop/style/numeric_predicate.rb +21 -5
  222. data/lib/rubocop/cop/style/one_line_conditional.rb +14 -3
  223. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -1
  224. data/lib/rubocop/cop/style/parentheses_around_condition.rb +6 -1
  225. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  226. data/lib/rubocop/cop/style/reduce_to_hash.rb +17 -1
  227. data/lib/rubocop/cop/style/redundant_begin.rb +20 -3
  228. data/lib/rubocop/cop/style/redundant_condition.rb +34 -17
  229. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +16 -1
  230. data/lib/rubocop/cop/style/redundant_exception.rb +15 -3
  231. data/lib/rubocop/cop/style/redundant_fetch_block.rb +10 -2
  232. data/lib/rubocop/cop/style/redundant_freeze.rb +10 -1
  233. data/lib/rubocop/cop/style/redundant_parentheses.rb +23 -2
  234. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  235. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +6 -5
  236. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +25 -5
  237. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  238. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  239. data/lib/rubocop/cop/style/require_order.rb +9 -0
  240. data/lib/rubocop/cop/style/rescue_modifier.rb +5 -9
  241. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  242. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  243. data/lib/rubocop/cop/style/sample.rb +9 -8
  244. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  245. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  246. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  247. data/lib/rubocop/cop/style/single_line_methods.rb +16 -4
  248. data/lib/rubocop/cop/style/special_global_vars.rb +1 -1
  249. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  250. data/lib/rubocop/cop/style/string_concatenation.rb +9 -3
  251. data/lib/rubocop/cop/style/struct_inheritance.rb +14 -0
  252. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  253. data/lib/rubocop/cop/style/symbol_proc.rb +23 -5
  254. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  255. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  256. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  257. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  258. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  259. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  260. data/lib/rubocop/cop/style.rb +0 -1
  261. data/lib/rubocop/cop/team.rb +10 -5
  262. data/lib/rubocop/cop/util.rb +1 -1
  263. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  264. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  265. data/lib/rubocop/cops_documentation_generator.rb +23 -2
  266. data/lib/rubocop/directive_comment.rb +37 -8
  267. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  268. data/lib/rubocop/formatter/disabled_config_formatter.rb +19 -1
  269. data/lib/rubocop/formatter/formatter_set.rb +6 -1
  270. data/lib/rubocop/formatter/github_actions_formatter.rb +6 -5
  271. data/lib/rubocop/formatter/html_formatter.rb +2 -2
  272. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  273. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  274. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  275. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  276. data/lib/rubocop/formatter/sarif_formatter.rb +186 -0
  277. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  278. data/lib/rubocop/formatter.rb +1 -0
  279. data/lib/rubocop/lsp/runtime.rb +47 -11
  280. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  281. data/lib/rubocop/lsp.rb +5 -2
  282. data/lib/rubocop/magic_comment.rb +27 -0
  283. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  284. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  285. data/lib/rubocop/mcp/scope.rb +65 -0
  286. data/lib/rubocop/mcp/server.rb +184 -76
  287. data/lib/rubocop/mcp/source_files.rb +35 -0
  288. data/lib/rubocop/offense_limit.rb +30 -0
  289. data/lib/rubocop/options.rb +97 -4
  290. data/lib/rubocop/path_util.rb +1 -1
  291. data/lib/rubocop/plugin/configuration_integrator.rb +12 -5
  292. data/lib/rubocop/plugin/loader.rb +1 -1
  293. data/lib/rubocop/rake_task.rb +1 -1
  294. data/lib/rubocop/remote_config.rb +8 -5
  295. data/lib/rubocop/result_cache.rb +36 -18
  296. data/lib/rubocop/runner.rb +160 -49
  297. data/lib/rubocop/server/cache.rb +2 -2
  298. data/lib/rubocop/unified_diff.rb +257 -0
  299. data/lib/rubocop/util.rb +69 -0
  300. data/lib/rubocop/version.rb +1 -1
  301. data/lib/rubocop.rb +9 -1
  302. metadata +21 -6
  303. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +0 -46
@@ -153,7 +153,7 @@ module RuboCop
153
153
  else
154
154
  column = node.source_range.column
155
155
  return if multiline_block?(left_brace, right_brace) &&
156
- aligned_braces?(inner, right_brace, column)
156
+ aligned_braces?(inner, right_brace, column, left_brace)
157
157
 
158
158
  space_inside_right_brace(inner, right_brace, column)
159
159
  end
@@ -163,14 +163,21 @@ module RuboCop
163
163
  left_brace.first_line != right_brace.first_line
164
164
  end
165
165
 
166
- def aligned_braces?(inner, right_brace, column)
167
- column == right_brace.column || column == inner_last_space_count(inner)
166
+ def aligned_braces?(inner, right_brace, column, left_brace)
167
+ column == right_brace.column ||
168
+ column == inner_last_space_count(inner) ||
169
+ aligned_with_opening_brace_line?(left_brace, right_brace)
168
170
  end
169
171
 
170
172
  def inner_last_space_count(inner)
171
173
  inner.split("\n").last.count(' ')
172
174
  end
173
175
 
176
+ def aligned_with_opening_brace_line?(left_brace, right_brace)
177
+ begins_its_line?(right_brace) &&
178
+ right_brace.column == left_brace.source_line[/\A\s*/].length
179
+ end
180
+
174
181
  def no_space_inside_left_brace(left_brace, args_delimiter)
175
182
  if pipe?(args_delimiter)
176
183
  if left_brace.end_pos == args_delimiter.begin_pos &&
@@ -197,9 +204,7 @@ module RuboCop
197
204
  'Space between { and | detected.')
198
205
  end
199
206
  else
200
- brace_with_space = range_with_surrounding_space(left_brace, side: :right)
201
- space(brace_with_space.begin_pos + 1, brace_with_space.end_pos,
202
- 'Space inside { detected.')
207
+ space_after_left_brace(left_brace)
203
208
  end
204
209
  end
205
210
 
@@ -207,6 +212,20 @@ module RuboCop
207
212
  args_delimiter&.is?('|')
208
213
  end
209
214
 
215
+ def space_after_left_brace(left_brace)
216
+ brace_with_space = range_with_surrounding_space(left_brace, side: :right)
217
+ return if brace_with_space.source.match?(/\R/)
218
+ return if comment_at?(brace_with_space.end_pos)
219
+
220
+ space(
221
+ brace_with_space.begin_pos + 1, brace_with_space.end_pos, 'Space inside { detected.'
222
+ )
223
+ end
224
+
225
+ def comment_at?(pos)
226
+ processed_source.comments.any? { |comment| comment.source_range.begin_pos == pos }
227
+ end
228
+
210
229
  def space_inside_right_brace(inner, right_brace, column)
211
230
  brace_with_space = range_with_surrounding_space(right_brace, side: :left)
212
231
  begin_pos = brace_with_space.begin_pos
@@ -216,7 +235,7 @@ module RuboCop
216
235
  begin_pos = end_pos - (right_brace.column - column)
217
236
  end
218
237
 
219
- if inner.end_with?(']')
238
+ if closing_bracket_on_own_line?(inner)
220
239
  end_pos -= 1
221
240
  begin_pos = end_pos - (inner_last_space_count(inner) - column)
222
241
  end
@@ -224,6 +243,10 @@ module RuboCop
224
243
  space(begin_pos, end_pos, 'Space inside } detected.')
225
244
  end
226
245
 
246
+ def closing_bracket_on_own_line?(inner)
247
+ inner[/[^\n]*\z/].match?(/\A\s*\]\z/)
248
+ end
249
+
227
250
  def no_space(begin_pos, end_pos, msg)
228
251
  if style == :space
229
252
  offense(begin_pos, end_pos, msg)
@@ -241,7 +264,7 @@ module RuboCop
241
264
  end
242
265
 
243
266
  def offense(begin_pos, end_pos, msg, style_param = 'EnforcedStyle')
244
- return if begin_pos > end_pos
267
+ return if begin_pos >= end_pos
245
268
 
246
269
  range = range_between(begin_pos, end_pos)
247
270
  add_offense(range, message: msg) do |corrector|
@@ -37,7 +37,7 @@ module RuboCop
37
37
  'should be %<possible>s.'
38
38
 
39
39
  def self.autocorrect_incompatible_with
40
- [Naming::BlockForwarding]
40
+ [Naming::BlockForwarding, Style::ArgumentsForwarding]
41
41
  end
42
42
 
43
43
  def on_new_investigation
@@ -56,7 +56,8 @@ module RuboCop
56
56
  end
57
57
 
58
58
  def on_send(node)
59
- return if node.parenthesized?
59
+ # `foo.*` is a regular method call, not a binary operator.
60
+ return if node.parenthesized? || node.loc.dot
60
61
 
61
62
  return unless (parent = node.parent)
62
63
  return unless operator?(parent)
@@ -21,7 +21,9 @@ module RuboCop
21
21
  #
22
22
  # Constructor calls (`Foo.new`) and keyword-argument validation are out of
23
23
  # scope: only positional arity of explicitly defined singleton methods is
24
- # checked.
24
+ # checked. `new` is skipped outright, because `Class#new` is not in the
25
+ # index: any `new` the index does find sits below it in the method
26
+ # resolution order, where Ruby would never reach it.
25
27
  #
26
28
  # @example
27
29
  # # Given the project defines:
@@ -45,8 +47,7 @@ module RuboCop
45
47
  MSG = 'Wrong number of arguments to `%<method>s` (given %<given>d, expected %<expected>s).'
46
48
 
47
49
  def on_send(node)
48
- return unless project_index
49
- return unless node.receiver&.const_type?
50
+ return unless checkable_call?(node)
50
51
 
51
52
  shape = resolved_signature_shape(node)
52
53
  return unless shape
@@ -61,6 +62,10 @@ module RuboCop
61
62
 
62
63
  private
63
64
 
65
+ def checkable_call?(node)
66
+ project_index && node.receiver&.const_type? && !node.method?(:new)
67
+ end
68
+
64
69
  def message(node, given, min, max)
65
70
  format(MSG, method: node.method_name, given: given, expected: expected_range(min, max))
66
71
  end
@@ -77,9 +82,14 @@ module RuboCop
77
82
  nil
78
83
  end
79
84
 
85
+ # A call with an explicit receiver cannot reach a private method, so its
86
+ # signature says nothing about the call. This matters because `Object`
87
+ # closes the singleton ancestry of every constant, and a bare `def` at
88
+ # the top level of any file — what every block-based DSL produces — is a
89
+ # private instance method of `Object`.
80
90
  def singleton_signature_shape(declaration, method_name)
81
91
  member = indexed_singleton_member(declaration, "#{method_name}()")
82
- return nil unless member
92
+ return nil unless member&.public?
83
93
 
84
94
  indexed_member_shape(member)
85
95
  end
@@ -106,6 +106,8 @@ module RuboCop
106
106
  def traverse_node(node, &block)
107
107
  # if the node is a block, any assignments are irrelevant
108
108
  return if node.any_block_type?
109
+ # an assignment inside `defined?` is never executed
110
+ return if node.defined_type?
109
111
 
110
112
  result = yield node if ASGN_TYPES.include?(node.type)
111
113
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/RedundantCopDisableDirective
4
- # rubocop:disable Style/DoubleCopDisableDirective
5
3
  module RuboCop
6
4
  module Cop
7
5
  module Lint
@@ -23,12 +21,11 @@ module RuboCop
23
21
  # # good
24
22
  # # rubocop:disable all
25
23
  #
26
- # # bad
24
+ # # bad - only the first directive takes effect
27
25
  # # rubocop:disable Layout/LineLength # rubocop:disable Style/Encoding
28
26
  #
29
27
  # # good
30
- # # rubocop:disable Layout/LineLength
31
- # # rubocop:disable Style/Encoding
28
+ # # rubocop:disable Layout/LineLength, Style/Encoding
32
29
  #
33
30
  # # bad
34
31
  # # rubocop:wrongmode Layout/LineLength
@@ -55,13 +52,19 @@ module RuboCop
55
52
  # # rubocop:disable Layout/LineLength
56
53
  #
57
54
  class CopDirectiveSyntax < Base
55
+ extend AutoCorrector
56
+
58
57
  COMMON_MSG = 'Malformed directive comment detected.'
59
58
 
60
59
  MISSING_MODE_NAME_MSG = 'The mode name is missing.'
61
- INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `disable-next`, `todo`, `todo-next`, `push`, or `pop`.' # rubocop:disable Layout/LineLength
60
+ INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `disable-next`, `enable-next`, `todo`, `todo-next`, `next`, `push`, or `pop`.' # rubocop:disable Layout/LineLength -- the message lists every mode and does not wrap
62
61
  MISSING_COP_NAME_MSG = 'The cop name is missing.'
62
+ MULTIPLE_DIRECTIVES_MSG = 'Only the first directive on a line takes effect. ' \
63
+ 'List the cop names in a single directive instead.'
63
64
  MALFORMED_COP_NAMES_MSG = 'Cop names must be separated by commas. ' \
64
65
  'Comment in the directive must start with `--`.'
66
+ INVALID_SIGNED_ARGS_MSG = '`push` and `next` arguments must be `+`- or `-`-prefixed ' \
67
+ 'cop names, and `pop` takes no arguments.'
65
68
  NEXT_DIRECTIVE_AT_EOL_MSG = 'A `-next` directive must be on its own line, above the ' \
66
69
  'statement it applies to.'
67
70
  INVALID_KEYWORD_MSG = 'The directive keyword must be `rubocop`, not `%<keyword>s`.'
@@ -89,7 +92,11 @@ module RuboCop
89
92
  private
90
93
 
91
94
  def check_directive(comment, directive_comment)
92
- if directive_comment.malformed?
95
+ if (directives = multiple_directives(comment, directive_comment))
96
+ add_offense(comment, message: "#{COMMON_MSG} #{MULTIPLE_DIRECTIVES_MSG}") do |corrector|
97
+ merge_directives(corrector, comment, directives)
98
+ end
99
+ elsif directive_comment.malformed?
93
100
  add_offense(comment, message: offense_message(directive_comment))
94
101
  elsif misplaced_next_directive?(directive_comment)
95
102
  add_offense(comment, message: NEXT_DIRECTIVE_AT_EOL_MSG)
@@ -98,13 +105,72 @@ module RuboCop
98
105
  end
99
106
  end
100
107
 
101
- # An EOL `disable-next` is not honored - it must fail loudly instead
102
- # of silently doing nothing.
108
+ # Everything after the first directive in a comment is trailing text as
109
+ # far as the parser is concerned, so a second directive written on the
110
+ # same line disables nothing at all. A valid `--` reason may mention a
111
+ # directive as plain text, which should not be treated as another one.
112
+ def multiple_directives(comment, directive_comment)
113
+ matches = comment.text.to_enum(:scan, DirectiveComment::DIRECTIVE_COMMENT_REGEXP)
114
+ .map { Regexp.last_match }
115
+ matches = filter_reason_matches(matches, directive_comment)
116
+
117
+ matches if matches.size > 1
118
+ end
119
+
120
+ def filter_reason_matches(matches, directive_comment)
121
+ return matches unless directive_comment.reason
122
+
123
+ [matches.first, *matches.drop(1).reject { |match| match.post_match.match?(/\S/) }]
124
+ end
125
+
126
+ def merge_directives(corrector, comment, directives)
127
+ return unless directives_adjacent?(comment, directives)
128
+ return unless (mode = merged_mode(directives))
129
+ return unless (cops = merged_cops(directives))
130
+
131
+ corrector.replace(comment, "# rubocop:#{mode} #{cops.join(', ')}")
132
+ end
133
+
134
+ # `all` covers every cop already, so folding it into a list of names
135
+ # would be a rewrite rather than a merge.
136
+ def merged_cops(directives)
137
+ cops = directives.flat_map { |match| match[2].to_s.split(',').map(&:strip) }
138
+ cops = cops.reject(&:empty?).uniq
139
+ cops unless cops.empty? || cops.include?('all')
140
+ end
141
+
142
+ # Anything written between the directives is a `--` reason or prose,
143
+ # and merging around it would either move it or drop it.
144
+ def directives_adjacent?(comment, directives)
145
+ text = comment.text
146
+ bounds = directives.map { |match| match.end(0) }
147
+ .zip(directives.map { |match| match.begin(0) }[1..] + [text.length])
148
+ bounds.all? { |from, to| text[from...to].strip.empty? }
149
+ end
150
+
151
+ # `todo` is an alias of `disable`, so those two combine. Anything else
152
+ # (an `enable` beside a `disable`, or directives of different scopes)
153
+ # means different things per directive and can't be folded into one.
154
+ def merged_mode(directives)
155
+ modes = directives.map { |match| match[1] }.uniq
156
+ return modes.first if modes.size == 1
157
+ return modes.first if (modes - %w[disable todo]).empty?
158
+
159
+ nil
160
+ end
161
+
162
+ # An EOL next-statement directive is not honored - it must fail
163
+ # loudly instead of silently doing nothing.
103
164
  def misplaced_next_directive?(directive_comment)
104
- directive_comment.disable_next? &&
165
+ next_statement_directive?(directive_comment) &&
105
166
  !processed_source.comment_config.comment_only_line?(directive_comment.line_number)
106
167
  end
107
168
 
169
+ def next_statement_directive?(directive_comment)
170
+ directive_comment.disable_next? || directive_comment.enable_next? ||
171
+ directive_comment.next?
172
+ end
173
+
108
174
  # rubocop:disable-next Metrics/MethodLength
109
175
  def offense_message(directive_comment)
110
176
  comment = directive_comment.comment
@@ -116,6 +182,8 @@ module RuboCop
116
182
  INVALID_MODE_NAME_MSG
117
183
  elsif directive_comment.missing_cop_name?
118
184
  MISSING_COP_NAME_MSG
185
+ elsif directive_comment.invalid_signed_args?
186
+ INVALID_SIGNED_ARGS_MSG
119
187
  else
120
188
  MALFORMED_COP_NAMES_MSG
121
189
  end
@@ -142,10 +210,10 @@ module RuboCop
142
210
  end
143
211
 
144
212
  def unknown_cop_name(directive_comment)
145
- return if directive_comment.push? || directive_comment.pop? || directive_comment.all_cops?
213
+ return if directive_comment.pop? || directive_comment.all_cops?
146
214
 
147
215
  registry = directive_comment.cop_registry
148
- directive_comment.raw_cop_names.find do |name|
216
+ directive_names(directive_comment).find do |name|
149
217
  next false if registry.department?(name)
150
218
 
151
219
  qualified = registry.qualified_cop_name(name, nil, warn: false)
@@ -153,6 +221,14 @@ module RuboCop
153
221
  end
154
222
  end
155
223
 
224
+ def directive_names(directive_comment)
225
+ if directive_comment.push? || directive_comment.next?
226
+ directive_comment.signed_args.values.flatten
227
+ else
228
+ directive_comment.raw_cop_names
229
+ end
230
+ end
231
+
156
232
  def unknown_cop_message(directive_comment, name)
157
233
  similar = NameSimilarity.find_similar_name(name, directive_comment.cop_registry.names)
158
234
  suggestion = similar ? " (did you mean `#{similar}`?)" : ''
@@ -162,5 +238,3 @@ module RuboCop
162
238
  end
163
239
  end
164
240
  end
165
- # rubocop:enable Lint/RedundantCopDisableDirective
166
- # rubocop:enable Style/DoubleCopDisableDirective
@@ -435,7 +435,7 @@ module RuboCop
435
435
 
436
436
  def anon_block_scope_id(anon_block)
437
437
  parent = anon_block.parent
438
- return unless parent&.type?(:any_block, :begin, :call, :casgn, :any_def)
438
+ return unless parent&.type?(:any_block, :begin, :call, :casgn, :ivasgn, :any_def)
439
439
 
440
440
  if (receiver = scope_receiver(parent, anon_block))
441
441
  "#{receiver.source}.#{parent.method_name}"
@@ -63,7 +63,7 @@ module RuboCop
63
63
  class EmptyBlock < Base
64
64
  MSG = 'Empty block detected.'
65
65
 
66
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
66
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler -- an empty block has no parameters to number
67
67
  return if node.body
68
68
  return if allow_empty_lambdas? && node.lambda_or_proc?
69
69
  return if cop_config['AllowComments'] && allow_comment?(node)
@@ -97,12 +97,14 @@ module RuboCop
97
97
  end
98
98
 
99
99
  def can_simplify_conditional?(node)
100
- node.else_branch && node.loc.else.source == 'else'
100
+ !node.elsif? && node.else_branch && node.loc.else.source == 'else'
101
101
  end
102
102
 
103
103
  def remove_empty_branch(corrector, node)
104
104
  range = if empty_if_branch?(node) && else_branch?(node)
105
105
  branch_range(node)
106
+ elsif node.condition.last_line == node.loc.else.line
107
+ node.source_range.with(end_pos: node.loc.else.begin_pos)
106
108
  else
107
109
  deletion_range(branch_range(node))
108
110
  end
@@ -48,7 +48,7 @@ module RuboCop
48
48
  add_offense(node.lhs) do |corrector|
49
49
  # Don't autocorrect `'foo' && return` because having `return` as
50
50
  # the leftmost node can lead to a void value expression syntax error.
51
- next if node.rhs.type?(:return, :break, :next)
51
+ next if void_value_expression?(node.rhs)
52
52
 
53
53
  corrector.replace(node, node.rhs.source)
54
54
  end
@@ -60,7 +60,7 @@ module RuboCop
60
60
  add_offense(node.lhs) do |corrector|
61
61
  # Don't autocorrect `'foo' && return` because having `return` as
62
62
  # the leftmost node can lead to a void value expression syntax error.
63
- next if node.rhs.type?(:return, :break, :next)
63
+ next if void_value_expression?(node.rhs)
64
64
 
65
65
  corrector.replace(node, node.rhs.source)
66
66
  end
@@ -98,6 +98,8 @@ module RuboCop
98
98
  end
99
99
  elsif node.condition.falsey_literal?
100
100
  add_offense(node.condition) do |corrector|
101
+ next if orphans_loop_control_keyword?(node)
102
+
101
103
  corrector.replace(node, node.body.child_nodes.map(&:source).join("\n"))
102
104
  end
103
105
  end
@@ -127,6 +129,8 @@ module RuboCop
127
129
  end
128
130
  elsif node.condition.truthy_literal?
129
131
  add_offense(node.condition) do |corrector|
132
+ next if orphans_loop_control_keyword?(node)
133
+
130
134
  corrector.replace(node, node.body.child_nodes.map(&:source).join("\n"))
131
135
  end
132
136
  end
@@ -175,6 +179,21 @@ module RuboCop
175
179
 
176
180
  private
177
181
 
182
+ # Unwrapping the body of a loop that runs once would leave a `break`,
183
+ # `next` or `redo` that belongs to it outside of any loop.
184
+ def orphans_loop_control_keyword?(node)
185
+ node.body.each_node(:next, :break, :redo).any? do |control|
186
+ inner = control.each_ancestor.take_while { |ancestor| !ancestor.equal?(node) }
187
+ inner.none? { |ancestor| ancestor.any_block_type? || ancestor.loop_keyword? }
188
+ end
189
+ end
190
+
191
+ def void_value_expression?(node)
192
+ node = node.children.last while node&.begin_type?
193
+
194
+ node&.type?(:return, :break, :next)
195
+ end
196
+
178
197
  def check_for_literal(node)
179
198
  cond = condition(node)
180
199
  if cond.literal?
@@ -267,6 +286,8 @@ module RuboCop
267
286
  "#{node.else_branch.source.sub('elsif', 'if')}\nend"
268
287
  elsif node.else? || node.ternary?
269
288
  node.else_branch.source
289
+ elsif node.value_used?
290
+ 'nil'
270
291
  else
271
292
  '' # Equivalent to removing the node
272
293
  end
@@ -24,30 +24,16 @@ module RuboCop
24
24
  MSG = 'Literal interpolation detected.'
25
25
  COMPOSITE = %i[array hash pair irange erange].freeze
26
26
 
27
- # rubocop:disable-next Metrics/AbcSize
28
27
  def on_interpolation(begin_node)
29
28
  final_node = begin_node.children.last
30
29
  return unless offending?(final_node)
31
-
32
- # %W and %I split the content into words before expansion
33
- # treating each interpolation as a word component, so
34
- # interpolation should not be removed if the expanded value
35
- # contains a space character.
36
- expanded_value = autocorrected_value(final_node)
37
- expanded_value = handle_special_regexp_chars(begin_node, expanded_value)
38
-
39
- return if in_array_percent_literal?(begin_node) && /\s|\A\z/.match?(expanded_value)
30
+ return unless (expanded_value = correctable_value(begin_node, final_node))
40
31
 
41
32
  add_offense(final_node) do |corrector|
42
33
  next if final_node.dstr_type? # nested, fixed in next iteration
34
+ next if follows_literal_interpolation?(begin_node)
43
35
 
44
- replacement = if final_node.str_type? && !final_node.value.valid_encoding?
45
- final_node.source.delete_prefix('"').delete_suffix('"')
46
- else
47
- expanded_value
48
- end
49
-
50
- corrector.replace(final_node.parent, replacement)
36
+ corrector.replace(final_node.parent, replacement(final_node, expanded_value))
51
37
  end
52
38
  end
53
39
 
@@ -73,6 +59,22 @@ module RuboCop
73
59
  node.array_type? && grandparent.regexp_type?
74
60
  end
75
61
 
62
+ def correctable_value(begin_node, final_node)
63
+ value = autocorrected_value(final_node)
64
+ value = handle_special_regexp_chars(begin_node, value)
65
+ return if starts_interpolation_after_hash_sign?(begin_node, value)
66
+
67
+ value = escape_trailing_hash_sign(begin_node, value)
68
+
69
+ # %W and %I split the content into words before expansion
70
+ # treating each interpolation as a word component, so
71
+ # interpolation should not be removed if the expanded value
72
+ # contains a space character.
73
+ return if in_array_percent_literal?(begin_node) && /\s|\A\z/.match?(value)
74
+
75
+ value
76
+ end
77
+
76
78
  # rubocop:disable-next Metrics/MethodLength, Metrics/CyclomaticComplexity
77
79
  def autocorrected_value(node)
78
80
  case node.type
@@ -116,6 +118,17 @@ module RuboCop
116
118
  end
117
119
  end
118
120
 
121
+ def starts_interpolation_after_hash_sign?(begin_node, value)
122
+ source = processed_source.buffer.source
123
+ range = begin_node.source_range
124
+ # Removing an empty value puts the `#` right before what follows the interpolation.
125
+ following = "#{value}#{source[range.end_pos]}"
126
+ return false unless following.start_with?('{', '@', '$')
127
+ return false unless source[range.begin_pos - 1] == '#'
128
+
129
+ unescaped_hash_sign?(source[0...range.begin_pos])
130
+ end
131
+
119
132
  def autocorrected_value_for_string(node)
120
133
  return node.source.delete_prefix('"').delete_suffix('"') unless node.value.valid_encoding?
121
134
 
@@ -180,7 +193,9 @@ module RuboCop
180
193
  end
181
194
 
182
195
  def space_literal?(node)
183
- node.str_type? && node.value.valid_encoding? && node.value.blank?
196
+ # `String#blank?` misses Unicode spaces such as U+3000 that
197
+ # `Layout/TrailingWhitespace` treats as trailing whitespace.
198
+ node.str_type? && node.value.valid_encoding? && node.value.match?(/\A[[:space:]]*\z/)
184
199
  end
185
200
 
186
201
  def ends_heredoc_line?(node)
@@ -188,7 +203,8 @@ module RuboCop
188
203
  return false unless grandparent&.dstr_type? && grandparent.heredoc?
189
204
 
190
205
  line = processed_source.lines[node.last_line - 1]
191
- line.size == node.loc.last_column + 1
206
+
207
+ line.size == node.parent.source_range.last_column
192
208
  end
193
209
 
194
210
  def in_array_percent_literal?(node)
@@ -198,6 +214,34 @@ module RuboCop
198
214
  grandparent = parent.parent
199
215
  grandparent&.array_type? && grandparent.percent_literal?
200
216
  end
217
+
218
+ def escape_trailing_hash_sign(begin_node, value)
219
+ following = processed_source.buffer.source[begin_node.source_range.end_pos]
220
+ return value unless %w[{ @ $].include?(following) && unescaped_hash_sign?(value)
221
+
222
+ "#{value.delete_suffix('#')}\\#"
223
+ end
224
+
225
+ def unescaped_hash_sign?(text)
226
+ text.end_with?('#') && text.delete_suffix('#')[/\\*\z/].length.even?
227
+ end
228
+
229
+ # Each interpolation is checked against the original source, so correcting it in the
230
+ # same pass as a preceding one that leaves a `#` (or nothing) could join that `#` and
231
+ # a `{` from this one. It's left for the next iteration instead.
232
+ def follows_literal_interpolation?(begin_node)
233
+ previous = begin_node.left_sibling
234
+ return false unless previous&.begin_type? && offending?(previous.children.last)
235
+
236
+ value = correctable_value(previous, previous.children.last)
237
+ value && (value.empty? || value.end_with?('#'))
238
+ end
239
+
240
+ def replacement(node, expanded_value)
241
+ return expanded_value unless node.str_type? && !node.value.valid_encoding?
242
+
243
+ node.source.delete_prefix('"').delete_suffix('"')
244
+ end
201
245
  end
202
246
  end
203
247
  end
@@ -42,6 +42,8 @@ module RuboCop
42
42
  # break if some_condition
43
43
  # end
44
44
  class Loop < Base
45
+ include Alignment
46
+ include RangeHelp
45
47
  extend AutoCorrector
46
48
 
47
49
  MSG = 'Use `Kernel#loop` with `break` rather than `begin/end/until`(or `while`).'
@@ -62,7 +64,8 @@ module RuboCop
62
64
  add_offense(node.loc.keyword) do |corrector|
63
65
  corrector.replace(body.loc.begin, 'loop do')
64
66
  corrector.remove(keyword_and_condition_range(node))
65
- corrector.insert_before(body.loc.end, build_break_line(node))
67
+
68
+ insert_break_line(corrector, node, body)
66
69
  end
67
70
  end
68
71
 
@@ -70,9 +73,28 @@ module RuboCop
70
73
  node.body.loc.end.end.join(node.source_range.end)
71
74
  end
72
75
 
76
+ def insert_break_line(corrector, node, body)
77
+ if body.single_line?
78
+ corrector.replace(space_before_end(body), build_break_line(node))
79
+ else
80
+ corrector.insert_before(body.loc.end, build_break_line(node))
81
+ end
82
+ end
83
+
84
+ def space_before_end(body)
85
+ range = range_with_surrounding_space(range: body.loc.end, side: :left, newlines: false)
86
+
87
+ range.with(end_pos: body.loc.end.begin_pos)
88
+ end
89
+
73
90
  def build_break_line(node)
74
91
  conditional_keyword = node.while_post_type? ? 'unless' : 'if'
75
- "break #{conditional_keyword} #{node.condition.source}\n#{indent(node)}"
92
+ break_line = "break #{conditional_keyword} #{node.condition.source}\n#{indent(node)}"
93
+ return break_line unless node.body.single_line?
94
+
95
+ # A single-line `begin ... end` keeps its body and `end` on one line,
96
+ # so the `break` has to start a line of its own.
97
+ "\n#{indent(node, offset: configured_indentation_width)}#{break_line}"
76
98
  end
77
99
  end
78
100
  end