rubocop 1.91.0 → 1.92.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +4 -1
  4. data/lib/rubocop/cached_data.rb +47 -7
  5. data/lib/rubocop/changed_files.rb +35 -13
  6. data/lib/rubocop/cli/command/cop_names.rb +57 -0
  7. data/lib/rubocop/cli/command/execute_runner.rb +2 -2
  8. data/lib/rubocop/cli/command/explain.rb +34 -0
  9. data/lib/rubocop/cli/command/show_cops.rb +6 -0
  10. data/lib/rubocop/cli/command/show_docs_url.rb +6 -4
  11. data/lib/rubocop/cli.rb +3 -2
  12. data/lib/rubocop/comment_config.rb +17 -8
  13. data/lib/rubocop/config.rb +0 -4
  14. data/lib/rubocop/config_loader.rb +6 -3
  15. data/lib/rubocop/config_loader_resolver.rb +37 -12
  16. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  17. data/lib/rubocop/config_obsoletion.rb +9 -0
  18. data/lib/rubocop/config_validator.rb +11 -0
  19. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  20. data/lib/rubocop/cop/badge.rb +1 -1
  21. data/lib/rubocop/cop/base.rb +42 -21
  22. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  23. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  24. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  25. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  26. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  27. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +37 -7
  28. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  29. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +25 -4
  30. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +34 -0
  31. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  32. data/lib/rubocop/cop/documentation.rb +1 -1
  33. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  34. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  35. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  36. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  37. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  38. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  39. data/lib/rubocop/cop/layout/argument_alignment.rb +5 -1
  40. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  41. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  42. data/lib/rubocop/cop/layout/class_structure.rb +29 -7
  43. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  44. data/lib/rubocop/cop/layout/dot_position.rb +25 -15
  45. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +21 -8
  46. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  47. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  48. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  49. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +9 -3
  50. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  51. data/lib/rubocop/cop/layout/end_alignment.rb +21 -25
  52. data/lib/rubocop/cop/layout/extra_spacing.rb +21 -6
  53. data/lib/rubocop/cop/layout/first_argument_indentation.rb +27 -1
  54. data/lib/rubocop/cop/layout/first_array_element_line_break.rb +15 -0
  55. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +16 -8
  56. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  57. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  58. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  59. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +7 -3
  60. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  61. data/lib/rubocop/cop/layout/indentation_consistency.rb +22 -5
  62. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  63. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  64. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  65. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  66. data/lib/rubocop/cop/layout/line_length.rb +176 -26
  67. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  68. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  69. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +9 -0
  70. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  71. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +30 -5
  72. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  73. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  74. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +16 -3
  75. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  76. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  77. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  78. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  79. data/lib/rubocop/cop/layout/space_around_operators.rb +31 -4
  80. data/lib/rubocop/cop/layout/space_before_block_braces.rb +16 -1
  81. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  82. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +32 -9
  83. data/lib/rubocop/cop/layout/space_inside_range_literal.rb +29 -11
  84. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +2 -12
  85. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  86. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  87. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -1
  88. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  89. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +4 -0
  90. data/lib/rubocop/cop/lint/interpolation_check.rb +20 -17
  91. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  92. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  93. data/lib/rubocop/cop/lint/loop.rb +24 -2
  94. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  95. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  96. data/lib/rubocop/cop/lint/nested_percent_literal.rb +3 -0
  97. data/lib/rubocop/cop/lint/number_conversion.rb +3 -1
  98. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +22 -1
  99. data/lib/rubocop/cop/lint/percent_string_array.rb +75 -9
  100. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +39 -9
  101. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  102. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +11 -4
  103. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +47 -1
  104. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -0
  105. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  106. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  107. data/lib/rubocop/cop/lint/useless_setter_call.rb +7 -4
  108. data/lib/rubocop/cop/lint/void.rb +8 -1
  109. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  110. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  111. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  112. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  113. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -19
  114. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +8 -2
  115. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  116. data/lib/rubocop/cop/mixin/comments_help.rb +1 -1
  117. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  118. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +105 -0
  119. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  120. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +41 -6
  121. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  122. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  123. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  124. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  125. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  126. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  127. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  128. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  129. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  130. data/lib/rubocop/cop/mixin/statement_modifier.rb +39 -5
  131. data/lib/rubocop/cop/mixin/trailing_comma.rb +1 -1
  132. data/lib/rubocop/cop/mixin/visibility_help.rb +2 -1
  133. data/lib/rubocop/cop/mixin.rb +2 -0
  134. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  135. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  136. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  137. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  138. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  139. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  140. data/lib/rubocop/cop/offense.rb +59 -3
  141. data/lib/rubocop/cop/registry.rb +1 -1
  142. data/lib/rubocop/cop/security/json_load.rb +16 -4
  143. data/lib/rubocop/cop/style/access_modifier_declarations.rb +37 -2
  144. data/lib/rubocop/cop/style/alias.rb +18 -5
  145. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  146. data/lib/rubocop/cop/style/and_or.rb +37 -10
  147. data/lib/rubocop/cop/style/arguments_forwarding.rb +32 -3
  148. data/lib/rubocop/cop/style/attr.rb +3 -4
  149. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  150. data/lib/rubocop/cop/style/block_delimiters.rb +35 -3
  151. data/lib/rubocop/cop/style/case_equality.rb +14 -3
  152. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  153. data/lib/rubocop/cop/style/class_and_module_children.rb +57 -16
  154. data/lib/rubocop/cop/style/class_methods_definitions.rb +15 -0
  155. data/lib/rubocop/cop/style/collection_querying.rb +79 -46
  156. data/lib/rubocop/cop/style/combinable_loops.rb +89 -11
  157. data/lib/rubocop/cop/style/concat_array_literals.rb +58 -7
  158. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  159. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  160. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  161. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  162. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  163. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +53 -11
  164. data/lib/rubocop/cop/style/endless_method.rb +104 -11
  165. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  166. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  167. data/lib/rubocop/cop/style/float_division.rb +37 -2
  168. data/lib/rubocop/cop/style/for.rb +53 -2
  169. data/lib/rubocop/cop/style/format_string.rb +28 -6
  170. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  171. data/lib/rubocop/cop/style/guard_clause.rb +31 -22
  172. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +4 -0
  173. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  174. data/lib/rubocop/cop/style/hash_lookup_method.rb +42 -9
  175. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  176. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  177. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  178. data/lib/rubocop/cop/style/if_unless_modifier.rb +32 -26
  179. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  180. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  181. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  182. data/lib/rubocop/cop/style/it_block_parameter.rb +94 -4
  183. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  184. data/lib/rubocop/cop/style/lambda.rb +34 -1
  185. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  186. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  187. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +80 -13
  188. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +26 -2
  189. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  190. data/lib/rubocop/cop/style/method_def_parentheses.rb +38 -4
  191. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  192. data/lib/rubocop/cop/style/mixin_grouping.rb +12 -3
  193. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  194. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  195. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  196. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  197. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  198. data/lib/rubocop/cop/style/mutable_constant.rb +52 -7
  199. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  200. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  201. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  202. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  203. data/lib/rubocop/cop/style/next.rb +24 -10
  204. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  205. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  206. data/lib/rubocop/cop/style/not.rb +10 -5
  207. data/lib/rubocop/cop/style/numeric_predicate.rb +28 -5
  208. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  209. data/lib/rubocop/cop/style/parentheses_around_condition.rb +19 -1
  210. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  211. data/lib/rubocop/cop/style/quoted_symbols.rb +5 -7
  212. data/lib/rubocop/cop/style/raise_args.rb +80 -11
  213. data/lib/rubocop/cop/style/reduce_to_hash.rb +47 -33
  214. data/lib/rubocop/cop/style/redundant_begin.rb +45 -5
  215. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  216. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  217. data/lib/rubocop/cop/style/redundant_exception.rb +27 -11
  218. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  219. data/lib/rubocop/cop/style/redundant_freeze.rb +11 -3
  220. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +24 -1
  221. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  222. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  223. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  224. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  225. data/lib/rubocop/cop/style/redundant_self.rb +9 -1
  226. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +28 -0
  227. data/lib/rubocop/cop/style/redundant_sort.rb +38 -4
  228. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  229. data/lib/rubocop/cop/style/require_order.rb +25 -8
  230. data/lib/rubocop/cop/style/rescue_modifier.rb +11 -9
  231. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  232. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  233. data/lib/rubocop/cop/style/sample.rb +9 -8
  234. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  235. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  236. data/lib/rubocop/cop/style/send_with_literal_method_name.rb +4 -0
  237. data/lib/rubocop/cop/style/single_line_methods.rb +42 -4
  238. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  239. data/lib/rubocop/cop/style/string_concatenation.rb +21 -4
  240. data/lib/rubocop/cop/style/struct_inheritance.rb +6 -0
  241. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  242. data/lib/rubocop/cop/style/symbol_proc.rb +24 -6
  243. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  244. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  245. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  246. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  247. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  248. data/lib/rubocop/cop/team.rb +10 -5
  249. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  250. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  251. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  252. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  253. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  254. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  255. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  256. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  257. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  258. data/lib/rubocop/lsp/routes.rb +23 -23
  259. data/lib/rubocop/lsp/runtime.rb +47 -11
  260. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  261. data/lib/rubocop/lsp.rb +5 -2
  262. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  263. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  264. data/lib/rubocop/mcp/scope.rb +65 -0
  265. data/lib/rubocop/mcp/server.rb +184 -76
  266. data/lib/rubocop/mcp/source_files.rb +35 -0
  267. data/lib/rubocop/offense_limit.rb +30 -0
  268. data/lib/rubocop/options.rb +45 -10
  269. data/lib/rubocop/plugin/configuration_integrator.rb +36 -8
  270. data/lib/rubocop/remote_config.rb +4 -1
  271. data/lib/rubocop/result_cache.rb +20 -13
  272. data/lib/rubocop/runner.rb +110 -42
  273. data/lib/rubocop/server/cache.rb +6 -6
  274. data/lib/rubocop/target_ruby.rb +21 -9
  275. data/lib/rubocop/util.rb +69 -0
  276. data/lib/rubocop/version.rb +1 -1
  277. data/lib/rubocop.rb +5 -0
  278. metadata +16 -5
