rubocop 1.89.0 → 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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +74 -7
  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 +46 -30
  10. data/lib/rubocop/config.rb +23 -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 +1 -2
  14. data/lib/rubocop/cop/autocorrect_logic.rb +8 -0
  15. data/lib/rubocop/cop/base.rb +34 -2
  16. data/lib/rubocop/cop/corrector.rb +3 -3
  17. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -2
  18. data/lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb +1 -2
  19. data/lib/rubocop/cop/layout/block_alignment.rb +1 -2
  20. data/lib/rubocop/cop/layout/class_structure.rb +102 -26
  21. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -2
  22. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +63 -5
  23. data/lib/rubocop/cop/layout/end_alignment.rb +1 -2
  24. data/lib/rubocop/cop/layout/extra_spacing.rb +105 -3
  25. data/lib/rubocop/cop/layout/first_argument_indentation.rb +21 -1
  26. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +1 -2
  27. data/lib/rubocop/cop/layout/line_length.rb +1 -2
  28. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -2
  29. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +5 -5
  30. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -2
  31. data/lib/rubocop/cop/layout/space_around_operators.rb +7 -0
  32. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +2 -4
  33. data/lib/rubocop/cop/lint/ambiguous_range.rb +1 -2
  34. data/lib/rubocop/cop/lint/argument_mismatch.rb +89 -0
  35. data/lib/rubocop/cop/lint/constant_reassignment.rb +17 -11
  36. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +84 -8
  37. data/lib/rubocop/cop/lint/deprecated_reference.rb +11 -2
  38. data/lib/rubocop/cop/lint/duplicate_methods.rb +112 -14
  39. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +1 -2
  40. data/lib/rubocop/cop/lint/ineffective_access_modifier.rb +1 -2
  41. data/lib/rubocop/cop/lint/interpolation_check.rb +1 -2
  42. data/lib/rubocop/cop/lint/literal_as_condition.rb +3 -6
  43. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +3 -6
  44. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +25 -12
  45. data/lib/rubocop/cop/lint/name_typo.rb +53 -6
  46. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +15 -0
  47. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +98 -19
  48. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +47 -7
  49. data/lib/rubocop/cop/lint/redundant_require_statement.rb +1 -2
  50. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -2
  51. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -2
  52. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +1 -2
  53. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -2
  54. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -2
  55. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +2 -4
  56. data/lib/rubocop/cop/lint/super_argument_mismatch.rb +179 -0
  57. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +1 -2
  58. data/lib/rubocop/cop/lint/to_enum_arguments.rb +1 -2
  59. data/lib/rubocop/cop/lint/unreachable_code.rb +1 -2
  60. data/lib/rubocop/cop/lint/unused_private_method.rb +94 -17
  61. data/lib/rubocop/cop/lint/useless_access_modifier.rb +1 -2
  62. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -2
  63. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +36 -0
  64. data/lib/rubocop/cop/lint/useless_method_definition.rb +1 -1
  65. data/lib/rubocop/cop/lint/useless_rescue.rb +1 -2
  66. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +61 -7
  67. data/lib/rubocop/cop/lint/void.rb +1 -2
  68. data/lib/rubocop/cop/lint.rb +2 -0
  69. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -2
  70. data/lib/rubocop/cop/migration/department_name.rb +1 -2
  71. data/lib/rubocop/cop/mixin/comments_help.rb +1 -2
  72. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +1 -2
  73. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +1 -2
  74. data/lib/rubocop/cop/mixin/hash_subset.rb +1 -2
  75. data/lib/rubocop/cop/mixin/indexed_method_arity.rb +94 -0
  76. data/lib/rubocop/cop/mixin/line_length_help.rb +7 -1
  77. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +51 -6
  78. data/lib/rubocop/cop/mixin/range_help.rb +1 -2
  79. data/lib/rubocop/cop/mixin/statement_modifier.rb +13 -7
  80. data/lib/rubocop/cop/mixin/trailing_comma.rb +2 -4
  81. data/lib/rubocop/cop/mixin.rb +4 -0
  82. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +16 -0
  83. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -4
  84. data/lib/rubocop/cop/naming/method_name.rb +5 -2
  85. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -2
  86. data/lib/rubocop/cop/offense.rb +27 -4
  87. data/lib/rubocop/cop/registry.rb +34 -10
  88. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -2
  89. data/lib/rubocop/cop/style/arguments_forwarding.rb +3 -6
  90. data/lib/rubocop/cop/style/array_first_last.rb +1 -2
  91. data/lib/rubocop/cop/style/bitwise_predicate.rb +1 -2
  92. data/lib/rubocop/cop/style/block_delimiters.rb +3 -6
  93. data/lib/rubocop/cop/style/case_like_if.rb +2 -4
  94. data/lib/rubocop/cop/style/class_and_module_children.rb +1 -2
  95. data/lib/rubocop/cop/style/combinable_loops.rb +1 -2
  96. data/lib/rubocop/cop/style/concat_array_literals.rb +1 -2
  97. data/lib/rubocop/cop/style/conditional_assignment.rb +1 -2
  98. data/lib/rubocop/cop/style/constant_visibility.rb +1 -2
  99. data/lib/rubocop/cop/style/copyright.rb +1 -2
  100. data/lib/rubocop/cop/style/directive_scope.rb +171 -0
  101. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +36 -10
  102. data/lib/rubocop/cop/style/empty_case_condition.rb +1 -2
  103. data/lib/rubocop/cop/style/format_string_token.rb +1 -2
  104. data/lib/rubocop/cop/style/guard_clause.rb +1 -2
  105. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -2
  106. data/lib/rubocop/cop/style/hash_syntax.rb +1 -2
  107. data/lib/rubocop/cop/style/identical_conditional_branches.rb +2 -4
  108. data/lib/rubocop/cop/style/if_inside_else.rb +1 -2
  109. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -2
  110. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +1 -2
  111. data/lib/rubocop/cop/style/map_to_hash.rb +1 -2
  112. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +2 -4
  113. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +1 -2
  114. data/lib/rubocop/cop/style/multiline_method_signature.rb +1 -2
  115. data/lib/rubocop/cop/style/negated_if_else_condition.rb +1 -2
  116. data/lib/rubocop/cop/style/negative_array_index.rb +2 -4
  117. data/lib/rubocop/cop/style/nil_comparison.rb +1 -2
  118. data/lib/rubocop/cop/style/operator_method_call.rb +1 -2
  119. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -2
  120. data/lib/rubocop/cop/style/redundant_assignment.rb +1 -2
  121. data/lib/rubocop/cop/style/redundant_condition.rb +2 -4
  122. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +1 -2
  123. data/lib/rubocop/cop/style/redundant_each.rb +1 -2
  124. data/lib/rubocop/cop/style/redundant_format.rb +2 -4
  125. data/lib/rubocop/cop/style/redundant_parentheses.rb +2 -4
  126. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +1 -2
  127. data/lib/rubocop/cop/style/redundant_return.rb +1 -2
  128. data/lib/rubocop/cop/style/redundant_self_assignment.rb +1 -2
  129. data/lib/rubocop/cop/style/redundant_string_escape.rb +1 -2
  130. data/lib/rubocop/cop/style/rescue_modifier.rb +1 -2
  131. data/lib/rubocop/cop/style/safe_navigation.rb +1 -2
  132. data/lib/rubocop/cop/style/sample.rb +11 -6
  133. data/lib/rubocop/cop/style/select_by_regexp.rb +1 -2
  134. data/lib/rubocop/cop/style/semicolon.rb +2 -4
  135. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -2
  136. data/lib/rubocop/cop/style/sole_nested_conditional.rb +1 -2
  137. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +2 -0
  138. data/lib/rubocop/cop/style/string_concatenation.rb +18 -5
  139. data/lib/rubocop/cop/style/super_arguments.rb +1 -2
  140. data/lib/rubocop/cop/style/symbol_proc.rb +1 -2
  141. data/lib/rubocop/cop/style/time_now.rb +41 -0
  142. data/lib/rubocop/cop/style/yoda_condition.rb +1 -2
  143. data/lib/rubocop/cop/style.rb +2 -0
  144. data/lib/rubocop/cop/team.rb +11 -3
  145. data/lib/rubocop/cop/util.rb +2 -4
  146. data/lib/rubocop/cop/variable_force/variable.rb +1 -2
  147. data/lib/rubocop/cops_documentation_generator.rb +3 -6
  148. data/lib/rubocop/directive_comment.rb +37 -3
  149. data/lib/rubocop/formatter/emacs_style_formatter.rb +3 -1
  150. data/lib/rubocop/formatter/html_formatter.rb +2 -4
  151. data/lib/rubocop/formatter/json_formatter.rb +11 -6
  152. data/lib/rubocop/formatter/junit_formatter.rb +26 -9
  153. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -2
  154. data/lib/rubocop/formatter/simple_text_formatter.rb +4 -3
  155. data/lib/rubocop/formatter/tap_formatter.rb +3 -1
  156. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -2
  157. data/lib/rubocop/lockfile.rb +18 -0
  158. data/lib/rubocop/lsp/diagnostic.rb +4 -8
  159. data/lib/rubocop/lsp/routes.rb +48 -19
  160. data/lib/rubocop/mcp/server.rb +1 -2
  161. data/lib/rubocop/options.rb +10 -6
  162. data/lib/rubocop/path_util.rb +1 -2
  163. data/lib/rubocop/plugin/configuration_integrator.rb +6 -3
  164. data/lib/rubocop/rake_task.rb +1 -2
  165. data/lib/rubocop/rspec/expect_offense.rb +2 -4
  166. data/lib/rubocop/rspec/shared_contexts.rb +4 -2
  167. data/lib/rubocop/runner.rb +26 -23
  168. data/lib/rubocop/server/cache.rb +1 -2
  169. data/lib/rubocop/server/cli.rb +2 -4
  170. data/lib/rubocop/server.rb +14 -0
  171. data/lib/rubocop/target_finder.rb +4 -1
  172. data/lib/rubocop/todo_audit.rb +111 -0
  173. data/lib/rubocop/version.rb +3 -5
  174. data/lib/rubocop.rb +3 -0
  175. data/lib/ruby_lsp/rubocop/addon.rb +1 -2
  176. metadata +13 -5
