rubocop 1.88.2 → 1.89.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +67 -11
  3. data/lib/rubocop/comment_config.rb +35 -19
  4. data/lib/rubocop/config.rb +53 -0
  5. data/lib/rubocop/config_validator.rb +38 -1
  6. data/lib/rubocop/cop/badge.rb +8 -0
  7. data/lib/rubocop/cop/base.rb +6 -4
  8. data/lib/rubocop/cop/bundler.rb +19 -0
  9. data/lib/rubocop/cop/corrector.rb +1 -1
  10. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  11. data/lib/rubocop/cop/gemspec.rb +22 -0
  12. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  13. data/lib/rubocop/cop/generator.rb +4 -0
  14. data/lib/rubocop/cop/layout/class_structure.rb +8 -2
  15. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  16. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  17. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  18. data/lib/rubocop/cop/layout/line_length.rb +85 -12
  19. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  20. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  21. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +33 -34
  22. data/lib/rubocop/cop/layout.rb +114 -0
  23. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  24. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  25. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  26. data/lib/rubocop/cop/lint/constant_reassignment.rb +1 -10
  27. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  28. data/lib/rubocop/cop/lint/deprecated_reference.rb +185 -0
  29. data/lib/rubocop/cop/lint/duplicate_methods.rb +202 -22
  30. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  31. data/lib/rubocop/cop/lint/literal_as_condition.rb +5 -0
  32. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +1 -1
  33. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  34. data/lib/rubocop/cop/lint/name_typo.rb +223 -0
  35. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +0 -4
  36. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +7 -0
  37. data/lib/rubocop/cop/lint/to_enum_arguments.rb +37 -1
  38. data/lib/rubocop/cop/lint/unused_private_method.rb +157 -0
  39. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  40. data/lib/rubocop/cop/lint/void.rb +17 -6
  41. data/lib/rubocop/cop/lint.rb +176 -0
  42. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  43. data/lib/rubocop/cop/metrics.rb +31 -0
  44. data/lib/rubocop/cop/migration/department_name.rb +0 -4
  45. data/lib/rubocop/cop/migration.rb +13 -0
  46. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  47. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  48. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  49. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  50. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  51. data/lib/rubocop/cop/mixin/statement_modifier.rb +42 -6
  52. data/lib/rubocop/cop/mixin.rb +78 -77
  53. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  54. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  55. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  56. data/lib/rubocop/cop/naming.rb +31 -0
  57. data/lib/rubocop/cop/offense.rb +10 -6
  58. data/lib/rubocop/cop/registry.rb +153 -43
  59. data/lib/rubocop/cop/security.rb +19 -0
  60. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  61. data/lib/rubocop/cop/style/class_and_module_children.rb +78 -18
  62. data/lib/rubocop/cop/style/conditional_assignment.rb +22 -0
  63. data/lib/rubocop/cop/style/copyright.rb +6 -1
  64. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +41 -3
  65. data/lib/rubocop/cop/style/documentation.rb +47 -0
  66. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  67. data/lib/rubocop/cop/style/if_unless_modifier.rb +7 -48
  68. data/lib/rubocop/cop/style/lambda.rb +4 -0
  69. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +20 -3
  70. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  71. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  72. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  73. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  74. data/lib/rubocop/cop/style/negative_array_index.rb +3 -1
  75. data/lib/rubocop/cop/style/redundant_assignment.rb +11 -0
  76. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  77. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  78. data/lib/rubocop/cop/style/redundant_format.rb +12 -3
  79. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  80. data/lib/rubocop/cop/style/redundant_parentheses.rb +63 -63
  81. data/lib/rubocop/cop/style/safe_navigation.rb +10 -0
  82. data/lib/rubocop/cop/style/sole_nested_conditional.rb +20 -3
  83. data/lib/rubocop/cop/style/static_class.rb +21 -0
  84. data/lib/rubocop/cop/style.rb +310 -0
  85. data/lib/rubocop/cop/team.rb +48 -3
  86. data/lib/rubocop/cop/variable_force/variable.rb +18 -6
  87. data/lib/rubocop/cops_documentation_generator.rb +1 -1
  88. data/lib/rubocop/directive_comment.rb +2 -0
  89. data/lib/rubocop/feature_loader.rb +0 -6
  90. data/lib/rubocop/lsp/routes.rb +41 -3
  91. data/lib/rubocop/lsp/runtime.rb +4 -0
  92. data/lib/rubocop/lsp/server.rb +4 -0
  93. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  94. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  95. data/lib/rubocop/project_index_loader.rb +16 -0
  96. data/lib/rubocop/result_cache.rb +6 -4
  97. data/lib/rubocop/rspec/shared_contexts.rb +14 -2
  98. data/lib/rubocop/runner.rb +45 -20
  99. data/lib/rubocop/server/cache.rb +10 -5
  100. data/lib/rubocop/target_ruby.rb +1 -1
  101. data/lib/rubocop/version.rb +1 -1
  102. data/lib/rubocop.rb +17 -626
  103. metadata +18 -3
