rubocop 1.87.0 → 1.88.1

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 (162) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +78 -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 +17 -2
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +5 -3
  7. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  9. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  10. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  11. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  12. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  13. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  14. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  15. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  16. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  17. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  18. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  19. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  20. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  21. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  22. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  23. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  24. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  25. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  26. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  27. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  28. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  29. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  30. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  31. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  32. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  33. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  34. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  35. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  36. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  37. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  38. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  39. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  40. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  41. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  42. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  43. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  44. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  45. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  46. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  47. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  48. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  49. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  50. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  51. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  52. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  53. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  54. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  55. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  56. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  57. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  58. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  59. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  60. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  61. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  62. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  63. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  64. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  65. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  66. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  67. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  68. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  69. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  70. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +35 -9
  71. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  72. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  73. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  74. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  75. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  76. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  77. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  78. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  79. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  80. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  81. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  82. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  83. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  84. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  85. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  86. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  87. data/lib/rubocop/cop/style/alias.rb +1 -1
  88. data/lib/rubocop/cop/style/and_or.rb +1 -1
  89. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  90. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  91. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  92. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  93. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  94. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  95. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  96. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  97. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  98. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  99. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  100. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  101. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  102. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  103. data/lib/rubocop/cop/style/date_time.rb +2 -2
  104. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  105. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  106. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  107. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  108. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  109. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  110. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  111. data/lib/rubocop/cop/style/env_home.rb +4 -0
  112. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  113. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  114. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  115. data/lib/rubocop/cop/style/file_null.rb +4 -2
  116. data/lib/rubocop/cop/style/file_write.rb +17 -14
  117. data/lib/rubocop/cop/style/format_string.rb +13 -1
  118. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  119. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  120. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  121. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  122. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  123. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  124. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  125. data/lib/rubocop/cop/style/lambda.rb +7 -1
  126. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  127. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  128. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  129. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  130. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  131. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  132. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  133. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  134. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  135. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  136. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  137. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  138. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  139. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  140. data/lib/rubocop/cop/style/parallel_assignment.rb +19 -3
  141. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  142. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  143. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  144. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  145. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  146. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  147. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  148. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  149. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  150. data/lib/rubocop/cop/style/semicolon.rb +20 -5
  151. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  152. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  153. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  154. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  155. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  156. data/lib/rubocop/cop/style/word_array.rb +1 -0
  157. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  158. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  159. data/lib/rubocop/runner.rb +5 -3
  160. data/lib/rubocop/server/core.rb +6 -0
  161. data/lib/rubocop/version.rb +1 -1
  162. metadata +3 -3
@@ -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
@@ -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)
@@ -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
@@ -58,7 +58,7 @@ module RuboCop
58
58
  return unless node.method?(:define_method)
59
59
 
60
60
  method_name = node.send_node.first_argument
61
- return if method_name.basic_literal? && allowed?(method_name.value)
61
+ return if method_name&.basic_literal? && allowed?(method_name.value)
62
62
 
63
63
  check_code_length(node)
64
64
  end
@@ -12,9 +12,15 @@ module RuboCop
12
12
  # nodes count. In contrast to the CyclomaticComplexity cop, this cop
13
13
  # considers `else` nodes as adding complexity.
14
14
  #
15
+ # A `case`/`in` branch whose pattern is a simple literal (e.g. `in 1`, `in "red"`, `in 1..10`)
16
+ # or a constant/type (e.g. `in Integer`) and has no guard is just as easy to read as a `when`
17
+ # branch, so it is discounted the same way. Branches with structural patterns (e.g. array,
18
+ # hash, or find patterns), bindings, alternatives, or a guard add the full complexity of
19
+ # a decision point.
20
+ #
15
21
  # @example
16
22
  #
17
- # def my_method # 1
23
+ # def example_1 # 1
18
24
  # if cond # 1
19
25
  # case var # 2 (0.8 + 4 * 0.2, rounded)
20
26
  # when 1 then func_one
@@ -26,33 +32,58 @@ module RuboCop
26
32
  # do_something until a && b # 2
27
33
  # end # ===
28
34
  # end # 7 complexity points
35
+ #
36
+ # def example_2 # 1
37
+ # case color # 1 (3 * 0.2, rounded)
38
+ # in "red" then func_red
39
+ # in "blue" then func_blue
40
+ # in "green" then func_green
41
+ # end # ===
42
+ # end # 2 complexity points
29
43
  class PerceivedComplexity < CyclomaticComplexity
