rubocop 1.87.0 → 1.88.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 (193) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +80 -72
  3. data/config/obsoletion.yml +21 -1
  4. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -0
  5. data/lib/rubocop/cop/base.rb +30 -10
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +5 -3
  7. data/lib/rubocop/cop/commissioner.rb +13 -11
  8. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  9. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  10. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  11. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  12. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +12 -2
  13. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  14. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  15. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  16. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  17. data/lib/rubocop/cop/layout/comment_indentation.rb +9 -2
  18. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  19. data/lib/rubocop/cop/layout/else_alignment.rb +1 -14
  20. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  21. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  22. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  23. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  24. data/lib/rubocop/cop/layout/line_length.rb +10 -5
  25. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +6 -1
  26. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  27. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  28. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  29. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  30. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  31. data/lib/rubocop/cop/lint/debugger.rb +13 -2
  32. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  33. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  34. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  35. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  36. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  37. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  38. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  39. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  40. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  41. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  42. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  43. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  44. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  45. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  46. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +10 -0
  47. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  48. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  49. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  50. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  51. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  52. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  53. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  54. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  55. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  56. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  57. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  58. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  59. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  60. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  61. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  62. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  63. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  64. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  65. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  66. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  67. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  68. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  69. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  70. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  71. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  72. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  73. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  74. data/lib/rubocop/cop/lint/to_json.rb +8 -1
  75. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  76. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  77. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +35 -9
  78. data/lib/rubocop/cop/lint/unreachable_code.rb +9 -4
  79. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  80. data/lib/rubocop/cop/lint/useless_numeric_operation.rb +10 -10
  81. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  82. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  83. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  84. data/lib/rubocop/cop/lint/void.rb +2 -2
  85. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  86. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  87. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  88. data/lib/rubocop/cop/mixin/allowed_methods.rb +5 -5
  89. data/lib/rubocop/cop/mixin/allowed_pattern.rb +5 -1
  90. data/lib/rubocop/cop/mixin/forbidden_pattern.rb +5 -1
  91. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  92. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  93. data/lib/rubocop/cop/mixin/statement_modifier.rb +1 -1
  94. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  95. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  96. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  97. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  98. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  99. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  100. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  101. data/lib/rubocop/cop/style/alias.rb +1 -1
  102. data/lib/rubocop/cop/style/and_or.rb +1 -1
  103. data/lib/rubocop/cop/style/arguments_forwarding.rb +1 -1
  104. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  105. data/lib/rubocop/cop/style/array_intersect.rb +8 -4
  106. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  107. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  108. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  109. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  110. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  111. data/lib/rubocop/cop/style/collection_compact.rb +1 -1
  112. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  113. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  114. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  115. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  116. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  117. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  118. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  119. data/lib/rubocop/cop/style/date_time.rb +2 -2
  120. data/lib/rubocop/cop/style/def_with_parentheses.rb +6 -2
  121. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  122. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  123. data/lib/rubocop/cop/style/documentation_method.rb +5 -1
  124. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  125. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  126. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  127. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  128. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  129. data/lib/rubocop/cop/style/env_home.rb +4 -0
  130. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  131. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  132. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  133. data/lib/rubocop/cop/style/file_null.rb +4 -2
  134. data/lib/rubocop/cop/style/file_write.rb +17 -14
  135. data/lib/rubocop/cop/style/format_string.rb +13 -1
  136. data/lib/rubocop/cop/style/hash_conversion.rb +14 -6
  137. data/lib/rubocop/cop/style/hash_lookup_method.rb +2 -6
  138. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  139. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  140. data/lib/rubocop/cop/style/identical_conditional_branches.rb +15 -4
  141. data/lib/rubocop/cop/style/if_unless_modifier.rb +5 -5
  142. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  143. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  144. data/lib/rubocop/cop/style/invertible_unless_condition.rb +25 -5
  145. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  146. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  147. data/lib/rubocop/cop/style/lambda.rb +7 -1
  148. data/lib/rubocop/cop/style/lambda_call.rb +11 -0
  149. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  150. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  151. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +11 -5
  152. data/lib/rubocop/cop/style/method_def_parentheses.rb +5 -1
  153. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  154. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +10 -7
  155. data/lib/rubocop/cop/style/mixin_usage.rb +1 -1
  156. data/lib/rubocop/cop/style/module_function.rb +4 -2
  157. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  158. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  159. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  160. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  161. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  162. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  163. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  164. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  165. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  166. data/lib/rubocop/cop/style/parallel_assignment.rb +19 -3
  167. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  168. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  169. data/lib/rubocop/cop/style/redundant_current_directory_in_path.rb +13 -1
  170. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  171. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  172. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  173. data/lib/rubocop/cop/style/redundant_heredoc_delimiter_quotes.rb +6 -1
  174. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  175. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  176. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  177. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  178. data/lib/rubocop/cop/style/semicolon.rb +20 -5
  179. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  180. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  181. data/lib/rubocop/cop/style/struct_inheritance.rb +3 -1
  182. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  183. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  184. data/lib/rubocop/cop/style/trivial_accessors.rb +1 -1
  185. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  186. data/lib/rubocop/cop/style/word_array.rb +1 -0
  187. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  188. data/lib/rubocop/cop/team.rb +18 -4
  189. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  190. data/lib/rubocop/runner.rb +55 -13
  191. data/lib/rubocop/server/core.rb +6 -0
  192. data/lib/rubocop/version.rb +1 -1
  193. metadata +3 -3