@@ -50,21 +50,37 @@ module RuboCop
50
50
 
51
51
  def initialize(cops = [], options = {})
52
52
  @departments = Set.new
53
+ # Maps each badge to its cop class, or to the fully qualified constant name (a `String`)
54
+ # when the cop is registered for lazy loading and its file is not loaded yet.
55
+ # A single insertion-ordered map keeps the registration order stable no matter when
56
+ # a lazy-loaded cop's file gets loaded, because reassigning an existing key preserves
57
+ # its position; the cop execution order depends on this order.
53
58
  @cops_by_badge = {}
54
- @lazy_loaded_cops_by_badge = {}
55
59
 
56
60
  @enrollment_queue = cops
57
61
  @options = options
58
62
 
59
63
  @enabled_cache = {}.compare_by_identity
60
64
  @disabled_cache = {}.compare_by_identity
65
+ @disabled_names_cache = {}.compare_by_identity
61
66
  @warnings = {}
62
67
  end
63
68
 
64
- def lazy_load(cop_name, constant_name)
65
- badge = Badge.parse(cop_name)
69
+ # Registers a cop by its badge and constant name without loading the class.
70
+ # The constant is resolved (loading the cop's file through `autoload`) only
71
+ # when the cop class itself is needed.
72
+ #
73
+ # @param badge [Badge, String] the badge, or the qualified cop name
74
+ # @param constant_name [String] the fully qualified constant name
75
+ def lazy_load(badge, constant_name)
76
+ badge = Badge.parse(badge) if badge.is_a?(String)
77
+
78
+ # File any cops enlisted earlier first, so that the registration
79
+ # order follows the call order.
80
+ clear_enrollment_queue
81
+
66
82
  @departments << badge.department
67
- @lazy_loaded_cops_by_badge[badge] = constant_name
83
+ @cops_by_badge[badge] = constant_name unless @cops_by_badge[badge].is_a?(Class)
68
84
  end
69
85
 
70
86
  def enlist(cop)
@@ -72,7 +88,16 @@ module RuboCop
72
88
  end
73
89
 
74
90
  def dismiss(cop)
75
- raise "Cop #{cop} could not be dismissed" unless @enrollment_queue.delete(cop)
91
+ dismissed = !@enrollment_queue.delete(cop).nil?
92
+
93
+ # A lazy-loaded cop that was not enlisted yet is dismissed by removing its registration.
94
+ # Anonymous cop classes cannot have a badge.
95
+ if !cop.name.nil? && @cops_by_badge[cop.badge].is_a?(String)
96
+ @cops_by_badge.delete(cop.badge)
97
+ dismissed = true
98
+ end
99
+
100
+ raise "Cop #{cop} could not be dismissed" unless dismissed
76
101
  end
77
102
 
78
103
  # @return [Array<Symbol>] list of departments for current cops.
@@ -83,12 +108,26 @@ module RuboCop
83
108
 
84
109
  # @return [Registry] Cops for that specific department.
85
110
  def with_department(department)
86
- with(cops.select { |cop| cop.department == department })
111
+ filter_by_badge { |badge| badge.department == department }
87
112
  end
88
113
 
89
114
  # @return [Registry] Cops not for a specific department.
90
115
  def without_department(department)
91
- with(cops.reject { |cop| cop.department == department })
116
+ filter_by_badge { |badge| badge.department != department }
117
+ end
118
+
119
+ # Returns a new registry containing the cops whose badge satisfies the given block,
120
+ # keeping lazy-loaded cops unloaded.
121
+ #
122
+ # @api private
123
+ # @return [Registry]
124
+ def filter_by_badge(options = {})
125
+ clear_enrollment_queue
126
+ copy = self.class.new([], options)
127
+ @cops_by_badge.each do |badge, cop_or_name|
128
+ copy.add_entry(badge, cop_or_name) if yield(badge)
129
+ end
130
+ copy
92
131
  end
