rubocop 1.87.0 → 1.88.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +75 -71
  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 +17 -2
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +2 -2
  7. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  8. data/lib/rubocop/cop/layout/block_alignment.rb +41 -4
  9. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  10. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  11. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  12. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  13. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  14. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  15. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  16. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  17. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  18. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  19. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  20. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  21. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  22. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  23. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  24. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  25. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  26. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  27. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  28. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  29. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  30. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  31. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  32. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  33. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  34. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  35. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  36. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  37. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  38. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  39. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  40. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  41. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  42. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  43. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  44. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  45. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  46. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  47. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  48. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  49. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  50. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  51. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  52. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  53. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  54. data/lib/rubocop/cop/lint/to_enum_arguments.rb +28 -1
  55. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  56. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  57. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +3 -1
  58. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  59. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  60. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  61. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  62. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  63. data/lib/rubocop/cop/style/alias.rb +1 -1
  64. data/lib/rubocop/cop/style/and_or.rb +1 -1
  65. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  66. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  67. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  68. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  69. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  70. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  71. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  72. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  73. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  74. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  75. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  76. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  77. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  78. data/lib/rubocop/cop/style/date_time.rb +2 -2
  79. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  80. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  81. data/lib/rubocop/cop/style/file_write.rb +17 -14
  82. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  83. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  84. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  85. data/lib/rubocop/cop/style/parallel_assignment.rb +8 -1
  86. data/lib/rubocop/cop/style/redundant_format.rb +1 -0
  87. data/lib/rubocop/cop/style/semicolon.rb +16 -1
  88. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  89. data/lib/rubocop/cop/style/word_array.rb +1 -0
  90. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  91. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  92. data/lib/rubocop/server/core.rb +6 -0
  93. data/lib/rubocop/version.rb +1 -1
  94. metadata +3 -3
@@ -38,10 +38,10 @@ module RuboCop
38
38
  GLOB_METHODS = %i[glob []].freeze
39
39
 
40
40
  def on_send(node)
41
- return unless (receiver = node.receiver)
42
- return unless receiver.receiver&.const_type? && receiver.receiver.short_name == :Dir
43
- return unless GLOB_METHODS.include?(receiver.method_name)
44
- return if multiple_argument?(receiver)
41
+ return unless dir_glob?(node.receiver)
42
+ # `sort` with a comparator block or block-pass changes the order, so it is
43
+ # not redundant with the default sorting performed by `Dir.glob`/`Dir[]`.
44
+ return if sort_with_comparator?(node) || multiple_argument?(node.receiver)
45
45
 
46
46
  selector = node.loc.selector
47
47
 
@@ -53,9 +53,20 @@ module RuboCop
53
53
 
54
54
  private
55
55
 
56
+ def dir_glob?(receiver)
57
+ return false unless receiver&.receiver&.const_type?
58
+ return false unless receiver.receiver.short_name == :Dir
59
+
60
+ GLOB_METHODS.include?(receiver.method_name)
61
+ end
62
+
56
63
  def multiple_argument?(glob_method)
57
64
  glob_method.arguments.count >= 2 || glob_method.first_argument&.splat_type?
58
65
  end
66
+
67
+ def sort_with_comparator?(node)
68
+ node.parent&.any_block_type? || node.last_argument&.block_pass_type?
69
+ end
59
70
  end
60
71
  end
61
72
  end
@@ -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
@@ -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
 
@@ -68,7 +68,9 @@ module RuboCop
68
68
 
69
69
  expr.text.scan(/(?<!\\)\]/) do
70
70
  pos = Regexp.last_match.begin(0)
71
- next if pos.zero? # if the unescaped bracket is the first character, Ruby does not warn
71
+ # If the unescaped bracket is the first character of the regexp, Ruby does not warn.
72
+ # `pos` is relative to the sub-expression, so add its start offset (`expr.ts`).
73
+ next if (expr.ts + pos).zero?
72
74
 
73
75
  location = range_at_index(node, expr.ts, pos)
74
76
 
@@ -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)
@@ -107,11 +107,15 @@ module RuboCop
107
107
  end
