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
@@ -15,31 +15,24 @@ module RuboCop
15
15
  # would yield false positives. For example, `String` implements `length`
16
16
  # and `size`, but it doesn't include `Enumerable`.
17
17
  #
18
+ # When `count` is used without a block, it measures the collection's
19
+ # size, so the offense is corrected to `empty?` or `!empty?`. The
20
+ # predicate methods `any?`, `none?` and `one?` test element truthiness
21
+ # instead and would return different results for collections with falsey
22
+ # elements (e.g. `[false].count.zero?` is `false`, but `[false].none?`
23
+ # is `true`). For the same reason, `count == 1` and `count > 1` without
24
+ # a block are not flagged.
25
+ #
18
26
  # @safety
19
27
  # The cop is unsafe because receiver might not include `Enumerable`, or
20
28
  # it has nonstandard implementation of `count` or any replacement
21
29
  # methods.
22
30
  #
23
- # It's also unsafe because for collections with falsey values, expressions
24
- # with `count` without a block return a different result than methods `any?`,
25
- # `none?` and `one?`:
26
- #
27
- # [source,ruby]
28
- # ----
29
- # [nil, false].count.positive?
30
- # [nil].count == 1
31
- # # => true
32
- #
33
- # [nil, false].any?
34
- # [nil].one?
35
- # # => false
36
- #
37
- # [nil].count == 0
38
- # # => false
39
- #
40
- # [nil].none?
41
- # # => true
42
- # ----
31
+ # Blockless `count` is corrected to `empty?`, which `Enumerable` doesn't
32
+ # define. `Range`, `Enumerator` (including lazy ones), `Struct`, `StringIO`
33
+ # and any class that just includes `Enumerable` respond to `count` but not
34
+ # to `empty?`, so `(1...1).count.zero?` is `true` while `(1...1).empty?`
35
+ # raises `NoMethodError`.
43
36
  #
44
37
  # Autocorrection is unsafe when replacement methods don't iterate over
45
38
  # every element in collection and the given block runs side effects:
@@ -60,12 +53,14 @@ module RuboCop
60
53
  # x.count > 0
61
54
  # x.count != 0
62
55
  #
56
+ # # good
57
+ # !x.empty?
58
+ #
59
+ # # bad
63
60
  # x.count(&:foo?).positive?
64
61
  # x.count { |item| item.foo? }.positive?
65
62
  #
66
63
  # # good
67
- # x.any?
68
- #
69
64
  # x.any?(&:foo?)
70
65
  # x.any? { |item| item.foo? }
71
66
  #
@@ -74,24 +69,33 @@ module RuboCop
74
69
  # x.count == 0
75
70
  #
76
71
  # # good
77
- # x.none?
72
+ # x.empty?
78
73
  #
79
74
  # # bad
80
- # x.count == 1
81
- # x.one?
75
+ # x.count(&:foo?).zero?
76
+ # x.count(&:foo?) == 0
77
+ #
78
+ # # good
79
+ # x.none?(&:foo?)
80
+ #
81
+ # # bad
82
+ # x.count(&:foo?) == 1
83
+ #
84
+ # # good
85
+ # x.one?(&:foo?)
82
86
  #
83
87
  # @example AllCops:ActiveSupportExtensionsEnabled: false (default)
84
88
  #
85
89
  # # good
86
- # x.count > 1
90
+ # x.count(&:foo?) > 1
87
91
  #
88
92
  # @example AllCops:ActiveSupportExtensionsEnabled: true
89
93
  #
90
94
  # # bad
91
- # x.count > 1
95
+ # x.count(&:foo?) > 1
92
96
  #
93
97
  # # good
94
- # x.many?
98
+ # x.many?(&:foo?)
95
99
  #
96
100
  class CollectionQuerying < Base
97
101
  include RangeHelp
@@ -102,13 +106,21 @@ module RuboCop
102
106
  RESTRICT_ON_SEND = %i[positive? > != zero? ==].freeze
103
107
 
104
108
  REPLACEMENTS = {
105
- [:positive?, nil] => :any?,
106
- [:>, 0] => :any?,
107
- [:!=, 0] => :any?,
108
- [:zero?, nil] => :none?,
109
- [:==, 0] => :none?,
110
- [:==, 1] => :one?,
111
- [:>, 1] => :many?
109
+ [:positive?, nil] => 'any?',
110
+ [:>, 0] => 'any?',
111
+ [:!=, 0] => 'any?',
112
+ [:zero?, nil] => 'none?',
113
+ [:==, 0] => 'none?',
114
+ [:==, 1] => 'one?',
115
+ [:>, 1] => 'many?'
116
+ }.freeze
117
+
118
+ NO_BLOCK_REPLACEMENTS = {
119
+ [:positive?, nil] => '!empty?',
120
+ [:>, 0] => '!empty?',
121
+ [:!=, 0] => '!empty?',
122
+ [:zero?, nil] => 'empty?',
123
+ [:==, 0] => 'empty?'
112
124
  }.freeze
