rubocop 1.91.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 (236) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +3 -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_loader.rb +6 -3
  14. data/lib/rubocop/config_loader_resolver.rb +37 -12
  15. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  16. data/lib/rubocop/config_validator.rb +11 -0
  17. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  18. data/lib/rubocop/cop/base.rb +42 -21
  19. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  20. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  21. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  22. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  23. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  24. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +22 -7
  25. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  26. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +23 -2
  27. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  28. data/lib/rubocop/cop/documentation.rb +1 -1
  29. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  30. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  31. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  32. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  33. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  34. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  35. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  36. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  37. data/lib/rubocop/cop/layout/class_structure.rb +9 -1
  38. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  39. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +14 -5
  40. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  41. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  42. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +1 -1
  43. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  44. data/lib/rubocop/cop/layout/end_alignment.rb +16 -10
  45. data/lib/rubocop/cop/layout/extra_spacing.rb +4 -0
  46. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -0
  47. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +5 -3
  48. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  49. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  50. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  51. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +2 -0
  52. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  53. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  54. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  55. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  56. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  57. data/lib/rubocop/cop/layout/line_length.rb +52 -18
  58. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  59. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  60. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +7 -0
  61. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  62. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +14 -0
  63. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  64. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  65. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +15 -2
  66. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  67. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  68. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  69. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  70. data/lib/rubocop/cop/layout/space_before_block_braces.rb +4 -1
  71. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  72. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +31 -8
  73. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  74. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  75. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  76. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  77. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  78. data/lib/rubocop/cop/lint/loop.rb +24 -2
  79. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  80. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  81. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  82. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +12 -1
  83. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  84. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  85. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  86. data/lib/rubocop/cop/lint/void.rb +8 -1
  87. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  88. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  89. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  90. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  91. data/lib/rubocop/cop/mixin/check_line_breakable.rb +17 -14
  92. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +5 -1
  93. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  94. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  95. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +69 -0
  96. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  97. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +15 -1
  98. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  99. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  100. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  101. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  102. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  103. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  104. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  105. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  106. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  107. data/lib/rubocop/cop/mixin/statement_modifier.rb +22 -2
  108. data/lib/rubocop/cop/mixin.rb +2 -0
  109. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  110. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  111. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  112. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  113. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  114. data/lib/rubocop/cop/offense.rb +59 -3
  115. data/lib/rubocop/cop/registry.rb +1 -1
  116. data/lib/rubocop/cop/security/json_load.rb +16 -4
  117. data/lib/rubocop/cop/style/access_modifier_declarations.rb +7 -0
  118. data/lib/rubocop/cop/style/alias.rb +18 -5
  119. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  120. data/lib/rubocop/cop/style/and_or.rb +4 -0
  121. data/lib/rubocop/cop/style/arguments_forwarding.rb +4 -1
  122. data/lib/rubocop/cop/style/attr.rb +3 -4
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  124. data/lib/rubocop/cop/style/block_delimiters.rb +29 -3
  125. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +54 -16
  127. data/lib/rubocop/cop/style/class_methods_definitions.rb +8 -0
  128. data/lib/rubocop/cop/style/combinable_loops.rb +85 -11
  129. data/lib/rubocop/cop/style/concat_array_literals.rb +46 -7
  130. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  131. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  132. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  133. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  134. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  135. data/lib/rubocop/cop/style/endless_method.rb +67 -6
  136. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  137. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  138. data/lib/rubocop/cop/style/float_division.rb +22 -2
  139. data/lib/rubocop/cop/style/for.rb +40 -2
  140. data/lib/rubocop/cop/style/format_string.rb +28 -6
  141. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  142. data/lib/rubocop/cop/style/guard_clause.rb +9 -1
  143. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  144. data/lib/rubocop/cop/style/hash_lookup_method.rb +32 -9
  145. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  146. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  147. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  148. data/lib/rubocop/cop/style/if_unless_modifier.rb +19 -20
  149. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  150. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  151. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  152. data/lib/rubocop/cop/style/it_block_parameter.rb +32 -3
  153. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  154. data/lib/rubocop/cop/style/lambda.rb +18 -0
  155. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  156. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  157. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +45 -10
  158. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +25 -2
  159. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  160. data/lib/rubocop/cop/style/method_def_parentheses.rb +29 -3
  161. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  162. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  163. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  164. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  165. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  166. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  167. data/lib/rubocop/cop/style/mutable_constant.rb +28 -1
  168. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  169. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  170. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  171. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  172. data/lib/rubocop/cop/style/next.rb +24 -10
  173. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  174. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  175. data/lib/rubocop/cop/style/not.rb +10 -5
  176. data/lib/rubocop/cop/style/numeric_predicate.rb +21 -5
  177. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  178. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  179. data/lib/rubocop/cop/style/reduce_to_hash.rb +17 -1
  180. data/lib/rubocop/cop/style/redundant_begin.rb +20 -3
  181. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  182. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  183. data/lib/rubocop/cop/style/redundant_exception.rb +15 -3
  184. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  185. data/lib/rubocop/cop/style/redundant_freeze.rb +10 -1
  186. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  187. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  188. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  189. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  190. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  191. data/lib/rubocop/cop/style/require_order.rb +9 -0
  192. data/lib/rubocop/cop/style/rescue_modifier.rb +5 -9
  193. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  194. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  195. data/lib/rubocop/cop/style/sample.rb +9 -8
  196. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  197. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  198. data/lib/rubocop/cop/style/single_line_methods.rb +16 -4
  199. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  200. data/lib/rubocop/cop/style/string_concatenation.rb +9 -3
  201. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  202. data/lib/rubocop/cop/style/symbol_proc.rb +23 -5
  203. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  204. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  205. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  206. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  207. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  208. data/lib/rubocop/cop/team.rb +10 -5
  209. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  210. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  211. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  212. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  213. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  214. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  215. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  216. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  217. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  218. data/lib/rubocop/lsp/runtime.rb +47 -11
  219. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  220. data/lib/rubocop/lsp.rb +5 -2
  221. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  222. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  223. data/lib/rubocop/mcp/scope.rb +65 -0
  224. data/lib/rubocop/mcp/server.rb +184 -76
  225. data/lib/rubocop/mcp/source_files.rb +35 -0
  226. data/lib/rubocop/offense_limit.rb +30 -0
  227. data/lib/rubocop/options.rb +45 -10
  228. data/lib/rubocop/plugin/configuration_integrator.rb +12 -5
  229. data/lib/rubocop/remote_config.rb +4 -1
  230. data/lib/rubocop/result_cache.rb +20 -13
  231. data/lib/rubocop/runner.rb +109 -41
  232. data/lib/rubocop/server/cache.rb +2 -2
  233. data/lib/rubocop/util.rb +69 -0
  234. data/lib/rubocop/version.rb +1 -1
  235. data/lib/rubocop.rb +5 -0
  236. metadata +16 -5
