rubocop 1.88.2 → 1.90.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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +139 -16
  3. data/exe/rubocop +1 -1
  4. data/lib/rubocop/cached_data.rb +7 -2
  5. data/lib/rubocop/cli/command/execute_runner.rb +31 -0
  6. data/lib/rubocop/cli.rb +2 -4
  7. data/lib/rubocop/comment_config/directive_range.rb +32 -0
  8. data/lib/rubocop/comment_config/disable_next.rb +97 -0
  9. data/lib/rubocop/comment_config.rb +81 -49
  10. data/lib/rubocop/config.rb +76 -5
  11. data/lib/rubocop/config_loader.rb +1 -2
  12. data/lib/rubocop/config_loader_resolver.rb +1 -2
  13. data/lib/rubocop/config_validator.rb +39 -3
  14. data/lib/rubocop/cop/autocorrect_logic.rb +8 -0
  15. data/lib/rubocop/cop/badge.rb +8 -0
  16. data/lib/rubocop/cop/base.rb +40 -6
  17. data/lib/rubocop/cop/bundler.rb +19 -0
  18. data/lib/rubocop/cop/corrector.rb +4 -4
  19. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  20. data/lib/rubocop/cop/gemspec.rb +22 -0
  21. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  22. data/lib/rubocop/cop/generator.rb +4 -0
  23. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -2
  24. data/lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb +1 -2
  25. data/lib/rubocop/cop/layout/block_alignment.rb +1 -2
  26. data/lib/rubocop/cop/layout/class_structure.rb +108 -26
  27. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -2
  28. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +63 -5
  29. data/lib/rubocop/cop/layout/end_alignment.rb +1 -2
  30. data/lib/rubocop/cop/layout/extra_spacing.rb +105 -3
  31. data/lib/rubocop/cop/layout/first_argument_indentation.rb +21 -1
  32. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  33. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  34. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  35. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +1 -2
  36. data/lib/rubocop/cop/layout/line_length.rb +84 -12
  37. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -2
  38. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  39. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  40. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -2
  41. data/lib/rubocop/cop/layout/space_around_operators.rb +7 -0
  42. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +31 -34
  43. data/lib/rubocop/cop/layout.rb +114 -0
  44. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  45. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  46. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  47. data/lib/rubocop/cop/lint/ambiguous_range.rb +1 -2
  48. data/lib/rubocop/cop/lint/argument_mismatch.rb +89 -0
  49. data/lib/rubocop/cop/lint/constant_reassignment.rb +18 -21
  50. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  51. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +84 -8
  52. data/lib/rubocop/cop/lint/deprecated_reference.rb +194 -0
  53. data/lib/rubocop/cop/lint/duplicate_methods.rb +300 -22
  54. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +1 -2
  55. data/lib/rubocop/cop/lint/ineffective_access_modifier.rb +1 -2
  56. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  57. data/lib/rubocop/cop/lint/interpolation_check.rb +1 -2
  58. data/lib/rubocop/cop/lint/literal_as_condition.rb +8 -6
  59. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +4 -7
  60. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +25 -12
  61. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  62. data/lib/rubocop/cop/lint/name_typo.rb +270 -0
  63. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +15 -4
  64. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +98 -19
  65. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +47 -7
  66. data/lib/rubocop/cop/lint/redundant_require_statement.rb +1 -2
  67. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -2
  68. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -2
  69. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +1 -2
  70. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -2
  71. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +8 -2
  72. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +2 -4
  73. data/lib/rubocop/cop/lint/super_argument_mismatch.rb +179 -0
  74. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +1 -2
  75. data/lib/rubocop/cop/lint/to_enum_arguments.rb +38 -3
  76. data/lib/rubocop/cop/lint/unreachable_code.rb +1 -2
  77. data/lib/rubocop/cop/lint/unused_private_method.rb +234 -0
  78. data/lib/rubocop/cop/lint/useless_access_modifier.rb +1 -2
  79. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -2
  80. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +36 -0
  81. data/lib/rubocop/cop/lint/useless_method_definition.rb +1 -1
  82. data/lib/rubocop/cop/lint/useless_rescue.rb +1 -2
  83. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  84. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +61 -7
  85. data/lib/rubocop/cop/lint/void.rb +18 -8
  86. data/lib/rubocop/cop/lint.rb +178 -0
  87. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -2
  88. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  89. data/lib/rubocop/cop/metrics.rb +31 -0
  90. data/lib/rubocop/cop/migration/department_name.rb +1 -6
  91. data/lib/rubocop/cop/migration.rb +13 -0
  92. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  93. data/lib/rubocop/cop/mixin/comments_help.rb +1 -2
  94. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +1 -2
  95. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  96. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  97. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +1 -2
  98. data/lib/rubocop/cop/mixin/hash_subset.rb +1 -2
  99. data/lib/rubocop/cop/mixin/indexed_method_arity.rb +94 -0
  100. data/lib/rubocop/cop/mixin/line_length_help.rb +7 -1
  101. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +51 -6
  102. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  103. data/lib/rubocop/cop/mixin/range_help.rb +1 -2
  104. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  105. data/lib/rubocop/cop/mixin/statement_modifier.rb +46 -4
  106. data/lib/rubocop/cop/mixin/trailing_comma.rb +2 -4
  107. data/lib/rubocop/cop/mixin.rb +82 -77
  108. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  109. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +16 -0
  110. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -4
  111. data/lib/rubocop/cop/naming/method_name.rb +5 -2
  112. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  113. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -2
  114. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  115. data/lib/rubocop/cop/naming.rb +31 -0
  116. data/lib/rubocop/cop/offense.rb +37 -10
  117. data/lib/rubocop/cop/registry.rb +180 -46
  118. data/lib/rubocop/cop/security.rb +19 -0
  119. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -2
  120. data/lib/rubocop/cop/style/arguments_forwarding.rb +3 -6
  121. data/lib/rubocop/cop/style/array_first_last.rb +1 -2
  122. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +1 -2
  124. data/lib/rubocop/cop/style/block_delimiters.rb +3 -6
  125. data/lib/rubocop/cop/style/case_like_if.rb +2 -4
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +79 -20
  127. data/lib/rubocop/cop/style/combinable_loops.rb +1 -2
  128. data/lib/rubocop/cop/style/concat_array_literals.rb +1 -2
  129. data/lib/rubocop/cop/style/conditional_assignment.rb +23 -2
  130. data/lib/rubocop/cop/style/constant_visibility.rb +1 -2
  131. data/lib/rubocop/cop/style/copyright.rb +5 -1
  132. data/lib/rubocop/cop/style/directive_scope.rb +171 -0
  133. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +76 -12
  134. data/lib/rubocop/cop/style/documentation.rb +47 -0
  135. data/lib/rubocop/cop/style/empty_case_condition.rb +1 -2
  136. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  137. data/lib/rubocop/cop/style/format_string_token.rb +1 -2
  138. data/lib/rubocop/cop/style/guard_clause.rb +1 -2
  139. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -2
  140. data/lib/rubocop/cop/style/hash_syntax.rb +1 -2
  141. data/lib/rubocop/cop/style/identical_conditional_branches.rb +2 -4
  142. data/lib/rubocop/cop/style/if_inside_else.rb +1 -2
  143. data/lib/rubocop/cop/style/if_unless_modifier.rb +8 -50
  144. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +1 -2
  145. data/lib/rubocop/cop/style/lambda.rb +4 -0
  146. data/lib/rubocop/cop/style/map_to_hash.rb +1 -2
  147. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +22 -7
  148. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +1 -2
  149. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  150. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  151. data/lib/rubocop/cop/style/multiline_method_signature.rb +1 -2
  152. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  153. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  154. data/lib/rubocop/cop/style/negated_if_else_condition.rb +1 -2
  155. data/lib/rubocop/cop/style/negative_array_index.rb +3 -3
  156. data/lib/rubocop/cop/style/nil_comparison.rb +1 -2
  157. data/lib/rubocop/cop/style/operator_method_call.rb +1 -2
  158. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -2
  159. data/lib/rubocop/cop/style/redundant_assignment.rb +12 -2
  160. data/lib/rubocop/cop/style/redundant_condition.rb +2 -4
  161. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  162. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +1 -2
  163. data/lib/rubocop/cop/style/redundant_each.rb +1 -2
  164. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  165. data/lib/rubocop/cop/style/redundant_format.rb +14 -7
  166. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  167. data/lib/rubocop/cop/style/redundant_parentheses.rb +65 -67
  168. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +1 -2
  169. data/lib/rubocop/cop/style/redundant_return.rb +1 -2
  170. data/lib/rubocop/cop/style/redundant_self_assignment.rb +1 -2
  171. data/lib/rubocop/cop/style/redundant_string_escape.rb +1 -2
  172. data/lib/rubocop/cop/style/rescue_modifier.rb +1 -2
  173. data/lib/rubocop/cop/style/safe_navigation.rb +11 -2
  174. data/lib/rubocop/cop/style/sample.rb +11 -6
  175. data/lib/rubocop/cop/style/select_by_regexp.rb +1 -2
  176. data/lib/rubocop/cop/style/semicolon.rb +2 -4
  177. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -2
  178. data/lib/rubocop/cop/style/sole_nested_conditional.rb +21 -5
  179. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +2 -0
  180. data/lib/rubocop/cop/style/static_class.rb +21 -0
  181. data/lib/rubocop/cop/style/string_concatenation.rb +18 -5
  182. data/lib/rubocop/cop/style/super_arguments.rb +1 -2
  183. data/lib/rubocop/cop/style/symbol_proc.rb +1 -2
  184. data/lib/rubocop/cop/style/time_now.rb +41 -0
  185. data/lib/rubocop/cop/style/yoda_condition.rb +1 -2
  186. data/lib/rubocop/cop/style.rb +312 -0
  187. data/lib/rubocop/cop/team.rb +59 -6
  188. data/lib/rubocop/cop/util.rb +2 -4
  189. data/lib/rubocop/cop/variable_force/variable.rb +19 -8
  190. data/lib/rubocop/cops_documentation_generator.rb +4 -7
  191. data/lib/rubocop/directive_comment.rb +39 -3
  192. data/lib/rubocop/feature_loader.rb +0 -6
  193. data/lib/rubocop/formatter/emacs_style_formatter.rb +3 -1
  194. data/lib/rubocop/formatter/html_formatter.rb +2 -4
  195. data/lib/rubocop/formatter/json_formatter.rb +11 -6
  196. data/lib/rubocop/formatter/junit_formatter.rb +26 -9
  197. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -2
  198. data/lib/rubocop/formatter/simple_text_formatter.rb +4 -3
  199. data/lib/rubocop/formatter/tap_formatter.rb +3 -1
  200. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -2
  201. data/lib/rubocop/lockfile.rb +18 -0
  202. data/lib/rubocop/lsp/diagnostic.rb +4 -8
  203. data/lib/rubocop/lsp/routes.rb +89 -22
  204. data/lib/rubocop/lsp/runtime.rb +4 -0
  205. data/lib/rubocop/lsp/server.rb +4 -0
  206. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  207. data/lib/rubocop/mcp/server.rb +1 -2
  208. data/lib/rubocop/options.rb +10 -6
  209. data/lib/rubocop/path_util.rb +1 -2
  210. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  211. data/lib/rubocop/plugin/configuration_integrator.rb +6 -3
  212. data/lib/rubocop/project_index_loader.rb +16 -0
  213. data/lib/rubocop/rake_task.rb +1 -2
  214. data/lib/rubocop/result_cache.rb +6 -4
  215. data/lib/rubocop/rspec/expect_offense.rb +2 -4
  216. data/lib/rubocop/rspec/shared_contexts.rb +16 -2
  217. data/lib/rubocop/runner.rb +71 -43
  218. data/lib/rubocop/server/cache.rb +11 -7
  219. data/lib/rubocop/server/cli.rb +2 -4
  220. data/lib/rubocop/server.rb +14 -0
  221. data/lib/rubocop/target_finder.rb +4 -1
  222. data/lib/rubocop/target_ruby.rb +1 -1
  223. data/lib/rubocop/todo_audit.rb +111 -0
  224. data/lib/rubocop/version.rb +3 -5
  225. data/lib/rubocop.rb +20 -626
  226. data/lib/ruby_lsp/rubocop/addon.rb +1 -2
  227. metadata +28 -5
