rubocop 1.84.2 → 1.85.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +83 -4
  3. data/config/obsoletion.yml +5 -0
  4. data/lib/rubocop/cli/command/mcp.rb +19 -0
  5. data/lib/rubocop/cli.rb +6 -3
  6. data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
  7. data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
  9. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  10. data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
  11. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  12. data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
  13. data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
  14. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
  15. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
  16. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
  17. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +7 -1
  18. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  19. data/lib/rubocop/cop/layout/indentation_width.rb +1 -1
  20. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
  21. data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
  22. data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
  23. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  24. data/lib/rubocop/cop/layout/space_around_keyword.rb +1 -1
  25. data/lib/rubocop/cop/lint/constant_resolution.rb +1 -1
  26. data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
  27. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  28. data/lib/rubocop/cop/lint/interpolation_check.rb +7 -2
  29. data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
  30. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +3 -1
  31. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +0 -9
  32. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +7 -6
  33. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
  34. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
  35. data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
  36. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  37. data/lib/rubocop/cop/lint/void.rb +32 -12
  38. data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
  39. data/lib/rubocop/cop/migration/department_name.rb +12 -1
  40. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  41. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +1 -1
  42. data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
  43. data/lib/rubocop/cop/mixin/hash_transform_method.rb +10 -60
  44. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  45. data/lib/rubocop/cop/security/eval.rb +15 -2
  46. data/lib/rubocop/cop/style/accessor_grouping.rb +4 -2
  47. data/lib/rubocop/cop/style/alias.rb +4 -1
  48. data/lib/rubocop/cop/style/array_join.rb +4 -2
  49. data/lib/rubocop/cop/style/ascii_comments.rb +5 -2
  50. data/lib/rubocop/cop/style/attr.rb +5 -2
  51. data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
  52. data/lib/rubocop/cop/style/begin_block.rb +3 -1
  53. data/lib/rubocop/cop/style/block_delimiters.rb +2 -2
  54. data/lib/rubocop/cop/style/case_equality.rb +4 -0
  55. data/lib/rubocop/cop/style/class_and_module_children.rb +10 -2
  56. data/lib/rubocop/cop/style/colon_method_call.rb +3 -1
  57. data/lib/rubocop/cop/style/copyright.rb +1 -1
  58. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  59. data/lib/rubocop/cop/style/each_with_object.rb +2 -0
  60. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  61. data/lib/rubocop/cop/style/empty_class_definition.rb +21 -20
  62. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  63. data/lib/rubocop/cop/style/encoding.rb +7 -1
  64. data/lib/rubocop/cop/style/end_block.rb +3 -1
  65. data/lib/rubocop/cop/style/endless_method.rb +8 -3
  66. data/lib/rubocop/cop/style/file_open.rb +63 -0
  67. data/lib/rubocop/cop/style/for.rb +3 -0
  68. data/lib/rubocop/cop/style/format_string_token.rb +29 -2
  69. data/lib/rubocop/cop/style/global_vars.rb +4 -1
  70. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
  71. data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
  72. data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
  73. data/lib/rubocop/cop/style/if_unless_modifier.rb +3 -3
  74. data/lib/rubocop/cop/style/inline_comment.rb +4 -1
  75. data/lib/rubocop/cop/style/map_join.rb +123 -0
  76. data/lib/rubocop/cop/style/multiline_if_then.rb +3 -1
  77. data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
  78. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  79. data/lib/rubocop/cop/style/not.rb +2 -0
  80. data/lib/rubocop/cop/style/numeric_literals.rb +2 -1
  81. data/lib/rubocop/cop/style/one_class_per_file.rb +95 -0
  82. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
  83. data/lib/rubocop/cop/style/parallel_assignment.rb +4 -0
  84. data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
  85. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  86. data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
  87. data/lib/rubocop/cop/style/proc.rb +3 -2
  88. data/lib/rubocop/cop/style/reduce_to_hash.rb +169 -0
  89. data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
  90. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  91. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
  92. data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
  93. data/lib/rubocop/cop/style/redundant_parentheses.rb +6 -3
  94. data/lib/rubocop/cop/style/redundant_return.rb +3 -1
  95. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +104 -0
  96. data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
  97. data/lib/rubocop/cop/style/select_by_range.rb +197 -0
  98. data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
  99. data/lib/rubocop/cop/style/semicolon.rb +2 -0
  100. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  101. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -1
  102. data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
  103. data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
  104. data/lib/rubocop/cop/style/tally_method.rb +181 -0
  105. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  106. data/lib/rubocop/cop/variable_force/branch.rb +2 -2
  107. data/lib/rubocop/directive_comment.rb +2 -1
  108. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  109. data/lib/rubocop/lsp/diagnostic.rb +1 -0
  110. data/lib/rubocop/mcp/server.rb +174 -0
  111. data/lib/rubocop/options.rb +10 -1
  112. data/lib/rubocop/server/cache.rb +5 -7
  113. data/lib/rubocop/target_ruby.rb +18 -12
  114. data/lib/rubocop/version.rb +1 -1
  115. data/lib/rubocop.rb +14 -0
  116. metadata +34 -3
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for consecutive calls to `select`/`filter`/`find_all` and `reject`
7
+ # on the same receiver with the same block body, where `partition` could be
8
+ # used instead. Also detects two `select` or two `reject` calls where one
9
+ # block negates the other with `!`. Using `partition` reduces two collection
10
+ # traversals to one.
11
+ #
12
+ # @safety
13
+ # This cop is unsafe because:
14
+ #
15
+ # * `Hash#select` and `Hash#reject` return hashes, but `Hash#partition`
16
+ # returns nested arrays.
17
+ # * When the receiver has side effects, calling it once (with `partition`)
18
+ # versus twice (with `select` + `reject`) may produce different results.
19
+ # * Custom classes may override `select`/`reject` without providing a
20
+ # compatible `partition` method.
21
+ #
22
+ # @example
23
+ # # bad
24
+ # positives = array.select { |x| x > 0 }
25
+ # negatives = array.reject { |x| x > 0 }
26
+ #
27
+ # # bad
28
+ # positives = array.filter { |x| x > 0 }
29
+ # negatives = array.reject { |x| x > 0 }
30
+ #
31
+ # # bad
32
+ # negatives = array.reject { |x| x > 0 }
33
+ # positives = array.select { |x| x > 0 }
34
+ #
35
+ # # bad
36
+ # positives = array.select(&:positive?)
37
+ # negatives = array.reject(&:positive?)
38
+ #
39
+ # # bad
40
+ # positives = array.select(&:positive?)
41
+ # negatives = array.reject { |x| x.positive? }
42
+ #
43
+ # # bad
44
+ # positives = array.select { |x| x.positive? }
45
+ # non_positives = array.select { |x| !x.positive? }
46
+ #
47
+ # # good
48
+ # positives, negatives = array.partition { |x| x > 0 }
49
+ #
50
+ # # good
51
+ # positives, non_positives = array.partition { |x| x.positive? }
52
+ #
53
+ # # good
54
+ # positives, negatives = array.partition(&:positive?)
55
+ #
56
+ class PartitionInsteadOfDoubleSelect < Base
57
+ include RangeHelp
58
+ extend AutoCorrector
59
+
60
+ MSG = 'Use `partition` instead of consecutive `%<first>s` and `%<second>s` calls.'
61
+
62
+ SELECT_METHODS = %i[select filter find_all].freeze
63
+ CANDIDATE_METHODS = (SELECT_METHODS + %i[reject]).to_set.freeze
64
+ RESTRICT_ON_SEND = (SELECT_METHODS + %i[reject]).freeze
65
+
66
+ # @!method symbol_proc_method?(node)
67
+ def_node_matcher :symbol_proc_method?, <<~PATTERN
68
+ (block _ (args (arg _name)) (send (lvar _name) $_method_name))
69
+ PATTERN
70
+
71
+ def on_block(node)
72
+ return unless CANDIDATE_METHODS.include?(node.method_name)
73
+
74
+ find_and_register_offense(node)
75
+ end
76
+ alias on_numblock on_block
77
+ alias on_itblock on_block
78
+
79
+ def on_send(node)
80
+ return unless node.last_argument&.block_pass_type?
81
+
82
+ find_and_register_offense(node)
83
+ end
84
+ alias on_csend on_send
85
+
86
+ private
87
+
88
+ def find_and_register_offense(node)
89
+ container = node_container(node)
90
+ return unless container
91
+
92
+ sibling_container = container.left_sibling
93
+ sibling = find_matching_candidate(node, sibling_container)
94
+ return unless sibling
95
+
96
+ register_offense(node, sibling, container, sibling_container)
97
+ end
98
+
99
+ def node_container(node)
100
+ parent = node.parent
101
+ if parent&.begin_type?
102
+ node
103
+ elsif parent&.assignment? && parent.parent&.begin_type?
104
+ parent
105
+ end
106
+ end
107
+
108
+ def find_matching_candidate(node, sibling_container)
109
+ return unless sibling_container
110
+
111
+ sibling = extract_candidate(sibling_container)
112
+ return unless sibling
113
+ return unless node.receiver == sibling.receiver
114
+ return unless matching_pair?(node, sibling)
115
+
116
+ sibling
117
+ end
118
+
119
+ def matching_pair?(node, sibling)
120
+ (complementary_pair?(node, sibling) && equivalent_predicate?(node, sibling)) ||
121
+ (node.method?(sibling.method_name) && negated_predicate?(node, sibling))
122
+ end
123
+
124
+ def extract_candidate(container)
125
+ extract_block(container) || extract_block_pass_send(container)
126
+ end
127
+
128
+ def extract_block(container)
129
+ if container.any_block_type?
130
+ container
131
+ elsif container.assignment?
132
+ rhs = container.children.last
133
+ rhs if rhs&.any_block_type?
134
+ end
135
+ end
136
+
137
+ def extract_block_pass_send(container)
138
+ node = container.assignment? ? container.children.last : container
139
+ return unless node&.type?(:call)
140
+ return unless node.last_argument&.block_pass_type?
141
+
142
+ node
143
+ end
144
+
145
+ def complementary_pair?(node1, node2)
146
+ m1 = node1.method_name
147
+ m2 = node2.method_name
148
+ (SELECT_METHODS.include?(m1) && m2 == :reject) ||
149
+ (m1 == :reject && SELECT_METHODS.include?(m2))
150
+ end
151
+
152
+ def equivalent_predicate?(node1, node2)
153
+ if node1.any_block_type? && node2.any_block_type?
154
+ same_block_contents?(node1, node2)
155
+ elsif node1.any_block_type?
156
+ block_matches_block_pass?(node1, node2)
157
+ elsif node2.any_block_type?
158
+ block_matches_block_pass?(node2, node1)
159
+ else
160
+ node1.last_argument == node2.last_argument
161
+ end
162
+ end
163
+
164
+ def same_block_contents?(block1, block2)
165
+ return false unless block1.type == block2.type
166
+
167
+ if block1.block_type?
168
+ block1.arguments == block2.arguments &&
169
+ block1.body == block2.body
170
+ else
171
+ block1.body == block2.body
172
+ end
173
+ end
174
+
175
+ def block_matches_block_pass?(block_node, send_node)
176
+ method_name = symbol_proc_method?(block_node)
177
+ return false unless method_name
178
+
179
+ sym_node = send_node.last_argument.children.first
180
+ sym_node.sym_type? && sym_node.children.first == method_name
181
+ end
182
+
183
+ def negated_predicate?(node1, node2)
184
+ return false unless node1.any_block_type? && node2.any_block_type?
185
+ return false unless node1.type == node2.type
186
+ return false if node1.block_type? && node1.arguments != node2.arguments
187
+
188
+ negated_body?(node1.body, node2.body) || negated_body?(node2.body, node1.body)
189
+ end
190
+
191
+ def negated_body?(body1, body2)
192
+ body1&.send_type? && body1.method?(:!) && body1.receiver == body2
193
+ end
194
+
195
+ def register_offense(node, sibling, container, sibling_container)
196
+ message = format(MSG, first: sibling.method_name, second: node.method_name)
197
+
198
+ add_offense(container, message: message) do |corrector|
199
+ next unless both_lvasgn?(container, sibling_container)
200
+
201
+ autocorrect(corrector, node, sibling, container, sibling_container)
202
+ end
203
+ end
204
+
205
+ def both_lvasgn?(container, sibling_container)
206
+ container.lvasgn_type? && sibling_container.lvasgn_type?
207
+ end
208
+
209
+ def autocorrect(corrector, node, sibling, container, sibling_container)
210
+ if complementary_pair?(node, sibling)
211
+ select_var, reject_var =
212
+ complementary_variable_order(sibling, container, sibling_container)
213
+ partition_node = select_node_for(sibling, container)
214
+ else
215
+ select_var, reject_var, partition_node =
216
+ negation_partition_args(node, sibling, container, sibling_container)
217
+ end
218
+
219
+ partition_call = build_partition_call(partition_node)
220
+ replacement = "#{select_var}, #{reject_var} = #{partition_call}"
221
+
222
+ corrector.replace(sibling_container, replacement)
223
+ range = range_by_whole_lines(container.source_range, include_final_newline: true)
224
+ corrector.remove(range)
225
+ end
226
+
227
+ def complementary_variable_order(sibling, container, sibling_container)
228
+ if SELECT_METHODS.include?(sibling.method_name)
229
+ [sibling_container.children.first, container.children.first]
230
+ else
231
+ [container.children.first, sibling_container.children.first]
232
+ end
233
+ end
234
+
235
+ def negation_partition_args(node, sibling, container, sibling_container)
236
+ node_is_negated = negated_body?(node.body, sibling.body)
237
+ is_select = SELECT_METHODS.include?(node.method_name)
238
+ # For select: non-negated is truthy (first). For reject: negated is truthy (first).
239
+ node_is_truthy = is_select != node_is_negated
240
+ partition_node = node_is_negated ? sibling : node
241
+
242
+ if node_is_truthy
243
+ [container.children.first, sibling_container.children.first, partition_node]
244
+ else
245
+ [sibling_container.children.first, container.children.first, partition_node]
246
+ end
247
+ end
248
+
249
+ def select_node_for(sibling, container)
250
+ if SELECT_METHODS.include?(sibling.method_name)
251
+ sibling
252
+ else
253
+ container.children.last
254
+ end
255
+ end
256
+
257
+ def build_partition_call(node)
258
+ source = node.source
259
+ send_node = node.any_block_type? ? node.send_node : node
260
+ selector = send_node.loc.selector
261
+ offset = node.source_range.begin_pos
262
+ method_start = selector.begin_pos - offset
263
+ method_end = selector.end_pos - offset
264
+
265
+ "#{source[0...method_start]}partition#{source[method_end..]}"
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
@@ -4,6 +4,8 @@ module RuboCop
4
4
  module Cop
