rubocop 1.86.1 → 1.86.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +8 -1
  3. data/lib/rubocop/cli/command/auto_generate_config.rb +27 -1
  4. data/lib/rubocop/cli/command/list_enabled_cops_for.rb +40 -0
  5. data/lib/rubocop/cli/command/show_docs_url.rb +3 -7
  6. data/lib/rubocop/cli/command/suggest_extensions.rb +1 -1
  7. data/lib/rubocop/cli.rb +4 -7
  8. data/lib/rubocop/comment_config.rb +12 -15
  9. data/lib/rubocop/cop/autocorrect_logic.rb +2 -1
  10. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +1 -5
  11. data/lib/rubocop/cop/correctors.rb +28 -0
  12. data/lib/rubocop/cop/exclude_limit.rb +31 -5
  13. data/lib/rubocop/cop/gemspec/require_mfa.rb +3 -3
  14. data/lib/rubocop/cop/internal_affairs/location_line_equality_comparison.rb +1 -0
  15. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +1 -1
  16. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +26 -1
  17. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +3 -13
  18. data/lib/rubocop/cop/lint/require_relative_self_path.rb +2 -0
  19. data/lib/rubocop/cop/lint/useless_assignment.rb +3 -8
  20. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +18 -7
  21. data/lib/rubocop/cop/mixin/configurable_max.rb +6 -5
  22. data/lib/rubocop/cop/mixin.rb +85 -0
  23. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +1 -1
  24. data/lib/rubocop/cop/offense.rb +8 -0
  25. data/lib/rubocop/cop/registry.rb +19 -24
  26. data/lib/rubocop/cop/style/copyright.rb +21 -10
  27. data/lib/rubocop/cop/style/date_time.rb +2 -2
  28. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +6 -1
  29. data/lib/rubocop/cop/style/hash_lookup_method.rb +12 -7
  30. data/lib/rubocop/cop/style/if_inside_else.rb +15 -2
  31. data/lib/rubocop/cop/style/module_member_existence_check.rb +6 -3
  32. data/lib/rubocop/cop/style/reduce_to_hash.rb +16 -0
  33. data/lib/rubocop/cop/style/redundant_self.rb +2 -2
  34. data/lib/rubocop/cop/style/regexp_literal.rb +29 -0
  35. data/lib/rubocop/cop/style/sole_nested_conditional.rb +4 -2
  36. data/lib/rubocop/cop/style/symbol_proc.rb +3 -3
  37. data/lib/rubocop/cop/style/while_until_modifier.rb +16 -0
  38. data/lib/rubocop/cop/team.rb +86 -35
  39. data/lib/rubocop/formatter/disabled_config_formatter.rb +4 -1
  40. data/lib/rubocop/lsp/runtime.rb +1 -2
  41. data/lib/rubocop/options.rb +8 -4
  42. data/lib/rubocop/rspec/shared_contexts.rb +21 -0
  43. data/lib/rubocop/runner.rb +77 -55
  44. data/lib/rubocop/target_finder.rb +13 -6
  45. data/lib/rubocop/version.rb +1 -1
  46. data/lib/rubocop.rb +7 -96
  47. metadata +5 -2
@@ -109,7 +109,7 @@ module RuboCop
109
109
  # @_foo = calculate_expensive_thing
110
110
  # end
111
111
  #
112
- # @example EnforcedStyleForLeadingUnderscores :optional
112
+ # @example EnforcedStyleForLeadingUnderscores: optional
113
113
  # # bad
114
114
  # def foo
115
115
  # @something ||= calculate_expensive_thing
@@ -98,6 +98,14 @@ module RuboCop
98
98
  freeze
99
99
  end
100
100
 
101
+ def marshal_dump
102
+ [@severity, @location, @message, @cop_name, @status]
103
+ end
104
+
105
+ def marshal_load(array)
106
+ @severity, @location, @message, @cop_name, @status = array
107
+ end
108
+
101
109
  # @api public
102
110
  #