@@ -10,6 +10,32 @@ 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. With `AllCops/ActiveSupportExtensionsEnabled: true`, Active
23
+ # Support's redefinition markers (`silence_redefinition_of_method` and
24
+ # `redefine_method`) are honored the same way.
25
+ #
26
+ # NOTE: Methods defined with `define_method` are not recorded in the project
27
+ # index, so a duplicate whose other definition uses `define_method` cannot
28
+ # be detected across files.
29
+ #
30
+ # Cross-file duplicates whose other definition lives in a file matching one
31
+ # of the `AllowedCrossFilePaths` patterns are not reported. This suits files
32
+ # that redefine application methods but are never loaded together with them,
33
+ # such as standalone scripts. Patterns are matched with the same glob (or
34
+ # regexp) semantics as `Exclude`, against the other file's path relative to
35
+ # the directory of the `.rubocop.yml` that configures the cop; absolute
36
+ # patterns are matched against the absolute path. Offenses inside such files
37
+ # themselves are best silenced with an ordinary per-cop `Exclude`.
38
+ #
13
39
  # @example
14
40
  #
15
41
  # # bad
@@ -118,16 +144,64 @@ module RuboCop
118
144
  # delegate :foo, to: :bar
119
145
  # end
120
146
  #
147
+ # # good - Active Support's redefinition markers signal an intentional
148
+ # # redefinition of a method defined in another file
149
+ # silence_redefinition_of_method :foo
150
+ # def foo
151
+ # 1
152
+ # end
153
+ #
154
+ # @example AllowedCrossFilePaths: ['script/**/*'] (default: [])
155
+ # # Cross-file duplicates whose other definition lives in a file
156
+ # # matching one of the patterns are not reported.
157
+ #
158
+ # # good - assuming `AppHelper#format` is also defined in
159
+ # # `script/backfill.rb`, which is never loaded with this file
160
+ # class AppHelper
161
+ # def format
162
+ # end
163
+ # end
164
+ #
165
+ # @example DelegatingMethods: ['delegate', 'expose'] (default: ['delegate'])
166
+ # # A project's own `delegate`-shaped macros can be registered so the
167
+ # # methods they define are recognized (with
168
+ # # `AllCops/ActiveSupportExtensionsEnabled: true`).
169
+ #
170
+ # # bad
171
+ # def foo
172
+ # 1
173
+ # end
174
+ #
175
+ # expose :foo, to: :bar
176
+ #
121
177
  class DuplicateMethods < Base # rubocop:disable Metrics/ClassLength