@@ -42,29 +42,70 @@ module RuboCop
42
42
  # # good
43
43
  # # rubocop:disable Layout/LineLength -- comment
44
44
  #
45
+ # # bad
46
+ # # rucocop:disable Layout/LineLength
47
+ #
48
+ # # good
49
+ # # rubocop:disable Layout/LineLength
50
+ #
51
+ # # bad
52
+ # # rubocop:disable Layout/LineLenght
53
+ #
54
+ # # good
55
+ # # rubocop:disable Layout/LineLength
56
+ #
45
57
  class CopDirectiveSyntax < Base
46
58
  COMMON_MSG = 'Malformed directive comment detected.'
47
59
 
48
60
  MISSING_MODE_NAME_MSG = 'The mode name is missing.'
49
- INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `todo`, `push`, or `pop`.' # rubocop:disable Layout/LineLength
61
+ INVALID_MODE_NAME_MSG = 'The mode name must be one of `enable`, `disable`, `disable-next`, `todo`, `todo-next`, `push`, or `pop`.' # rubocop:disable Layout/LineLength
50
62
  MISSING_COP_NAME_MSG = 'The cop name is missing.'
51
63
  MALFORMED_COP_NAMES_MSG = 'Cop names must be separated by commas. ' \
52
64
  'Comment in the directive must start with `--`.'