@@ -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
@@ -116,31 +116,88 @@ module RuboCop
116
116
  end
117
117
 
118
118
  def first_code_token
119
- @first_code_token ||= processed_source.sorted_tokens.find { |token| !token.comment? }
119
+ processed_source.sorted_tokens.find { |token| !token.comment? }
120
120
  end
121
121
 
122
122
  def check_encoding_comment(comment)
123
+ return unless misplaced_encoding_position?(comment)
124
+
125
+ add_offense(comment, message: MSG_ENCODING) do |corrector|
126
+ correct_encoding_comment(corrector, comment)
127
+ end
128
+ end
129
+
130
+ def misplaced_encoding_position?(comment)
123
131
  line = comment.source_range.line
124
- return if line == effective_encoding_line && comment_starts_line?(comment)
132
+ return false if line == effective_encoding_line && comment_starts_line?(comment)
133
+
125
134
  # Leave `# frozen_string_literal: true` + `# encoding: x` runs at the
126
135
  # very top to Lint/OrderedMagicComments, which already reorders them.
127
- return if preceded_only_by_magic_comments?(comment)
136
+ !preceded_only_by_magic_comments?(comment)
137
+ end
128
138
 
129
- add_offense(comment, message: MSG_ENCODING) do |corrector|
139
+ # With an effective encoding comment already in place, moving another
140
+ # one in would override it. Drop an identical duplicate and leave a
141
+ # conflicting one for a human to resolve.
142
+ def correct_encoding_comment(corrector, comment)
143
+ effective_comment = effective_encoding_comment
144
+
145
+ if effective_comment.nil?
130
146
  move_comment(corrector, comment, effective_encoding_line)