93
132
 
94
133
  # @return [Boolean] Checks if given name is department
@@ -97,7 +136,7 @@ module RuboCop
97
136
  end
98
137
 
99
138
  def contains_cop_matching?(names)
100
- cops.any? { |cop| cop.match?(names) }
139
+ registered_badges.any? { |badge| badge.match_name?(names) }
101
140
  end
102
141
 
103
142
  # Convert a user provided cop name into a properly namespaced name
@@ -160,9 +199,10 @@ module RuboCop
160
199
 
161
200
  def unqualified_cop_names
162
201
  clear_enrollment_queue
163
- @unqualified_cop_names ||=
164
- (@cops_by_badge.keys | @lazy_loaded_cops_by_badge.keys)
165
- .to_set { |badge| File.basename(badge.to_s) } << 'RedundantCopDisableDirective'
202
+
203
+ @unqualified_cop_names ||= @cops_by_badge.keys.to_set do |badge|
204
+ File.basename(badge.to_s)
205
+ end << 'RedundantCopDisableDirective'
166
206
  end
167
207
 
168
208
  def qualify_badge(badge)
@@ -187,51 +227,64 @@ module RuboCop
187
227
 
188
228
  def length
189
229
  clear_enrollment_queue
190
- @cops_by_badge.size + @lazy_loaded_cops_by_badge.size
230
+ @cops_by_badge.size
191
231
  end
192
232
 
233
+ # Returns the enabled cop classes, loading lazy-loaded cops only when they are enabled.
234
+ #
235
+ # @return [Array<Class>]
193
236
  def enabled(config)
194
- @enabled_cache[config] ||= select { |cop| enabled?(cop, config) }
237
+ @enabled_cache[config] ||= registered_badges.filter_map do |badge|
238
+ next unless enabled_cop_name?(badge.to_s, config)
239
+
240
+ cop_or_name = @cops_by_badge[badge]
241
+ cop_or_name.is_a?(String) ? load_lazy_cop(badge) : cop_or_name
242
+ end
195
243
  end
196
244
 
197
245
  def disabled(config)
198
246
  @disabled_cache[config] ||= reject { |cop| enabled?(cop, config) }
199
247
  end
200
248
 
201
- def enabled?(cop, config)
202
- return true if options[:only]&.include?(cop.cop_name)
203
-
204
- # We need to use `cop_name` in this case, because `for_cop` uses caching
205
- # which expects cop names or cop classes as keys.
206
- cfg = config.for_cop(cop.cop_name)
207
-
208
- cop_enabled = cfg.fetch('Enabled') == true || enabled_pending_cop?(cfg, config)
209
-
210
- if options.fetch(:safe, false)
211
- cop_enabled && cfg.fetch('Safe', true)
212
- else
213
- cop_enabled
249
+ # Returns the names of the disabled cops without loading any of them.
250
+ #
251
+ # @api private
252
+ # @return [Array<String>]
253
+ def disabled_names(config)
254
+ @disabled_names_cache[config] ||= registered_badges.filter_map do |badge|
255
+ name = badge.to_s
256
+ name unless enabled_cop_name?(name, config)
214
257
  end
215
258
  end
216
259
 
217
- def enabled_pending_cop?(cop_cfg, config)
260
+ def enabled?(cop, config)
261
+ enabled_cop_name?(cop.cop_name, config)
262
+ end
263
+
264
+ # @param cop [Class, String, nil] the cop class or the qualified cop name
265
+ def enabled_pending_cop?(cop_cfg, config, cop = nil)
218
266
  return false if @options[:disable_pending_cops]
267
+ return false unless cop_cfg.fetch('Enabled') == 'pending'
268
+ return true if @options[:enable_pending_cops]
269
+ return config.enabled_new_cops? unless cop
219
270
 
220
- cop_cfg.fetch('Enabled') == 'pending' &&
221
- (@options[:enable_pending_cops] || config.enabled_new_cops?)
271
+ cop_name = cop.is_a?(String) ? cop : cop.cop_name
272
+ config.enabled_new_cop?(cop_name)
222
273
  end
223
274
 
224
275
  def names
225
276
  clear_enrollment_queue