@@ -60,19 +60,18 @@ module RuboCop
60
60
  # end
61
61
  #
62
62
  class CombinableLoops < Base
63
+ include RangeHelp
63
64
  extend AutoCorrector
64
65
 
65
66
  MSG = 'Combine this loop with the previous loop.'
66
67
 
67
- # rubocop:disable-next Metrics/CyclomaticComplexity
68
68
  def on_block(node)
69
69
  return unless node.parent&.begin_type?
70
70
  return unless collection_looping_method?(node)
71
- return unless same_collection_looping_block?(node, node.left_sibling)
72
- return unless node.body && node.left_sibling.body
71
+ return unless combinable_looping_blocks?(node, node.left_sibling)
73
72
 
74
73
  add_offense(node) do |corrector|
75
- next unless node.arguments == node.left_sibling.arguments
74
+ next unless combinable_blocks?(node, node.left_sibling)
76
75
 
77
76
  combine_with_left_sibling(corrector, node)
78
77
  end
@@ -87,9 +86,7 @@ module RuboCop
87
86
  return unless node.body && node.left_sibling.body
88
87
 
89
88
  add_offense(node) do |corrector|
90
- # Combining loops with different iteration variables would leave the second
91
- # body referencing an undefined variable, so only autocorrect when they match.
92
- next unless node.variable == node.left_sibling.variable
89
+ next unless combinable_fors?(node, node.left_sibling)
93
90
 
94
91
  combine_with_left_sibling(corrector, node)
95
92
  end