5
5
  module Style
6
6
  # Enforces the consistent usage of `%`-literal delimiters.
7
+ # Using consistent delimiters across the codebase reduces
8
+ # cognitive load when reading `%`-literals.
7
9
  #
8
10
  # Specify the 'default' key to set all preferred delimiters at once. You
9
11
  # can continue to specify individual preferred delimiters to override the
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Looks for uses of `any?`, `all?`, `none?`, or `one?` with a block
7
+ # containing only an `is_a?`, `kind_of?`, or `instance_of?` check, and
8
+ # suggests using the predicate method with the class argument directly.
9
+ #
10
+ # @safety
11
+ # This cop is unsafe because `instance_of?` checks for an exact class
12
+ # match, while the pattern argument uses `===` which also matches
13
+ # subclasses. For `is_a?` and `kind_of?`, the behavior is equivalent.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # array.any? { |x| x.is_a?(Integer) }
18
+ # array.all? { |x| x.kind_of?(String) }
19
+ # array.none? { |x| x.is_a?(Float) }
20
+ # array.one? { |x| x.instance_of?(Symbol) }
21
+ #
22
+ # # good
23
+ # array.any?(Integer)
24
+ # array.all?(String)
25
+ # array.none?(Float)
26
+ # array.one?(Symbol)
27
+ class PredicateWithKind < Base
28
+ extend AutoCorrector
29
+ include RangeHelp
30
+
31
+ MSG = 'Prefer `%<replacement>s` to `%<original>s` with a kind check.'
32
+ RESTRICT_ON_SEND = %i[any? all? none? one?].freeze
33
+ KIND_METHODS = %i[is_a? kind_of? instance_of?].to_set.freeze
34
+
35
+ # @!method kind_check?(node)
36
+ def_node_matcher :kind_check?, <<~PATTERN
37
+ {
38
+ (block call (args (arg $_)) $(send (lvar _) %KIND_METHODS _))
39
+ (numblock call $1 $(send (lvar _) %KIND_METHODS _))
40
+ (itblock call $_ $(send (lvar _) %KIND_METHODS _))
41
+ }
42
+ PATTERN
43
+
44
+ # @!method kind_call?(node, name)
45
+ def_node_matcher :kind_call?, <<~PATTERN
46
+ (send (lvar %1) %KIND_METHODS _)
47
+ PATTERN
48
+
49
+ def on_send(node)
50
+ return unless (block_node = node.block_node)
51
+ return if block_node.body&.begin_type?
52
+ return unless (kind_check_node = extract_send_node(block_node))
53
+
54
+ klass = kind_check_node.first_argument
55
+ replacement = "#{node.method_name}(#{klass.source})"
56
+
57
+ register_offense(node, block_node, klass, replacement)
58
+ end
59
+ alias on_csend on_send
60
+
61
+ private
62
+
63
+ def extract_send_node(block_node)
64
+ return unless (block_arg_name, kind_check_node = kind_check?(block_node))
65
+
66
+ block_arg_name = :"_#{block_arg_name}" if block_node.numblock_type?
67
+ block_arg_name = :it if block_node.type?(:itblock)
68
+
69
+ kind_check_node if kind_call?(kind_check_node, block_arg_name)
70
+ end
71
+
72
+ def register_offense(node, block_node, klass, replacement)
73
+ original = "#{node.method_name} { ... }"
74
+ message = format(MSG, replacement: replacement, original: original)
75
+
76
+ add_offense(block_node, message: message) do |corrector|
77
+ range = range_between(node.loc.selector.begin_pos, block_node.loc.end.end_pos)
78
+ corrector.replace(range, "#{node.method_name}(#{klass.source})")
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -3,8 +3,9 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks for uses of Proc.new where Kernel#proc
7
- # would be more appropriate.
6
+ # Checks for uses of `Proc.new` where `Kernel#proc`
7
+ # would be more appropriate. `proc` is the shorter and
8
+ # more idiomatic way to create procs in Ruby.
8
9
  #
