rubocop 1.84.2 → 1.86.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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +86 -14
  3. data/config/obsoletion.yml +5 -0
  4. data/lib/rubocop/cache_config.rb +1 -1
  5. data/lib/rubocop/cli/command/auto_generate_config.rb +1 -1
  6. data/lib/rubocop/cli/command/mcp.rb +19 -0
  7. data/lib/rubocop/cli/command/show_cops.rb +2 -2
  8. data/lib/rubocop/cli/command/show_docs_url.rb +1 -1
  9. data/lib/rubocop/cli.rb +6 -3
  10. data/lib/rubocop/config.rb +1 -1
  11. data/lib/rubocop/config_finder.rb +1 -1
  12. data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
  13. data/lib/rubocop/config_store.rb +1 -1
  14. data/lib/rubocop/config_validator.rb +1 -1
  15. data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
  16. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
  17. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  18. data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
  19. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  20. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -2
  21. data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
  22. data/lib/rubocop/cop/layout/dot_position.rb +1 -1
  23. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +9 -2
  24. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +1 -1
  25. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  26. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
  27. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
  28. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
  29. data/lib/rubocop/cop/layout/end_alignment.rb +3 -2
  30. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +7 -1
  31. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  32. data/lib/rubocop/cop/layout/indentation_width.rb +1 -1
  33. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
  34. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +28 -3
  35. data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
  36. data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
  37. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  38. data/lib/rubocop/cop/layout/space_around_keyword.rb +3 -1
  39. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +1 -0
  40. data/lib/rubocop/cop/lint/constant_reassignment.rb +59 -9
  41. data/lib/rubocop/cop/lint/constant_resolution.rb +1 -1
  42. data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
  43. data/lib/rubocop/cop/lint/duplicate_methods.rb +50 -8
  44. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  45. data/lib/rubocop/cop/lint/empty_conditional_body.rb +6 -1
  46. data/lib/rubocop/cop/lint/empty_in_pattern.rb +8 -1
  47. data/lib/rubocop/cop/lint/empty_when.rb +8 -1
  48. data/lib/rubocop/cop/lint/interpolation_check.rb +7 -2
  49. data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
  50. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +3 -1
  51. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  52. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +0 -9
  53. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +23 -6
  54. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +17 -0
  55. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
  56. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +1 -0
  57. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
  58. data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
  59. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  60. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +4 -4
  61. data/lib/rubocop/cop/lint/useless_default_value_argument.rb +2 -0
  62. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +6 -5
  63. data/lib/rubocop/cop/lint/void.rb +32 -12
  64. data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
  65. data/lib/rubocop/cop/migration/department_name.rb +12 -1
  66. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  67. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +2 -2
  68. data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
  69. data/lib/rubocop/cop/mixin/hash_transform_method.rb +10 -60
  70. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  71. data/lib/rubocop/cop/registry.rb +20 -13
  72. data/lib/rubocop/cop/security/eval.rb +15 -2
  73. data/lib/rubocop/cop/style/accessor_grouping.rb +4 -2
  74. data/lib/rubocop/cop/style/alias.rb +4 -1
  75. data/lib/rubocop/cop/style/and_or.rb +1 -0
  76. data/lib/rubocop/cop/style/arguments_forwarding.rb +25 -7
  77. data/lib/rubocop/cop/style/array_join.rb +4 -2
  78. data/lib/rubocop/cop/style/ascii_comments.rb +6 -3
  79. data/lib/rubocop/cop/style/attr.rb +5 -2
  80. data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
  81. data/lib/rubocop/cop/style/begin_block.rb +3 -1
  82. data/lib/rubocop/cop/style/block_delimiters.rb +25 -33
  83. data/lib/rubocop/cop/style/case_equality.rb +4 -0
  84. data/lib/rubocop/cop/style/class_and_module_children.rb +10 -2
  85. data/lib/rubocop/cop/style/collection_compact.rb +36 -16
  86. data/lib/rubocop/cop/style/colon_method_call.rb +3 -1
  87. data/lib/rubocop/cop/style/concat_array_literals.rb +2 -0
  88. data/lib/rubocop/cop/style/conditional_assignment.rb +0 -4
  89. data/lib/rubocop/cop/style/copyright.rb +1 -1
  90. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  91. data/lib/rubocop/cop/style/each_with_object.rb +2 -0
  92. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  93. data/lib/rubocop/cop/style/empty_class_definition.rb +43 -20
  94. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  95. data/lib/rubocop/cop/style/encoding.rb +7 -1
  96. data/lib/rubocop/cop/style/end_block.rb +3 -1
  97. data/lib/rubocop/cop/style/endless_method.rb +8 -3
  98. data/lib/rubocop/cop/style/file_open.rb +84 -0
  99. data/lib/rubocop/cop/style/for.rb +3 -0
  100. data/lib/rubocop/cop/style/format_string_token.rb +29 -2
  101. data/lib/rubocop/cop/style/global_vars.rb +5 -2
  102. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
  103. data/lib/rubocop/cop/style/hash_lookup_method.rb +7 -0
  104. data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
  105. data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
  106. data/lib/rubocop/cop/style/if_unless_modifier.rb +14 -3
  107. data/lib/rubocop/cop/style/if_with_semicolon.rb +7 -5
  108. data/lib/rubocop/cop/style/inline_comment.rb +4 -1
  109. data/lib/rubocop/cop/style/ip_addresses.rb +1 -2
  110. data/lib/rubocop/cop/style/magic_comment_format.rb +2 -2
  111. data/lib/rubocop/cop/style/map_join.rb +123 -0
  112. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +5 -3
  113. data/lib/rubocop/cop/style/multiline_if_then.rb +3 -1
  114. data/lib/rubocop/cop/style/mutable_constant.rb +1 -1
  115. data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
  116. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  117. data/lib/rubocop/cop/style/non_nil_check.rb +5 -11
  118. data/lib/rubocop/cop/style/not.rb +2 -0
  119. data/lib/rubocop/cop/style/numeric_literals.rb +3 -2
  120. data/lib/rubocop/cop/style/one_class_per_file.rb +95 -0
  121. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
  122. data/lib/rubocop/cop/style/parallel_assignment.rb +4 -0
  123. data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
  124. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  125. data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
  126. data/lib/rubocop/cop/style/proc.rb +3 -2
  127. data/lib/rubocop/cop/style/raise_args.rb +1 -1
  128. data/lib/rubocop/cop/style/reduce_to_hash.rb +184 -0
  129. data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
  130. data/lib/rubocop/cop/style/redundant_each.rb +3 -3
  131. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  132. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
  133. data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
  134. data/lib/rubocop/cop/style/redundant_parentheses.rb +22 -22
  135. data/lib/rubocop/cop/style/redundant_percent_q.rb +4 -1
  136. data/lib/rubocop/cop/style/redundant_return.rb +3 -1
  137. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +0 -5
  138. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +104 -0
  139. data/lib/rubocop/cop/style/safe_navigation.rb +7 -7
  140. data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
  141. data/lib/rubocop/cop/style/select_by_range.rb +197 -0
  142. data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
  143. data/lib/rubocop/cop/style/semicolon.rb +2 -0
  144. data/lib/rubocop/cop/style/single_line_block_params.rb +2 -2
  145. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -1
  146. data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
  147. data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
  148. data/lib/rubocop/cop/style/symbol_proc.rb +4 -3
  149. data/lib/rubocop/cop/style/tally_method.rb +181 -0
  150. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  151. data/lib/rubocop/cop/style/trailing_method_end_statement.rb +1 -0
  152. data/lib/rubocop/cop/style/yoda_expression.rb +1 -1
  153. data/lib/rubocop/cop/variable_force/branch.rb +2 -2
  154. data/lib/rubocop/directive_comment.rb +2 -1
  155. data/lib/rubocop/formatter/disabled_config_formatter.rb +1 -1
  156. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  157. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  158. data/lib/rubocop/formatter/simple_text_formatter.rb +0 -2
  159. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  160. data/lib/rubocop/formatter.rb +22 -21
  161. data/lib/rubocop/lsp/diagnostic.rb +1 -0
  162. data/lib/rubocop/lsp/routes.rb +10 -3
  163. data/lib/rubocop/mcp/server.rb +198 -0
  164. data/lib/rubocop/options.rb +10 -1
  165. data/lib/rubocop/path_util.rb +14 -2
  166. data/lib/rubocop/plugin/loader.rb +1 -1
  167. data/lib/rubocop/result_cache.rb +22 -10
  168. data/lib/rubocop/rspec/shared_contexts.rb +11 -2
  169. data/lib/rubocop/runner.rb +8 -3
  170. data/lib/rubocop/server/cache.rb +5 -7
  171. data/lib/rubocop/server/core.rb +2 -0
  172. data/lib/rubocop/target_finder.rb +1 -1
  173. data/lib/rubocop/target_ruby.rb +18 -12
  174. data/lib/rubocop/version.rb +2 -2
  175. data/lib/rubocop.rb +14 -0
  176. metadata +20 -3
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for redundant `keyword_init` option for `Struct.new`.
7
+ #
8
+ # Since Ruby 3.2, `keyword_init` in `Struct.new` defaults to `nil` behavior.
9
+ # Therefore, this cop detects and autocorrects redundant `keyword_init: nil`
10
+ # and `keyword_init: true` in `Struct.new`.
11
+ #
12
+ # @safety
13
+ # This autocorrect is unsafe because when the value of `keyword_init` changes
14
+ # from `true` to `nil`, the return value of `Struct#keyword_init?` changes.
15
+ #
16
+ # @example
17
+ #
18
+ # # bad
19
+ # Struct.new(:foo, keyword_init: nil)
20
+ # Struct.new(:foo, keyword_init: true)
21
+ #
22
+ # # good
23
+ # Struct.new(:foo)
24
+ #
25
+ class RedundantStructKeywordInit < Base
26
+ extend AutoCorrector
27
+ extend TargetRubyVersion
28
+
29
+ MSG = 'Remove the redundant `keyword_init: %<value>s`.'
30
+ RESTRICT_ON_SEND = %i[new].freeze
31
+
32
+ minimum_target_ruby_version 3.2
33
+
34
+ # @!method struct_new?(node)
35
+ def_node_matcher :struct_new?, <<~PATTERN
36
+ (call (const {nil? cbase} :Struct) :new ...)
37
+ PATTERN
38
+
39
+ # @!method keyword_init?(node)
40
+ def_node_matcher :keyword_init?, <<~PATTERN
41
+ {#redundant_keyword_init? #keyword_init_false?}
42
+ PATTERN
43
+
44
+ # @!method redundant_keyword_init?(node)
45
+ def_node_matcher :redundant_keyword_init?, <<~PATTERN
46
+ (pair (sym :keyword_init) {(true) (nil)})
47
+ PATTERN
48
+
49
+ # @!method keyword_init_false?(node)
50
+ def_node_matcher :keyword_init_false?, <<~PATTERN
51
+ (pair (sym :keyword_init) (false))
52
+ PATTERN
53
+
54
+ def on_send(node)
55
+ return if !struct_new?(node) || node.arguments.none? || !node.last_argument.hash_type?
56
+
57
+ keyword_init_nodes = select_keyword_init_nodes(node)
58
+ return if keyword_init_nodes.any? { |node| keyword_init_false?(node) }
59
+
60
+ redundant_keyword_init_nodes = select_redundant_keyword_init_nodes(keyword_init_nodes)
61
+
62
+ redundant_keyword_init_nodes.each do |redundant_keyword_init|
63
+ register_offense(redundant_keyword_init)
64
+ end
65
+ end
66
+ alias on_csend on_send
67
+
68
+ private
69
+
70
+ def select_keyword_init_nodes(node)
71
+ node.last_argument.pairs.select do |pair|
72
+ keyword_init?(pair)
73
+ end
74
+ end
75
+
76
+ def select_redundant_keyword_init_nodes(keyword_init_nodes)
77
+ keyword_init_nodes.select do |keyword_init_node|
78
+ redundant_keyword_init?(keyword_init_node)
79
+ end
80
+ end
81
+
82
+ def register_offense(keyword_init)
83
+ message = format(MSG, value: keyword_init.value.source)
84
+
85
+ add_offense(keyword_init, message: message) do |corrector|
86
+ range = range(keyword_init)
87
+
88
+ corrector.remove(range)
89
+ end
90
+ end
91
+
92
+ def range(redundant_keyword_init)
93
+ if redundant_keyword_init.parent.left_siblings.last.is_a?(AST::Node)
94
+ beginning_of_range = redundant_keyword_init.parent.left_siblings.last.source_range.end
95
+
96
+ beginning_of_range.join(redundant_keyword_init.source_range.end)
97
+ else
98
+ redundant_keyword_init
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -367,13 +367,13 @@ module RuboCop
367
367
  def unsafe_method_used?(node, method_chain, method)
368
368
  return true if unsafe_method?(node, method)
369
369
 
370
- method.each_ancestor(:send).any? do |ancestor|
371
- break true unless config.cop_enabled?('Lint/SafeNavigationChain')
372
-
373
- break true if unsafe_method?(node, ancestor)
374
- break true if nil_methods.include?(ancestor.method_name)
375
- break false if ancestor == method_chain
370
+ method.each_ancestor(:send) do |ancestor|
371
+ return true unless config.cop_enabled?('Lint/SafeNavigationChain')
372
+ return true if unsafe_method?(node, ancestor)
373
+ return true if nil_methods.include?(ancestor.method_name)
374
+ return false if ancestor == method_chain
376
375
  end
376
+ false
377
377
  end
378
378
 
379
379
  def unsafe_method?(node, send_node)
@@ -409,7 +409,7 @@ module RuboCop
409
409
  start_method,
410
410
  method_chain)