147
+ elsif comment_encoding(effective_comment) == comment_encoding(comment)
148
+ corrector.remove(comment_removal_range(comment))
131
149
  end
132
150
  end
133
151
 
152
+ def comment_encoding(comment)
153
+ Encoding.find(MagicComment.parse(comment.text).encoding)
154
+ rescue ArgumentError
155
+ nil
156
+ end
157
+
134
158
  def check_top_block_comment(comment, directive)
135
159
  return unless first_code_token
136
160
  return unless comment.source_range.begin_pos > first_code_token.begin_pos
137
161
 
138
162
  message = format(MSG_AFTER_CODE, directive: directive)
139
163
  add_offense(comment, message: message) do |corrector|
140
- move_comment(corrector, comment, effective_encoding_line)
164
+ # Both would be inserted at the top, in no reliable order. Leave
165
+ # this one to the next pass, once the encoding comment is in place.
166
+ next if encoding_comment_moving_in?
167
+
168
+ move_comment(corrector, comment, top_block_insertion_line)
141
169
  end
142
170
  end
143
171
 
172
+ def encoding_comment_moving_in?
173
+ return false if effective_encoding_comment
174
+
175
+ processed_source.comments.any? do |comment|
176
+ known_encoding_comment?(comment) && misplaced_encoding_position?(comment)
177
+ end
178
+ end
179
+
180
+ def effective_encoding_comment
181
+ processed_source.comments.find do |comment|
182
+ comment.source_range.line == effective_encoding_line &&
183
+ comment_starts_line?(comment) &&
184
+ MagicComment.parse(comment.text).encoding_specified?
185
+ end
186
+ end
187
+
188
+ def known_encoding_comment?(comment)
189
+ return false unless magic_comment_shaped?(comment.text)
190
+
191
+ magic_comment = MagicComment.parse(comment.text)
192
+ magic_comment.encoding_specified? && known_encoding?(magic_comment.encoding)
193
+ end
194
+
195
+ # Insert below an effective encoding comment rather than above it,
196
+ # which would push it off the only lines where Ruby honors it.
197
+ def top_block_insertion_line
198
+ effective_encoding_comment ? effective_encoding_line + 1 : effective_encoding_line
199
+ end
200
+
144
201
  def check_magic_comment_above_shebang