@@ -110,25 +107,102 @@ module RuboCop
110
107
  sibling.send_node.arguments == node.send_node.arguments
111
108
  end
112
109
 
110
+ def combinable_looping_blocks?(node, sibling)
111
+ same_collection_looping_block?(node, sibling) && node.body && sibling.body
112
+ end
113
+
113
114
  def same_collection_looping_for?(node, sibling)
114
115
  sibling&.for_type? && node.collection == sibling.collection
115
116
  end
116
117
 
118
+ def combinable_blocks?(node, sibling)
119
+ return false unless node.arguments == sibling.arguments
120
+ # Numbered and `it` parameters are implicit, so `arguments` is empty for both.
121
+ return false unless node.argument_list == sibling.argument_list
122
+ return false if directive_after_opening?(node)
123
+
124
+ !node.body.type?(:rescue, :ensure) && !sibling.body.type?(:rescue, :ensure)
125
+ end
126
+
127
+ # Combining loops with different iteration variables would leave the second
128
+ # body referencing an undefined variable, so only autocorrect when they match.
129
+ def combinable_fors?(node, sibling)
130
+ node.variable == sibling.variable && !directive_after_opening?(node)
131
+ end
132
+
133
+ # The opening is removed, so a directive after it would end up on a line of
134
+ # its own, where it covers the rest of the file instead of a single line.
135
+ def directive_after_opening?(node)
136
+ opening_line = loop_opening(node).line
137
+ return false if opening_line == node.body.first_line
138
+
139
+ comment = processed_source.comment_at_line(opening_line)
140
+ comment && DirectiveComment.new(comment).start_with_marker?
141
+ end
142
+
143
+ # The block arguments or `{`/`do` of a block, or the `do` or collection of a `for` loop.
144
+ def loop_opening(node)
145
+ if node.for_type?
146
+ node.loc.begin || node.collection.source_range
147
+ else
148
+ (node.block_type? && node.arguments.source_range) || node.loc.begin
149
+ end
150
+ end
151
+
117
152
  def combine_with_left_sibling(corrector, node)
118
- corrector.remove(node.left_sibling.body.source_range.end.join(node.left_sibling.loc.end))
119
- corrector.remove(node.source_range.begin.join(node.body.source_range.begin))
153
+ corrector.remove(closing_with_preceding_space(node.left_sibling, node))
154
+ corrector.remove(opening_with_following_space(node))
120
155
 
121
156
  correct_end_of_block(corrector, node)
122
157
  end
123
158
 
159
+ # Comments and heredoc bodies before the closing delimiter are kept. When
160
+ # the next loop starts on the delimiter's line, the line break before the
161
+ # delimiter is kept too, so that a comment doesn't swallow the next loop.
162
+ def closing_with_preceding_space(loop, next_loop)
163
+ separate_lines = !same_line?(loop.loc.end, next_loop)
164
+
165
+ range_with_surrounding_space(
166
+ loop.loc.end, side: :left, newlines: separate_lines, whitespace: separate_lines
167
+ )
168
+ end
169
+
170
+ # Comments between the opening and the body are kept.
171
+ def opening_with_following_space(node)
172
+ opening = node.source_range.begin.join(loop_opening(node))
173
+
174
+ range_with_surrounding_space(opening, side: :right, whitespace: true)
175
+ end
176
+
124
177
  def correct_end_of_block(corrector, node)
125
178
  return unless node.left_sibling.respond_to?(:braces?)
126
- return if node.right_sibling&.any_block_type?
179
+ return if combined_with_right_sibling?(node)
127
180
 
128
- end_of_block = node.left_sibling.braces? ? '}' : ' end'
181
+ end_of_block = first_combined_block(node).braces? ? '}' : ' end'
129
182
  corrector.remove(node.loc.end)
130
183
  corrector.insert_before(node.source_range.end, end_of_block)
131
184
  end
185
+
186
+ def combined_with_right_sibling?(node)
187
+ sibling = node.right_sibling
188
+ return false unless sibling&.any_block_type?
189
+ # A disabled loop isn't corrected, so it doesn't get merged into this one.
190
+ return false unless enabled_lines?(sibling.source_range)
191
+
192
+ same_collection_looping_block?(sibling, node) && sibling.body &&
193
+ combinable_blocks?(sibling, node)
194
+ end
195
+
196
+ def first_combined_block(node)
197
+ first = node.left_sibling
198
+
199
+ while (previous = first.left_sibling) && enabled_lines?(first.source_range) &&
200
+ combinable_looping_blocks?(first, previous) && combinable_blocks?(first, previous)
201
+ first = previous
202
+ end
203
+
204
+ first
205
+ end
132
206
  end