65
+ NEXT_DIRECTIVE_AT_EOL_MSG = 'A `-next` directive must be on its own line, above the ' \
66
+ 'statement it applies to.'
67
+ INVALID_KEYWORD_MSG = 'The directive keyword must be `rubocop`, not `%<keyword>s`.'
68
+ UNKNOWN_COP_MSG = 'Unknown cop name `%<name>s`%<suggestion>s.'
69
+
70
+ # A comment that looks like an attempted directive: any keyword
71
+ # followed by a colon and a valid mode name.
72
+ NEAR_MISS_KEYWORD_REGEXP = /
73
+ \A\#+\s*(?<keyword>[A-Za-z][\w-]*)\s*:\s*
74
+ (?:#{DirectiveComment::MODES_PATTERN})\b
75
+ /x.freeze
53
76
 
54
77
  def on_new_investigation
55
78
  processed_source.comments.each do |comment|
56
79
  directive_comment = DirectiveComment.new(comment)
57
- next unless directive_comment.start_with_marker?
58
- next unless directive_comment.malformed?
59
-
60
- message = offense_message(directive_comment)
61
- add_offense(comment, message: message)
80
+ if directive_comment.start_with_marker?
81
+ check_directive(comment, directive_comment)
82
+ elsif (keyword = near_miss_keyword(comment))
83
+ message = format("#{COMMON_MSG} #{INVALID_KEYWORD_MSG}", keyword: keyword)
84
+ add_offense(comment, message: message)
85
+ end
62
86
  end
63
87
  end
64
88
 
65
89
  private
66
90
 
67
- # rubocop:disable Metrics/MethodLength
91
+ def check_directive(comment, directive_comment)
92
+ if directive_comment.malformed?
93
+ add_offense(comment, message: offense_message(directive_comment))
94
+ elsif misplaced_next_directive?(directive_comment)
95
+ add_offense(comment, message: NEXT_DIRECTIVE_AT_EOL_MSG)
96
+ elsif (name = unknown_cop_name(directive_comment))
97
+ add_offense(comment, message: unknown_cop_message(directive_comment, name))
98
+ end
99
+ end
100
+
101
+ # An EOL `disable-next` is not honored - it must fail loudly instead
102
+ # of silently doing nothing.
103
+ def misplaced_next_directive?(directive_comment)
104
+ directive_comment.disable_next? &&
105
+ !processed_source.comment_config.comment_only_line?(directive_comment.line_number)
106
+ end
107
+
108
+ # rubocop:disable-next Metrics/MethodLength
68
109
  def offense_message(directive_comment)
69
110
  comment = directive_comment.comment
70
111
  after_marker = comment.text.sub(DirectiveComment::DIRECTIVE_MARKER_REGEXP, '')
@@ -81,7 +122,42 @@ module RuboCop
81
122
 
82
123
  "#{COMMON_MSG} #{additional_msg}"
83
124
  end
84
- # rubocop:enable Metrics/MethodLength
125
+
126
+ def near_miss_keyword(comment)
127
+ match = comment.text.match(NEAR_MISS_KEYWORD_REGEXP)
128
+ return unless match
129
+
130
+ keyword = match[:keyword]
131
+ return if keyword == 'rubocop'
132
+
133
+ keyword if similar_to_rubocop?(keyword)
134
+ end
135
+
136
+ def similar_to_rubocop?(keyword)
137
+ return true if keyword.casecmp?('rubocop')
138
+ # `DidYouMean` is not always available - see `NameSimilarity`.
139
+ return false unless defined?(DidYouMean::Levenshtein)
140
+
141
+ DidYouMean::Levenshtein.distance(keyword.downcase, 'rubocop') <= 2
142
+ end
143
+
144
+ def unknown_cop_name(directive_comment)
145
+ return if directive_comment.push? || directive_comment.pop? || directive_comment.all_cops?
146
+
147
+ registry = directive_comment.cop_registry
148
+ directive_comment.raw_cop_names.find do |name|
149
+ next false if registry.department?(name)
150
+
151
+ qualified = registry.qualified_cop_name(name, nil, warn: false)
152
+ !registry.contains_cop_matching?([qualified])
153
+ end
154
+ end
155
+
156
+ def unknown_cop_message(directive_comment, name)
157
+ similar = NameSimilarity.find_similar_name(name, directive_comment.cop_registry.names)
158
+ suggestion = similar ? " (did you mean `#{similar}`?)" : ''
159
+ format(UNKNOWN_COP_MSG, name: name, suggestion: suggestion)
160
+ end
85
161
  end
86
162
  end
87
163
  end
@@ -150,10 +150,19 @@ module RuboCop
150
150
  false
151
151
  end
152
152
 
153
+ # Enumerated in a single pass that stops at the first non-deprecated
154
+ # definition, since materializing every definition of a declaration
155
+ # that is almost never deprecated dominates the cop's cost.
153
156
  def deprecated?(declaration)
154
- definitions = declaration.definitions.to_a
157
+ any = false
155
158
 
156
- definitions.any? && definitions.all?(&:deprecated?)
159
+ declaration.definitions.each do |definition|
160
+ return false unless definition.deprecated?
161
+
162
+ any = true
163
+ end
164
+
165
+ any
157
166
  end
158
167
 
159
168
  # References from a definition that is itself documented as deprecated
@@ -19,7 +19,22 @@ module RuboCop
19
19
  # file is wrapped in a conditional, so a platform-specific redefinition in
20
20
  # another file may still be reported. Aliasing the method to itself (see above)
21
21
  # before redefining marks the redefinition as intentional and is respected
22
- # across files.
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`.
23
38
  #
24
39
  # @example
25
40
  #
@@ -129,6 +144,24 @@ module RuboCop
129
144
  # delegate :foo, to: :bar
130
145
  # end
131
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
+ #
132
165
  # @example DelegatingMethods: ['delegate', 'expose'] (default: ['delegate'])
133
166
  # # A project's own `delegate`-shaped macros can be registered so the
134
167
  # # methods they define are recognized (with
@@ -160,13 +193,14 @@ module RuboCop
160
193
  super
161
194
  @definitions = {}
162
195
  @scopes = Hash.new { |hash, key| hash[key] = [] }
163
- @self_aliased = Set.new
196
+ @intentionally_redefined = Set.new
164
197
  end
165
198
 
166
199
  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
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
170
204
  super
171
205
  end
172
206
 
@@ -199,7 +233,7 @@ module RuboCop
199
233
  return unless name && original_name
200
234
 
201
235
  if name == original_name
202
- track_self_alias(node, name)
236
+ track_intentional_redefinition(node, name)
203
237
  return
204
238
  end
205
239
  return if node.ancestors.any?(&:if_type?)
@@ -241,6 +275,15 @@ module RuboCop
241
275
  )
242
276
  PATTERN
243
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
+
244
287
  # @!method sym_name(node)
245
288
  def_node_matcher :sym_name, '(sym $_name)'
246
289
 
@@ -263,7 +306,7 @@ module RuboCop
263
306
 
264
307
  if name && original_name
265
308
  if name == original_name
266
- track_self_alias(node, name)
309
+ track_intentional_redefinition(node, name)
267
310
  return
268
311
  end
269
312
  return if inside_condition?(node)
@@ -283,6 +326,12 @@ module RuboCop
283
326
  return if inside_condition?(node)
284
327
 
285
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?
286
335
  end
287
336
  end
288
337
 
@@ -441,27 +490,39 @@ module RuboCop
441
490
  if scope && !@scopes[scope].include?(key)
442
491
  @definitions[key] = node
443
492
  @scopes[scope] << key
444
- elsif intentional_cross_file_redefinition?(node, method_name, key)
493
+ elsif intentional_cross_file_redefinition?(node, method_name, key) ||
494
+ allowed_cross_file_redefinition?(node, key)
445
495
  @definitions[key] = node
446
496
  else
447
497
  add_offense(location(node), message: message_for_dup(node, method_name, key))
448
498
  end
449
499
  end
450
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
+
451
511
  # The self-alias trick (`alias foo foo` or `alias_method :foo, :foo` right before
452
512
  # a `def`) suppresses Ruby's method redefinition warning, signaling an intentional
453
- # redefinition of a method defined in another file.
513
+ # redefinition of a method defined in another file. Active Support's redefinition
514
+ # markers (tracked when `ActiveSupportExtensionsEnabled` is on) signal the same.
454
515
  def intentional_cross_file_redefinition?(node, method_name, key)
455
- @self_aliased.include?(method_name) &&
516
+ @intentionally_redefined.include?(method_name) &&
456
517
  @definitions[key].source_range.source_buffer.name !=
457
518
  node.source_range.source_buffer.name
458
519
  end
459
520
 
460
- def track_self_alias(node, name)
521
+ def track_intentional_redefinition(node, name)
461
522
  scope = node.parent_module_name
462
523
  return unless scope
463
524
 
464
- @self_aliased << "#{humanize_scope(scope)}#{name}"
525
+ @intentionally_redefined << "#{humanize_scope(scope)}#{name}"
465
526
  end
466
527
 
467
528
  def method_key(node, method_name)
@@ -544,7 +605,7 @@ module RuboCop
544
605
 
545
606
  def check_cross_file_duplicate(node, method_name)
546
607
  return unless project_index
547
- return if @self_aliased.include?(method_name)
608
+ return if @intentionally_redefined.include?(method_name)
548
609
  return if node.each_ancestor(:any_def).any?
549
610
  return unless (prior = cross_file_prior_definition(method_name))
550
611
 
@@ -559,12 +620,27 @@ module RuboCop
559
620
 
560
621
  definitions = definitions_in_other_files(
561
622
  indexed_definitions(match[:owner], match[:separator], match[:name])
562
- )
623
+ ).reject { |definition| allowed_cross_file_path?(definition.location.to_file_path) }
563
624
  return if definitions.empty? || cross_file_self_alias_trick?(definitions)
625
+ return if cross_file_redefinition_marker?(definitions, match[:name])
564
626
 
565
627
  first_indexed_definition(definitions)
566
628
  end
567
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
+
568
644
  def indexed_definitions(owner, separator, name)
569
645
  namespace = separator == '.' ? "#{owner}::<#{owner.split('::').last}>" : owner
570
646
 
@@ -601,6 +677,28 @@ module RuboCop
601
677
  others.any? { |definition| alias_paths.include?(definition.location.to_file_path) }
602
678
  end
603
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
+
604
702
  def first_indexed_definition(definitions)
605
703
  definitions.find { |definition| !definition.is_a?(Rubydex::MethodAliasDefinition) }
606
704
  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)
@@ -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,7 +248,7 @@ 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
 
@@ -281,7 +279,6 @@ module RuboCop
281
279
  ignore_node(node)
282
280
  end
283
281
  end
284
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
285
282
  end
286
283
  end
287
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
@@ -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)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/RedundantCopDisableDirective
3
+ # rubocop:disable-next Lint/RedundantCopDisableDirective
4
4
  module RuboCop
5
5
  module Cop
6
6
  module Lint
@@ -55,7 +55,7 @@ module RuboCop
55
55
  class MissingCopEnableDirective < Base
56
56
  include RangeHelp
57
57
 
58
- MSG = 'Re-enable %<cop>s %<type>s with `# rubocop:enable` after disabling it.'
58
+ MSG = 'Re-enable %<cop>s %<type>s with `%<directive>s` after disabling it.'
59
59
  MSG_BOUND = 'Re-enable %<cop>s %<type>s within %<max_range>s lines after disabling it.'
60
60
 
61
61
  def on_new_investigation
@@ -72,7 +72,12 @@ module RuboCop
72
72
 
73
73
  def each_missing_enable
74
74
  processed_source.disabled_line_ranges.each do |cop, line_ranges|
75
- line_ranges.each { |line_range| yield cop, line_range }
75
+ line_ranges.each do |line_range|
76
+ # A `disable-next` scope closes itself with its statement.
77
+ next if line_range.respond_to?(:directive) && line_range.directive.disable_next?
78
+
79
+ yield cop, line_range
80
+ end
76
81
  end
77
82
  end
78
83
 
@@ -80,17 +85,22 @@ module RuboCop
80
85
  # This has to remain a strict inequality to handle
81
86
  # the case when max_range is Float::INFINITY
82
87
  return true if line_range.max - line_range.min < max_range + 2
88
+ # A name that is not registered as written (e.g. a wrongly-namespaced
89
+ # cop) suppresses nothing; `Lint/RedundantCopDisableDirective` reports
90
+ # it, so demanding its re-enablement would only add noise.
91
+ return true unless Registry.global.names.include?(cop)
92
+
93
+ expected_open_range?(cop, line_range)
94
+ end
95
+
96
+ def expected_open_range?(cop, line_range)
83
97
  # This cop is disabled in the config, it is not expected to be re-enabled
84
98
  return true if line_range.min == CommentConfig::CONFIG_DISABLED_LINE_RANGE_MIN
85
99
 
86
- cop_class = RuboCop::Cop::Registry.global.find_by_cop_name cop
87
- if cop_class &&
100
+ cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop)
101
+ !!(cop_class &&
88
102
  !processed_source.registry.enabled?(cop_class, config) &&
89
- line_range.max == Float::INFINITY
90
- return true
91
- end
92
-
93
- false
103
+ line_range.max == Float::INFINITY)
94
104
  end
95
105
 
96
106
  def max_range
@@ -98,13 +108,17 @@ module RuboCop
98
108
  end
99
109
 
100
110
  def message(cop, comment, type = 'cop')
111
+ directive = DirectiveComment.new(comment)
101
112
  if department_enabled?(cop, comment)
102
113
  type = 'department'
103
114
  cop = cop.split('/').first
104
115
  end
105
116
 
106
117
  if max_range == Float::INFINITY
107
- format(MSG, cop: cop, type: type)
118
+ # A range opened by `rubocop:push` is closed by `rubocop:pop`,
119
+ # not by `rubocop:enable`.
120
+ enabling_directive = directive.push? ? '# rubocop:pop' : '# rubocop:enable'
121
+ format(MSG, cop: cop, type: type, directive: enabling_directive)
108
122
  else
109
123
  format(MSG_BOUND, cop: cop, type: type, max_range: max_range)
110
124
  end
@@ -117,4 +131,3 @@ module RuboCop
117
131
  end
118
132
  end
119
133
  end
120
- # rubocop:enable Lint/RedundantCopDisableDirective