145
202
  shebang_comment = processed_source.comments.find do |comment|
146
203
  comment.source_range.line > 1 && comment.text.start_with?('#!') &&
@@ -166,12 +223,7 @@ module RuboCop
166
223
  end
167
224
 
168
225
  def move_comment(corrector, comment, target_line)
169
- removal_range = if comment_starts_line?(comment)
170
- range_by_whole_lines(comment.source_range, include_final_newline: true)
171
- else
172
- range_with_surrounding_space(comment.source_range, side: :left)
173
- end
174
- corrector.remove(removal_range)
226
+ corrector.remove(comment_removal_range(comment))
175
227
  target_range = processed_source.buffer.line_range(target_line)
176
228
  if comment.source_range.line < target_line
177
229
  corrector.insert_after(target_range, "\n#{comment.text}")
@@ -179,6 +231,14 @@ module RuboCop
179
231
  corrector.insert_before(target_range, "#{comment.text}\n")
180
232
  end
181
233
  end
234
+
235
+ def comment_removal_range(comment)
236
+ if comment_starts_line?(comment)
237
+ range_by_whole_lines(comment.source_range, include_final_newline: true)
238
+ else
239
+ range_with_surrounding_space(comment.source_range, side: :left)
240
+ end
241
+ end
182
242
  end
183
243
  end
184
244
  end
@@ -62,7 +62,7 @@ module RuboCop
62
62
  each_missing_enable do |cop, line_range|
63
63
  next if acceptable_range?(cop, line_range)
64
64
 
65
- comment = processed_source.comment_at_line(line_range.begin)
65
+ comment = opening_comment(line_range)
66
66
 
67
67
  add_offense(comment, message: message(cop, comment))
68
68
  end
@@ -73,14 +73,29 @@ module RuboCop
73
73
  def each_missing_enable
74
74
  processed_source.disabled_line_ranges.each do |cop, line_ranges|
75
75
  line_ranges.each do |line_range|
76
- # A `disable-next` scope closes itself with its statement.
77
- next if line_range.respond_to?(:directive) && line_range.directive.disable_next?
76
+ # A `disable-next` or `next` scope closes itself with its statement.
77
+ next if statement_scoped?(line_range)
78
78
 
79
79
  yield cop, line_range
80
80
  end
81
81
  end
82
82
  end
83
83
 
84
+ def statement_scoped?(line_range)
85
+ directive = line_range.respond_to?(:directive) && line_range.directive
86
+ directive && (directive.disable_next? || directive.next?)
87
+ end
88
+
89
+ # A disable suspended by `enable-next` or `next +Cop` reopens below the
90
+ # statement, on a code line, so the comment that opened it has to come
91
+ # from the range rather than from its first line.
92
+ def opening_comment(line_range)
93
+ directive = line_range.respond_to?(:directive) && line_range.directive
94
+ return directive.comment if directive
95
+
96
+ processed_source.comment_at_line(line_range.begin)
97
+ end
98
+
84
99
  def acceptable_range?(cop, line_range)
85
100
  # This has to remain a strict inequality to handle
86
101
  # the case when max_range is Float::INFINITY
@@ -40,6 +40,7 @@ module RuboCop
40
40
  # NestedPercentLiteral violation to be emitted.
41
41
  PERCENT_LITERAL_TYPES = PreferredDelimiters::PERCENT_LITERAL_TYPES