226
- @cops_by_badge.keys.map(&:to_s) | @lazy_loaded_cops_by_badge.keys.map(&:to_s)
277
+ @cops_by_badge.keys.map(&:to_s)
227
278
  end
228
279
 
229
280
  def cops_for_department(department)
230
- cops.select { |cop| cop.department == department.to_sym }
281
+ with_department(department.to_sym).cops
231
282
  end
232
283
 
233
284
  def names_for_department(department)
234
- cops_for_department(department).map(&:cop_name)
285
+ department = department.to_sym
286
+
287
+ registered_badges.filter_map { |badge| badge.to_s if badge.department == department }
235
288
  end
236
289
 
237
290
  def ==(other)
@@ -259,7 +312,9 @@ module RuboCop
259
312
  def find_by_cop_name(cop_name)
260
313
  clear_enrollment_queue
261
314
  badge = Badge.parse(cop_name)
262
- @cops_by_badge[badge] || load_lazy_cop(badge)
315
+ cop_or_name = @cops_by_badge[badge]
316
+
317
+ cop_or_name.is_a?(String) ? load_lazy_cop(badge) : cop_or_name
263
318
  end
264
319
 
265
320
  # When a cop name is given returns a single-element array with the cop class.
@@ -287,35 +342,90 @@ module RuboCop
287
342
  @warnings[path]
288
343
  end
289
344
 
345
+ protected
346
+
347
+ # Adds an already-loaded cop class or a lazy-load constant name under the given badge,
348
+ # used to build filtered copies.
349
+ def add_entry(badge, cop_or_name)
350
+ @departments << badge.department
351
+ @cops_by_badge[badge] = cop_or_name
352
+ end
353
+
290
354
  private
291
355
 
292
356
  def initialize_copy(reg)
293
- initialize(reg.cops, reg.options)
357
+ super
358
+ @cops_by_badge = @cops_by_badge.dup
359
+ @enrollment_queue = @enrollment_queue.dup
360
+ @departments = @departments.dup
361
+ @enabled_cache = {}.compare_by_identity
362
+ @disabled_cache = {}.compare_by_identity
363
+ @disabled_names_cache = {}.compare_by_identity
364
+ @warnings = {}
365
+ @unqualified_cop_names = nil
294
366
  end
295
367
 
296
368
  def clear_enrollment_queue
297
369
  return if @enrollment_queue.empty?
298
370
 
299
371
  @enrollment_queue.each do |cop|
372
+ # Reassigning a badge that was registered for lazy loading keeps
373
+ # its original position in the insertion-ordered map.
300
374
  @cops_by_badge[cop.badge] = cop
301
375
  @departments << cop.department
302
376
  end
303
377
  @enrollment_queue = []
304
378
  end
305
379
 
306
- def load_all_lazy_cops
307
- @lazy_loaded_cops_by_badge.each_key { |badge| load_lazy_cop(badge) }
380
+ def registered_badges
381
+ clear_enrollment_queue
382
+ @cops_by_badge.keys
308
383
  end
309
384
 
310
- def load_lazy_cop(badge)
311
- constant_name = @lazy_loaded_cops_by_badge.delete(badge)
312
- return unless constant_name
385
+ def enabled_cop_name?(cop_name, config)
386
+ return true if options[:only]&.include?(cop_name)
313
387
 
314
- @cops_by_badge[badge] = Kernel.const_get(constant_name)
388
+ # We need to use the cop name in this case, because `for_cop` uses
389
+ # caching which expects cop names or cop classes as keys.
390
+ cfg = config.for_cop(cop_name)
391
+
392
+ cop_enabled = cfg.fetch('Enabled') == true || enabled_pending_cop?(cfg, config, cop_name)
393
+
394
+ if options.fetch(:safe, false)
395
+ cop_enabled && cfg.fetch('Safe', true)
396
+ else
397
+ cop_enabled
398
+ end
315
399
  end
316
400
 
