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
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Lint
6
+ # Checks for calls to methods and references to constants that are documented
7
+ # as deprecated with a YARD `@deprecated` tag.
8
+ #
9
+ # The check is powered by the project-wide index, so it only runs when
10
+ # `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed.
11
+ # Without the index the cop does nothing.
12
+ #
13
+ # Only references that can be resolved without type inference are checked:
14
+ # constants, method calls without an explicit receiver (or with `self`),
15
+ # which are looked up in the enclosing class or module and its ancestry,
16
+ # and calls whose receiver is a constant, which are looked up in that
17
+ # namespace's singleton class. Calls on arbitrary objects are not checked.
18
+ #
19
+ # References made from a definition that is itself deprecated are allowed,
20
+ # so deprecated implementations can keep calling each other.
21
+ #
22
+ # @example
23
+ # # Given a deprecated method and constant:
24
+ # #
25
+ # # class Api
26
+ # # # @deprecated Use `#new_method` instead.
27
+ # # def old_method
28
+ # # end
29
+ # #
30
+ # # # @deprecated
31
+ # # OLD_TIMEOUT = 10
32
+ # # end
33
+ #
34
+ # # bad
35
+ # class Client < Api
36
+ # def call
37
+ # old_method
38
+ # end
39
+ #
40
+ # def timeout
41
+ # OLD_TIMEOUT
42
+ # end
43
+ # end
44
+ #
45
+ # # good
46
+ # class Client < Api
47
+ # def call
48
+ # new_method
49
+ # end
50
+ #
51
+ # def timeout
52
+ # NEW_TIMEOUT
53
+ # end
54
+ # end
55
+ #
56
+ class DeprecatedReference < Base
57
+ include ProjectIndexHelp
58
+
59
+ METHOD_MSG = 'Method `%<name>s` is deprecated%<detail>s'
60
+ CONSTANT_MSG = 'Constant `%<name>s` is deprecated%<detail>s'
61
+
62
+ DEPRECATED_TAG = '@deprecated'
63
+ DEPRECATION_DETAIL = /#{DEPRECATED_TAG}\s+(.+)/.freeze
64
+
65
+ def on_new_investigation
66
+ @namespace_cache = {}
67
+ super
68
+ end
69
+
70
+ def on_send(node)
71
+ return unless project_index
72
+
73
+ declaration = method_declaration_for(node)
74
+ return unless declaration && deprecated?(declaration)
75
+ return if within_deprecated_definition?(node)
76
+
77
+ message = format(METHOD_MSG, name: node.method_name, detail: detail_for(declaration))
78
+ add_offense(node.loc.selector, message: message)
79
+ end
80
+ alias on_csend on_send
81
+
82
+ def on_const(node)
83
+ return unless project_index
84
+ return if node.parent&.defined_module
85
+
86
+ declaration = resolve_constant_in_index(node)
87
+ return unless declaration && deprecated?(declaration)
88
+ return if within_deprecated_definition?(node)
89
+
90
+ message = format(CONSTANT_MSG, name: node.const_name, detail: detail_for(declaration))
91
+ add_offense(node, message: message)
92
+ end
93
+
94
+ private
95
+
96
+ def method_declaration_for(node)
97
+ receiver = node.receiver
98
+
99
+ if receiver.nil? || receiver.self_type?
100
+ implicit_receiver_declaration(node)
101
+ elsif receiver.const_type?
102
+ const_receiver_declaration(node, receiver)
103
+ end
104
+ end
105
+
106
+ def implicit_receiver_declaration(node)
107
+ namespace = enclosing_namespace(node)
108
+ return nil unless namespace.is_a?(Rubydex::Namespace)
109
+
110
+ if singleton_context?(node)
111
+ indexed_singleton_member(namespace, "#{node.method_name}()")
112
+ else
113
+ namespace.find_member("#{node.method_name}()")
114
+ end
115
+ end
116
+
117
+ def const_receiver_declaration(node, receiver)
118
+ namespace = resolve_constant_in_index(receiver)
119
+ return nil unless namespace.is_a?(Rubydex::Namespace)
120
+
121
+ indexed_singleton_member(namespace, "#{node.method_name}()")
122
+ end
123
+
124
+ def enclosing_namespace(node)
125
+ namespace_node = node.each_ancestor(:class, :module).first
126
+ return project_index['Object'] unless namespace_node
127
+
128
+ @namespace_cache.fetch(namespace_node) do
129
+ @namespace_cache[namespace_node] = resolve_constant_in_index(namespace_node.identifier)
130
+ end
131
+ end
132
+
133
+ # Whether an implicit-receiver call runs with the class or module itself
134
+ # as `self` (directly in a namespace body, in a singleton method, or
135
+ # inside `class << self`) rather than inside an instance method.
136
+ def singleton_context?(node)
137
+ instance_def_seen = false
138
+
139
+ node.each_ancestor do |ancestor|
140
+ case ancestor.type
141
+ when :defs, :sclass
142
+ return true
143
+ when :def
144
+ instance_def_seen = true
145
+ when :class, :module
146
+ return !instance_def_seen
147
+ end
148
+ end
149
+
150
+ false
151
+ end
152
+
153
+ def deprecated?(declaration)
154
+ definitions = declaration.definitions.to_a
155
+
156
+ definitions.any? && definitions.all?(&:deprecated?)
157
+ end
158
+
159
+ # References from a definition that is itself documented as deprecated
160
+ # are allowed.
161
+ def within_deprecated_definition?(node)
162
+ node.each_ancestor(:any_def, :class, :module).any? do |ancestor|
163
+ comments = processed_source.ast_with_comments[ancestor]
164
+ comments.any? { |comment| comment.text.include?(DEPRECATED_TAG) }
165
+ end
166
+ end
167
+
168
+ def detail_for(declaration)
169
+ text = deprecation_text(declaration)
170
+ return '.' if text.nil? || text.empty?
171
+
172
+ text.end_with?('.') ? ": #{text}" : ": #{text}."
173
+ end
174
+
175
+ def deprecation_text(declaration)
176
+ tag_comment = declaration.definitions.to_a.filter_map do |definition|
177
+ definition.comments.to_a.map(&:string).find { |string| string.include?(DEPRECATED_TAG) }
178
+ end.first
179
+
180
+ tag_comment && tag_comment[DEPRECATION_DETAIL, 1]&.strip
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -10,6 +10,17 @@ module RuboCop
10
10
  # the developer intends to suppress Ruby's method redefinition warnings.