9
10
  # @example
10
11
  # # bad
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for `each_with_object`, `inject`, and `reduce` calls that build
7
+ # a hash from an enumerable, where `to_h` with a block could be used instead.
8
+ #
9
+ # This cop complements `Style/HashTransformKeys` and `Style/HashTransformValues`,
10
+ # which handle hash-to-hash transformations with destructured key-value pairs.
11
+ # This cop targets the case where a hash is built from individual elements
12
+ # (non-destructured block parameter).
13
+ #
14
+ # @safety
15
+ # This cop is unsafe because it cannot guarantee that the receiver
16
+ # is an `Enumerable` by static analysis, so the correction may
17
+ # not be actually equivalent. Additionally, `each_with_object` returns
18
+ # the hash object while `to_h` returns a new hash, which could matter
19
+ # if the hash object identity is important.
20
+ #
21
+ # @example
22
+ # # bad
23
+ # array.each_with_object({}) { |elem, hash| hash[elem.id] = elem.name }
24
+ #
25
+ # # bad
26
+ # array.inject({}) { |hash, elem| hash[elem.id] = elem.name; hash }
27
+ #
28
+ # # bad
29
+ # array.reduce({}) { |hash, elem| hash[elem.id] = elem.name; hash }
30
+ #
31
+ # # bad
32
+ # array.each_with_object({}) { |elem, hash| hash[elem] = elem.to_s }
33
+ #
34
+ # # good
35
+ # array.to_h { |elem| [elem.id, elem.name] }
36
+ #
37
+ # # good
38
+ # array.to_h { |elem| [elem, elem.to_s] }
39
+ #
40
+ class ReduceToHash < Base
41
+ extend AutoCorrector
42
+ extend TargetRubyVersion
43
+ include RangeHelp
44
+
45
+ minimum_target_ruby_version 2.6
46
+
47
+ MSG = 'Use `to_h { ... }` instead of `%<method>s`.'
48
+ RESTRICT_ON_SEND = %i[each_with_object inject reduce].freeze
49
+
50
+ # each_with_object({}) { |elem, hash| hash[key] = value }
51
+ # @!method each_with_object_to_hash?(node)
52
+ def_node_matcher :each_with_object_to_hash?, <<~PATTERN
53
+ {
54
+ (block
55
+ (call _ :each_with_object (hash))
56
+ (args (arg _elem) (arg _hash))
57
+ (send (lvar _hash) :[]= $_key $_value))
58
+ (numblock
59
+ (call _ :each_with_object (hash))
60
+ 2
61
+ (send (lvar :_2) :[]= $_key $_value))
62
+ }
63
+ PATTERN
64
+
65
+ # inject/reduce({}) { |hash, elem| hash[key] = value; hash }
66
+ # @!method inject_to_hash?(node)
67
+ def_node_matcher :inject_to_hash?, <<~PATTERN
68
+ {
69
+ (block
70
+ (call _ {:inject :reduce} (hash))
71
+ (args (arg _hash) (arg _elem))
72
+ (begin
73
+ (send (lvar _hash) :[]= $_key $_value)
74
+ (lvar _hash)))
75
+ (numblock
76
+ (call _ {:inject :reduce} (hash))
77
+ 2
78
+ (begin
79
+ (send (lvar :_1) :[]= $_key $_value)
80
+ (lvar :_1)))
81
+ }
82
+ PATTERN
83
+
84
+ def on_send(node)
85
+ block_node = node.block_node
86
+ return unless block_node
87
+
88
+ check_offense(node, block_node)
89
+ end
90
+ alias on_csend on_send
91
+
92
+ private
93
+
94
+ def check_offense(node, block_node)
95
+ key, value = if node.method?(:each_with_object)
96
+ each_with_object_to_hash?(block_node)
97
+ else
98
+ inject_to_hash?(block_node)
99
+ end
100
+ return unless key
101
+
102
+ register_offense(node, block_node, key, value)
103
+ end
104
+
105
+ def register_offense(send_node, block_node, key_expr, value_expr)
106
+ message = format(MSG, method: send_node.method_name)
107
+
108
+ add_offense(send_node.loc.selector, message: message) do |corrector|
109
+ corrector.replace(
110
+ replacement_range(send_node, block_node),
111
+ replacement(block_node, key_expr, value_expr)
112
+ )
113
+ end
114
+ end
115
+
116
+ def replacement(block_node, key_expr, value_expr)
117
+ key_source = adjusted_source(key_expr, block_node)
118
+ value_source = adjusted_source(value_expr, block_node)
119
+ body = "[#{key_source}, #{value_source}]"
120
+
121
+ if block_node.numblock_type?
122
+ block_node.braces? ? "to_h { #{body} }" : do_end_replacement(block_node, body)
123
+ else
124
+ named_block_replacement(block_node, body)
125
+ end
126
+ end
127
+
128
+ def named_block_replacement(block_node, body)
129
+ arg = element_arg_source(block_node)
130
+ if block_node.braces?
131
+ "to_h { |#{arg}| #{body} }"
132
+ else
133
+ do_end_replacement(block_node, body, arg)
134
+ end
135
+ end
136
+
137
+ def do_end_replacement(block_node, body, arg = nil)
138
+ args = arg ? " |#{arg}|" : ''
139
+ "to_h do#{args}\n#{indent(block_node)} #{body}\n#{indent(block_node)}end"
140
+ end
141
+
142
+ def replacement_range(send_node, block_node)
143
+ range_between(send_node.loc.selector.begin_pos, block_node.source_range.end_pos)
144
+ end
145
+
146
+ def element_arg_source(block_node)
147
+ if block_node.method?(:each_with_object)
148
+ block_node.first_argument.source
149
+ else
150
+ block_node.arguments[1].source
151
+ end
152
+ end
153
+
154
+ def adjusted_source(expr_node, block_node)
155
+ source = expr_node.source
156
+ return source unless block_node.numblock_type?
157
+ return source if block_node.method?(:each_with_object)
158
+
159
+ # For inject/reduce numblocks, _2 is the element (becomes _1)
160
+ source.gsub('_2', '_1')
161
+ end
162
+
163
+ def indent(node)
164
+ ' ' * node.source_range.column
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -3,9 +3,9 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks for redundant `begin` blocks.
7
- #
8
- # Currently it checks for code like this:
6
+ # Checks for redundant `begin` blocks. A `begin` block is redundant
7
+ # when the `rescue`/`ensure` can be handled by the enclosing method
8
+ # or block definition directly, avoiding unnecessary indentation.
9
9
  #