317
- def with(cops)
318
- self.class.new(cops)
401
+ def load_all_lazy_cops
402
+ # Take a snapshot because loading a cop can load (and thereby register)
403
+ # another lazy-loaded cop, e.g. its superclass.
404
+ badges = @cops_by_badge.keys
405
+ badges.each { |badge| load_lazy_cop(badge) if @cops_by_badge[badge].is_a?(String) }
406
+ end
407
+
408
+ # @return [Class, nil] the loaded cop class, or nil if the cop excluded
409
+ # itself from the registry while being loaded
410
+ def load_lazy_cop(badge)
411
+ constant_name = @cops_by_badge[badge]
412
+ return constant_name unless constant_name.is_a?(String)
413
+
414
+ cop = Kernel.const_get(constant_name)
415
+ if equal?(self.class.global)
416
+ # Resolving the constant fires the autoload, which makes
417
+ # `Base.inherited` enlist the class into the global registry.
418
+ # Enrolling through the queue keeps `exclude_from_registry` honored:
419
+ # a cop that dismissed itself leaves a dangling constant name behind
420
+ # and is removed from the registry.
421
+ clear_enrollment_queue
422
+ cop_or_name = @cops_by_badge[badge]
423
+ @cops_by_badge.delete(badge) if cop_or_name.is_a?(String)
424
+ cop_or_name.is_a?(Class) ? cop_or_name : nil
425
+ else
426
+ # `Base.inherited` enlists into the global registry, not this copy.
427
+ @cops_by_badge[badge] = cop
428
+ end
319
429
  end
320
430
 
321
431
  def resolve_badge(given_badge, real_badge, source_path, warn: true)
@@ -335,7 +445,7 @@ module RuboCop
335
445
 
336
446
  def registered?(badge)
337
447
  clear_enrollment_queue
338
- @cops_by_badge.key?(badge) || @lazy_loaded_cops_by_badge.key?(badge)
448
+ @cops_by_badge.key?(badge)
339
449
  end
340
450
  end
341
451
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Cops for the `Security` department. The department's cops are registered for lazy loading
6
+ # and their files are loaded on demand.
7
+ module Security
8
+ extend LazyLoader
9
+
10
+ register_cop :CompoundHash, "#{__dir__}/security/compound_hash"
11
+ register_cop :Eval, "#{__dir__}/security/eval"
12
+ register_cop :IoMethods, "#{__dir__}/security/io_methods"
13
+ register_cop :JSONLoad, "#{__dir__}/security/json_load"
14
+ register_cop :MarshalLoad, "#{__dir__}/security/marshal_load"
15
+ register_cop :Open, "#{__dir__}/security/open"
16
+ register_cop :YAMLLoad, "#{__dir__}/security/yaml_load"
17
+ end
18
+ end
19
+ end
@@ -30,6 +30,11 @@ module RuboCop
30
30
  # only cases where exactly one argument is provided can be replaced with
31
31
  # `Array#intersect?` and are handled by this cop.
32
32
  #
33
+ # NOTE: In the block form, `include?` is only detected when its receiver is
34
+ # an array literal, because `include?` is defined with different semantics
35
+ # on many non-array classes (e.g. `String#include?` checks for substrings).
36
+ # `member?` does not have this restriction.
37
+ #
33
38
  # @safety
34
39
  # This cop cannot guarantee that `array1` and `array2` are
35
40
  # actually arrays while method `intersect?` is for arrays only.
@@ -47,7 +52,7 @@ module RuboCop
47
52
  #
48
53
  # # bad
49
54
  # array1.any? { |elem| array2.member?(elem) }
50
- # array1.none? { |elem| array2.include?(elem) }
55
+ # array1.none? { |elem| [1, 2].include?(elem) }
51
56
  #
52
57
  # # good
53
58
  # array1.intersect?(array2)
@@ -121,15 +126,15 @@ module RuboCop
121
126
  (block
122
127
  (call $_receiver ${:any? :none?})
123
128
  (args (arg _key))
124
- (send $_argument {:member? :include?} (lvar _key))
129
+ (send $!nil? ${:member? :include?} (lvar _key))
125
130
  )
126
131
  (numblock
127
132
  (call $_receiver ${:any? :none?}) 1
128
- (send $_argument {:member? :include?} (lvar :_1))
133
+ (send $!nil? ${:member? :include?} (lvar :_1))
129
134
  )
130
135
  (itblock
131
136
  (call $_receiver ${:any? :none?}) :it
132
- (send $_argument {:member? :include?} (lvar :it))
137
+ (send $!nil? ${:member? :include?} (lvar :it))
133
138
  )
134
139
  }
135
140
  PATTERN
@@ -156,7 +161,10 @@ module RuboCop
156
161
  alias on_csend on_send
157
162
 