411
411
  start_method.each_ancestor do |ancestor|
412
- break unless %i[send block].include?(ancestor.type)
412
+ break unless ancestor.type?(:call, :any_block)
413
413
  next if !ancestor.send_type? || ancestor.operator_method?
414
414
 
415
415
  corrector.insert_before(ancestor.loc.dot, '&')
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Looks for places where a subset of an Enumerable (array,
7
+ # range, set, etc.; see note below) is calculated based on a class type
8
+ # check, and suggests `grep` or `grep_v` instead.
9
+ #
10
+ # NOTE: Hashes do not behave as you may expect with `grep`, which
11
+ # means that `hash.grep` is not equivalent to `hash.select`. Although
12
+ # RuboCop is limited by static analysis, this cop attempts to avoid
13
+ # registering an offense when the receiver is a hash (hash literal,
14
+ # `Hash.new`, `Hash#[]`, or `to_h`/`to_hash`).
15
+ #
16
+ # @safety
17
+ # Autocorrection is marked as unsafe because the cop cannot guarantee
18
+ # that the receiver is actually an array by static analysis, so the
19
+ # correction may not be actually equivalent.
20
+ #
21
+ # @example
22
+ # # bad (select or find_all)
23
+ # array.select { |x| x.is_a?(Foo) }
24
+ # array.select { |x| x.kind_of?(Foo) }
25
+ #
26
+ # # bad (reject)
27
+ # array.reject { |x| x.is_a?(Foo) }
28
+ #
29
+ # # bad (negative form)
30
+ # array.reject { |x| !x.is_a?(Foo) }
31
+ #
32
+ # # good
33
+ # array.grep(Foo)
34
+ # array.grep_v(Foo)
35
+ class SelectByKind < Base
36
+ extend AutoCorrector
37
+ include RangeHelp
38
+
39
+ MSG = 'Prefer `%<replacement>s` to `%<original_method>s` with a kind check.'
40
+ RESTRICT_ON_SEND = %i[select filter find_all reject].freeze
41
+ SELECT_METHODS = %i[select filter find_all].freeze
42
+ CLASS_CHECK_METHODS = %i[is_a? kind_of?].to_set.freeze
43
+
44
+ # @!method class_check?(node)
45
+ def_node_matcher :class_check?, <<~PATTERN
46
+ {
47
+ (block call (args (arg $_)) ${(send (lvar _) %CLASS_CHECK_METHODS _)})
48
+ (block call (args (arg $_)) ${(send (send (lvar _) %CLASS_CHECK_METHODS _) :!)})
49
+ (numblock call $1 ${(send (lvar _) %CLASS_CHECK_METHODS _)})
50
+ (numblock call $1 ${(send (send (lvar _) %CLASS_CHECK_METHODS _) :!)})
51
+ (itblock call $_ ${(send (lvar _) %CLASS_CHECK_METHODS _)})
52
+ (itblock call $_ ${(send (send (lvar _) %CLASS_CHECK_METHODS _) :!)})
53
+ }
54
+ PATTERN
55
+
56
+ # Returns true if a node appears to return a hash
57
+ # @!method creates_hash?(node)
58
+ def_node_matcher :creates_hash?, <<~PATTERN
59
+ {
60
+ (call (const _ :Hash) {:new :[]} ...)
61
+ (block (call (const _ :Hash) :new ...) ...)
62
+ (call _ { :to_h :to_hash } ...)
63
+ }
64
+ PATTERN
65
+
66
+ # @!method env_const?(node)
67
+ def_node_matcher :env_const?, <<~PATTERN
68
+ (const {nil? cbase} :ENV)
69
+ PATTERN
70
+
71
+ # @!method calls_lvar?(node, name)
72
+ def_node_matcher :calls_lvar?, <<~PATTERN
73
+ (send (lvar %1) %CLASS_CHECK_METHODS _)
74
+ PATTERN
75
+
76
+ # @!method negated_calls_lvar?(node, name)
77
+ def_node_matcher :negated_calls_lvar?, <<~PATTERN
78
+ (send (send (lvar %1) %CLASS_CHECK_METHODS _) :!)
79
+ PATTERN
80
+
81
+ def on_send(node)
82
+ return unless (block_node = node.block_node)
83
+ return if block_node.body&.begin_type?
84
+ return if receiver_allowed?(block_node.receiver)
85
+ return unless (class_check_send_node = extract_send_node(block_node))
86
+
87
+ replacement = replacement(class_check_send_node, node)
88
+ class_constant = find_class_constant(class_check_send_node)
89
+
90
+ register_offense(node, block_node, class_constant, replacement)
91
+ end
92
+ alias on_csend on_send
93
+
94
+ private
95
+
96
+ def receiver_allowed?(node)
97
+ return false unless node
98
+
99
+ node.hash_type? || creates_hash?(node) || env_const?(node)
100
+ end
101
+
102
+ def replacement(class_check_send_node, node)
103
+ negated = negated?(class_check_send_node)
104
+
105
+ method_name = node.method_name
106
+
107
+ if SELECT_METHODS.include?(method_name)
108
+ negated ? 'grep_v' : 'grep'
109
+ else # reject
110
+ negated ? 'grep' : 'grep_v'
111
+ end
112
+ end
113
+
114
+ def register_offense(node, block_node, class_constant, replacement)
115
+ message = format(MSG, replacement: replacement, original_method: node.method_name)
116
+
117
+ add_offense(block_node, message: message) do |corrector|
118
+ if class_constant
119
+ range = range_between(node.loc.selector.begin_pos, block_node.loc.end.end_pos)
120
+ corrector.replace(range, "#{replacement}(#{class_constant.source})")
121
+ end
122
+ end
123
+ end
124
+
125
+ def extract_send_node(block_node)
126
+ return unless (block_arg_name, class_check_send_node = class_check?(block_node))
127
+
128
+ block_arg_name = :"_#{block_arg_name}" if block_node.numblock_type?
129
+ block_arg_name = :it if block_node.type?(:itblock)
130
+
131
+ inner_node = unwrap_negation(class_check_send_node)
132
+
133
+ if calls_lvar?(inner_node, block_arg_name) ||
134
+ negated_calls_lvar?(class_check_send_node, block_arg_name)
135
+ class_check_send_node
136
+ end
137
+ end
138
+
139
+ def negated?(class_check_send_node)
140
+ class_check_send_node.send_type? && class_check_send_node.method?(:!)
141
+ end
142
+
143
+ def unwrap_negation(node)
144
+ if node.send_type? && node.method?(:!)
145
+ node.receiver
146
+ else
147
+ node
148
+ end
149
+ end
150
+
151
+ def find_class_constant(node)
152
+ inner_node = unwrap_negation(node)
153
+ inner_node.first_argument if inner_node.send_type?
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Looks for places where a subset of an Enumerable (array,
7
+ # range, set, etc.; see note below) is calculated based on a range
8
+ # check, and suggests `grep` or `grep_v` instead.
9
+ #
10
+ # NOTE: Hashes do not behave as you may expect with `grep`, which
11
+ # means that `hash.grep` is not equivalent to `hash.select`. Although
12
+ # RuboCop is limited by static analysis, this cop attempts to avoid
13
+ # registering an offense when the receiver is a hash (hash literal,
14
+ # `Hash.new`, `Hash#[]`, or `to_h`/`to_hash`).
15
+ #
16
+ # @safety
17
+ # Autocorrection is marked as unsafe because the cop cannot guarantee
18
+ # that the receiver is actually an array by static analysis, so the
19
+ # correction may not be actually equivalent.
20
+ #
21
+ # @example
22
+ # # bad (select or find_all)
23
+ # array.select { |x| x.between?(1, 10) }
24
+ # array.select { |x| (1..10).cover?(x) }
25
+ # array.select { |x| (1..10).include?(x) }
26
+ #
27
+ # # bad (reject)
28
+ # array.reject { |x| x.between?(1, 10) }
29
+ #
30
+ # # bad (find or detect)
31
+ # array.find { |x| x.between?(1, 10) }
32
+ # array.detect { |x| (1..10).cover?(x) }
33
+ #
34
+ # # bad (negative form)
35
+ # array.reject { |x| !x.between?(1, 10) }
36
+ # array.find { |x| !(1..10).cover?(x) }
37
+ #
38
+ # # good
39
+ # array.grep(1..10)
40
+ # array.grep_v(1..10)
41
+ # array.grep(1..10).first
42
+ # array.grep_v(1..10).first
43
+ class SelectByRange < Base
44
+ extend AutoCorrector
45
+ include RangeHelp
46
+
47
+ MSG = 'Prefer `%<replacement>s` to `%<original_method>s` with a range check.'
48
+ RESTRICT_ON_SEND = %i[select filter find_all reject find detect].freeze
49
+ SELECT_METHODS = %i[select filter find_all].freeze
50
+ FIND_METHODS = %i[find detect].freeze
51
+
52
+ # @!method range_check?(node)
53
+ # Matches: x.between?(min, max) or (min..max).cover?(x) or (min..max).include?(x)
54
+ def_node_matcher :range_check?, <<~PATTERN
55
+ {
56
+ (block call (args (arg $_)) ${(send (lvar _) :between? _ _)})
57
+ (block call (args (arg $_)) ${(send {range (begin range)} {:cover? :include?} (lvar _))})
58
+ (block call (args (arg $_)) ${(send (send (lvar _) :between? _ _) :!)})
59
+ (block call (args (arg $_)) ${(send (send {range (begin range)} {:cover? :include?} (lvar _)) :!)})
60
+ (block call (args (arg $_)) ${(send (begin (send (lvar _) :between? _ _)) :!)})
61
+ (block call (args (arg $_)) ${(send (begin (send {range (begin range)} {:cover? :include?} (lvar _))) :!)})
62
+ (numblock call $1 ${(send (lvar _) :between? _ _)})
63
+ (numblock call $1 ${(send {range (begin range)} {:cover? :include?} (lvar _))})
64
+ (numblock call $1 ${(send (send (lvar _) :between? _ _) :!)})
65
+ (numblock call $1 ${(send (send {range (begin range)} {:cover? :include?} (lvar _)) :!)})
66
+ (numblock call $1 ${(send (begin (send (lvar _) :between? _ _)) :!)})
67
+ (numblock call $1 ${(send (begin (send {range (begin range)} {:cover? :include?} (lvar _))) :!)})
68
+ (itblock call $_ ${(send (lvar _) :between? _ _)})
69
+ (itblock call $_ ${(send {range (begin range)} {:cover? :include?} (lvar _))})
70
+ (itblock call $_ ${(send (send (lvar _) :between? _ _) :!)})
71
+ (itblock call $_ ${(send (send {range (begin range)} {:cover? :include?} (lvar _)) :!)})
72
+ (itblock call $_ ${(send (begin (send (lvar _) :between? _ _)) :!)})
73
+ (itblock call $_ ${(send (begin (send {range (begin range)} {:cover? :include?} (lvar _))) :!)})
74
+ }
75
+ PATTERN
76
+
77
+ # Returns true if a node appears to return a hash
78
+ # @!method creates_hash?(node)
79
+ def_node_matcher :creates_hash?, <<~PATTERN
80
+ {
81
+ (call (const _ :Hash) {:new :[]} ...)
82
+ (block (call (const _ :Hash) :new ...) ...)
83
+ (call _ { :to_h :to_hash } ...)
84
+ }
85
+ PATTERN
86
+
87
+ # @!method env_const?(node)
88
+ def_node_matcher :env_const?, <<~PATTERN
89
+ (const {nil? cbase} :ENV)
90
+ PATTERN
91
+
92
+ # @!method between_call?(node, name)
93
+ def_node_matcher :between_call?, <<~PATTERN
94
+ (send (lvar %1) :between? _ _)
95
+ PATTERN
96
+
97
+ # @!method range_cover_call?(node, name)
98
+ def_node_matcher :range_cover_call?, <<~PATTERN
99
+ (send {range (begin range)} {:cover? :include?} (lvar %1))
100
+ PATTERN
101
+
102
+ def on_send(node)
103
+ return unless (block_node = node.block_node)
104
+ return if block_node.body&.begin_type?
105
+ return if receiver_allowed?(block_node.receiver)
106
+ return unless (range_check_send_node = extract_send_node(block_node))
107
+
108
+ replacement = replacement(range_check_send_node, node)
109
+ range_literal = find_range(range_check_send_node)
110
+
111
+ register_offense(node, block_node, range_literal, replacement)
112
+ end
113
+ alias on_csend on_send
114
+
115
+ private
116
+
117
+ def receiver_allowed?(node)
118
+ return false unless node
119
+
120
+ node.hash_type? || creates_hash?(node) || env_const?(node)
121
+ end
122
+
123
+ def replacement(range_check_send_node, node)
124
+ negated = negated?(range_check_send_node)
125
+ method_name = node.method_name
126
+
127
+ if SELECT_METHODS.include?(method_name)
128
+ negated ? 'grep_v' : 'grep'
129
+ elsif FIND_METHODS.include?(method_name)
130
+ negated ? 'grep_v(...).first' : 'grep(...).first'
131
+ else # reject
132
+ negated ? 'grep' : 'grep_v'
133
+ end
134
+ end
135
+
136
+ def register_offense(node, block_node, range_literal, replacement)
137
+ message = format(MSG, replacement: replacement, original_method: node.method_name)
138
+
139
+ add_offense(block_node, message: message) do |corrector|
140
+ if range_literal
141
+ range = range_between(node.loc.selector.begin_pos, block_node.loc.end.end_pos)
142
+ grep_method = replacement.include?('grep_v') ? 'grep_v' : 'grep'
143
+ suffix = replacement.include?('.first') ? '.first' : ''
144
+ corrector.replace(range, "#{grep_method}(#{range_literal})#{suffix}")
145
+ end
146
+ end
147
+ end
148
+
149
+ def extract_send_node(block_node)
150
+ return unless (block_arg_name, range_check_send_node = range_check?(block_node))
151
+
152
+ block_arg_name = :"_#{block_arg_name}" if block_node.numblock_type?
153
+ block_arg_name = :it if block_node.type?(:itblock)
154
+
155
+ inner_node = unwrap_negation(range_check_send_node)
156
+
157
+ range_check_send_node if calls_lvar_in_range_check?(inner_node, block_arg_name)
158
+ end
159
+
160
+ def calls_lvar_in_range_check?(node, block_arg_name)
161
+ between_call?(node, block_arg_name) || range_cover_call?(node, block_arg_name)
162
+ end
163
+
164
+ def negated?(range_check_send_node)
165
+ range_check_send_node.send_type? && range_check_send_node.method?(:!)
166
+ end
167
+
168
+ def unwrap_negation(node)
169
+ if node.send_type? && node.method?(:!)
170
+ receiver = node.receiver
171
+ receiver = receiver.children.first if receiver.begin_type?
172
+ receiver
173
+ else
174
+ node
175
+ end
176
+ end
177
+
178
+ def find_range(node)
179
+ inner = unwrap_negation(node)
180
+
181
+ if inner.method?(:between?)
182
+ # x.between?(min, max) -> min..max
183
+ min = inner.first_argument.source
184
+ max = inner.arguments[1].source
185
+ "#{min}..#{max}"
186
+ else
187
+ # (min..max).cover?(x) or (min..max).include?(x)
188
+ receiver = inner.receiver
189
+ # Unwrap begin node from parentheses
190
+ receiver = receiver.children.first if receiver.begin_type?
191
+ receiver.source
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -39,6 +39,9 @@ module RuboCop
39
39
  # array.reject { |x| x =~ /regexp/ }
40
40
  # array.reject { |x| /regexp/ =~ x }
41
41
  #
42
+ # # bad (negative form)
43
+ # array.reject { |x| !x.match? /regexp/ }
44
+ #
42
45
  # # good
43
46
  # array.grep(regexp)
44
47
  # array.grep_v(regexp)
@@ -48,18 +51,19 @@ module RuboCop
48
51
 
49
52
  MSG = 'Prefer `%<replacement>s` to `%<original_method>s` with a regexp match.'
50
53
  RESTRICT_ON_SEND = %i[select filter find_all reject].freeze
51
- REPLACEMENTS = { select: 'grep', filter: 'grep', find_all: 'grep', reject: 'grep_v' }.freeze
52
- OPPOSITE_REPLACEMENTS = {
53
- select: 'grep_v', filter: 'grep_v', find_all: 'grep_v', reject: 'grep'
54
- }.freeze
55
- REGEXP_METHODS = %i[match? =~ !~].to_set.freeze
54
+ SELECT_METHODS = %i[select filter find_all].freeze
55
+ REGEXP_METHODS = %i[match? =~].to_set.freeze
56
+ REGEXP_METHODS_NEGATED = %i[!~].to_set.freeze
56
57
 
57
58
  # @!method regexp_match?(node)
58
59
  def_node_matcher :regexp_match?, <<~PATTERN
59
60
  {
60
- (block call (args (arg $_)) ${(send _ %REGEXP_METHODS _) match-with-lvasgn})
61
- (numblock call $1 ${(send _ %REGEXP_METHODS _) match-with-lvasgn})
62
- (itblock call $_ ${(send _ %REGEXP_METHODS _) match-with-lvasgn})
61
+ (block call (args (arg $_)) ${(send _ %REGEXP_METHODS _) (send _ %REGEXP_METHODS_NEGATED _) match-with-lvasgn})
62
+ (block call (args (arg $_)) ${(send (send _ %REGEXP_METHODS _) :!) (send (begin (send _ %REGEXP_METHODS _)) :!) (send match-with-lvasgn :!) (send (begin match-with-lvasgn) :!)})
63
+ (numblock call $1 ${(send _ %REGEXP_METHODS _) (send _ %REGEXP_METHODS_NEGATED _) match-with-lvasgn})
64
+ (numblock call $1 ${(send (send _ %REGEXP_METHODS _) :!) (send (begin (send _ %REGEXP_METHODS _)) :!) (send match-with-lvasgn :!) (send (begin match-with-lvasgn) :!)})
65
+ (itblock call $_ ${(send _ %REGEXP_METHODS _) (send _ %REGEXP_METHODS_NEGATED _) match-with-lvasgn})
66
+ (itblock call $_ ${(send (send _ %REGEXP_METHODS _) :!) (send (begin (send _ %REGEXP_METHODS _)) :!) (send match-with-lvasgn :!) (send (begin match-with-lvasgn) :!)})
63
67
  }
64
68
  PATTERN
65
69
 
@@ -84,6 +88,12 @@ module RuboCop
84
88
  (send (lvar %1) ...)
85
89
  (send ... (lvar %1))
86
90
  (match-with-lvasgn regexp (lvar %1))
91
+ (send (send (lvar %1) ...) :!)
92
+ (send (send ... (lvar %1)) :!)
93
+ (send (match-with-lvasgn regexp (lvar %1)) :!)
94
+ (send (begin (send (lvar %1) ...)) :!)
95
+ (send (begin (send ... (lvar %1))) :!)
96
+ (send (begin (match-with-lvasgn regexp (lvar %1))) :!)
87
97
  }
88
98
  PATTERN
89
99
 
@@ -97,7 +107,7 @@ module RuboCop
97
107
  return if match_predicate_without_receiver?(regexp_method_send_node)
98
108
 
99
109
  replacement = replacement(regexp_method_send_node, node)
100
- return if target_ruby_version <= 2.2 && replacement == 'grep_v'
110
+ return if target_ruby_version <= 2.2 && replacement.include?('grep_v')
101
111
 
102
112
  regexp = find_regexp(regexp_method_send_node, block_node)
103
113
 
@@ -115,11 +125,14 @@ module RuboCop
115
125
  end
116
126
 
117
127
  def replacement(regexp_method_send_node, node)
118
- opposite = opposite?(regexp_method_send_node)
119
-
128
+ negated = negated?(regexp_method_send_node)
120
129
  method_name = node.method_name
121
130
 
122
- opposite ? OPPOSITE_REPLACEMENTS[method_name] : REPLACEMENTS[method_name]
131
+ if SELECT_METHODS.include?(method_name)
132
+ negated ? 'grep_v' : 'grep'
133
+ else # reject
134
+ negated ? 'grep' : 'grep_v'
135
+ end
123
136
  end
124
137
 
125
138
  def register_offense(node, block_node, regexp, replacement)
@@ -138,30 +151,47 @@ module RuboCop
138
151
  return unless (block_arg_name, regexp_method_send_node = regexp_match?(block_node))
139
152
 
140
153
  block_arg_name = :"_#{block_arg_name}" if block_node.numblock_type?
154
+ block_arg_name = :it if block_node.type?(:itblock)
141
155
 
142
156
  return unless calls_lvar?(regexp_method_send_node, block_arg_name)
143
157
 
144
158
  regexp_method_send_node
145
159
  end
146
160
 
147
- def opposite?(regexp_method_send_node)
148
- regexp_method_send_node.send_type? && regexp_method_send_node.method?(:!~)
161
+ def negated?(regexp_method_send_node)
162
+ return true if regexp_method_send_node.send_type? && regexp_method_send_node.method?(:!)
163
+
164
+ inner = unwrap_negation(regexp_method_send_node)
165
+ inner.send_type? && inner.method?(:!~)
166
+ end
167
+
168
+ def unwrap_negation(node)
169
+ if node.send_type? && node.method?(:!)
170
+ receiver = node.receiver
171
+ receiver = receiver.children.first if receiver.begin_type?
172
+ receiver
173
+ else
174
+ node
175
+ end
149
176
  end
150
177
 
151
178
  def find_regexp(node, block)
152
- return node.child_nodes.first if node.match_with_lvasgn_type?
179
+ inner = unwrap_negation(node)
180
+
181
+ return inner.child_nodes.first if inner.match_with_lvasgn_type?
153
182
 
154
- if node.receiver.lvar_type? &&
183
+ if inner.receiver.lvar_type? &&
155
184
  (block.type?(:numblock, :itblock) ||
156
- node.receiver.source == block.first_argument.source)
157
- node.first_argument
158
- elsif node.first_argument.lvar_type?
159
- node.receiver
185
+ inner.receiver.source == block.first_argument.source)
186
+ inner.first_argument
187
+ elsif inner.first_argument&.lvar_type?
188
+ inner.receiver
160
189
  end