@@ -183,7 +183,7 @@ module RuboCop
183
183
  def_node_matcher :conversion_with_default?, <<~PATTERN
184
184
  {
185
185
  (or $(csend _ :to_h) (hash))
186
- (or (block $(csend _ :to_h) ...) (hash))
186
+ (or (any_block $(csend _ :to_h) ...) (hash))
187
187
  (or $(csend _ :to_a) (array))
188
188
  (or $(csend _ :to_i) (int 0))
189
189
  (or $(csend _ :to_f) (float 0.0))
@@ -191,7 +191,6 @@ module RuboCop
191
191
  }
192
192
  PATTERN
193
193
 
194
- # rubocop:disable Metrics/AbcSize
195
194
  def on_csend(node)
196
195
  range = node.loc.dot
197
196
 
@@ -204,14 +203,10 @@ module RuboCop
204
203
  end
205
204
  end
206
205
 
207
- unless assume_receiver_instance_exists?(node.receiver)
208
- return if !guaranteed_instance?(node.receiver) && !check?(node)
209
- return if respond_to_nil_method?(node)
210
- end
206
+ return if guarded_by_nil_receiver?(node)
211
207
 
212
208
  add_offense(range) { |corrector| corrector.replace(range, '.') }
213
209
  end
214
- # rubocop:enable Metrics/AbcSize
215
210
 
216
211
  # rubocop:disable Metrics/AbcSize
217
212
  def on_or(node)
@@ -230,6 +225,18 @@ module RuboCop
230
225
 
231
226
  private
232
227
 
228
+ # Returns true when the `&.` is meaningful because the receiver may actually be nil.
229
+ def guarded_by_nil_receiver?(node)
230
+ return false if assume_receiver_instance_exists?(node.receiver)
231
+
232
+ guaranteed_instance = guaranteed_instance?(node.receiver)
233
+ return true if !guaranteed_instance && !check?(node)
234
+
235
+ # `nil.respond_to?(<nil method>)` is `true`, so `&.` is meaningful when the receiver
236
+ # may be nil. A guaranteed instance can never be nil, so `&.` is still redundant there.
237
+ respond_to_nil_method?(node) && !guaranteed_instance
238
+ end
239
+
233
240
  def assume_receiver_instance_exists?(receiver)
234
241
  return true if receiver.const_type? && !receiver.short_name.match?(SNAKE_CASE)
235
242
 
@@ -122,6 +122,10 @@ module RuboCop
122
122
 
123
123
  grandparent = node.parent.parent
124
124
  return if grandparent && !ASSIGNMENT_TYPES.include?(grandparent.type)
125
+ # An empty array/percent literal (`*[]`, `*%w()`, ...) expands to nothing, so
126
+ # removing the splat would produce invalid or semantically different code.
127
+ elsif expanded_item.array_type? && expanded_item.children.empty?
128
+ return
125
129
  end
126
130
 
127
131
  yield