158
163
  def on_block(node)
159
- return unless (receiver, method_name, argument = any_none_block_intersection(node))
164
+ return unless (captures = any_none_block_intersection(node))
165
+
166
+ receiver, method_name, argument, block_method = captures
167
+ return if uncorrectable_block_intersection?(node, method_name, argument, block_method)
160
168
 
161
169
  dot = node.send_node.loc.dot.source
162
170
  bang = method_name == :any? ? '' : '!'
@@ -186,6 +194,17 @@ module RuboCop
186
194
  STRAIGHT_METHODS.include?(method_name.to_sym)
187
195
  end
188
196
 
197
+ def uncorrectable_block_intersection?(node, method_name, argument, block_method)
198
+ # `a&.none? { |elem| b.include?(elem) }` returns `nil` when `a` is `nil`,
199
+ # but the negated rewrite `!a&.intersect?(b)` returns `true` there, flipping the result.
200
+ return true if method_name == :none? && node.send_node.safe_navigation?
201
+
202
+ # `include?` is defined with different semantics on many non-array classes
203
+ # (e.g. `String#include?` checks for substrings), so it is only a reliable
204
+ # signal of an intersection check when the receiver is an array literal.
205
+ block_method == :include? && !argument.array_type?
206
+ end
207
+
189
208
  def register_offense(node, replacement)
190
209
  message = format(MSG, replacement: replacement, existing: node.source)
191
210
 
@@ -23,10 +23,15 @@ module RuboCop
23
23
  #
24
24
  # Moving from `compact` to `nested` children requires knowledge of whether the
25
25
  # outer parent is a module or a class. Moving from `nested` to `compact` requires
26
- # verification that the outer parent is defined elsewhere. RuboCop does not
27
- # have the knowledge to perform either operation safely and thus requires
26
+ # verification that the outer parent is defined elsewhere. By default RuboCop does
27
+ # not have the knowledge to perform either operation safely and thus requires
28
28
  # manual oversight.
29
29
  #
30
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed,
31
+ # the project-wide index is consulted to resolve whether the outer parent is a
32
+ # class or a module, and compacting is skipped when the outer parent is not
33
+ # defined elsewhere.
34
+ #
30
35
  # @example EnforcedStyle: nested (default)
31
36
  # # bad
32
37
  # class Foo::Bar
@@ -53,6 +58,7 @@ module RuboCop
53
58
  class ClassAndModuleChildren < Base
54
59
  include Alignment
55
60
  include ConfigurableEnforcedStyle
61
+ include ProjectIndexHelp
56
62
  include RangeHelp
57
63
  extend AutoCorrector
58
64
 
@@ -60,7 +66,7 @@ module RuboCop
60
66
  COMPACT_MSG = 'Use compact module/class definition instead of nested style.'
61
67
 
62
68
  def on_class(node)
63
- return if node.parent_class && style != :nested
69
+ return if node.parent_class && style_for_classes != :nested
64
70
 
65
71
  check_style(node, node.body, style_for_classes)
66
72
  end
@@ -72,7 +78,7 @@ module RuboCop
72
78
  private
73
79
 
74
80
  def nest_or_compact(corrector, node)
75
- style = node.class_type? ? style_for_classes : style_for_modules
81
+ style = style_for_kind(node.type)
76
82
 
77
83
  if style == :nested
78
84
  nest_definition(corrector, node)
@@ -82,21 +88,40 @@ module RuboCop
82
88
  end
83
89
 
84
90
  def nest_definition(corrector, node)
91
+ keyword = namespace_keyword(node)
92
+ # A namespace wrapper whose style resolves to `compact` would itself violate
93
+ # that style, making autocorrection ping-pong between the two forms.
94
+ return if style_for_kind(keyword.to_sym) == :compact
95
+
85
96
  padding = indentation(node) + leading_spaces(node)
86
97
  padding_for_trailing_end = padding.sub(' ' * node.loc.end.column, '')
87
98
 
88
- replace_namespace_keyword(corrector, node)
99
+ corrector.replace(node.loc.keyword, keyword)
89
100
  split_on_double_colon(corrector, node, padding)
90
101
  add_trailing_end(corrector, node, padding_for_trailing_end)
91
102
  end
92
103
 