103
111
  # @!attribute [r] correctable?
@@ -49,9 +49,8 @@ module RuboCop
49
49
  attr_reader :options, :warnings
50
50
 
51
51
  def initialize(cops = [], options = {})
52
- @registry = {}
53
- @departments = {}
54
- @cops_by_cop_name = Hash.new { |hash, key| hash[key] = [] }
52
+ @departments = Set.new
53
+ @cops_by_badge = {}
55
54
 
56
55
  @enrollment_queue = cops
57
56
  @options = options
@@ -72,22 +71,17 @@ module RuboCop
72
71
  # @return [Array<Symbol>] list of departments for current cops.
73
72
  def departments
74
73
  clear_enrollment_queue
75
- @departments.keys
74
+ @departments.to_a
76
75
  end
77
76
 
78
77
  # @return [Registry] Cops for that specific department.
79
78
  def with_department(department)
80
- clear_enrollment_queue
81
- with(@departments.fetch(department, []))
79
+ with(cops.select { |cop| cop.department == department })
82
80
  end
83
81
 
84
82
  # @return [Registry] Cops not for a specific department.
85
83
  def without_department(department)
86
- clear_enrollment_queue
87
- without_department = @departments.dup
88
- without_department.delete(department)
89
-
90
- with(without_department.values.flatten)
84
+ with(cops.reject { |cop| cop.department == department })
91
85
  end
92
86
 
93
87
  # @return [Boolean] Checks if given name is department
@@ -160,31 +154,31 @@ module RuboCop
160
154
  def unqualified_cop_names
161
155
  clear_enrollment_queue
162
156
  @unqualified_cop_names ||=
163
- Set.new(@cops_by_cop_name.keys.map { |qn| File.basename(qn) }) <<
157
+ Set.new(@cops_by_badge.keys.map { |badge| File.basename(badge.to_s) }) <<
164
158
  'RedundantCopDisableDirective'
165
159
  end
166
160
 
167
161
  def qualify_badge(badge)
168
162
  clear_enrollment_queue
169
163
  @departments
170
- .map { |department, _| badge.with_department(department) }
164
+ .map { |department| badge.with_department(department) }
171
165
  .select { |potential_badge| registered?(potential_badge) }
172
166
  end
173
167
 
174
168
  # @return [Hash{String => Array<Class>}]
175
169
  def to_h
176
170
  clear_enrollment_queue
177
- @cops_by_cop_name
171
+ @cops_by_badge.to_h { |_badge, cop| [cop.cop_name, [cop]] }
178
172
  end
179
173
 
180
174
  def cops
181
175
  clear_enrollment_queue
182
- @registry.values
176
+ @cops_by_badge.values
183
177
  end
184
178
 
185
179
  def length
186
180
  clear_enrollment_queue
187
- @registry.size
181
+ @cops_by_badge.size
188
182
  end
189
183
 
190
184
  def enabled(config)
@@ -219,7 +213,8 @@ module RuboCop
219
213
  end
220
214
 
221
215
  def names
222
- cops.map(&:cop_name)
216
+ clear_enrollment_queue
217
+ @cops_by_badge.keys.map(&:to_s)
223
218
  end
224
219
 
225
220
  def cops_for_department(department)
@@ -236,7 +231,7 @@ module RuboCop
236
231
 
237
232
  def sort!
238
233
  clear_enrollment_queue
239
- @registry = @registry.sort_by { |badge, _| badge.cop_name }.to_h
234
+ @cops_by_badge = @cops_by_badge.sort_by { |badge, _cop| badge.cop_name }.to_h
240
235
 
241
236
  self
242
237
  end
@@ -252,7 +247,9 @@ module RuboCop
252
247
  # @param [String] cop_name
253
248
  # @return [Class, nil]
254
249
  def find_by_cop_name(cop_name)
255
- to_h[cop_name].first
250
+ clear_enrollment_queue
251
+ badge = Badge.parse(cop_name)
252
+ @cops_by_badge[badge]
256
253
  end