42
42
  REGEXES = PERCENT_LITERAL_TYPES.map { |percent_literal| /\A#{percent_literal}\W/ }.freeze
43
+ FORMAT_SEQUENCE = /\A#{RuboCop::Cop::Utils::FormatString::SEQUENCE}\z/.freeze
43
44
 
44
45
  def on_array(node)
45
46
  process(node, *PERCENT_LITERAL_TYPES)
@@ -54,6 +55,8 @@ module RuboCop
54
55
  def contains_percent_literals?(node)
55
56
  node.each_child_node.any? do |child|
56
57
  literal = child.children.first.to_s.scrub
58
+ next false if literal.match?(FORMAT_SEQUENCE)
59
+
57
60
  REGEXES.any? { |regex| literal.match?(regex) }
58
61
  end
59
62
  end
@@ -132,7 +132,7 @@ module RuboCop
132
132
 
133
133
  def handle_as_symbol(node)
134
134
  to_method_symbol(node) do |receiver, sym_node, to_method|
135
- next if receiver.nil? || !node.arguments.one?
135
+ next if receiver.nil? || !node.arguments.one? || node.operator_method?
136
136
 
137
137
  message = format(
138
138
  MSG,
@@ -140,6 +140,8 @@ module RuboCop
140
140
  corrected_method: correct_sym_method(to_method)
141
141
  )
142
142
  add_offense(node, message: message) do |corrector|
143
+ next if part_of_ignored_node?(node)
144
+
143
145
  remove_parentheses(corrector, node) if node.parenthesized?
144
146
 
145
147
  corrector.replace(sym_node, correct_sym_method(to_method))
@@ -57,7 +57,7 @@ module RuboCop
57
57
  node = node.children.first while node&.begin_type? && node.children.one?
58
58
  return false unless node
59
59
 
60
- keyword_operator?(node) || modifier_expression?(node)
60
+ keyword_operator?(node) || modifier_expression?(node) || command_call_operand?(node)
61
61
  end
62
62
 
63
63
  def keyword_operator?(node)
@@ -69,6 +69,27 @@ module RuboCop
69
69
  node.type?(:if, :while, :until) && node.modifier_form?
70
70
  end
71
71
 
72
+ def command_call_operand?(node)
73
+ if node.assignment? || (node.call_type? && node.assignment_method?)
74
+ command_call_value?(node.children.last)
75
+ elsif node.send_type? && node.prefix_bang?
76
+ command_call_value?(node.receiver)
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ def command_call_value?(node)
83
+ command_call?(node) || command_call_operand?(node)
84
+ end
85
+
86
+ def command_call?(node)
87
+ node = node.send_node if node.any_block_type?
88
+ return false unless node.type?(:call, :super, :yield)
89
+
90
+ node.arguments? && !node.parenthesized? && !node.operator_method?
91
+ end
92
+
72
93
  def chained_calls?(node)
73
94
  first_argument = node.first_argument
74
95
  first_argument.call_type? && (node.children.last&.children&.count || 0) > 1
@@ -9,6 +9,11 @@ module RuboCop
9
9
  # example, mistranslating an array of literals to percent string notation)
10
10
  # rather than meant to be part of the resulting strings.
11
11
  #
12
+ # When a quoted string contains whitespace, `%w` splits it into separate values,
13
+ # so removing the quotes would not restore the intended string.
14
+ # Such an array is corrected to an array literal instead, as long as its content
15
+ # reads as one with only string elements.
16
+ #
12
17
  # @safety
13
18
  # The cop is unsafe because the correction changes the values in the array
14
19
  # and that might have been done purposely.
@@ -26,6 +31,12 @@ module RuboCop
26
31
  #
27
32
  # # good
28
33
  # %w(foo bar)
34
+ #
35
+ # # bad - the quoted strings are split at whitespace
36
+ # %w('foo bar', 'baz')
37
+ #
38
+ # # good
39
+ # ['foo bar', 'baz']
29
40
  class PercentStringArray < Base
30
41
  include PercentLiteral
31
42
  extend AutoCorrector
@@ -36,27 +47,69 @@ module RuboCop
36
47
 
37
48
  MSG = "Within `%w`/`%W`, quotes and ',' are unnecessary and may be " \
38
49
  'unwanted in the resulting strings.'
50
+ MSG_SPLIT_STRING = 'Within `%w`/`%W`, quoted strings are split at whitespace; ' \
51
+ 'use an array literal instead.'
39
52
 
40
53
  def on_array(node)
41
54
  process(node, '%w', '%W')
42
55
  end
43
56
 
44
57
  def on_percent_literal(node)
45
- return unless contains_quotes_or_commas?(node)
58
+ if split_quoted_string?(node)
59
+ register_split_quoted_string(node)
60
+ elsif contains_quotes_or_commas?(node)
61
+ register_quotes_or_commas(node)
62
+ end
63
+ end
46
64
 
47
- add_offense(node) do |corrector|
48
- node.each_value do |value|
49
- range = value.source_range
65
+ private
50
66
 
51
- match = range.source.match(TRAILING_QUOTE)
52
- corrector.remove_trailing(range, match[0].length) if match
67
+ def split_quoted_string?(node)
68
+ sources = node.values.map { |value| value.source.delete_suffix(',') }
53
69
 
54
- corrector.remove_leading(range, 1) if LEADING_QUOTE.match?(range.source)
55
- end
70
+ sources.each_with_index.any? do |source, index|
71
+ next false unless (quote = unclosed_quote(source))
72
+
73
+ sources.drop(index + 1).any? { |rest| closing_quote?(rest, quote) }
56
74
  end
57
75
  end
58
76
 
59
- private
77
+ def unclosed_quote(source)
78
+ quote = source[0]
79
+ return unless LEADING_QUOTE.match?(quote)
80
+
81
+ quote if source.length > 1 && !source.end_with?(quote)
82
+ end
83
+
84
+ def closing_quote?(source, quote)
85
+ source.end_with?(quote) && (source.length == 1 || !source.start_with?(quote))
86
+ end
87
+
88
+ def register_split_quoted_string(node)
89
+ add_offense(node, message: MSG_SPLIT_STRING) do |corrector|
90
+ next unless (array_literal = array_literal_source(node))
91
+
92
+ corrector.replace(node, array_literal)
93
+ end
94
+ end
95
+
96
+ def array_literal_source(node)
97
+ content = range_between(node.loc.begin.end_pos, node.loc.end.begin_pos).source
98
+ array_literal = "[#{content}]"
99
+
100
+ array_literal if string_array?(parse(array_literal))
101
+ end
102
+
103
+ def string_array?(processed_source)
104
+ return false unless processed_source.valid_syntax? && processed_source.comments.empty?
105
+ return false unless (array = processed_source.ast).array_type?
106
+
107
+ array.children.all? { |child| quoted_string?(child) }
108
+ end
109
+
110
+ def quoted_string?(node)
111
+ node.type?(:str, :dstr) && node.loc?(:begin)
112
+ end
60
113
 
61
114
  def contains_quotes_or_commas?(node)
62
115
  node.values.any? do |value|
@@ -68,6 +121,19 @@ module RuboCop
68
121
  QUOTES_AND_COMMAS.any? { |pat| literal.match?(pat) }
69
122
  end
70
123
  end
124
+
125
+ def register_quotes_or_commas(node)
126
+ add_offense(node) do |corrector|
127
+ node.each_value do |value|
128
+ range = value.source_range
129
+
130
+ match = range.source.match(TRAILING_QUOTE)
131
+ corrector.remove_trailing(range, match[0].length) if match
132
+
133
+ corrector.remove_leading(range, 1) if LEADING_QUOTE.match?(range.source)
134
+ end
135
+ end
136
+ end
71
137
  end
72
138
  end
73
139
  end
@@ -110,7 +110,7 @@ module RuboCop
110
110
  # A pending cop that is not enabled in this run produces no
111
111
  # offenses, so its directives cannot be judged - they typically
112
112
  # prepare the code for the moment the cop gets enabled.
113
- next if pending_cop_not_run?(cop)
113
+ next if pending_cop_not_run?(cop) || disabled_by_preview?(cop)
114
114
 
115
115
  each_already_disabled(cop, line_ranges, &block)
116
116
  each_line_range(cop, line_ranges, &block)
@@ -128,6 +128,17 @@ module RuboCop
128
128
  !processed_source.registry.enabled_pending_cop?(cop_cfg, config, cop)
129
129
  end
130
130
 
131
+ # A cop that only the `Preview` defaults disable runs again as soon as
132
+ # preview is turned off, so its directives are still needed.
133
+ def disabled_by_preview?(cop)
134
+ return false unless preview? && all_cop_names.include?(cop)
135
+
136
+ preview_defaults = ConfigLoader.default_configuration[cop]&.fetch('Preview', nil)
137
+ return false unless preview_defaults.is_a?(Hash) && preview_defaults['Enabled'] == false
138
+
139
+ !config.cop_enabled?(cop)
140
+ end
141
+
131
142
  def each_line_range(cop, line_ranges)
132
143
  line_ranges.each_with_index do |line_range, line_range_index|
133
144
  next if should_skip_line_range?(cop, line_range)
@@ -135,8 +146,8 @@ module RuboCop
135
146
  comment = directive_comment(line_range)
136
147
  next if skip_directive?(comment)
137
148
 
138
- next_range = line_ranges[line_range_index + 1]
139
- redundant = find_redundant_directive(cop, comment, line_range, next_range)
149
+ following_ranges = line_ranges[(line_range_index + 1)..]
150
+ redundant = find_redundant_directive(cop, comment, line_range, following_ranges)
140
151
  yield comment, redundant if redundant
141
152
  end
142
153
  end
@@ -180,16 +191,35 @@ module RuboCop
180
191
  directive.push? || directive.pop? || directive.next?
181
192
  end
182
193
 
183
- def find_redundant_directive(cop, comment, line_range, next_range)
184
- if all_disabled?(comment)
185
- find_redundant_all(line_range, next_range)
186
- elsif department_disabled?(cop, comment)
187
- find_redundant_department(cop, line_range)
194
+ def find_redundant_directive(cop, comment, line_range, following_ranges)
195
+ return find_redundant_all(line_range, following_ranges.first) if all_disabled?(comment)
196
+
197
+ range = line_range_through_repeated_disables(cop, line_range, following_ranges)
198
+ if department_disabled?(cop, comment)
199
+ find_redundant_department(cop, range)
188
200
  else
189
- find_redundant_cop(cop, line_range)
201
+ find_redundant_cop(cop, range)
190
202
  end
191
203
  end
192
204
 
205
+ def line_range_through_repeated_disables(cop, line_range, following_ranges)
206
+ last_range = line_range
207
+ following_ranges.each do |range|
208
+ break unless followed_ranges?(last_range, range) && repeated_disable?(cop, range)
209
+
210
+ last_range = range
211
+ end
212
+
213
+ line_range.begin..last_range.end
214
+ end
215
+
216
+ def repeated_disable?(cop, range)
217
+ comment = directive_comment(range)
218
+ return false if comment.nil? || skip_directive?(comment)
219
+
220
+ !all_disabled?(comment) && !department_disabled?(cop, comment)
221
+ end
222
+
193
223
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
194
224
  def each_already_disabled(cop, line_ranges)
195
225
  line_ranges.each_cons(2) do |previous_range, range|
@@ -28,6 +28,7 @@ module RuboCop
28
28
  # end
29
29
  #
30
30
  class RedundantDirGlobSort < Base
31
+ include RangeHelp
31
32
  extend AutoCorrector
32
33
  extend TargetRubyVersion
33
34
 
@@ -46,8 +47,7 @@ module RuboCop
46
47
  selector = node.loc.selector
47
48
 
48
49
  add_offense(selector) do |corrector|
49
- corrector.remove(selector)
50
- corrector.remove(node.loc.dot)
50
+ autocorrect(corrector, node.loc.dot, selector)
51
51
  end
52
52
  end
53
53
 
@@ -67,6 +67,17 @@ module RuboCop
67
67
  def sort_with_comparator?(node)
68
68
  node.parent&.any_block_type? || node.last_argument&.block_pass_type?
69
69
  end
70
+
71
+ def autocorrect(corrector, dot, selector)
72
+ range = dot.join(selector)
73
+
74
+ if processed_source.contains_comment?(range)
75
+ corrector.remove(dot)
76
+ corrector.remove(selector)
77
+ else
78
+ corrector.remove(range_with_surrounding_space(range, side: :left))
79
+ end
80
+ end
70
81
  end
71
82
  end
72
83
  end
@@ -118,10 +118,7 @@ module RuboCop
118
118
  def redundant_splat_expansion(node)
119
119
  literal_expansion(node) do |expanded_item|
120
120
  if expanded_item.send_type?
121
- return if array_new_inside_array_literal?(expanded_item)
122
-
123
- grandparent = node.parent.parent
124
- return if grandparent && !ASSIGNMENT_TYPES.include?(grandparent.type)
121
+ return unless redundant_array_new_splat?(node, expanded_item)
125
122
  # An empty array/percent literal (`*[]`, `*%w()`, ...) expands to nothing, so
126
123
  # removing the splat would produce invalid or semantically different code.
127
124
  elsif expanded_item.array_type? && expanded_item.children.empty?
@@ -132,6 +129,16 @@ module RuboCop
132
129
  end
133
130
  end
134
131
 
132
+ # A splatted `Array.new` passed as a method argument provides the arguments,
133
+ # so the splat is only redundant directly inside an array.
134
+ def redundant_array_new_splat?(node, array_new_node)
135
+ return false unless node.parent.array_type?
136
+ return false if array_new_inside_array_literal?(array_new_node)
137
+
138
+ grandparent = node.parent.parent
139
+ !grandparent || ASSIGNMENT_TYPES.include?(grandparent.type)
140
+ end
141
+
135
142
  def array_new_inside_array_literal?(array_new_node)
136
143
  return false unless array_new?(array_new_node)
137
144
 
@@ -60,7 +60,7 @@ module RuboCop
60
60
 
61
61
  rest_operands = grouped_operands[begin_of_rest_operands..]
62
62
  rest_operands.each do |operand|
63
- next if already_appropriate_call?(operand, dot_op)
63
+ next unless correction_needed?(operand, dot_op, grouped_operands)
64
64
 
65
65
  register_offense(operand, dot_op)
66
66
  end
@@ -100,12 +100,58 @@ module RuboCop
100
100
  end
101
101
  end
102
102
 
103
+ def correction_needed?(operand, dot_op, grouped_operands)
104
+ return false if already_appropriate_call?(operand, dot_op)
105
+
106
+ dot_op == '&.' || receiver_guaranteed?(operand, grouped_operands)
107
+ end
108
+
103
109
  def already_appropriate_call?(operand, dot_op)
104
110
  return true if operand.safe_navigation? && dot_op == '&.'
105
111
 
106
112
  (operand.dot? || operand.operator_method?) && dot_op == '.'
107
113
  end
108
114
 
115
+ def receiver_guaranteed?(operand, grouped_operands)
116
+ grouped_operands.take_while { |guard| !guard.equal?(operand) }.any? do |guard|
117
+ guaranteed_by?(guard, operand)
118
+ end
119
+ end
120
+
121
+ def guaranteed_by?(guard, operand)
122
+ return false unless (path = logical_path(guard, operand))
123
+
124
+ truthiness = path.last.first.and_type? ? :truthy : :falsey
125
+ path[0...-1].reverse_each do |node, child|
126
+ return false unless (truthiness = child_truthiness(node, child, truthiness))
127
+ end
128
+
129
+ !guard.csend_type? || truthiness == :truthy
130
+ end
131
+
132
+ def logical_path(guard, operand)
133
+ path = []
134
+ child = guard
135
+
136
+ guard.each_ancestor do |node|
137
+ unless node.begin_type?
138
+ return nil unless node.operator_keyword?
139
+
140
+ path << [node, child]
141
+ end
142
+ return path if operand.each_ancestor.any? { |ancestor| ancestor.equal?(node) }
143
+
144
+ child = node
145
+ end
146
+ end
147
+
148
+ def child_truthiness(node, child, truthiness)
149
+ deciding = node.and_type? ? :truthy : :falsey
150
+ return truthiness == deciding ? deciding : :unknown if node.lhs.equal?(child)
151
+
152
+ deciding if truthiness == deciding
153
+ end
154
+
109
155
  def register_offense(operand, dot_operator)
110
156
  offense_range = operand.operator_method? ? operand : operand.loc.dot
111
157
  message = dot_operator == '.' ? USE_DOT_MSG : USE_SAFE_NAVIGATION_MSG
@@ -185,6 +185,7 @@ module RuboCop
185
185
  ignore_node(key)
186
186
 
187
187
  next if requires_quotes?(key)
188
+ next if key.parent.value_omission?
188
189
  next if properly_quoted?(key.source, %("#{key.value}"))
189
190
 
190
191
  correction = %("#{key.value}")
@@ -53,12 +53,22 @@ module RuboCop
53
53
  return if node.each_descendant(:dstr).any?
54
54
 
55
55
  regexp_constructor(node) do |text|
56
+ next unless verbatim_string?(text)
57
+
56
58
  detect_offenses_in_tree(text, parse_regexp(text.value))
57
59
  end
58
60
  end
59
61
 
60
62
  private
61
63
 
64
+ # Offsets into the string's value only point at the right place in the source
65
+ # when the string has no escape sequences, so anything else is left alone.
66
+ def verbatim_string?(node)
67
+ return false unless node.loc?(:begin) && node.loc?(:end)
68
+
69
+ node.loc.begin.end.join(node.loc.end.begin).source == node.value
70
+ end
71
+
62
72
  # When a character class opens with a bare `]` (e.g. `[^]]`), `regexp_parser` parses
63
73
  # `[^]` / `[]` as an empty set and reports the closing `]` as a separate literal.
64
74
  # Ruby treats that `]` as the end of the class, not as an unescaped bracket,
@@ -97,7 +107,7 @@ module RuboCop
97
107
  location = range_at_index(node, expr.ts, pos)
98
108
 
99
109
  add_offense(location) do |corrector|
100
- corrector.replace(location, '\]')
110
+ corrector.replace(location, escaped_bracket(node))
101
111
  end
102
112
  end
103
113
 
@@ -108,6 +118,13 @@ module RuboCop
108
118
  adjustment = index + offset
109
119
  node.loc.begin.end.adjust(begin_pos: adjustment, end_pos: adjustment + 1)
110
120
  end
121
+
122
+ # A double-quoted string turns `\]` into `]`, so its backslash needs escaping too.
123
+ def escaped_bracket(node)
124
+ return '\]' if node.regexp_type? || node.single_quoted? || node.percent_literal?(:q)
125
+
126
+ '\\\\]'
127
+ end
111
128
  end
112
129
  end
113
130
  end