113
125
 
114
126
  # @!method count_predicate(node)
@@ -132,28 +144,49 @@ module RuboCop
132
144
  def on_send(node)
133
145
  return unless (count_node = count_predicate(node))
134
146
 
135
- replacement_method = replacement_method(node)
147
+ replacement = replacement_method(node, count_node)
136
148
 
137
- return unless replacement_supported?(replacement_method)
149
+ return unless replacement_supported?(node, replacement)
138
150
 
139
151
  offense_range = count_node.loc.selector.join(node.source_range.end)
140
- add_offense(offense_range,
141
- message: format(MSG, prefer: replacement_method)) do |corrector|
142
- corrector.replace(count_node.loc.selector, replacement_method)
143
- corrector.remove(removal_range(node))
152
+ add_offense(offense_range, message: format(MSG, prefer: replacement)) do |corrector|
153
+ autocorrect(corrector, node, count_node, replacement)
144
154
  end
145
155
  end
146
156
 
147
157
  private
148
158
 
149
- def replacement_method(node)
150
- REPLACEMENTS.fetch([node.method_name, node.first_argument&.value])
159
+ def replacement_method(node, count_node)
160
+ key = [node.method_name, node.first_argument&.value]
161
+ return REPLACEMENTS.fetch(key) if block_given_to_count?(count_node)
162
+
163
+ NO_BLOCK_REPLACEMENTS[key]
164
+ end
165
+
166
+ def block_given_to_count?(count_node)
167
+ count_node.parent&.any_block_type? || count_node.last_argument&.block_pass_type?
168
+ end
169
+
170
+ def replacement_supported?(node, replacement)
171
+ return false unless replacement
172
+ return false if replacement == 'many?' && !active_support_extensions_enabled?
173
+
174
+ !(replacement.start_with?('!') && receiver_of_call?(node))
151
175
  end
152
176
 
153
- def replacement_supported?(method_name)
154
- return true if active_support_extensions_enabled?
177
+ def receiver_of_call?(node)
178
+ node.parent&.call_type? && node.parent.receiver == node && node.parent.loc.dot
179
+ end
180
+
181
+ def autocorrect(corrector, node, count_node, replacement)
182
+ if replacement.start_with?('!')
183
+ corrector.insert_before(count_node, '!')
184
+ corrector.replace(count_node.loc.selector, 'empty?')
185
+ else
186
+ corrector.replace(count_node.loc.selector, replacement)
187
+ end
155
188
 
156
- method_name != :many?
189
+ corrector.remove(removal_range(node))
157
190
  end
158
191
 
159
192
  def removal_range(node)
@@ -60,19 +60,22 @@ 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
+ def self.autocorrect_incompatible_with
69
+ [Layout::BlockEndNewline, Layout::RedundantLineBreak]
70
+ end
71
+
68
72
  def on_block(node)
69
73
  return unless node.parent&.begin_type?
70
74
  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
75
+ return unless combinable_looping_blocks?(node, node.left_sibling)
73
76
 
74
77
  add_offense(node) do |corrector|
75
- next unless node.arguments == node.left_sibling.arguments
78
+ next unless combinable_blocks?(node, node.left_sibling)
76
79
 
77
80
  combine_with_left_sibling(corrector, node)
78
81
  end
@@ -87,9 +90,7 @@ module RuboCop
87
90
  return unless node.body && node.left_sibling.body
88
91
 
89
92
  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
93
+ next unless combinable_fors?(node, node.left_sibling)
93
94
 
94
95
  combine_with_left_sibling(corrector, node)
95
96
  end
@@ -110,25 +111,102 @@ module RuboCop
110
111
  sibling.send_node.arguments == node.send_node.arguments
111
112
  end
112
113
 
114
+ def combinable_looping_blocks?(node, sibling)
115
+ same_collection_looping_block?(node, sibling) && node.body && sibling.body
116
+ end
117
+
113
118
  def same_collection_looping_for?(node, sibling)
114
119
  sibling&.for_type? && node.collection == sibling.collection
115
120
  end
116
121
 