133
207
  end
134
208
  end
@@ -23,6 +23,7 @@ module RuboCop
23
23
  # list.push(qux, quux, corge)
24
24
  #
25
25
  class ConcatArrayLiterals < Base
26
+ include RangeHelp
26
27
  extend AutoCorrector
27
28
 
28
29
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
@@ -30,6 +31,14 @@ module RuboCop
30
31
  'Use `push` with elements as arguments without array brackets instead of `%<current>s`.'
31
32
  RESTRICT_ON_SEND = %i[concat].freeze
32
33
 
34
+ # `Style/MethodCallWithArgsParentheses` removing the parentheses of the `push(...)`
35
+ # this cop writes in the same pass drops the line continuation a multiline call needs.
36
+ # `Style/TrailingCommaInArguments` adds a comma after the array literal this cop
37
+ # replaces with its elements in the same pass.
38
+ def self.autocorrect_incompatible_with
39
+ [Style::MethodCallWithArgsParentheses, Style::TrailingCommaInArguments]
40
+ end
41
+
33
42
  # rubocop:disable-next Metrics
34
43
  def on_send(node)
35
44
  return if node.arguments.empty?
@@ -54,17 +63,13 @@ module RuboCop
54
63
  if use_percent_literal
55
64
  next unless prefer
56
65
 
57
- corrector.replace(offense, prefer)
66
+ rebuild_call(corrector, offense, prefer)
58
67
  elsif node.arguments.any? { |argument| argument.children.empty? }
59
68
  # In-place bracket removal would leave dangling commas (e.g.
60
69
  # `concat([], [b])` -> `push(, b)`), so rebuild the call instead.
61
- corrector.replace(offense, preferred_method(node))
70
+ rebuild_call(corrector, offense, preferred_method(node))
62
71
  else
63
- corrector.replace(node.loc.selector, 'push')
64
- node.arguments.each do |argument|
65
- corrector.remove(argument.loc.begin)
66
- corrector.remove(argument.loc.end)
67
- end
72
+ remove_brackets(corrector, node)
68
73
  end
69
74
  end
70
75
  end
@@ -94,6 +99,40 @@ module RuboCop
94
99
  arg.children.all? { |child| child.type?(:str, :sym) }
95
100
  end
96
101
  end
102
+
103
+ def rebuild_call(corrector, offense, replacement)
104
+ # The comments inside the call would be lost.
105
+ return if comment_in?(offense)
106
+
107
+ corrector.replace(offense, replacement)
108
+ end
109
+
110
+ def comment_in?(range)
111
+ processed_source.each_comment_in_lines(range.line..range.last_line).any? do |comment|
112
+ range.contains?(comment.source_range)
113
+ end
114
+ end
115
+
116
+ def remove_brackets(corrector, node)
117
+ closing_ranges = node.arguments.map { |argument| closing_bracket_range(node, argument) }
118
+ # A comment or a heredoc body there can't be moved out of the way.
119
+ return unless closing_ranges.all? { |range| range.source.match?(/\A[\s,]*\]\z/) }
120
+
121
+ corrector.replace(node.loc.selector, 'push')
122
+ node.arguments.zip(closing_ranges) do |argument, closing_range|
123
+ corrector.remove(argument.loc.begin)
124
+ corrector.remove(closing_range)
125
+ end
126
+ end
127
+
128
+ # Whatever sits between the last element and the `]` of an array that isn't the
129
+ # last argument would end up before the following comma (a newline before it is a
130
+ # syntax error), so it's removed along with the `]`.
131
+ def closing_bracket_range(node, argument)
132
+ return argument.loc.end if argument.equal?(node.last_argument)
133
+
134
+ range_between(argument.children.last.source_range.end_pos, argument.loc.end.end_pos)
135
+ end
97
136
  end
98
137
  end
99
138
  end
@@ -223,6 +223,10 @@ module RuboCop
223
223
  }