257
254
 
258
255
  # When a cop name is given returns a single-element array with the cop class.
@@ -289,10 +286,8 @@ module RuboCop
289
286
  return if @enrollment_queue.empty?
290
287
 
291
288
  @enrollment_queue.each do |cop|
292
- @registry[cop.badge] = cop
293
- @departments[cop.department] ||= []
294
- @departments[cop.department] << cop
295
- @cops_by_cop_name[cop.cop_name] << cop
289
+ @cops_by_badge[cop.badge] = cop
290
+ @departments << cop.department
296
291
  end
297
292
  @enrollment_queue = []
298
293
  end
@@ -318,7 +313,7 @@ module RuboCop
318
313
 
319
314
  def registered?(badge)
320
315
  clear_enrollment_queue
321
- @registry.key?(badge)
316
+ @cops_by_badge.key?(badge)
322
317
  end
323
318
  end
324
319
  end
@@ -46,15 +46,16 @@ module RuboCop
46
46
  token = insert_notice_before(processed_source)
47
47
  range = token.nil? ? range_between(0, 0) : token.pos
48
48
 
49
- corrector.insert_before(range, "#{autocorrect_notice}\n")
49
+ corrector.insert_before(range, "#{normalized_autocorrect_notice}\n")
50
50
  end
51
51
 
52
- def notice
53
- cop_config['Notice']
54
- end
52
+ def normalized_autocorrect_notice
53
+ autocorrect_notice.lines.map do |line|
54
+ next line if line.start_with?('#')
55
+ next "#\n" if line.chomp.empty?
55
56
 
56
- def autocorrect_notice
57
- cop_config['AutocorrectNotice']
57
+ "# #{line}"
58
+ end.join
58
59
  end
59
60
 
60
61
  def verify_autocorrect_notice!
@@ -62,8 +63,7 @@ module RuboCop
62
63
  raise Warning, "#{cop_name}: #{AUTOCORRECT_EMPTY_WARNING}"
63
64
  end
64
65
 