10
10
  # @example
11
11
  #
@@ -52,7 +52,7 @@ module RuboCop
52
52
  ${nil? basic_literal? const_type?})
53
53
  PATTERN
54
54
 
55
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
55
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
56
56
  redundant_fetch_block_candidate?(node) do |send, body|
57
57
  return if should_not_check?(send, body)
58
58
 
@@ -15,6 +15,9 @@ module RuboCop
15
15
  # # bad
16
16
  # "#{foo} bar".dup
17
17
  #
18
+ # # bad
19
+ # String.new("#{foo} bar")
20
+ #
18
21
  # # good
19
22
  # "#{foo} bar"
20
23
  #
@@ -25,19 +28,32 @@ module RuboCop
25
28
 
26
29
  MSG = "Don't unfreeze interpolated strings as they are already unfrozen."
27
30
 
28
- RESTRICT_ON_SEND = %i[+@ dup].freeze
29
-
30
31
  minimum_target_ruby_version 3.0
31
32
 
32
- def on_send(node)
33
- return if node.arguments?
34
- return unless (receiver = node.receiver)
35
- return unless receiver.dstr_type?
36
- return if uninterpolated_string?(receiver) || uninterpolated_heredoc?(receiver)
33
+ # @!method redundant_unfreeze?(node)
34
+ def_node_matcher :redundant_unfreeze?, <<~PATTERN
35
+ {
36
+ (send dstr_type? {:+@ :dup})
37
+ (send (const nil? :String) :new dstr_type?)
38
+ }
39
+ PATTERN
40
+
41
+ def on_dstr(node)
42
+ return if uninterpolated_string?(node) || uninterpolated_heredoc?(node)
43
+ return unless redundant_unfreeze?(node.parent)
44
+
45
+ add_offense(offense_range(node.parent)) do |corrector|
46
+ corrector.replace(node.parent, node.source)
47
+ end
48
+ end
49
+
50
+ private
37
51
 
38
- add_offense(node.loc.selector) do |corrector|
39
- corrector.remove(node.loc.selector)
40
- corrector.remove(node.loc.dot) unless node.unary_operation?
52
+ def offense_range(node)
53
+ if node.method?(:new)
54
+ node.source_range.begin.join(node.loc.selector)
55
+ else
56
+ node.loc.selector
41
57
  end
42
58
  end
43
59
  end