178
+ include ProjectIndexHelp
179
+
122
180
  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
181
+
182
+ # Method names the cop registers that can be looked up in the project index:
183
+ # a fully qualified namespace followed by `#` (instance) or `.` (singleton)
184
+ # and the method name.
185
+ INDEXABLE_METHOD_NAME =
186
+ /\A(?<owner>[A-Z]\w*(?:::[A-Z]\w*)*)(?<separator>[#.])(?<name>[^#.]+)\z/.freeze
187
+ # No `RESTRICT_ON_SEND`: the delegating method names are configurable
188
+ # (`DelegatingMethods`), so `on_send` must see every call. The handler
189
+ # returns quickly for calls that are not method definitions, and
190
+ # benchmarks show no measurable cost over the previous fixed list.
126
191
 
127
192
  def initialize(config = nil, options = nil)
128
193
  super
129
194
  @definitions = {}
130
195
  @scopes = Hash.new { |hash, key| hash[key] = [] }
196
+ @intentionally_redefined = Set.new
197
+ end
198
+
199
+ def on_new_investigation
200
+ # The self-alias trick and Active Support's redefinition markers declare
201
+ # an intentional redefinition only within the file that uses them, so the
202
+ # tracked names do not carry over.
203
+ @intentionally_redefined = Set.new
204
+ super
131
205
  end
132
206
 
133
207
  def on_def(node)
@@ -157,7 +231,11 @@ module RuboCop
157
231
  def on_alias(node)
158
232
  name, original_name = method_alias?(node)
159
233
  return unless name && original_name
160
- return if name == original_name
234
+
235
+ if name == original_name
236
+ track_intentional_redefinition(node, name)
237
+ return
238
+ end
161
239
  return if node.ancestors.any?(&:if_type?)
162
240
 
163
241
  found_instance_method(node, name)
@@ -168,9 +246,12 @@ module RuboCop
168
246
  (send nil? :alias_method (sym $_name) (sym $_original_name))
169
247
  PATTERN
170
248
 
171
- # @!method delegate_method?(node)
172
- def_node_matcher :delegate_method?, <<~PATTERN
173
- (send nil? :delegate
249
+ # Matches the argument shape of an Active Support `delegate` call
250
+ # (`delegate :a, :b, to: :target`), regardless of the method name; the
251
+ # name is checked separately against `DelegatingMethods`.
252
+ # @!method delegate_args(node)
253
+ def_node_matcher :delegate_args, <<~PATTERN
254
+ (send nil? _
174
255
  ({sym str} $_)+
175
256
  (hash <(pair (sym :to) {sym str}) ...>)
176
257
  )
@@ -194,6 +275,15 @@ module RuboCop
194
275
  )
195
276
  PATTERN
196
277
 
278
+ # Matches Active Support's markers of an intentional method redefinition:
279
+ # `silence_redefinition_of_method :name` suppresses Ruby's redefinition
280
+ # warning for a subsequent definition of `name`, and
281
+ # `redefine_method(:name) { ... }` both silences the warning and redefines.
282
+ # @!method active_support_redefinition_marker(node)
283
+ def_node_matcher :active_support_redefinition_marker, <<~PATTERN
284
+ (send nil? {:silence_redefinition_of_method :redefine_method} ({sym str} $_) ...)
285
+ PATTERN
286
+
197
287
  # @!method sym_name(node)
198
288
  def_node_matcher :sym_name, '(sym $_name)'
199
289
 
@@ -204,17 +294,27 @@ module RuboCop
204
294
  ...)
205
295
  PATTERN
206
296
 
297
+ # @!method class_new_block?(node)
298
+ def_node_matcher :class_new_block?, <<~PATTERN
299
+ (block
300
+ (send (const _ :Class) :new ...)
301
+ ...)
302
+ PATTERN
303
+
207
304
  def on_send(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
208
305
  name, original_name = alias_method?(node)
209
306
 
210
307
  if name && original_name
211
- return if name == original_name
308
+ if name == original_name
309
+ track_intentional_redefinition(node, name)
310
+ return
311
+ end
212
312
  return if inside_condition?(node)
213
313
 
214
314
  found_instance_method(node, name)
215
315
  elsif (attr = node.attribute_accessor?)
216
316
  on_attr(node, *attr)
217
- elsif active_support_extensions_enabled? && (names = delegate_method?(node))
317
+ elsif delegating_method?(node) && (names = delegate_args(node))
218
318
  return if inside_condition?(node)
219
319
 
220
320
  on_delegate(node, names)
@@ -226,6 +326,12 @@ module RuboCop
226
326
  return if inside_condition?(node)
227
327
 
228
328
  names.each { |name| found_instance_method(node, name) }
329
+ elsif (name = active_support_redefinition_marker(node))
330
+ # `redefine_method` takes a block; scope resolution must start from the
331
+ # block node, since `parent_module_name` cannot see through a block
332
+ # ancestor that is not a module constructor.
333
+ track_intentional_redefinition(node.block_node || node, name) if
334
+ active_support_extensions_enabled?
229
335
  end
230
336
  end
231
337
 
@@ -257,6 +363,19 @@ module RuboCop
257
363
  current: source_location(node))
258
364
  end
259
365
 
366
+ # Recognizing a delegating call as a method definition is Active
367
+ # Support behavior, so it stays gated on `ActiveSupportExtensionsEnabled`
368
+ # as it was when only `delegate` was supported. `DelegatingMethods` lets
369
+ # a project register its own `delegate`-shaped macros.
370
+ def delegating_method?(node)
371
+ active_support_extensions_enabled? &&
372
+ delegating_methods.include?(node.method_name.to_s)
373
+ end
374
+
375
+ def delegating_methods
376
+ cop_config.fetch('DelegatingMethods', ['delegate'])
377
+ end
378
+
260
379
  def on_delegate(node, method_names)
261
380
  name_prefix = delegate_prefix(node)
262
381
 
@@ -318,13 +437,27 @@ module RuboCop
318
437
  parent = anon_block.parent
319
438
  return unless parent&.type?(:any_block, :begin, :call, :casgn, :any_def)
320
439
 
321
- if (receiver = named_receiver(parent))
440
+ if (receiver = scope_receiver(parent, anon_block))
322
441
  "#{receiver.source}.#{parent.method_name}"
323
442
  elsif !parent.begin_type? || parent.parent&.any_block_type?
324
- source_location(anon_block)
443
+ anon_block_identity(anon_block)
325
444
  end
326
445
  end
327
446
 
447
+ # When a Class.new block is passed as an argument to a named-receiver
448
+ # method call (e.g. T.cast(Class.new(Base) do ... end, ...)), the
449
+ # receiver-based scope id (e.g. "T.cast") is the same for every call,
450
+ # causing false positives for methods defined in distinct anonymous
451
+ # classes. Return nil so the block falls through to the unique
452
+ # source-location-based scope id.
453
+ # Module.new blocks are excluded because they may be intentionally
454
+ # mixed into the same target via prepend/include/extend.
455
+ def scope_receiver(parent, anon_block)
456
+ return if class_new_block?(anon_block) && parent.call_type?
457
+
458
+ named_receiver(parent)
459
+ end
460
+
328
461
  def named_receiver(node)
329
462
  receiver = node.receiver
330
463
  receiver unless class_or_module_new_block?(receiver)
@@ -340,27 +473,57 @@ module RuboCop
340
473
  found_method(node, "#{singleton_receiver_node.method_name}.#{name}")
341
474
  end
342
475
 
343
- # rubocop:disable Metrics/AbcSize
344
476
  def found_method(node, method_name, scope_id: nil)
345
477
  key = method_key(node, method_name)
346
478
  key = "#{key}@#{scope_id}" if scope_id
347
479
  scope = node.each_ancestor(:rescue, :ensure).first&.type
348
480
 
349
481
  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)