@@ -18,6 +18,7 @@ module RuboCop
18
18
  # * `to_sym` when called on a symbol literal or interpolated symbol.
19
19
  # * `to_i` when called on an integer literal or with `Integer()`.
20
20
  # * `to_f` when called on a float literal or with `Float()`.
21
+ # * `to_d` when called with `BigDecimal()`.
21
22
  # * `to_r` when called on a rational literal or with `Rational()`.
22
23
  # * `to_c` when called on a complex literal or with `Complex()`.
23
24
  # * `to_a` when called on an array literal, or with `Array.new`, `Array()` or `Array[]`.
@@ -63,6 +64,12 @@ module RuboCop
63
64
  # # in this case, `Integer()` could return `nil`
64
65
  # Integer(var, exception: false).to_i
65
66
  #
67
+ # # bad
68
+ # BigDecimal(var).to_d
69
+ #
70
+ # # good
71
+ # BigDecimal(var)
72
+ #
66
73
  # # bad - chaining the same conversion
67
74
  # foo.to_s.to_s
68
75
  #
@@ -62,7 +62,7 @@ module RuboCop
62
62
  {
63
63
  (block
64
64
  $(call _ {:each_with_index :with_index} ...)
65
- (args (arg _)) ...)
65
+ {(args (arg _)) (args)} ...)
66
66
  (numblock
67
67
  $(call _ {:each_with_index :with_index} ...) 1 ...)