93
- def replace_namespace_keyword(corrector, node)
104
+ def namespace_keyword(node)
105
+ indexed_namespace_keyword(node) || heuristic_namespace_keyword(node)
106
+ end
107
+
108
+ def indexed_namespace_keyword(node)
109
+ return nil unless project_index
110
+
111
+ declaration = resolve_constant_in_index(node.identifier.namespace)
112
+
113
+ case declaration
114
+ when Rubydex::Class then 'class'
115
+ when Rubydex::Module then 'module'
116
+ end
117
+ end
118
+
119
+ def heuristic_namespace_keyword(node)
94
120
  class_definition = node.left_sibling&.each_node(:class)&.find do |class_node|
95
121
  class_node.identifier == node.identifier.namespace
96
122
  end
97
- namespace_keyword = class_definition ? 'class' : 'module'
98
123
 
99
- corrector.replace(node.loc.keyword, namespace_keyword)
124
+ class_definition ? 'class' : 'module'
100
125
  end
101
126
 
102
127
  def split_on_double_colon(corrector, node, padding)
@@ -114,11 +139,41 @@ module RuboCop
114
139
  end
115
140
 
116
141
  def compact_definition(corrector, node)
142
+ # Compacting produces a definition whose type's style resolves to `nested`,
143
+ # making autocorrection ping-pong between the two forms.
144
+ return if style_for_kind(node.body.type) == :nested
145
+ return unless compactible_namespace?(node)
146
+
117
147
  compact_node(corrector, node)
118
148
  remove_end(corrector, node.body)
119
149
  unindent(corrector, node)
120
150
  end
121
151
 
152
+ # Compacting removes this definition of the namespace, so the result raises
153
+ # `NameError` at load time unless the namespace is also defined somewhere else.
154
+ # With the project index this is verified before correcting; without it the
155
+ # correction is performed regardless (the cop's autocorrection is unsafe).
156
+ def compactible_namespace?(node)
157
+ return true unless project_index
158
+
159
+ declaration = resolve_constant_in_index(node.identifier)
160
+ return false unless declaration.is_a?(Rubydex::Namespace)
161
+
162
+ declaration.definitions.any? { |definition| definition_elsewhere?(definition, node) }
163
+ end
164
+
165
+ def definition_elsewhere?(definition, node)
166
+ location = definition.location
167
+ return true unless location.uri.start_with?(FILE_URI_PREFIX)
168
+
169
+ !same_file?(location.to_file_path, processed_source.file_path) ||
170
+ location.to_display.start_line != node.first_line
171
+ rescue StandardError
172
+ # A path that cannot be converted or compared cannot prove the
173
+ # namespace is defined elsewhere; err on not correcting.
174
+ false
175
+ end
176
+
122
177
  def compact_node(corrector, node)
123
178
  range = range_between(node.loc.keyword.begin_pos, node.body.loc.name.end_pos)
124
179
  corrector.replace(range, compact_replacement(node))
@@ -162,23 +217,24 @@ module RuboCop
162
217
  column_delta = configured_indentation_width - spaces_size(last_child_leading_spaces)
163
218
  return if column_delta.zero?
164
219
 
165
- AlignmentCorrector.correct(corrector, processed_source, node, column_delta)
220
+ AlignmentCorrector.correct(
221
+ corrector, processed_source, node, column_delta, tab_indentation: true
222
+ )
166
223
  end
167
224
 
168
225
  def leading_spaces(node)
169
226
  node.source_range.source_line[/\A\s*/]
170
227
  end
171
228
 
229
+ # A tab counts as `configured_indentation_width` columns, matching the
230
+ # width `AlignmentCorrector` uses to convert `column_delta` back into
231
+ # tabs. Using `Layout/IndentationStyle`'s own `IndentationWidth` here
232
+ # would make the delta and the correction disagree.
172
233
  def spaces_size(spaces_string)
173
- mapping = { "\t" => tab_indentation_width }
234
+ mapping = { "\t" => configured_indentation_width }
174
235
  spaces_string.chars.sum { |character| mapping.fetch(character, 1) }
175
236
  end
176
237
 
177
- def tab_indentation_width
178
- config.for_cop('Layout/IndentationStyle')['IndentationWidth'] ||
179
- configured_indentation_width
180
- end
181
-
182
238
  def check_style(node, body, style)
183
239
  return if node.identifier.namespace&.cbase_type?
184
240
  return unless const_namespace?(node.identifier.namespace)
@@ -218,7 +274,7 @@ module RuboCop
218
274
  end