482
+ found_redefinition(node, method_name, key, scope)
359
483
  else
360
484
  @definitions[key] = node
485
+ check_cross_file_duplicate(node, method_name) if scope_id.nil? && scope.nil?
361
486
  end
362
487
  end
363
- # rubocop:enable Metrics/AbcSize
488
+
489
+ def found_redefinition(node, method_name, key, scope)
490
+ if scope && !@scopes[scope].include?(key)
491
+ @definitions[key] = node
492
+ @scopes[scope] << key
493
+ elsif intentional_cross_file_redefinition?(node, method_name, key) ||
494
+ allowed_cross_file_redefinition?(node, key)
495
+ @definitions[key] = node
496
+ else
497
+ add_offense(location(node), message: message_for_dup(node, method_name, key))
498
+ end
499
+ end
500
+
501
+ # `AllowedCrossFilePaths` also covers duplicates detected without the index,
502
+ # when the cop instance carries definitions over from an earlier file of the
503
+ # same run.
504
+ def allowed_cross_file_redefinition?(node, key)
505
+ other_path = @definitions[key].source_range.source_buffer.name
506
+ return false if other_path == node.source_range.source_buffer.name
507
+
508
+ allowed_cross_file_path?(other_path)
509
+ end
510
+
511
+ # The self-alias trick (`alias foo foo` or `alias_method :foo, :foo` right before
512
+ # a `def`) suppresses Ruby's method redefinition warning, signaling an intentional
513
+ # redefinition of a method defined in another file. Active Support's redefinition
514
+ # markers (tracked when `ActiveSupportExtensionsEnabled` is on) signal the same.
515
+ def intentional_cross_file_redefinition?(node, method_name, key)
516
+ @intentionally_redefined.include?(method_name) &&
517
+ @definitions[key].source_range.source_buffer.name !=
518
+ node.source_range.source_buffer.name
519
+ end
520
+
521
+ def track_intentional_redefinition(node, name)
522
+ scope = node.parent_module_name
523
+ return unless scope
524
+
525
+ @intentionally_redefined << "#{humanize_scope(scope)}#{name}"
526
+ end
364
527
 