11
11
  # See https://bugs.ruby-lang.org/issues/13574.
12
12
  #
13
+ # By default the cop can only detect duplicates within a single file.
14
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed,
15
+ # the cop additionally consults the project-wide index and reports methods
16
+ # whose duplicate definition lives in another file.
17
+ #
18
+ # NOTE: The project index does not record whether a definition in another
19
+ # file is wrapped in a conditional, so a platform-specific redefinition in
20
+ # another file may still be reported. Aliasing the method to itself (see above)
21
+ # before redefining marks the redefinition as intentional and is respected
22
+ # across files.
23
+ #
13
24
  # @example
14
25
  #
15
26
  # # bad
@@ -118,16 +129,45 @@ module RuboCop
118
129
  # delegate :foo, to: :bar
119
130
  # end
120
131
  #
132
+ # @example DelegatingMethods: ['delegate', 'expose'] (default: ['delegate'])
133
+ # # A project's own `delegate`-shaped macros can be registered so the
134
+ # # methods they define are recognized (with
135
+ # # `AllCops/ActiveSupportExtensionsEnabled: true`).
136
+ #
137
+ # # bad
138
+ # def foo
139
+ # 1
140
+ # end
141
+ #
142
+ # expose :foo, to: :bar
143
+ #
121
144
  class DuplicateMethods < Base # rubocop:disable Metrics/ClassLength
145
+ include ProjectIndexHelp
146
+
122
147
  MSG = 'Method `%<method>s` is defined at both %<defined>s and %<current>s.'