65
- regex = Regexp.new(notice)
66
- return if autocorrect_notice.gsub(/^# */, '').match?(regex)
66
+ return if normalized_autocorrect_notice.gsub(/^# */, '').match?(notice_regexp)
67
67
 
68
68
  message = "AutocorrectNotice '#{autocorrect_notice}' must match Notice /#{notice}/"
69
69
  raise Warning, "#{cop_name}: #{message}"
@@ -91,18 +91,29 @@ module RuboCop
91
91
  end
92
92
 
93
93
  def notice_found?(processed_source)
94
- notice_regexp = Regexp.new(notice.lines.map(&:strip).join)
95
94
  multiline_notice = +''
96
95
  processed_source.tokens.each do |token|
97
96
  break unless token.comment?
98
97
 
99
- multiline_notice << token.text.sub(/\A# */, '')
98
+ multiline_notice << token.text.sub(/\A# */, '') << "\n"
100
99
 
101
100
  break if notice_regexp.match?(token.text)
102
101
  end
103
102
 
104
103
  multiline_notice.match?(notice_regexp)
105
104
  end
105
+
106
+ def notice_regexp
107
+ @notice_regexp ||= Regexp.new(notice.sub(/\A(?:\\A|\^)?#(?:\\s[*+?]?|\s)*/, ''))
108
+ end
109
+
110
+ def notice
111
+ cop_config['Notice']
112
+ end
113
+
114
+ def autocorrect_notice
115
+ cop_config['AutocorrectNotice']
116
+ end
106
117
  end
107
118
  end
108
119
  end
@@ -3,8 +3,8 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks for consistent usage of the `DateTime` class over the
7
- # `Time` class. This cop is disabled by default since these classes,
6
+ # Checks for consistent usage of the `Time` class over the
7
+ # `DateTime` class. This cop is disabled by default since these classes,
8
8
  # although highly overlapping, have particularities that make them not
9
9
  # replaceable in certain situations when dealing with multiple timezones
10
10
  # and/or DST.
@@ -161,7 +161,12 @@ module RuboCop
161
161
  source = source.gsub(COMMENT_REGEXP, '')
162
162
  return if source.blank?
163
163
 
164
- /\s*#{Regexp.escape(source.strip)}/
164
+ # Treat `\#` (an escaped interpolation marker in the heredoc) as matching
165
+ # either `\#` or `#` in the comment, since the comment may show either
166
+ # the literal source form or the runtime appearance.
167
+ segments = source.strip.split('\\#', -1).map { |segment| Regexp.escape(segment) }
168
+
169
+ /\s*#{segments.join('\\\\?#')}/
165
170
  end
166
171
  end
167
172
  end
@@ -82,18 +82,23 @@ module RuboCop
82
82
  end
83
83
 
84
84
  def correct_fetch_to_brackets(corrector, node)
85
- receiver = node.receiver.source
86
85
  key = node.first_argument.source
87
- replacement = "#{receiver}[#{key}]"
88
- replacement = "(#{replacement})" if node.csend_type?
89
- corrector.replace(node, replacement)
86
+
87
+ if node.csend_type?
88
+ corrector.replace(node, "(#{node.receiver.source}[#{key}])")
89
+ else
90
+ corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
91
+ end
90
92
  end
91
93
 
92
94
  def correct_brackets_to_fetch(corrector, node)
93
- receiver = node.receiver.source
94
95
  key = node.first_argument.source
95
- operator = node.csend_type? ? '&.' : '.'
96
- corrector.replace(node, "#{receiver}#{operator}fetch(#{key})")
96
+
97
+ if node.csend_type?
98
+ corrector.replace(node.loc.dot.join(node.source_range.end), "&.fetch(#{key})")
99
+ else
100
+ corrector.replace(node.loc.selector.join(node.source_range.end), ".fetch(#{key})")
101
+ end
97
102
  end
98
103
  end
99
104
  end
@@ -64,7 +64,7 @@ module RuboCop
64
64
 
65
65
  MSG = 'Convert `if` nested inside `else` to `elsif`.'
66
66
 
67
- # rubocop:disable Metrics/CyclomaticComplexity
67
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
68
  def on_if(node)
69
69
  return if node.ternary? || node.unless?
70
70
 
@@ -72,6 +72,7 @@ module RuboCop
72
72
 
73
73
  return unless else_branch&.if_type? && else_branch.if?
74
74
  return if allow_if_modifier_in_else_branch?(else_branch)
75
+ return if comments_between_else_and_if?(node, else_branch)
75
76
 
76
77
  add_offense(else_branch.loc.keyword) do |corrector|
77
78
  next if part_of_ignored_node?(node)
@@ -80,7 +81,7 @@ module RuboCop
80
81
  ignore_node(node)
81
82
  end
82
83
  end
83
- # rubocop:enable Metrics/CyclomaticComplexity
84
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
84
85
 
85
86
  private
86
87
 
@@ -135,6 +136,18 @@ module RuboCop
135
136
  range_between(node.loc.keyword.begin_pos, condition.source_range.end_pos)
136
137
  end
137
138
 
139
+ def comments_between_else_and_if?(node, else_branch)
140
+ return false if else_branch.modifier_form?
141
+
142
+ else_end = node.loc.else.end_pos
143
+ if_begin = else_branch.loc.keyword.begin_pos
144
+
145
+ processed_source.comments.any? do |comment|
146
+ comment_pos = comment.source_range.begin_pos
147
+ comment_pos > else_end && comment_pos < if_begin
148
+ end
149
+ end
150
+
138
151
  def allow_if_modifier_in_else_branch?(else_branch)
139
152
  allow_if_modifier? && else_branch&.modifier_form?
140
153
  end
@@ -13,6 +13,12 @@ module RuboCop
13
13
  # array, while `method_defined?` will do direct method lookup, which is much
14
14
  # faster and consumes less memory.
15
15
  #
16
+ # NOTE: `constants.include?` is not handled by this cop because
17
+ # `Module#const_defined?` has different lookup behavior than
18
+ # `Module#constants` - `const_defined?` searches up to `Object`
19
+ # (top-level constants like `String`, `Integer`, etc.) while
20
+ # `constants` does not, which can cause behavior changes after autocorrection.
21
+ #
16
22
  # @example
17
23
  # # bad
18
24
  # Array.instance_methods.include?(:size)
@@ -28,14 +34,12 @@ module RuboCop
28
34
  #
29
35
  # # bad
30
36
  # Array.class_variables.include?(:foo)
31
- # Array.constants.include?(:foo)
32
37
  # Array.private_instance_methods.include?(:foo)
33
38
  # Array.protected_instance_methods.include?(:foo)
34
39
  # Array.public_instance_methods.include?(:foo)
35
40
  #
36
41
  # # good
37
42
  # Array.class_variable_defined?(:foo)
38
- # Array.const_defined?(:foo)
39
43
  # Array.private_method_defined?(:foo)
40
44
  # Array.protected_method_defined?(:foo)
41
45
  # Array.public_method_defined?(:foo)
@@ -55,7 +59,6 @@ module RuboCop
55
59
 
56
60
  METHOD_REPLACEMENTS = {
57
61
  class_variables: :class_variable_defined?,
58
- constants: :const_defined?,
59
62
  instance_methods: :method_defined?,
60
63
  private_instance_methods: :private_method_defined?,
61
64
  protected_instance_methods: :protected_method_defined?,
@@ -99,6 +99,7 @@ module RuboCop
99
99
  end
100
100
  return unless key
101
101
  return if accumulator_used_in_expressions?(block_node, key, value)
102
+ return if nested_match?(key) || nested_match?(value)
102
103
 
103
104
  register_offense(node, block_node, key, value)
104
105
  end
@@ -108,6 +109,21 @@ module RuboCop
108
109
  references_variable?(key, acc_name) || references_variable?(value, acc_name)
109
110
  end
110
111
 
112
+ def nested_match?(node)
113
+ node.each_node(:call).any? do |send_node|
114
+ next false unless RESTRICT_ON_SEND.include?(send_node.method_name)
115
+
116
+ inner_block = send_node.block_node
117
+ next false unless inner_block
118
+
119
+ if send_node.method?(:each_with_object)
120
+ each_with_object_to_hash?(inner_block)
121
+ else
122
+ inject_to_hash?(inner_block)
123
+ end
124
+ end
125
+ end
126
+
111
127
  def accumulator_name(block_node)
112
128
  index = block_node.method?(:each_with_object) ? 1 : 0
113
129
  block_node.argument_list[index].name
@@ -59,7 +59,7 @@ module RuboCop
59
59
 
60
60
  def initialize(config = nil, options = nil)
61
61
  super
62
- @allowed_send_nodes = []
62
+ @allowed_send_nodes = Set.new.compare_by_identity
63
63
  @local_variables_scopes = Hash.new { |hash, key| hash[key] = [] }.compare_by_identity
64
64
  end
65
65
 
@@ -187,7 +187,7 @@ module RuboCop
187
187
  def allow_self(node)
188
188
  return unless node.send_type? && node.self_receiver?
189
189
 
190
- @allowed_send_nodes << node
190
+ @allowed_send_nodes.add(node)
191
191
  end
192
192
 
193
193
  def add_lhs_to_local_variables_scopes(rhs, lhs)
@@ -98,7 +98,16 @@ module RuboCop
98
98
  MSG_USE_SLASHES = 'Use `//` around regular expression.'
99
99
  MSG_USE_PERCENT_R = 'Use `%r` around regular expression.'
100
100
 
101
+ PAIR_DELIMITER_PATTERNS = {
102
+ ['(', ')'] => /\\.|[()]/,
103
+ ['[', ']'] => /\\.|[\[\]]/,
104
+ ['{', '}'] => /\\.|[{}]/,
105
+ ['<', '>'] => /\\.|[<>]/
106
+ }.freeze
107
+
101
108
  def on_regexp(node)
109
+ return if slash_literal?(node) && percent_r_delimiters_conflict?(node)
110
+
102
111
  message = if slash_literal?(node)
103
112
  MSG_USE_PERCENT_R unless allowed_slash_literal?(node)
104
113
  else
@@ -115,6 +124,26 @@ module RuboCop
115
124
 
116
125
  private
117
126
 
127
+ def percent_r_delimiters_conflict?(node)
128
+ opening, closing = preferred_delimiters
129
+ return false unless (pattern = PAIR_DELIMITER_PATTERNS[[opening, closing]])
130
+
131
+ !balanced_delimiters?(node_body(node), opening, closing, pattern)
132
+ end
133
+
134
+ def balanced_delimiters?(text, opening, closing, pattern)
135
+ depth = 0
136
+ text.scan(pattern) do |match|
137
+ if match == opening
138
+ depth += 1
139
+ elsif match == closing
140
+ depth -= 1
141
+ return false if depth.negative?
142
+ end
143
+ end
144
+ depth.zero?
145
+ end
146
+
118
147
  def allowed_slash_literal?(node)
119
148
  (style == :slashes && !contains_disallowed_slash?(node)) || allowed_mixed_slash?(node)
120
149
  end
@@ -65,7 +65,10 @@ module RuboCop
65
65
 
66
66
  message = format(MSG, conditional_type: node.keyword)
67
67
  add_offense(if_branch.loc.keyword, message: message) do |corrector|
68
+ next if ignored_node?(node)
69
+
68
70
  autocorrect(corrector, node, if_branch)
71
+ ignore_node(if_branch)
69
72
  end
70
73
  end
71
74
 
@@ -115,9 +118,8 @@ module RuboCop
115
118
  end
116
119
 
117
120
  def correct_node(corrector, node)
118
- corrector.replace(node.loc.keyword, 'if') if node.unless? && !part_of_ignored_node?(node)
121
+ corrector.replace(node.loc.keyword, 'if') if node.unless?
119
122
  corrector.replace(node.condition, chainable_condition(node))
120
- ignore_node(node)
121
123
  end
122
124
 
123
125
  def correct_for_guard_condition_style(corrector, node, if_branch)
@@ -260,10 +260,10 @@ module RuboCop
260
260
  end
261
261
 
262
262
  def begin_pos_for_replacement(node)
263
- expr = node.send_node.source_range
263
+ send_node = node.send_node
264
264
 
265
- if (paren_pos = (expr.source =~ /\(\s*\)$/))
266
- expr.begin_pos + paren_pos
265
+ if send_node.parenthesized? && send_node.arguments.empty?
266
+ send_node.loc.begin.begin_pos
267
267
  else
268
268
  node.loc.begin.begin_pos
269
269
  end
@@ -16,6 +16,11 @@ module RuboCop
16
16
  # # good
17
17
  # x += 1 while x < 10
18
18
  #
19
+ # # good
20
+ # while x < 10
21
+ # y += 1 if x.odd?
22
+ # end
23
+ #
19
24
  # # bad
20
25
  # until x > 10
21
26
  # x += 1
@@ -24,6 +29,11 @@ module RuboCop
24
29
  # # good
25
30
  # x += 1 until x > 10
26
31
  #
32
+ # # good
33
+ # until x > 10
34
+ # y += 1 unless x.even?
35
+ # end
36
+ #
27
37
  # # bad
28
38
  # x += 100 while x < 500 # a long comment that makes code too long if it were a single line
29
39
  #
@@ -45,6 +55,12 @@ module RuboCop
45
55
  end
46
56
  end
47
57
  alias on_until on_while
58
+
59
+ private
60
+
61
+ def non_eligible_body?(body)
62
+ body&.conditional? || super
63
+ end
48
64
  end
49
65
  end
50
66
  end