219
275
 
220
276
  def autocorrect(corrector, node)
221
- return if node.class_type? && node.parent_class && style != :nested
277
+ return if node.class_type? && node.parent_class && style_for_classes != :nested
222
278
 
223
279
  nest_or_compact(corrector, node)
224
280
  end
@@ -231,12 +287,16 @@ module RuboCop
231
287
  node.identifier.source.include?('::')
232
288
  end
233
289
 
290
+ def style_for_kind(kind)
291
+ kind == :class ? style_for_classes : style_for_modules
292
+ end
293
+
234
294
  def style_for_classes
235
- cop_config['EnforcedStyleForClasses'] || style
295
+ cop_config['EnforcedStyleForClasses']&.to_sym || style
236
296
  end
237
297
 
238
298
  def style_for_modules
239
- cop_config['EnforcedStyleForModules'] || style
299
+ cop_config['EnforcedStyleForModules']&.to_sym || style
240
300
  end
241
301
  end
242
302
  end
@@ -203,6 +203,7 @@ module RuboCop
203
203
  class ConditionalAssignment < Base
204
204
  include ConditionalAssignmentHelper
205
205
  include ConfigurableEnforcedStyle
206
+ include ReparsedEquivalence
206
207
  extend AutoCorrector
207
208
 
208
209
  MSG = 'Use the return of the conditional for variable assignment and comparison.'
@@ -288,6 +289,7 @@ module RuboCop
288
289
  # else slot, but `else_branch` correctly reports there is no `else` clause.
289
290
  return unless assignment.else_branch
290
291
  return if allowed_single_line?([*branches, else_branch])
292
+ return unless safe_to_correct?(node)
291
293
 
292
294
  add_offense(node, message: ASSIGN_TO_CONDITION_MSG) do |corrector|
293
295
  autocorrect(corrector, node)
@@ -367,6 +369,7 @@ module RuboCop
367
369
  return unless allowed_statements?(branches)
368
370
  return if allowed_single_line?(branches)
369
371
  return if correction_exceeds_line_limit?(node, branches)
372
+ return unless safe_to_correct?(node)
370
373
 
371
374
  add_offense(node) { |corrector| autocorrect(corrector, node) }
372
375
  end
@@ -379,6 +382,25 @@ module RuboCop
379
382
  end
380
383
  end
381
384
 
385
+ # The correction rewrites the conditional structurally, so full AST
386
+ # equivalence cannot be asserted, but the result must at least reparse
387
+ # to valid Ruby. Applying the correction to a throwaway buffer here
388
+ # also exercises the branch-shape handling before an offense is even
389
+ # reported, so unusual branches that the corrector cannot handle are
390
+ # left alone instead of crashing or emitting invalid code - the bug
391
+ # class behind most of this cop's history.
392
+ def safe_to_correct?(node)
393
+ correction_parses?(node)
394
+ rescue StandardError
395
+ false
396
+ end
397
+
398
+ # `ReparsedEquivalence#correction_parses?` contract: apply the exact
399
+ # correction this cop would perform.
400
+ def apply_reparse_correction(corrector, node)
401
+ autocorrect(corrector, node)
402
+ end
403
+
382
404
  def allowed_statements?(branches)
383
405
  return false unless branches.all?
384
406
 
@@ -25,20 +25,25 @@ module RuboCop
25
25
  MSG = 'Include a copyright notice matching /%<notice>s/ before any code.'
26
26
  AUTOCORRECT_EMPTY_WARNING = 'An AutocorrectNotice must be defined in your RuboCop config'
27
27
 
28
+ # rubocop:disable Metrics/AbcSize
28
29
  def on_new_investigation
29
30
  return if notice.empty? || notice_found?(processed_source)
30
31
 
31
- verify_autocorrect_notice!
32
32
  message = format(MSG, notice: notice)
33
33
  if processed_source.blank?
34
34
  add_global_offense(message)
35
35
  else
36
36
  offense_range = source_range(processed_source.buffer, 1, 0)
37
37
  add_offense(offense_range, message: message) do |corrector|
38
+ next unless autocorrect?
39
+
40
+ verify_autocorrect_notice!
41
+
38
42
  autocorrect(corrector)
39
43
  end
40
44
  end
41
45
  end
46
+ # rubocop:enable Metrics/AbcSize
42
47
 
43
48
  private
44
49