122
+ def combinable_blocks?(node, sibling)
123
+ return false unless node.arguments == sibling.arguments
124
+ # Numbered and `it` parameters are implicit, so `arguments` is empty for both.
125
+ return false unless node.argument_list == sibling.argument_list
126
+ return false if directive_after_opening?(node)
127
+
128
+ !node.body.type?(:rescue, :ensure) && !sibling.body.type?(:rescue, :ensure)
129
+ end
130
+
131
+ # Combining loops with different iteration variables would leave the second
132
+ # body referencing an undefined variable, so only autocorrect when they match.
133
+ def combinable_fors?(node, sibling)
134
+ node.variable == sibling.variable && !directive_after_opening?(node)
135
+ end
136
+
137
+ # The opening is removed, so a directive after it would end up on a line of
138
+ # its own, where it covers the rest of the file instead of a single line.
139
+ def directive_after_opening?(node)
140
+ opening_line = loop_opening(node).line
141
+ return false if opening_line == node.body.first_line
142
+
143
+ comment = processed_source.comment_at_line(opening_line)
144
+ comment && DirectiveComment.new(comment).start_with_marker?
145
+ end
146
+
147
+ # The block arguments or `{`/`do` of a block, or the `do` or collection of a `for` loop.
148
+ def loop_opening(node)
149
+ if node.for_type?
150
+ node.loc.begin || node.collection.source_range
151
+ else
152
+ (node.block_type? && node.arguments.source_range) || node.loc.begin
153
+ end
154
+ end
155
+
117
156
  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))
157
+ corrector.remove(closing_with_preceding_space(node.left_sibling, node))
158
+ corrector.remove(opening_with_following_space(node))
120
159
 
121
160
  correct_end_of_block(corrector, node)
122
161
  end
123
162
 
163
+ # Comments and heredoc bodies before the closing delimiter are kept. When
164
+ # the next loop starts on the delimiter's line, the line break before the
165
+ # delimiter is kept too, so that a comment doesn't swallow the next loop.
166
+ def closing_with_preceding_space(loop, next_loop)
167
+ separate_lines = !same_line?(loop.loc.end, next_loop)
168
+
169
+ range_with_surrounding_space(
170
+ loop.loc.end, side: :left, newlines: separate_lines, whitespace: separate_lines
171
+ )
172
+ end
173
+
174
+ # Comments between the opening and the body are kept.
175
+ def opening_with_following_space(node)
176
+ opening = node.source_range.begin.join(loop_opening(node))
177
+
178
+ range_with_surrounding_space(opening, side: :right, whitespace: true)
179
+ end
180
+
124
181
  def correct_end_of_block(corrector, node)
125
182
  return unless node.left_sibling.respond_to?(:braces?)
126
- return if node.right_sibling&.any_block_type?
183
+ return if combined_with_right_sibling?(node)
127
184
 
128
- end_of_block = node.left_sibling.braces? ? '}' : ' end'
185
+ end_of_block = first_combined_block(node).braces? ? '}' : ' end'
129
186
  corrector.remove(node.loc.end)
130
187
  corrector.insert_before(node.source_range.end, end_of_block)
131
188
  end
189
+
190
+ def combined_with_right_sibling?(node)
191
+ sibling = node.right_sibling
192
+ return false unless sibling&.any_block_type?
193
+ # A disabled loop isn't corrected, so it doesn't get merged into this one.
194
+ return false unless enabled_lines?(sibling.source_range)
195
+
196
+ same_collection_looping_block?(sibling, node) && sibling.body &&
197
+ combinable_blocks?(sibling, node)
198
+ end
199
+
200
+ def first_combined_block(node)
201
+ first = node.left_sibling
202
+
203
+ while (previous = first.left_sibling) && enabled_lines?(first.source_range) &&
204
+ combinable_looping_blocks?(first, previous) && combinable_blocks?(first, previous)
205
+ first = previous
206
+ end
207
+
208
+ first
209
+ end
132
210
  end
133
211
  end
134
212
  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,52 @@ 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
+ replace_selector(corrector, node)
122
+
123
+ node.arguments.zip(closing_ranges) do |argument, closing_range|
124
+ corrector.remove(argument.loc.begin)
125
+ corrector.remove(closing_range)
126
+ end
127
+ end
128
+
129
+ # Whatever sits between the last element and the `]` of an array that isn't the
130
+ # last argument would end up before the following comma (a newline before it is a
131
+ # syntax error), so it's removed along with the `]`.
132
+ def closing_bracket_range(node, argument)
133
+ return argument.loc.end if argument.equal?(node.last_argument)
134
+
135
+ range_between(argument.children.last.source_range.end_pos, argument.loc.end.end_pos)
136
+ end
137
+
138
+ def replace_selector(corrector, node)
139
+ if node.parenthesized? || node.single_line?
140
+ corrector.replace(node.loc.selector, 'push')
141
+ else
142
+ range = node.loc.selector.join(node.first_argument.source_range.begin)
143
+
144
+ corrector.replace(range, 'push(')
145
+ corrector.insert_after(node.last_argument.loc.end, ')')
146
+ end
147
+ end
97
148
  end
98
149
  end
99
150
  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)