123
- RESTRICT_ON_SEND = %i[alias_method attr_reader attr_writer attr_accessor attr
124
- delegate def_delegator def_instance_delegator def_delegators
125
- def_instance_delegators].freeze
148
+
149
+ # Method names the cop registers that can be looked up in the project index:
150
+ # a fully qualified namespace followed by `#` (instance) or `.` (singleton)
151
+ # and the method name.
152
+ INDEXABLE_METHOD_NAME =
153
+ /\A(?<owner>[A-Z]\w*(?:::[A-Z]\w*)*)(?<separator>[#.])(?<name>[^#.]+)\z/.freeze
154
+ # No `RESTRICT_ON_SEND`: the delegating method names are configurable
155
+ # (`DelegatingMethods`), so `on_send` must see every call. The handler
156
+ # returns quickly for calls that are not method definitions, and
157
+ # benchmarks show no measurable cost over the previous fixed list.
126
158
 
127
159
  def initialize(config = nil, options = nil)
128
160
  super
129
161
  @definitions = {}
130
162
  @scopes = Hash.new { |hash, key| hash[key] = [] }
163
+ @self_aliased = Set.new
164
+ end
165
+
166
+ def on_new_investigation
167
+ # The self-alias trick declares an intentional redefinition only within
168
+ # the file that uses it, so the tracked names do not carry over.
169
+ @self_aliased = Set.new
170
+ super
131
171
  end
132
172
 
133
173
  def on_def(node)
@@ -157,7 +197,11 @@ module RuboCop
157
197
  def on_alias(node)
158
198
  name, original_name = method_alias?(node)
159
199
  return unless name && original_name
160
- return if name == original_name
200
+
201
+ if name == original_name
202
+ track_self_alias(node, name)
203
+ return
204
+ end
161
205
  return if node.ancestors.any?(&:if_type?)
162
206
 
163
207
  found_instance_method(node, name)
@@ -168,9 +212,12 @@ module RuboCop
168
212
  (send nil? :alias_method (sym $_name) (sym $_original_name))
169
213
  PATTERN
170
214
 
171
- # @!method delegate_method?(node)
172
- def_node_matcher :delegate_method?, <<~PATTERN
173
- (send nil? :delegate
215
+ # Matches the argument shape of an Active Support `delegate` call
216
+ # (`delegate :a, :b, to: :target`), regardless of the method name; the
217
+ # name is checked separately against `DelegatingMethods`.
218
+ # @!method delegate_args(node)
219
+ def_node_matcher :delegate_args, <<~PATTERN
220
+ (send nil? _
174
221
  ({sym str} $_)+
175
222
  (hash <(pair (sym :to) {sym str}) ...>)
176
223
  )
@@ -204,17 +251,27 @@ module RuboCop
204
251
  ...)
205
252
  PATTERN
206
253
 
254
+ # @!method class_new_block?(node)
255
+ def_node_matcher :class_new_block?, <<~PATTERN
256
+ (block
257
+ (send (const _ :Class) :new ...)
258
+ ...)
259
+ PATTERN
260
+
207
261
  def on_send(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
208
262
  name, original_name = alias_method?(node)
209
263
 
210
264
  if name && original_name
211
- return if name == original_name
265
+ if name == original_name
266
+ track_self_alias(node, name)
267
+ return
268
+ end
212
269
  return if inside_condition?(node)
213
270
 
214
271
  found_instance_method(node, name)
215
272
  elsif (attr = node.attribute_accessor?)
216
273
  on_attr(node, *attr)
217
- elsif active_support_extensions_enabled? && (names = delegate_method?(node))
274
+ elsif delegating_method?(node) && (names = delegate_args(node))
218
275
  return if inside_condition?(node)
219
276
 
220
277
  on_delegate(node, names)
@@ -257,6 +314,19 @@ module RuboCop
257
314
  current: source_location(node))
258
315
  end
259
316
 
317
+ # Recognizing a delegating call as a method definition is Active
318
+ # Support behavior, so it stays gated on `ActiveSupportExtensionsEnabled`
319
+ # as it was when only `delegate` was supported. `DelegatingMethods` lets
320
+ # a project register its own `delegate`-shaped macros.
321
+ def delegating_method?(node)
322
+ active_support_extensions_enabled? &&
323
+ delegating_methods.include?(node.method_name.to_s)
324
+ end
325
+
326
+ def delegating_methods
327
+ cop_config.fetch('DelegatingMethods', ['delegate'])
328
+ end
329
+
260
330
  def on_delegate(node, method_names)
261
331
  name_prefix = delegate_prefix(node)
262
332
 
@@ -318,13 +388,27 @@ module RuboCop
318
388
  parent = anon_block.parent
319
389
  return unless parent&.type?(:any_block, :begin, :call, :casgn, :any_def)
320
390
 
321
- if (receiver = named_receiver(parent))
391
+ if (receiver = scope_receiver(parent, anon_block))
322
392
  "#{receiver.source}.#{parent.method_name}"
323
393
  elsif !parent.begin_type? || parent.parent&.any_block_type?
324
- source_location(anon_block)
394
+ anon_block_identity(anon_block)
325
395
  end
326
396
  end
327
397
 
398
+ # When a Class.new block is passed as an argument to a named-receiver
399
+ # method call (e.g. T.cast(Class.new(Base) do ... end, ...)), the
400
+ # receiver-based scope id (e.g. "T.cast") is the same for every call,
401
+ # causing false positives for methods defined in distinct anonymous
402
+ # classes. Return nil so the block falls through to the unique
403
+ # source-location-based scope id.
404
+ # Module.new blocks are excluded because they may be intentionally
405
+ # mixed into the same target via prepend/include/extend.
406
+ def scope_receiver(parent, anon_block)
407
+ return if class_new_block?(anon_block) && parent.call_type?
408
+
409
+ named_receiver(parent)
410
+ end
411
+
328
412
  def named_receiver(node)
329
413
  receiver = node.receiver
330
414
  receiver unless class_or_module_new_block?(receiver)
@@ -340,27 +424,45 @@ module RuboCop
340
424
  found_method(node, "#{singleton_receiver_node.method_name}.#{name}")
341
425
  end
342
426
 
343
- # rubocop:disable Metrics/AbcSize
344
427
  def found_method(node, method_name, scope_id: nil)
345
428
  key = method_key(node, method_name)
346
429
  key = "#{key}@#{scope_id}" if scope_id
347
430
  scope = node.each_ancestor(:rescue, :ensure).first&.type
348
431
 
349
432
  if @definitions.key?(key)
350
- if scope && !@scopes[scope].include?(key)
351
- @definitions[key] = node
352
- @scopes[scope] << key
353
- return
354
- end
355
-
356
- message = message_for_dup(node, method_name, key)
357
-
358
- add_offense(location(node), message: message)
433
+ found_redefinition(node, method_name, key, scope)
359
434
  else
360
435
  @definitions[key] = node
436
+ check_cross_file_duplicate(node, method_name) if scope_id.nil? && scope.nil?
437
+ end
438
+ end
439
+
440
+ def found_redefinition(node, method_name, key, scope)
441
+ if scope && !@scopes[scope].include?(key)
442
+ @definitions[key] = node
443
+ @scopes[scope] << key
444
+ elsif intentional_cross_file_redefinition?(node, method_name, key)
445
+ @definitions[key] = node
446
+ else
447
+ add_offense(location(node), message: message_for_dup(node, method_name, key))
361
448
  end
362
449
  end
363
- # rubocop:enable Metrics/AbcSize
450
+
451
+ # The self-alias trick (`alias foo foo` or `alias_method :foo, :foo` right before
452
+ # a `def`) suppresses Ruby's method redefinition warning, signaling an intentional
453
+ # redefinition of a method defined in another file.
454
+ def intentional_cross_file_redefinition?(node, method_name, key)
455
+ @self_aliased.include?(method_name) &&
456
+ @definitions[key].source_range.source_buffer.name !=
457
+ node.source_range.source_buffer.name
458
+ end
459
+
460
+ def track_self_alias(node, name)
461
+ scope = node.parent_module_name
462
+ return unless scope
463
+
464
+ @self_aliased << "#{humanize_scope(scope)}#{name}"
465
+ end
364
466
 
365
467
  def method_key(node, method_name)
366
468
  if (ancestor_def = node.each_ancestor(:any_def).first)
@@ -439,6 +541,84 @@ module RuboCop
439
541
  path = smart_path(range.source_buffer.name)
440
542
  "#{path}:#{range.line}"
441
543
  end
544
+
545
+ def check_cross_file_duplicate(node, method_name)
546
+ return unless project_index
547
+ return if @self_aliased.include?(method_name)
548
+ return if node.each_ancestor(:any_def).any?
549
+ return unless (prior = cross_file_prior_definition(method_name))
550
+
551
+ message = format(MSG, method: method_name,
552
+ defined: index_source_location(prior),
553
+ current: source_location(node))
554
+ add_offense(location(node), message: message)
555
+ end
556
+
557
+ def cross_file_prior_definition(method_name)
558
+ return unless (match = INDEXABLE_METHOD_NAME.match(method_name))
559
+
560
+ definitions = definitions_in_other_files(
561
+ indexed_definitions(match[:owner], match[:separator], match[:name])
562
+ )
563
+ return if definitions.empty? || cross_file_self_alias_trick?(definitions)
564
+
565
+ first_indexed_definition(definitions)
566
+ end
567
+
568
+ def indexed_definitions(owner, separator, name)
569
+ namespace = separator == '.' ? "#{owner}::<#{owner.split('::').last}>" : owner
570
+
571
+ if name.match?(/\A\w+=\z/)
572
+ # Rubydex indexes `attr_writer :foo` under `foo` rather than `foo=`, so
573
+ # writer definitions come from both the `foo=` and the `foo` declarations.
574
+ indexed_declaration_definitions(namespace, name) +
575
+ indexed_declaration_definitions(namespace, name.delete_suffix('='))
576
+ .select { |definition| writer_attr_definition?(definition) }
577
+ else
578
+ indexed_declaration_definitions(namespace, name).grep_v(Rubydex::AttrWriterDefinition)
579
+ end
580
+ end
581
+
582
+ def indexed_declaration_definitions(namespace, name)
583
+ project_index["#{namespace}##{name}()"]&.definitions.to_a
584
+ end
585
+
586
+ def writer_attr_definition?(definition)
587
+ definition.is_a?(Rubydex::AttrWriterDefinition) ||
588
+ definition.is_a?(Rubydex::AttrAccessorDefinition)
589
+ end
590
+
591
+ # An alias of the method alongside one of its definitions in another file may be
592
+ # the self-alias trick marking an intentional redefinition there, so no offense
593
+ # is registered. A genuine `alias` duplicate is still reported when the alias
594
+ # itself is inspected.
595
+ def cross_file_self_alias_trick?(definitions)
596
+ aliases, others = definitions.partition do |definition|
597
+ definition.is_a?(Rubydex::MethodAliasDefinition)
598
+ end
599
+ alias_paths = aliases.map { |definition| definition.location.to_file_path }
600
+
601
+ others.any? { |definition| alias_paths.include?(definition.location.to_file_path) }
602
+ end
603
+
604
+ def first_indexed_definition(definitions)
605
+ definitions.find { |definition| !definition.is_a?(Rubydex::MethodAliasDefinition) }
606
+ end
607
+
608
+ def index_source_location(definition)
609
+ location = definition.location
610
+ "#{smart_path(location.to_file_path)}:#{location.to_display.start_line}"
611
+ end
612
+
613
+ # Internal identity for an anonymous block, used as a scope key.
614
+ # Includes the source range's begin position to distinguish blocks
615
+ # that share the same line (e.g. two Class.new calls separated by `;`).
616
+ # The user-facing offense message still uses `source_location`, which
617
+ # shows only `path:line`.
618
+ def anon_block_identity(anon_block)
619
+ range = anon_block.source_range
620
+ "#{smart_path(range.source_buffer.name)}:#{range.line}:#{range.begin_pos}"
621
+ end
442
622
  end
443
623
  end
444
624
  end
@@ -12,6 +12,11 @@ module RuboCop
12
12
  # exception class handle `StandardError` and its subclasses,
13
13
  # but not `Exception` and its subclasses.
14
14
  #
15
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is
16
+ # installed, indirect inheritance is also detected: a class whose parent
17
+ # (defined anywhere in the project) ultimately inherits from `Exception`
18
+ # is reported, without autocorrection.
19
+ #
15
20
  # @example EnforcedStyle: standard_error (default)
16
21
  # # bad
17
22
  #
@@ -39,9 +44,11 @@ module RuboCop
39
44
  # C = Class.new(RuntimeError)
40
45
  class InheritException < Base
41
46
  include ConfigurableEnforcedStyle
47
+ include ProjectIndexHelp
42
48
  extend AutoCorrector
43
49
 
44
50
  MSG = 'Inherit from `%<prefer>s` instead of `Exception`.'
51
+ INDIRECT_MSG = 'Inherit from `%<prefer>s` instead of `Exception` (inherited via `%<via>s`).'
45
52
  PREFERRED_BASE_CLASS = {
46
53
  runtime_error: 'RuntimeError',
47
54
  standard_error: 'StandardError'
@@ -57,13 +64,20 @@ module RuboCop
57
64
  PATTERN
58
65
 
59
66
  def on_class(node)
60
- return unless node.parent_class && exception_class?(node.parent_class)
61
- return if inherit_exception_class_with_omitted_namespace?(node)
62
-
63
- message = message(node.parent_class)
64
-
65
- add_offense(node.parent_class, message: message) do |corrector|
66
- corrector.replace(node.parent_class, preferred_base_class)
67
+ parent_class = node.parent_class
68
+ return unless parent_class
69
+
70
+ if exception_class?(parent_class)
71
+ return if inherit_exception_class_with_omitted_namespace?(node)
72
+
73
+ add_offense(parent_class, message: message(parent_class)) do |corrector|
74
+ corrector.replace(parent_class, preferred_base_class)
75
+ end
76
+ elsif (via = inherits_exception_via(parent_class))
77
+ # No autocorrection: the `Exception` inheritance lives at another
78
+ # class' definition site, possibly in another file.
79
+ message = format(INDIRECT_MSG, prefer: preferred_base_class, via: via)
80
+ add_offense(parent_class, message: message)
67
81
  end
68
82
  end
69
83
 
@@ -88,6 +102,38 @@ module RuboCop
88
102
  class_node.const_name == 'Exception'
89
103
  end
90
104
 
105
+ # When `AllCops/UseProjectIndex` is enabled, indirect inheritance is
106
+ # detected by walking the parent's indexed ancestry: `Exception` itself
107
+ # is not indexed, so a chain ending in it shows up as an ancestor whose
108
+ # superclass reference is unresolved and literally named `Exception`.
109
+ # Returns the name of that ancestor, or `nil`.
110
+ def inherits_exception_via(parent_class)
111
+ return nil unless project_index && parent_class.const_type?
112
+
113
+ declaration = resolve_constant_in_index(parent_class)
114
+ return nil unless declaration.is_a?(Rubydex::Class)
115
+
116
+ exception_ancestor(declaration)&.name
117
+ rescue StandardError
118
+ nil
119
+ end
120
+
121
+ def exception_ancestor(declaration)
122
+ declaration.ancestors.find do |ancestor|
123
+ ancestor.is_a?(Rubydex::Class) && exception_superclass_reference?(ancestor)
124
+ end
125
+ end
126
+
127
+ def exception_superclass_reference?(ancestor)
128
+ ancestor.definitions.any? do |definition|
129
+ next false unless definition.is_a?(Rubydex::ClassDefinition)
130
+
131
+ superclass = definition.superclass
132
+ superclass.is_a?(Rubydex::UnresolvedConstantReference) &&
133
+ superclass.name.delete_prefix('::') == 'Exception'
134
+ end
135
+ end
136
+
91
137
  def inherit_exception_class_with_omitted_namespace?(class_node)
92
138
  return false if class_node.parent_class.namespace&.cbase_type?
93
139
 
@@ -254,6 +254,11 @@ module RuboCop
254
254
  def correct_if_node(node, cond)
255
255
  result = condition_evaluation?(node, cond)
256
256
 
257
+ # When the branch that survives the literal condition is missing,
258
+ # there is no meaningful correction, so no offense is registered.
259
+ surviving_branch = result ? node.if_branch : node.else_branch
260
+ return if surviving_branch.nil? && (node.elsif? || node.else?)
261
+
257
262
  new_node = if node.elsif? && result
258
263
  "else\n #{range_with_comments(node.if_branch).source}"
259
264
  elsif node.elsif? && !result
@@ -163,7 +163,7 @@ module RuboCop
163
163
  when :float
164
164
  node.children.last.to_f.to_s
165
165
  when :str
166
- "\\\"#{node.value.to_s.gsub('"') { '\\\\\"' }}\\\""
166
+ escape_string_content(node.value.inspect)
167
167
  when :sym
168
168
  autocorrected_value_in_hash_for_symbol(node)
169
169
  when :array
@@ -21,6 +21,13 @@ module RuboCop
21
21
  # `AllowedParentClasses` option to specify which classes should be allowed
22
22
  # *in addition to* `Object` and `BasicObject`.
23
23
  #
24
+ # When `AllCops/UseProjectIndex` is enabled and the `rubydex` gem is installed,
25
+ # the constructor check additionally consults the project-wide index: if the
26
+ # class' entire ancestry is resolvable and no ancestor defines `initialize`,
27
+ # no offense is registered, since `super` would only reach the no-op
28
+ # `Object#initialize`. Classes whose ancestry contains an unresolvable
29
+ # superclass or mixin (e.g. one defined in a gem) are still reported.
30
+ #
24
31
  # @example
25
32
  # # bad
26
33
  # class Employee < Person
@@ -83,6 +90,8 @@ module RuboCop
83
90
  # end
84
91
  #
85
92
  class MissingSuper < Base
93
+ include ProjectIndexHelp
94
+
86
95
  CONSTRUCTOR_MSG = 'Call `super` to initialize state of the parent class.'
87
96
  CALLBACK_MSG = 'Call `super` to invoke callback defined in the parent class.'
88
97
 
@@ -140,7 +149,8 @@ module RuboCop
140
149
 
141
150
  !allowed_class?(super_class)
142
151
  elsif (class_node = node.each_ancestor(:class).first)
143
- class_node.parent_class && !allowed_class?(class_node.parent_class)
152
+ class_node.parent_class && !allowed_class?(class_node.parent_class) &&
153
+ !index_verified_stateless_ancestry?(class_node)
144
154
  else
145
155
  false
146
156
  end
@@ -153,6 +163,26 @@ module RuboCop
153
163
  def allowed_classes
154
164
  @allowed_classes ||= STATELESS_CLASSES + cop_config.fetch('AllowedParentClasses', [])
155
165
  end
166
+
167
+ # With the project index, the offense is skipped when the class' whole ancestry
168
+ # is resolvable and none of the inherited ancestors defines `initialize` —
169
+ # `super` would only reach the no-op `Object#initialize`. An unresolvable
170
+ # superclass or mixin anywhere in the chain (e.g. a class from a gem) means
171
+ # the ancestry cannot be verified and the offense is kept.
172
+ def index_verified_stateless_ancestry?(class_node)
173
+ return false unless project_index
174
+
175
+ declaration = resolve_constant_in_index(class_node.identifier)
176
+ return false unless declaration.is_a?(Rubydex::Class)
177
+
178
+ ancestors = declaration.ancestors.to_a
179
+ inherited = ancestors.reject { |ancestor| ancestor.name == declaration.name }
180
+ return false if inherited.any? { |ancestor| ancestor.member('initialize()') }
181
+
182
+ # `extend` affects the singleton class and cannot introduce an
183
+ # inherited `initialize`, so unresolved extends are tolerated.
184
+ fully_resolved_index_ancestry?(declaration, ignore_extend: true)
185
+ end
156
186
  end
157
187
  end
158
188
  end