30
44
  MSG = 'Perceived complexity for `%<method>s` is too high. [%<complexity>d/%<max>d]'
31
45
 
32
- COUNTED_NODES = (CyclomaticComplexity::COUNTED_NODES - [:when] + [:case]).freeze
46
+ COUNTED_NODES = (
47
+ CyclomaticComplexity::COUNTED_NODES - %i[when in_pattern] + %i[case case_match]
48
+ ).freeze
33
49
 
34
50
  private
35
51
 
52
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
36
53
  def complexity_score_for(node)
37
54
  case node.type
38
55
  when :case
39
- # If cond is nil, that means each when has an expression that
40
- # evaluates to true or false. It's just an alternative to
41
- # if/elsif/elsif... so the when nodes count.
56
+ # If cond is nil, that means each when has an expression that evaluates to true or
57
+ # false. It's just an alternative to if/elsif/elsif... so the when nodes count.
42
58
  nb_branches = node.when_branches.length + (node.else_branch ? 1 : 0)
43
59
  if node.condition.nil?
44
60
  nb_branches
45
61
  else
46
- # Otherwise, the case node gets 0.8 complexity points and each
47
- # when gets 0.2.
62
+ # Otherwise, the case node gets 0.8 complexity points and each when gets 0.2.
48
63
  ((nb_branches * 0.2) + 0.8).round
49
64
  end
65
+ when :case_match
66
+ # Simple `in` branches are discounted like `when`, while structural patterns keep
67
+ # the full complexity of a decision point.
68
+ score = node.in_pattern_branches.sum { |branch| simple_in_pattern?(branch) ? 0.2 : 1 }
69
+ score += 0.2 if node.else_branch
70
+ score.round
50
71
  when :if
51
72
  node.else? && !node.elsif? ? 2 : 1
52
73
  else
53
74
  super
54
75
  end
55
76
  end
77
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
78
+
79
+ def simple_in_pattern?(in_pattern_node)
80
+ # `in_pattern_node.children[1]` is the guard (`if`/`unless`), or `nil`.
81
+ return false unless in_pattern_node.children[1].nil?
82
+
83
+ # A scalar literal, a literal range, or a constant/type is as easy to read as a `when`.
84
+ pattern = in_pattern_node.pattern
85
+ pattern.literal? || pattern.const_type?
86
+ end
56
87
  end
57
88
  end
58
89
  end
@@ -182,10 +182,18 @@ module RuboCop
182
182
  return ":\"#{value.source}\"" if value.dsym_type?
183
183
  return "\"#{value.source}\"" if value.dstr_type?
184
184
  return ":#{value.source}" if value.sym_type?
185
+ # The element of a `%w` array can contain characters that are special
186
+ # inside a single-quoted string (e.g. a `'`), so escape them rather than
187
+ # wrapping the raw source.
188
+ return to_single_quoted(value.value) if value.str_type?
185
189
 
186
190
  "'#{value.source}'"
187
191
  end
188
192
 
193
+ def to_single_quoted(string)
194
+ "'#{string.gsub(/['\\]/) { |character| "\\#{character}" }}'"
195
+ end
196
+
189
197
  def except_key(node)
190
198
  key_arg = node.argument_list.first.source
191
199
  body, = extract_body_if_negated(node.body)
@@ -94,6 +94,10 @@ module RuboCop
94
94
 
95
95
  return unless captures.use_transformed_argname?
96
96
 
97
+ # A splat transforming expression (e.g. `[k, *v]`) can't be used as a
98
+ # standalone block return value, so the rewrite would produce invalid Ruby.
99
+ return if captures.transforming_body_expr.splat_type?
100
+
97
101
  message = "Prefer `#{new_method_name}` over `#{match_desc}`."
98
102
  add_offense(node, message: message) do |corrector|
99
103
  correction = prepare_correction(node)
@@ -203,9 +203,10 @@ module RuboCop
203
203
 
204
204
  def match_acronym?(expected, name)
205
205
  expected = expected.to_s
206
- name = name.to_s
207
-
208
- allowed_acronyms.any? { |acronym| expected.gsub(acronym.capitalize, acronym) == name }
206
+ name = allowed_acronyms.reduce(name.to_s) do |result, acronym|
207
+ result.gsub(acronym, acronym.capitalize)
208
+ end
209
+ expected == name
209
210
  end
210
211
 
211
212
  def to_namespace(path) # rubocop:disable Metrics/AbcSize