365
528
  def method_key(node, method_name)
366
529
  if (ancestor_def = node.each_ancestor(:any_def).first)
@@ -439,6 +602,121 @@ module RuboCop
439
602
  path = smart_path(range.source_buffer.name)
440
603
  "#{path}:#{range.line}"
441
604
  end
605
+
606
+ def check_cross_file_duplicate(node, method_name)
607
+ return unless project_index
608
+ return if @intentionally_redefined.include?(method_name)
609
+ return if node.each_ancestor(:any_def).any?
610
+ return unless (prior = cross_file_prior_definition(method_name))
611
+
612
+ message = format(MSG, method: method_name,
613
+ defined: index_source_location(prior),
614
+ current: source_location(node))
615
+ add_offense(location(node), message: message)
616
+ end
617
+
618
+ def cross_file_prior_definition(method_name)
619
+ return unless (match = INDEXABLE_METHOD_NAME.match(method_name))
620
+
621
+ definitions = definitions_in_other_files(
622
+ indexed_definitions(match[:owner], match[:separator], match[:name])
623
+ ).reject { |definition| allowed_cross_file_path?(definition.location.to_file_path) }
624
+ return if definitions.empty? || cross_file_self_alias_trick?(definitions)
625
+ return if cross_file_redefinition_marker?(definitions, match[:name])
626
+
627
+ first_indexed_definition(definitions)
628
+ end
629
+
630
+ # Cross-file duplicates whose other definition site matches one of the
631
+ # `AllowedCrossFilePaths` patterns are skipped (e.g. standalone scripts that
632
+ # are never loaded together with the code they redefine). Patterns are
633
+ # matched like `Exclude` patterns, against the path relative to the
634
+ # directory of the config file that configures the cop; absolute patterns
635
+ # are matched against the absolute path.
636
+ def allowed_cross_file_path?(path)
637
+ patterns = cop_config.fetch('AllowedCrossFilePaths', [])
638
+ return false if patterns.empty?
639
+
640
+ relative = config.path_relative_to_config(path)
641
+ patterns.any? { |pattern| match_path?(pattern, relative) || match_path?(pattern, path) }
642
+ end
643
+
644
+ def indexed_definitions(owner, separator, name)
645
+ namespace = separator == '.' ? "#{owner}::<#{owner.split('::').last}>" : owner
646
+
647
+ if name.match?(/\A\w+=\z/)
648
+ # Rubydex indexes `attr_writer :foo` under `foo` rather than `foo=`, so
649
+ # writer definitions come from both the `foo=` and the `foo` declarations.
650
+ indexed_declaration_definitions(namespace, name) +
651
+ indexed_declaration_definitions(namespace, name.delete_suffix('='))
652
+ .select { |definition| writer_attr_definition?(definition) }
653
+ else
654
+ indexed_declaration_definitions(namespace, name).grep_v(Rubydex::AttrWriterDefinition)
655
+ end
656
+ end
657
+
658
+ def indexed_declaration_definitions(namespace, name)
659
+ project_index["#{namespace}##{name}()"]&.definitions.to_a
660
+ end
661
+
662
+ def writer_attr_definition?(definition)
663
+ definition.is_a?(Rubydex::AttrWriterDefinition) ||
664
+ definition.is_a?(Rubydex::AttrAccessorDefinition)
665
+ end
666
+
667
+ # An alias of the method alongside one of its definitions in another file may be
668
+ # the self-alias trick marking an intentional redefinition there, so no offense
669
+ # is registered. A genuine `alias` duplicate is still reported when the alias
670
+ # itself is inspected.
671
+ def cross_file_self_alias_trick?(definitions)
672
+ aliases, others = definitions.partition do |definition|
673
+ definition.is_a?(Rubydex::MethodAliasDefinition)
674
+ end
675
+ alias_paths = aliases.map { |definition| definition.location.to_file_path }
676
+
677
+ others.any? { |definition| alias_paths.include?(definition.location.to_file_path) }
678
+ end
679
+
680
+ # An Active Support redefinition marker alongside one of the definitions in
681
+ # another file marks the redefinition there as intentional, like the
682
+ # self-alias trick. The index does not record the marker's method-name
683
+ # argument, so the other file's source is searched for a marker naming
684
+ # the method.
685
+ def cross_file_redefinition_marker?(definitions, name)
686
+ return false unless active_support_extensions_enabled?
687
+
688
+ definitions.map { |definition| definition.location.to_file_path }.uniq.any? do |path|
689
+ redefinition_marker_in_file?(path, name)
690
+ end
691
+ end
692
+
693
+ def redefinition_marker_in_file?(path, name)
694
+ return false unless File.readable?(path)
695
+
696
+ escaped = Regexp.escape(name)
697
+ marker = /\b(?:silence_redefinition_of_method|redefine_method)\s*\(?\s*
698
+ (?::#{escaped}|(["'])#{escaped}\1)(?![\w=!?])/x
699
+ File.read(path).scrub.match?(marker)
700
+ end
701
+
702
+ def first_indexed_definition(definitions)
703
+ definitions.find { |definition| !definition.is_a?(Rubydex::MethodAliasDefinition) }
704
+ end
705
+
706
+ def index_source_location(definition)
707
+ location = definition.location
708
+ "#{smart_path(location.to_file_path)}:#{location.to_display.start_line}"
709
+ end
710
+
711
+ # Internal identity for an anonymous block, used as a scope key.
712
+ # Includes the source range's begin position to distinguish blocks
713
+ # that share the same line (e.g. two Class.new calls separated by `;`).
714
+ # The user-facing offense message still uses `source_location`, which
715
+ # shows only `path:line`.
716
+ def anon_block_identity(anon_block)
717
+ range = anon_block.source_range
718
+ "#{smart_path(range.source_buffer.name)}:#{range.line}:#{range.begin_pos}"
719
+ end
442
720
  end
443
721
  end
444
722
  end
@@ -28,7 +28,7 @@ module RuboCop
28
28
  FOR_METHOD = ' Or, if they were intended to be separate method ' \
29
29
  'arguments, separate them with a comma.'
30
30
 
31
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
31
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
32
32
  def on_dstr(node)
33
33
  each_bad_cons(node) do |lhs_node, rhs_node|
34
34
  range = lhs_node.source_range.join(rhs_node.source_range)
@@ -52,7 +52,6 @@ module RuboCop
52
52
  end
53
53
  end
54
54
  end
55
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
56
55
 
57
56
  private
58
57
 
@@ -80,7 +80,7 @@ module RuboCop
80
80
  alternative: alternative)
81
81
  end
82
82
 
83
- # rubocop:disable Metrics/CyclomaticComplexity
83
+ # rubocop:disable-next Metrics/CyclomaticComplexity
84
84
  def ineffective_modifier(node, ignored_methods = nil, modifier = nil, &block)
85
85
  node.each_child_node do |child|
86
86
  case child.type
@@ -97,7 +97,6 @@ module RuboCop
97
97
  end
98
98
  end
99
99
  end
100
- # rubocop:enable Metrics/CyclomaticComplexity
101
100
 
102
101
  def access_modifier?(node)
103
102
  node.bare_access_modifier? && !node.method?(:module_function)
@@ -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
 
@@ -41,7 +41,7 @@ module RuboCop
41
41
 
42
42
  private
43
43
 
44
- # rubocop:disable Metrics/CyclomaticComplexity
44
+ # rubocop:disable-next Metrics/CyclomaticComplexity
45
45
  def check(node)
46
46
  return if node.parent&.regexp_type?
47
47
  return unless /(?<!\\)#\{.*\}/.match?(node.source)
@@ -51,7 +51,6 @@ module RuboCop
51
51
 
52
52
  add_offense(node) { |corrector| autocorrect(corrector, node) }
53
53
  end
54
- # rubocop:enable Metrics/CyclomaticComplexity
55
54
 
56
55
  def autocorrect(corrector, node)
57
56
  starting_token, ending_token = if node.source.include?('"')
@@ -88,7 +88,7 @@ module RuboCop
88
88
  end
89
89
  end
90
90
 
91
- # rubocop:disable Metrics/AbcSize
91
+ # rubocop:disable-next Metrics/AbcSize
92
92
  def on_while_post(node)
93
93
  return if node.condition.source == 'true'
94
94
 
@@ -102,7 +102,6 @@ module RuboCop
102
102
  end
103
103
  end
104
104
  end
105
- # rubocop:enable Metrics/AbcSize
106
105
 
107
106
  def on_until(node)
108
107
  return if node.condition.source == 'false'
@@ -118,7 +117,7 @@ module RuboCop
118
117
  end
119
118
  end
120
119
 
121
- # rubocop:disable Metrics/AbcSize
120
+ # rubocop:disable-next Metrics/AbcSize
122
121
  def on_until_post(node)
123
122
  return if node.condition.source == 'false'
124
123
 
@@ -132,7 +131,6 @@ module RuboCop
132
131
  end
133
132
  end
134
133
  end
135
- # rubocop:enable Metrics/AbcSize
136
134
 
137
135
  def on_case(case_node)
138
136
  if (cond = case_node.condition)
@@ -250,10 +248,15 @@ module RuboCop
250
248
  end
251
249
  end
252
250
 
253
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
251
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
254
252
  def correct_if_node(node, cond)
255
253
  result = condition_evaluation?(node, cond)
256
254
 
255
+ # When the branch that survives the literal condition is missing,
256
+ # there is no meaningful correction, so no offense is registered.
257
+ surviving_branch = result ? node.if_branch : node.else_branch
258
+ return if surviving_branch.nil? && (node.elsif? || node.else?)
259
+
257
260
  new_node = if node.elsif? && result
258
261
  "else\n #{range_with_comments(node.if_branch).source}"
259
262
  elsif node.elsif? && !result
@@ -276,7 +279,6 @@ module RuboCop
276
279
  ignore_node(node)
277
280
  end
278
281
  end
279
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
280
282
  end
281
283
  end
282
284
  end
@@ -24,7 +24,7 @@ module RuboCop
24
24
  MSG = 'Literal interpolation detected.'
25
25
  COMPOSITE = %i[array hash pair irange erange].freeze
26
26
 
27
- # rubocop:disable Metrics/AbcSize
27
+ # rubocop:disable-next Metrics/AbcSize
28
28
  def on_interpolation(begin_node)
29
29
  final_node = begin_node.children.last
30
30
  return unless offending?(final_node)
@@ -50,7 +50,6 @@ module RuboCop
50
50
  corrector.replace(final_node.parent, replacement)
51
51
  end
52
52
  end
53
- # rubocop:enable Metrics/AbcSize
54
53
 
55
54
  private
56
55
 
@@ -74,7 +73,7 @@ module RuboCop
74
73
  node.array_type? && grandparent.regexp_type?
75
74
  end
76
75
 
77
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
76
+ # rubocop:disable-next Metrics/MethodLength, Metrics/CyclomaticComplexity
78
77
  def autocorrected_value(node)
79
78
  case node.type
80
79
  when :int
@@ -95,7 +94,6 @@ module RuboCop
95
94
  node.source.gsub('"', '\"')
96
95
  end
97
96
  end
98
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
99
97
 
100
98
  def handle_special_regexp_chars(begin_node, value)
101
99
  parent_node = begin_node.parent
@@ -155,7 +153,7 @@ module RuboCop
155
153
  "{#{hash_string}}"
156
154
  end
157
155
 
158
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
156
+ # rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize
159
157
  def autocorrected_value_in_hash(node)
160
158
  case node.type
161
159
  when :int
@@ -163,7 +161,7 @@ module RuboCop
163
161
  when :float
164
162
  node.children.last.to_f.to_s
165
163
  when :str
166
- "\\\"#{node.value.to_s.gsub('"') { '\\\\\"' }}\\\""
164
+ escape_string_content(node.value.inspect)
167
165
  when :sym
168
166
  autocorrected_value_in_hash_for_symbol(node)
169
167
  when :array
@@ -174,7 +172,6 @@ module RuboCop
174
172
  node.source.gsub('"', '\"')
175
173
  end
176
174
  end
177
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
178
175
 
179
176
  # Does node print its own source when converted to a string?
180
177
  def prints_as_self?(node)