224
224
  PATTERN
225
225
 
226
+ def self.autocorrect_incompatible_with
227
+ [Style::MethodCallWithArgsParentheses]
228
+ end
229
+
226
230
  ASSIGNMENT_TYPES.each do |type|
227
231
  define_method :"on_#{type}" do |node|
228
232
  return if part_of_ignored_node?(node)
@@ -33,7 +33,8 @@ module RuboCop
33
33
  # a `--` trailing justification comment is allowed, so a team can require
34
34
  # every disable to be documented instead of banning them outright. Enable
35
35
  # directives are not checked in this mode, since they end a suppression
36
- # rather than start one.
36
+ # rather than start one. Offenses in this mode are not autocorrected,
37
+ # since only a human can supply the missing justification.
37
38
  #
38
39
  # This cop cannot be disabled via directive comments when it is explicitly
39
40
  # enabled with `Enabled: true`. This prevents users from bypassing the cop
@@ -99,6 +100,8 @@ module RuboCop
99
100
  'comment are not permitted.'
100
101
 
101
102
  def on_new_investigation
103
+ @matching_pops = nil
104
+
102
105
  processed_source.comments.each do |comment|
103
106
  directive = DirectiveComment.new(comment)
104
107
  next if ignored?(directive)
@@ -108,7 +111,7 @@ module RuboCop
108
111
 
109
112
  next unless disallowed_cops.any?
110
113
 
111
- register_offense(comment, directive_cops, disallowed_cops)
114
+ register_offense(directive, directive_cops, disallowed_cops)
112
115
  end
113
116
  end
114
117
 
@@ -126,19 +129,61 @@ module RuboCop
126
129
  end
127
130
  end
128
131
 
129
- def register_offense(comment, directive_cops, disallowed_cops)
130
- add_offense(comment, message: offense_message(disallowed_cops)) do |corrector|
131
- replacement = ''
132
+ def register_offense(directive, directive_cops, disallowed_cops)
133
+ comment = directive.comment
132
134
 