108
108
 
109
109
  def find_method_definition(node, method_name)
110
- node.each_ancestor.lazy.map do |ancestor|
111
- ancestor.each_child_node(:def, :any_block).find do |child|
110
+ node.each_ancestor do |ancestor|
111
+ found = ancestor.each_child_node(:def, :any_block).find do |child|
112
112
  method_definition(child, method_name)
113
113
  end
114
- end.find(&:itself)
114
+ return found if found
115
+ # A method defined in an outer lexical scope does not define this scope's method,
116
+ # so stop searching once a class/module boundary is crossed without a match.
117
+ return nil if ancestor.type?(:class, :module, :sclass)
118
+ end
115
119
  end
116
120
 
117
121
  # `ruby2_keywords` is only allowed if there's a `restarg` and no keyword arguments
@@ -107,7 +107,10 @@ module RuboCop
107
107
  end
108
108
 
109
109
  def process_multiple_assignment(masgn_node)
110
- masgn_node.assignments.each_with_index do |lhs_node, index|
110
+ # Iterate the top-level destructuring slots so each maps to the right-hand side
111
+ # element at the same position. Using the flattened `assignments` would misalign
112
+ # the index when a slot is itself a nested destructuring (e.g. `(a, b), c = x, y`).
113
+ masgn_node.lhs.children.each_with_index do |lhs_node, index|
111
114
  next unless ASSIGNMENT_TYPES.include?(lhs_node.type)
112
115
 
113
116
  if masgn_node.rhs.array_type? && (rhs_node = masgn_node.rhs.children[index])
@@ -83,7 +83,7 @@ module RuboCop
83
83
 
84
84
  def autocorrect_block(corrector, node)
85
85
  block_arg = block_arg(node)
86
- return if block_reassigns_arg?(node, block_arg)
86
+ return unless reducible_to_body?(node, block_arg)
87
87
 
88
88
  source = node.body.source
89
89
  source.gsub!(/\b#{block_arg}\b/, '0') if block_arg
@@ -91,6 +91,27 @@ module RuboCop
91
91
  corrector.replace(node, fix_indentation(source, node.loc.column...node.body.loc.column))
92
92
  end
93
93
 
94
+ def reducible_to_body?(node, block_arg)
95
+ # A block with multiple arguments can't be reduced to its body (the extra arguments
96
+ # would become undefined references), and `next`/`break`/`redo` bound to the block
97
+ # become orphaned (a syntax error) once the block is removed.
98
+ return false if node.arguments.size > 1 || orphans_loop_control_keyword?(node)
99
+
100
+ # A lone non-simple argument (destructuring `|(a, b)|` or a splat `|*a|`) can't be
101
+ # substituted either, so reducing to the body would leave it referencing an
102
+ # undefined variable.
103
+ return false if node.arguments.one? && block_arg.nil?
104
+
105
+ !block_reassigns_arg?(node, block_arg)
106
+ end
107
+
108
+ def orphans_loop_control_keyword?(node)
109
+ node.body&.each_node(:next, :break, :redo)&.any? do |control|
110
+ inner = control.each_ancestor.take_while { |ancestor| !ancestor.equal?(node) }
111
+ inner.none? { |ancestor| ancestor.type?(:any_block, :while, :until, :for) }
112
+ end
113
+ end
114
+
94
115
  def fix_indentation(source, range)
95
116
  # Cleanup indentation in a multiline block
96
117
  source_lines = source.split("\n")
@@ -75,7 +75,7 @@ module RuboCop
75
75
  private
76
76
 
77
77
  def collection_threshold
78
- cop_config.fetch('LengthThreshold', Float::INFINITY)
78
+ cop_config.fetch('Max', Float::INFINITY)
79
79
  end
80
80
  end
81
81
  end
@@ -113,7 +113,7 @@ module RuboCop
113
113
  return :lexical
114
114
  when :def, :defs
115
115
  return :dynamic
116
- when :block
116
+ when :block, :numblock, :itblock
117
117
  return :instance_eval if parent.method?(:instance_eval)
118
118
 
119
119
  return :dynamic