161
190
  end
162
191
 
163
192
  def match_predicate_without_receiver?(node)
164
- node.send_type? && node.method?(:match?) && node.receiver.nil?
193
+ inner = unwrap_negation(node)
194
+ inner.send_type? && inner.method?(:match?) && inner.receiver.nil?
165
195
  end
166
196
  end
167
197
  end
@@ -5,6 +5,8 @@ module RuboCop
5
5
  module Style
6
6
  # Checks for multiple expressions placed on the same line.
7
7
  # It also checks for lines terminated with a semicolon.
8
+ # In idiomatic Ruby, each expression should be on its own line
9
+ # for readability.
8
10
  #
9
11
  # This cop has `AllowAsExpressionSeparator` configuration option.
10
12
  # It allows `;` to separate several expressions on the same line.
@@ -33,7 +33,7 @@ module RuboCop
33
33
 
34
34
  MSG = 'Name `%<method>s` block params `|%<params>s|`.'
35
35
 
36
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
36
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
37
37
  return unless node.single_line?
38
38
 
39
39
  return unless eligible_method?(node)
@@ -89,7 +89,7 @@ module RuboCop
89
89
  end
90
90
 
91
91
  def method_names
92
- methods.map { |method| method_name(method).to_sym }
92
+ @method_names ||= methods.map { |method| method_name(method).to_sym }.freeze
93
93
  end
94
94
 
95
95
  def method_name(method)
@@ -38,7 +38,7 @@ module RuboCop
38
38
 
39
39
  # rubocop:disable Metrics/AbcSize
40
40
  def on_block(node)
41
- return if !node.single_line? || node.braces?
41
+ return if node.multiline? || node.braces?
42
42
  return if single_line_blocks_preferred? && suitable_as_single_line?(node)
43
43
 
44
44
  add_offense(node) do |corrector|
@@ -4,7 +4,9 @@ module RuboCop
4
4
  module Cop
5
5
  module Style
6
6
  # Checks for single-line method definitions that contain a body.
7
- # It will accept single-line methods with no body.
7
+ # Single-line methods with a body are harder to read and debug
8
+ # than their multi-line equivalents. It will accept single-line
9
+ # methods with no body.
8
10
  #
9
11
  # Endless methods added in Ruby 3.0 are also accepted by this cop.
10
12
  #