133
- if directive_cops.length != disallowed_cops.length
134
- replacement = comment.text.sub(/#{Regexp.union(disallowed_cops)},?\s*/, '')
135
- .sub(/,\s*$/, '')
135
+ add_offense(comment, message: offense_message(disallowed_cops)) do |corrector|
136
+ # The remedy in `AllowWithReason` mode is to write the missing `--` justification,
137
+ # which cannot be autocorrected. Removing the directive would also unsuppress
138
+ # the disabled cops and let their autocorrections rewrite the annotated code.
139
+ next if allow_with_reason?
140
+
141
+ if directive_cops.length == disallowed_cops.length
142
+ remove_directive(corrector, directive)
143
+ elsif signed_args?(directive)
144
+ remove_signed_args(corrector, directive, disallowed_cops)
145
+ else
146
+ remove_cops(corrector, comment, disallowed_cops)
136
147
  end
148
+ end
149
+ end
150
+
151
+ # A `push` without its `pop` leaves an orphan `pop` behind, so the pair goes together.
152
+ def remove_directive(corrector, directive)
153
+ corrector.replace(directive.comment, '')
154
+ return unless directive.push? && (pop = matching_pop(directive.comment))
155
+
156
+ corrector.replace(pop, '')
157
+ end
158
+
159
+ def matching_pop(push_comment)
160
+ @matching_pops ||= pair_pushes_with_pops
161
+ @matching_pops[push_comment]
162
+ end
137
163
 
138
- corrector.replace(comment, replacement)
164
+ def pair_pushes_with_pops
165
+ pushes = []
166
+ processed_source.comments.each_with_object({}) do |comment, pairs|
167
+ directive = DirectiveComment.new(comment)
168
+ if directive.push?
169
+ pushes.push(comment)
170
+ elsif directive.pop? && (push = pushes.pop)
171
+ pairs[push] = comment
172
+ end
139
173
  end
140
174
  end
141
175
 
176
+ def remove_signed_args(corrector, directive, disallowed_cops)
177
+ range = directive.range
178
+ names = Regexp.union(disallowed_cops)
179
+ corrector.replace(range, range.source.gsub(/\s+[+-]#{names}(?!\S)/, ''))
180
+ end
181
+
182
+ def remove_cops(corrector, comment, disallowed_cops)
183
+ corrector.replace(comment, comment.text.sub(/#{Regexp.union(disallowed_cops)},?\s*/, '')
184
+ .sub(/,\s*$/, ''))
185
+ end
186
+
142
187
  def offense_message(disallowed_cops)
143
188
  if allow_with_reason?
144
189
  MSG_MISSING_REASON
@@ -150,10 +195,16 @@ module RuboCop
150
195
  end
151
196
 
152
197
  def directive_cops(directive)
198
+ return directive.signed_args.values.flatten if signed_args?(directive)
199
+
153
200
  match_captures = directive.match_captures
154
201
  match_captures && match_captures[1] ? match_captures[1].split(',').map(&:strip) : []
155
202
  end
156
203
 
204
+ def signed_args?(directive)
205
+ directive.push? || directive.next?
206
+ end
207
+
157
208
  def ignored?(directive)
158
209
  return true if exempt_mode?(directive)
159
210
 
@@ -169,6 +220,11 @@ module RuboCop
169
220
  end
170
221
 
171
222
  def allow_with_reason?
223
+ # TODO: The obsolete `AllowTrailingComment` is honored for backward compatibility
224
+ # and will be removed in RuboCop 2.0.
225
+ allow_trailing_comment = cop_config['AllowTrailingComment'] # rubocop:disable InternalAffairs/UndefinedConfig -- obsolete key, gone from default.yml on purpose
226
+ return allow_trailing_comment unless allow_trailing_comment.nil?
227
+
172
228
  cop_config['AllowWithReason']
173
229
  end
174
230
 
@@ -70,6 +70,7 @@ module RuboCop
70
70
 
71
71
  def on_send(node)
72
72
  return unless double_negative?(node) && node.prefix_bang?
73
+ return if part_of_longer_negation?(node)
73
74
  return if style == :allowed_in_returns && allowed_in_returns?(node)
74
75
 
75
76
  location = node.loc.selector
@@ -81,10 +82,20 @@ module RuboCop
81
82
 
82
83
  private
83
84
 
85
+ def part_of_longer_negation?(node)
86
+ return true if negation?(node.parent) && node.parent.receiver.equal?(node)
87
+
88
+ negation?(node.receiver.receiver)
89
+ end
90
+
84
91
  def allowed_in_returns?(node)
85
92
  node.parent&.return_type? || end_of_method_definition?(node)
86
93
  end
87
94
 
95
+ def negation?(node)
96
+ node&.send_type? && node.method?(:!)
97
+ end
98
+
88
99
  def end_of_method_definition?(node)
89
100
  return false unless (def_node = find_def_node_from_ascendant(node))
90
101
 
@@ -103,7 +103,7 @@ module RuboCop
103
103
  end
104
104
 
105
105
  def replacement_range(node)
106
- if hash_node(node) && first_argument_unparenthesized?(node)
106
+ if offense_hash_node?(node) && first_argument_unparenthesized?(node)
107
107
  # `some_method {}` is not same as `some_method Hash.new`
108
108
  # because the braces are interpreted as a block. We will have
109
109
  # to rewrite the arguments to wrap them in parenthesis.
@@ -102,8 +102,10 @@ module RuboCop
102
102
  compact_style? ? '; ' : "\n#{indent}"
103
103
  end
104
104
 
105
+ # A method with an argument spanning multiple lines (e.g. a hash default
106
+ # value) can't be put on a single line, so it's as compact as it gets.
105
107
  def compact?(node)
106
- node.single_line?
108
+ node.single_line? || node.arguments.any?(&:multiline?)
107
109
  end
108
110
 
109
111
  def expanded?(node)
@@ -131,7 +131,9 @@ module RuboCop
131
131
  #
132
132
  class EndlessMethod < Base
133
133
  include ConfigurableEnforcedStyle
134
+ include LineLengthHelp
134
135
  include EndlessMethodRewriter
136
+ include EndlessMethodBodyHelp
135
137
  extend TargetRubyVersion
136
138
  extend AutoCorrector
137
139
 
@@ -143,6 +145,12 @@ module RuboCop
143
145
  MSG_REQUIRE_SINGLE = 'Use endless method definitions for single line methods.'
144
146
  MSG_REQUIRE_ALWAYS = 'Use endless method definitions.'
145
147
 
148
+ # `Style/MethodDefParentheses` adds the same parentheses a parameter list needs in
149
+ # an endless definition, so letting both correct in the same pass would double them.
150
+ def self.autocorrect_incompatible_with
151
+ [Style::MethodCallWithArgsParentheses, Style::MethodDefParentheses]
152
+ end
153
+
146
154
  def on_def(node)
147
155
  return if node.assignment_method? || use_heredoc?(node)
148
156
 
@@ -213,12 +221,26 @@ module RuboCop
213
221
  return false unless (body = node.body)
214
222
  return true if body.any_str_type? && body.heredoc?
215
223
 
216
- body.each_descendant(:str).any?(&:heredoc?)
224
+ body.each_descendant(:any_str).any?(&:heredoc?)
217
225
  end
218
226
 
219
227
  def correct_to_endless(corrector, node)
220
- corrector.replace(signature_to_body_range(node), ' = ')
221
- corrector.remove(node.body.source_range.end.join(node.loc.end.end))
228
+ if unparenthesized_arguments?(node)
229
+ corrector.replace(name_to_arguments_range(node), '(')
230
+ corrector.replace(signature_to_body_range(node), ') = ')
231
+ else
232
+ corrector.replace(signature_to_body_range(node), ' = ')
233
+ end
234
+
235
+ corrector.remove(body_to_end_range(node))
236
+ end
237
+
238
+ def unparenthesized_arguments?(node)
239
+ node.arguments.any? && !node.arguments.loc.begin
240
+ end
241
+
242
+ def name_to_arguments_range(node)
243
+ node.loc.name.end.join(node.arguments.source_range.begin)
222
244
  end
223
245
 
224
246
  def signature_to_body_range(node)
@@ -227,6 +249,10 @@ module RuboCop
227
249
  signature_end.join(node.body.source_range.begin)
228
250
  end
229
251
 
252
+ def body_to_end_range(node)
253
+ node.body.source_range.end.join(node.loc.end.end)
254
+ end
255
+
230
256
  def endless_replacement(node)
231
257
  <<~RUBY.strip
232
258
  def #{receiver(node)}#{node.method_name}#{arguments(node)} = #{node.body.source}
@@ -238,11 +264,29 @@ module RuboCop
238
264
  end
239
265
 
240
266
  def arguments(node, missing = '')
241
- node.arguments.any? ? node.arguments.source : missing
267
+ return missing unless node.arguments.any?
268
+ return parenthesized_arguments(node) if unparenthesized_arguments?(node)
269
+
270
+ node.arguments.source
271
+ end
272
+
273
+ def parenthesized_arguments(node)
274
+ padding = space_inside_parens? ? ' ' : ''
275
+
276
+ "(#{padding}#{node.arguments.source}#{padding})"
277
+ end
278
+
279
+ def space_inside_parens?
280
+ return false unless config.cop_enabled?('Layout/SpaceInsideParens')
281
+
282
+ config.for_cop('Layout/SpaceInsideParens')['EnforcedStyle'] != 'no_space'
242
283
  end
243
284
 
244
285
  def can_be_made_endless?(node)
245
- node.body && !node.body.type?(:begin, :kwbegin, :rescue, :ensure)
286
+ return false unless node.body
287
+ return false if comment_in_discarded_range?(node)
288
+
289
+ endless_method_body?(node.body)
246
290
  end
247
291
 
248
292
  def single_line_when_made_endless?(node)
@@ -252,7 +296,24 @@ module RuboCop
252
296
  def too_long_when_made_endless?(node)
253
297
  return false unless config.cop_enabled?('Layout/LineLength')
254
298
 
255
- endless_replacement(node).length + node.loc.column > max_line_length
299
+ line_length(line_when_made_endless(node)) > max_line_length
300
+ end
301
+
302
+ def comment_in_discarded_range?(node)
303
+ discarded_ranges = [signature_to_body_range(node), body_to_end_range(node)]
304
+
305
+ processed_source.each_comment_in_lines(node.first_line..node.last_line).any? do |comment|
306
+ discarded_ranges.any? { |range| range.contains?(comment.source_range) }
307
+ end
308
+ end
309
+
310
+ def line_when_made_endless(node)
311
+ keyword_element = node.loc.keyword
312
+ code_before = keyword_element.source_line[0...keyword_element.column]
313
+ end_element = node.loc.end
314
+ code_after = end_element.source_line[end_element.last_column..]
315
+
316
+ "#{code_before}#{endless_replacement(node)}#{code_after}"
256
317
  end
257
318
  end
258
319
  end
@@ -5,11 +5,15 @@ module RuboCop
5
5
  module Style
6
6
  # Checks for exact regexp match inside `Regexp` literals.
7
7
  #
8
+ # `=~` returns an index and `match` returns `MatchData`, so they are only
9
+ # autocorrected when their value is used as a condition. The same goes for
10
+ # safe navigation calls, which return `nil` for a `nil` receiver.
11
+ #
8
12
  # @example
9
13
  #
10
14
  # # bad
11
15
  # string =~ /\Astring\z/
12
- # string === /\Astring\z/
16
+ # /\Astring\z/ === string
13
17
  # string.match(/\Astring\z/)
14
18
  # string.match?(/\Astring\z/)
15
19
  #
@@ -27,14 +31,15 @@ module RuboCop
27
31
 
28
32
  MSG = 'Use `%<prefer>s`.'
29
33
  RESTRICT_ON_SEND = %i[=~ === !~ match match?].freeze
34
+ BOOLEAN_METHODS = %i[=== !~ match?].to_set.freeze
30
35
 
36
+ # `String#===` is plain equality, so `===` only matches with the regexp as its receiver.
31
37
  # @!method exact_regexp_match(node)
32
38
  def_node_matcher :exact_regexp_match, <<~PATTERN
33
- (call
34
- _ {:=~ :=== :!~ :match :match?}
35
- (regexp
36
- (str $_)
37
- (regopt)))
39
+ {
40
+ (call _ {:=~ :!~ :match :match?} (regexp (str $_) (regopt)))
41
+ (send (regexp (str $_) (regopt)) :=== _)
42
+ }
38
43
  PATTERN
39
44
 
40
45
  def on_send(node)
@@ -44,16 +49,48 @@ module RuboCop
44
49
  return unless exact_match_pattern?(parsed_regexp)
45
50
 
46
51
  string = escape_single_quotes(parsed_regexp[1].text)
47
- prefer = "#{receiver.source} #{new_method(node)} '#{string}'"
52
+ subject = node.method?(:===) ? node.first_argument : receiver
53
+ prefer = "#{subject.source} #{new_method(node)} '#{string}'"
48
54
 
49
55
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
50
- corrector.replace(node, prefer)
56
+ autocorrect(corrector, node, prefer)
51
57
  end
52
58
  end
53
59
  alias on_csend on_send
54
60
 
55
61
  private
56
62
 
63
+ def autocorrect(corrector, node, prefer)
64
+ return unless boolean_result?(node) || truthiness_only?(node)
65
+
66
+ corrector.replace(node, parenthesize?(node) ? "(#{prefer})" : prefer)
67
+ end
68
+
69
+ # `=~` returns an index, `match` returns `MatchData` and `&.` can return `nil`,
70
+ # so replacing them with `==` is only safe where the value is used as a condition.
71
+ def boolean_result?(node)
72
+ node.send_type? && BOOLEAN_METHODS.include?(node.method_name)
73
+ end
74
+
75
+ def truthiness_only?(node)
76
+ return true unless node.value_used?
77
+
78
+ parent = node.parent
79
+ if parent.type?(:begin, :and, :or)
80
+ truthiness_only?(parent)
81
+ elsif parent.type?(:if, :while, :until)
82
+ parent.condition.equal?(node)
83
+ else
84
+ parent.send_type? && parent.method?(:!)
85
+ end
86
+ end
87
+
88
+ def parenthesize?(node)
89
+ return false unless (parent = node.parent)&.call_type?
90
+
91
+ parent.receiver.equal?(node) || (parent.operator_method? && !parent.method?(:[]))
92
+ end
93
+
57
94
  # Escape characters that are special inside a single-quoted string so the
58
95
  # generated literal (e.g. for `/\Afoo'bar\z/`) stays valid Ruby.
59
96
  def escape_single_quotes(text)