@@ -91,6 +91,8 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def on_new_investigation
94
+ return unless @flagged_terms_regex
95
+
94
96
  investigate_filepath if cop_config['CheckFilepaths']
95
97
  investigate_tokens
96
98
  end
@@ -144,7 +146,7 @@ module RuboCop
144
146
  def preprocess_flagged_terms
145
147
  allowed_strings = []
146
148
  flagged_term_strings = []
147
- cop_config['FlaggedTerms'].each do |term, term_definition|
149
+ (cop_config['FlaggedTerms'] || {}).each do |term, term_definition|
148
150
  next if term_definition.nil?
149
151
 
150
152
  allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))
@@ -181,7 +183,11 @@ module RuboCop
181
183
  end
182
184
 
183
185
  def set_regexes(flagged_term_strings, allowed_strings)
184
- @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
186
+ # With no flagged terms an empty regex would match everything, so leave the
187
+ # regex nil and let `on_new_investigation` treat the cop as a no-op.
188
+ unless flagged_term_strings.empty?
189
+ @flagged_terms_regex = array_to_ignorecase_regex(flagged_term_strings)
190
+ end
185
191
  @allowed_regex = array_to_ignorecase_regex(allowed_strings) unless allowed_strings.empty?
186
192
  end
187
193
 
@@ -174,6 +174,7 @@ module RuboCop
174
174
 
175
175
  method_node, method_name = find_definition(node)
176
176
  return unless method_node
177
+ return unless nameable_method?(method_name)
177
178
 
178
179
  body = method_node.body
179
180
  return unless body == node || body.children.last == node
@@ -209,6 +210,7 @@ module RuboCop
209
210
 
210
211
  method_node, method_name = find_definition(node)
211
212
  return false unless method_node
213
+ return false unless nameable_method?(method_name)
212
214
 
213
215
  defined_memoized?(method_node.body, arg.name) do |defined_ivar, return_ivar, ivar_assign|
214
216
  return false if matches?(method_name, ivar_assign)
@@ -250,6 +252,13 @@ module RuboCop
250
252
  nil
251
253
  end
252
254
 
255
+ # Operator and other non-word method names (e.g. `[]`, `+`, `<=>`) cannot form a
256
+ # valid instance variable name, so there is no matching ivar to enforce and a
257
+ # suggested correction like `@[]` would be invalid Ruby.
258
+ def nameable_method?(method_name)
259
+ /\A[a-zA-Z_]\w*\z/.match?(method_name.to_s.delete('!?='))
260
+ end
261
+
253
262
  def matches?(method_name, ivar_assign)
254
263
  return true if ivar_assign.nil? || INITIALIZE_METHODS.include?(method_name)
255
264
 
@@ -95,11 +95,14 @@ module RuboCop
95
95
 
96
96
  def autocorrect(corrector, node, range, offending_name, preferred_name)
97
97
  corrector.replace(range, preferred_name)
98
- correct_node(corrector, node.body, offending_name, preferred_name)
98
+ # Once the exception variable is reassigned, later references point to a
99
+ # different value, so stop correcting after the reassignment - both in the
100
+ # body and in the code following the `begin`/`rescue`.
101
+ return if correct_node(corrector, node.body, offending_name, preferred_name)
99
102
  return unless (kwbegin_node = node.parent.each_ancestor(:kwbegin).first)
100
103
 
101
104
  kwbegin_node.right_siblings.each do |child_node|
102
- correct_node(corrector, child_node, offending_name, preferred_name)
105
+ break if correct_node(corrector, child_node, offending_name, preferred_name)
103
106
  end
104
107
  end
105
108
 
@@ -113,6 +116,8 @@ module RuboCop
113
116
  end
114
117
  end
115
118
 
119
+ # Returns the reassignment node once the exception variable is reassigned (a truthy
120
+ # signal to stop correcting later references), or `nil` when no reassignment is found.
116
121
  # rubocop:disable Metrics/MethodLength
117
122
  def correct_node(corrector, node, offending_name, preferred_name)
118
123
  return unless node
@@ -131,9 +136,10 @@ module RuboCop
131
136
 
132
137
  if child_node.type?(:masgn, :lvasgn)
133
138
  correct_reassignment(corrector, child_node, offending_name, preferred_name)
134
- break
139
+ return child_node
135
140
  end
136
141
  end
142
+ nil
137
143
  end
138
144
  # rubocop:enable Metrics/MethodLength
139
145