68
68
  (itblock
@@ -5,6 +5,11 @@ module RuboCop
5
5
  module Lint
6
6
  # Checks for redundant `with_object`.
7
7
  #
8
+ # @safety
9
+ # This cop's autocorrection is unsafe because the return value changes:
10
+ # `each_with_object` returns the memo object, while the corrected `each` returns
11
+ # the receiver. This matters when the result of the expression is used.
12
+ #
8
13
  # @example
9
14
  # # bad
10
15
  # ary.each_with_object([]) do |v|
@@ -32,9 +32,12 @@ module RuboCop
32
32
  # end
33
33
  #
34
34
  class RefinementImportMethods < Base
35
+ extend AutoCorrector
35
36
  extend TargetRubyVersion
36
37
 
37
38
  MSG = 'Use `import_methods` instead of `%<current>s` because it is deprecated in Ruby 3.1.'
39
+ MSG_REMOVED = 'Use `import_methods` instead of `%<current>s` ' \
40
+ 'because it was removed in Ruby 3.2.'
38
41
  RESTRICT_ON_SEND = %i[include prepend].freeze
39
42
 
40
43
  minimum_target_ruby_version 3.1
@@ -44,7 +47,11 @@ module RuboCop
44
47
  return unless (parent = node.parent)
45
48
  return unless parent.block_type? && parent.method?(:refine)
46
49
 
47
- add_offense(node.loc.selector, message: format(MSG, current: node.method_name))
50
+ template = target_ruby_version >= 3.2 ? MSG_REMOVED : MSG
51
+ message = format(template, current: node.method_name)
52
+ add_offense(node.loc.selector, message: message) do |corrector|
53
+ corrector.replace(node.loc.selector, 'import_methods')
54
+ end
48
55
  end
49
56
  end
50
57
  end
@@ -26,7 +26,15 @@ module RuboCop
26
26
  return if node.ancestors.none?(&:conditional?)
27
27
  return if part_of_ignored_node?(node)
28
28
 
29
- add_offense(node) { |corrector| corrector.replace(node, "#{node.source} =~ $_") }
29
+ add_offense(node) do |corrector|
30
+ # `!` binds tighter than `=~`, so `!/foo/ =~ $_` would parse as
31
+ # `(!/foo/) =~ $_`. Wrap the match in parentheses to preserve the meaning.
32
+ if node.parent&.send_type? && node.parent.method?(:!)
33
+ corrector.replace(node.parent, "!(#{node.source} =~ $_)")
34
+ else
35
+ corrector.replace(node, "#{node.source} =~ $_")
36
+ end
37
+ end
30
38
 
31
39
  ignore_node(node)
32
40
  end
@@ -3,10 +3,13 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Lint
6
- # Checks for expressions where there is a call to a predicate
7
- # method with at least one argument, where no parentheses are used around
8
- # the parameter list, and a boolean operator, && or ||, is used in the
9
- # last argument.
6
+ # Checks for method calls with at least one argument where no parentheses
7
+ # are used around the parameter list, and the call could be misread as an
8
+ # operand of a boolean operator (`&&` or `||`). Two forms are flagged:
9
+ #
10
+ # * a predicate method whose last argument is a `&&`/`||` expression, and
11
+ # * any method whose first argument is a ternary expression with a
12
+ # `&&`/`||` condition.
10
13
  #
11
14
  # The idea behind warning for these constructs is that the user might
12
15
  # be under the impression that the return value from the method call is
@@ -23,6 +26,12 @@ module RuboCop
23
26
  # if day.is?(:tuesday) && month == :jan
24
27
  # # ...
25
28
  # end
29
+ #
30
+ # # bad
31
+ # foo a && b ? c : d
32
+ #
33
+ # # good
34
+ # foo(a && b ? c : d)
26
35
  class RequireParentheses < Base
27
36
  include RangeHelp
28
37
 
@@ -38,7 +38,8 @@ module RuboCop
38
38
  # 42)
39
39
  #
40
40
  class RequireRangeParentheses < Base
41
- MSG = 'Wrap the endless range literal `%<range>s` to avoid precedence ambiguity.'
41
+ MSG = 'Wrap the range literal `%<range>s` in parentheses ' \
42
+ 'to avoid confusion with an endless range.'
42
43
 
43
44
  def on_irange(node)
44
45
  return if node.parent&.begin_type?
@@ -40,11 +40,11 @@ module RuboCop
40
40
  def same_file?(file_path, required_feature)
41
41
  return false unless File.extname(file_path) == '.rb'
42
42
 
43
- file_path == required_feature || remove_ext(file_path) == required_feature
44
- end
45
-
46
- def remove_ext(file_path)
47
- File.basename(file_path, File.extname(file_path))
43
+ # `require_relative` is resolved relative to the current file's directory, so a
44
+ # bare `foo`/`foo.rb` (no path separator) requires the current file itself. Compare
45
+ # against the basename so this works whether `file_path` is relative or absolute.
46
+ basename = File.basename(file_path, '.rb')
47
+ required_feature == basename || required_feature == "#{basename}.rb"
48
48
  end
49
49
  end
50
50
  end
@@ -39,7 +39,7 @@ module RuboCop
39
39
 
40
40
  MSG = 'Rescuing from `%<invalid_exceptions>s` will raise a ' \
41
41
  '`TypeError` instead of catching the actual exception.'
42
- INVALID_TYPES = %i[array dstr float hash nil int str sym].freeze
42
+ INVALID_TYPES = %i[array complex dstr false float hash nil int rational str sym true].freeze
43
43
 
44
44
  def on_resbody(node)
45
45
  invalid_exceptions = invalid_exceptions(node.exceptions)
@@ -34,6 +34,7 @@ module RuboCop
34
34
  {
35
35
  (send $(csend ...) $_ ...)
36
36
  (send $(any_block (csend ...) ...) $_ ...)
37
+ (send $(begin (csend ...)) $_ ...)
37
38
  }
38
39
  PATTERN
39
40
 
@@ -26,7 +26,7 @@ module RuboCop
26
26
 
27
27
  # @!method safe_navigation_empty_in_conditional?(node)
28
28
  def_node_matcher :safe_navigation_empty_in_conditional?, <<~PATTERN
29
- (if (csend (send ...) :empty?) ...)
29
+ (if (csend !csend :empty?) ...)
30
30
  PATTERN
31
31
 
32
32
  def on_if(node)
@@ -46,7 +46,7 @@ module RuboCop
46
46
  message = format_message_from(processed_source)
47
47
 
48
48
  add_offense(comment, message: message) do
49
- autocorrect if autocorrect_requested?
49
+ autocorrect if autocorrect?
50
50
  end
51
51
  end
52
52
 
@@ -57,6 +57,10 @@ module RuboCop
57
57
  end
58
58
 
59
59
  def executable?(processed_source)
60
+ # Virtual sources (LSP buffers, programmatic `ProcessedSource`) have no file on
61
+ # disk to stat or `chmod`, so treat them as executable to skip the offense.
62
+ return true unless File.exist?(processed_source.file_path)
63
+
60
64
  # Returns true if stat is executable or if the operating system
61
65
  # doesn't distinguish executable files from nonexecutable files.
62
66
  # See at: https://github.com/ruby/ruby/blob/ruby_2_4/file.c#L5362
@@ -90,12 +90,35 @@ module RuboCop
90
90
  def on_or_asgn(node)
91
91
  return if allow_rbs_inline_annotation? && rbs_inline_annotation?(node.lhs)
92
92
 
93
- add_offense(node) if rhs_matches_lhs?(node.rhs, node.lhs)
93
+ add_offense(node) if or_and_asgn_self_assignment?(node.lhs, node.rhs)
94
94
  end
95
95
  alias on_and_asgn on_or_asgn
96
96
 
97
97
  private
98
98
 
99
+ def or_and_asgn_self_assignment?(lhs, rhs)
100
+ case lhs.type
101
+ when :casgn
102
+ rhs.const_type? && lhs.namespace == rhs.namespace && lhs.short_name == rhs.short_name
103
+ when :send, :csend
104
+ reader_self_assignment?(lhs, rhs)
105
+ else
106
+ rhs_matches_lhs?(rhs, lhs)
107
+ end
108
+ end
109
+
110
+ # Compares two reader calls (attribute `foo.bar` or key `hash['foo']`).
111
+ def reader_self_assignment?(lhs, rhs)
112
+ return false unless rhs.type == lhs.type
113
+ return false unless lhs.method?(rhs.method_name)
114
+ return false unless lhs.receiver == rhs.receiver
115
+ return false unless lhs.arguments == rhs.arguments
116
+
117
+ # `hash[foo] ||= hash[foo]` is intentionally allowed because a method-call key may
118
+ # return different results on each call.
119
+ lhs.arguments.none?(&:call_type?)
120
+ end
121
+
99
122
  def multiple_self_assignment?(node)
100
123
  lhs = node.lhs
101
124
  rhs = node.rhs
@@ -45,7 +45,7 @@ module RuboCop
45
45
  # @!method send_with_mixin_argument?(node)
46
46
  def_node_matcher :send_with_mixin_argument?, <<~PATTERN
47
47
  (send
48
- (const _ _) {:#{SEND_METHODS.join(' :')}}
48
+ {nil? self (const _ _)} {:#{SEND_METHODS.join(' :')}}
49
49
  ({sym str} $#mixin_method?)
50
50
  $(const _ _)+)
51
51
  PATTERN
@@ -75,6 +75,8 @@ module RuboCop
75
75
  end
76
76
 
77
77
  def same_conditions_node_different_branch?(variable, outer_local_variable)
78
+ return true if different_case_in_branch?(variable, outer_local_variable)
79
+
78
80
  variable_node = variable_node(variable)
79
81
  return false unless node_or_its_ascendant_conditional?(variable_node)
80
82
 
@@ -88,6 +90,18 @@ module RuboCop
88
90
  variable_node == outer_local_variable_node.else_branch
89
91
  end
90
92
 
93
+ # `case ... in` binds variables in the pattern itself, so a block argument in one
94
+ # `in` branch does not shadow a pattern variable from a different `in` branch of the
95
+ # same `case` (the branches are mutually exclusive).
96
+ def different_case_in_branch?(variable, outer_local_variable)
97
+ inner_branch = variable.scope.node.each_ancestor(:in_pattern).first
98
+ outer_branch = outer_local_variable.declaration_node.each_ancestor(:in_pattern).first
99
+
100
+ return false unless inner_branch && outer_branch
101
+
102
+ inner_branch != outer_branch && inner_branch.parent == outer_branch.parent
103
+ end
104
+
91
105
  def variable_node(variable)
92
106
  parent_node = variable.scope.node.parent
93
107
 
@@ -56,7 +56,9 @@ module RuboCop
56
56
  {array hash (send (const {nil? cbase} {:Array :Hash}) :new)}
57
57
  !#capacity_keyword_argument?
58
58
  ])
59
- (send (const {nil? cbase} :Hash) :new hash #capacity_keyword_argument?)
59
+ (send (const {nil? cbase} :Hash) :new
60
+ {array hash (send (const {nil? cbase} {:Array :Hash}) :new)}
61
+ #capacity_keyword_argument?)
60
62
  }
61
63
  PATTERN
62
64
 
@@ -88,6 +88,10 @@ module RuboCop
88
88
  return unless (method = numeric_constructor_rescue_nil(node))
89
89
  end
90
90
 
91
+ # `Integer(arg, exception: false)` already suppresses the conversion error, so there
92
+ # is nothing unintentionally swallowed; adding another `exception: false` is wrong.
93
+ return if exception_keyword_argument?(method)
94
+
91
95
  arguments = method.arguments.map(&:source) << 'exception: false'
92
96
  prefer = "#{method.method_name}(#{arguments.join(', ')})"
93
97
  prefer = "#{method.receiver.source}#{method.loc.dot.source}#{prefer}" if method.receiver
@@ -100,6 +104,14 @@ module RuboCop
100
104
 
101
105
  private
102
106
 
107
+ def exception_keyword_argument?(method)
108
+ method.arguments.any? do |argument|
109
+ argument.hash_type? && argument.pairs.any? do |pair|
110
+ pair.key.sym_type? && pair.key.value == :exception
111
+ end
112
+ end
113
+ end
114
+
103
115
  def expected_exception_classes_only?(exception_classes)
104
116
  return true unless (arguments = exception_classes.first)
105
117
 
@@ -77,11 +77,28 @@ module RuboCop
77
77
 
78
78
  def on_send(node)
79
79
  return unless node.receiver
80
+ return unless (correction = symbol_conversion_correction(node.receiver))
80
81
 
81
- if node.receiver.type?(:str, :sym)
82
- register_offense(node, correction: node.receiver.value.to_sym.inspect)
83
- elsif node.receiver.dstr_type?
84
- register_offense(node, correction: ":\"#{node.receiver.value.to_sym}\"")
82
+ register_offense(node, correction: correction)
83
+ end
84
+
85
+ def symbol_conversion_correction(receiver)
86
+ if receiver.type?(:str, :sym)
87
+ receiver.value.to_sym.inspect
88
+ elsif receiver.dstr_type? && !receiver.heredoc?
89
+ dstr_correction(receiver)
90
+ end
91
+ end
92
+
93
+ # Reuse the already-escaped inner source for a plain `"..."` string so embedded
94
+ # quotes stay escaped. Percent literals (`%{}`, `%Q{}`, ...) and adjacent string
95
+ # concatenation have multi-character or no delimiters, so slicing the source would
96
+ # corrupt them; fall back to the node's value there.
97
+ def dstr_correction(receiver)
98
+ if receiver.loc.begin&.source == '"'
99
+ ":\"#{receiver.source[1..-2]}\""
100
+ else
101
+ ":\"#{receiver.value.to_sym}\""
85
102
  end
86
103
  end
87
104
 
@@ -61,7 +61,7 @@ module RuboCop
61
61
  def arguments_match?(arguments, def_node)
62
62
  index = 0
63
63
 
64
- def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
64
+ all_present = def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
65
65
  send_arg = arguments[index]
66
66
  case def_arg.type
67
67
  when :arg, :restarg, :optarg
@@ -70,6 +70,33 @@ module RuboCop
70
70
 
71
71
  send_arg && argument_match?(send_arg, def_arg)
72
72
  end
73
+
74
+ all_present && !extra_positional_arguments?(arguments, def_node)
75
+ end
76
+
77
+ # The enumerator re-invokes the method with these arguments, so passing more
78
+ # positional arguments than the method accepts raises `ArgumentError` at runtime.
79
+ def extra_positional_arguments?(arguments, def_node)
80
+ return false if variadic_parameters?(def_node) || expandable_arguments?(arguments)
81
+
82
+ positional_arguments(arguments).size > positional_parameters(def_node).size
83
+ end
84
+
85
+ def variadic_parameters?(def_node)
86
+ def_node.arguments.any? { |arg| arg.type?(:restarg, :forward_arg) }
87
+ end
88
+
89
+ # A splat or argument forwarding on the call side can expand to any arity.
90
+ def expandable_arguments?(arguments)
91
+ arguments.any? { |arg| arg.type?(:splat, :forwarded_args, :forwarded_restarg) }
92
+ end
93
+
94
+ def positional_arguments(arguments)
95
+ arguments.reject { |arg| arg.type?(:hash, :kwsplat, :block_pass, :forwarded_kwrestarg) }
96
+ end
97
+
98
+ def positional_parameters(def_node)
99
+ def_node.arguments.select { |arg| arg.type?(:arg, :optarg) }
73
100
  end
74
101
 
75
102
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
@@ -82,9 +109,11 @@ module RuboCop
82
109
  when :optarg
83
110
  send_arg.source == def_arg_name.to_s
84
111
  when :kwoptarg, :kwarg
85
- send_arg.hash_type? &&
112
+ keyword_hash_argument?(send_arg) &&
86
113
  send_arg.pairs.any? { |pair| passing_keyword_arg?(pair, def_arg_name) }
87
114
  when :kwrestarg
115
+ return false unless keyword_hash_argument?(send_arg)
116
+
88
117
  send_arg.each_child_node(:kwsplat, :forwarded_kwrestarg).any? do |child|
89
118
  child.source == def_arg.source
90
119
  end
@@ -93,6 +122,10 @@ module RuboCop
93
122
  end
94
123
  end
95
124
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
125
+
126
+ def keyword_hash_argument?(send_arg)
127
+ send_arg.hash_type? && !send_arg.braces?
128
+ end
96
129
  end
97
130
  end
98
131
  end
@@ -36,9 +36,16 @@ module RuboCop
36
36
  # The following used `*_args` because `to_json(*args)` has
37
37
  # an offense of `Lint/UnusedMethodArgument` cop if `*args`
38
38
  # is not used.
39
- corrector.insert_after(node.loc.name, '(*_args)')
39
+ if (opening_parenthesis = node.each_child_node(:args).first.loc.begin)
40
+ # Explicit empty parentheses (`def to_json()`) are already present,
41
+ # so insert the argument inside them to avoid producing `to_json(*_args)()`.
42
+ corrector.insert_after(opening_parenthesis, '*_args')
43
+ else
44
+ corrector.insert_after(node.loc.name, '(*_args)')
45
+ end
40
46
  end
41
47
  end
48
+ alias on_defs on_def
42
49
  end
43
50
  end
44
51
  end
@@ -40,7 +40,7 @@ module RuboCop
40
40
  # top-level return node's ancestors should not be of block, def, or
41
41
  # defs type.
42
42
  def top_level_return?(return_node)
43
- return_node.each_ancestor(:block, :any_def).none?
43
+ return_node.each_ancestor(:any_block, :any_def).none?
44
44
  end
45
45
  end
46
46
  end
@@ -35,7 +35,10 @@ module RuboCop
35
35
  RESTRICT_ON_SEND = %i[attr_reader attr_writer attr_accessor attr].freeze
36
36
 
37
37
  def on_send(node)
38
- return unless node.attribute_accessor? && node.last_argument.def_type?
38
+ return unless node.attribute_accessor? && node.last_argument.any_def_type?
39
+ # A lone `def` argument (e.g. `attr_reader def foo; end`) has no preceding
40
+ # attribute, so there is no trailing comma to flag.
41
+ return unless node.arguments.size > 1
39
42
 
40
43
  trailing_comma = trailing_comma_range(node)
41
44
 
@@ -44,9 +44,7 @@ module RuboCop
44
44
 
45
45
  def on_regexp(node)
46
46
  RuboCop::Util.silence_warnings do
47
- node.parsed_tree&.each_expression do |expr|
48
- detect_offenses(node, expr)
49
- end
47
+ detect_offenses_in_tree(node, node.parsed_tree)
50
48
  end
51
49
  end
52
50
 
@@ -55,20 +53,46 @@ module RuboCop
55
53
  return if node.each_descendant(:dstr).any?
56
54
 
57
55
  regexp_constructor(node) do |text|
58
- parse_regexp(text.value)&.each_expression do |expr|
59
- detect_offenses(text, expr)
60
- end
56
+ detect_offenses_in_tree(text, parse_regexp(text.value))
61
57
  end
62
58
  end
63
59
 
64
60
  private
65
61
 
66
- def detect_offenses(node, expr)
67
- return unless expr.type?(:literal)
62
+ # When a character class opens with a bare `]` (e.g. `[^]]`), `regexp_parser` parses
63
+ # `[^]` / `[]` as an empty set and reports the closing `]` as a separate literal.
64
+ # Ruby treats that `]` as the end of the class, not as an unescaped bracket,
65
+ # so the first `]` following an empty set must be skipped.
66
+ def detect_offenses_in_tree(node, tree)
67
+ return unless tree
68
+
69
+ skip_class_closer = false
70
+ tree.each_expression do |expr|
71
+ if empty_character_set?(expr)
72
+ skip_class_closer = true
73
+ elsif expr.type?(:literal)
74
+ skip_class_closer = detect_offenses(node, expr, skip_class_closer)
75
+ end
76
+ end
77
+ end
78
+
79
+ def empty_character_set?(expr)
80
+ expr.type?(:set) && expr.expressions.empty?
81
+ end
68
82
 
83
+ def detect_offenses(node, expr, skip_class_closer)
69
84
  expr.text.scan(/(?<!\\)\]/) do
70
85
  pos = Regexp.last_match.begin(0)
71
- next if pos.zero? # if the unescaped bracket is the first character, Ruby does not warn
86
+
87
+ # The first `]` following an empty `[^]` / `[]` set closes the character class.
88
+ if skip_class_closer
89
+ skip_class_closer = false
90
+ next
91
+ end
92
+
93
+ # If the unescaped bracket is the first character of the regexp, Ruby does not warn.
94
+ # `pos` is relative to the sub-expression, so add its start offset (`expr.ts`).
95
+ next if (expr.ts + pos).zero?
72
96
 
73
97
  location = range_at_index(node, expr.ts, pos)
74
98
 
@@ -76,6 +100,8 @@ module RuboCop
76
100
  corrector.replace(location, '\]')
77
101
  end
78
102
  end
103
+
104
+ skip_class_closer
79
105
  end
80
106
 
81
107
  def range_at_index(node, index, offset)
@@ -52,10 +52,15 @@ module RuboCop
52
52
  def on_begin(node)
53
53
  expressions = *node
54
54
 
55
- expressions.each_cons(2) do |expression1, expression2|
56
- next unless flow_expression?(expression1)
57
-
58
- add_offense(expression2)
55
+ # Once a flow-of-control statement is reached, every following statement
56
+ # in the block is unreachable, not just the one immediately after it.
57
+ flow_reached = false
58
+ expressions.each_with_index do |expression, index|
59
+ if flow_reached
60
+ add_offense(expression)
61
+ elsif index < expressions.size - 1 && flow_expression?(expression)
62
+ flow_reached = true
63
+ end
59
64
  end
60
65
  end
61
66
 
@@ -67,11 +67,7 @@ module RuboCop
67
67
  range = offense_range(assignment)
68
68
 
69
69
  add_offense(range, message: message) do |corrector|
70
- # In cases like `x = 1, y = 2`, where removing a variable would cause a syntax error,
71
- # and where changing `x ||= 1` to `x = 1` would cause `NameError`,
72
- # the autocorrect will be skipped, even if the variable is unused.
73
- next if sequential_assignment?(assignment_node) ||
74
- assignment_node.parent&.or_asgn_type?
70
+ next if uncorrectable_assignment?(assignment_node)
75
71
 
76
72
  autocorrect(corrector, assignment)
77
73
  end
@@ -79,6 +75,15 @@ module RuboCop
79
75
  ignore_node(assignment_node) if chained_assignment?(assignment_node)
80
76
  end
81
77
 
78
+ # Autocorrect is skipped when removing a variable would cause a syntax error
79
+ # (`x = 1, y = 2`), or where rewriting `x ||= 1`/`x &&= 1` to `x = 1` would raise
80
+ # `NameError` because the variable is not declared before the operator assignment.
81
+ def uncorrectable_assignment?(assignment_node)
82
+ sequential_assignment?(assignment_node) ||
83
+ assignment_node.parent&.or_asgn_type? ||
84
+ assignment_node.parent&.and_asgn_type?
85
+ end
86
+
82
87
  def ignored_assignment?(variable, assignment_node, assignment)
83
88
  assignment.used? || part_of_ignored_node?(assignment_node) ||
84
89
  variable_in_loop_condition?(assignment_node, variable)