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
@@ -27,6 +27,13 @@ module RuboCop
27
27
  # alone is not reported, since the index does not see gems or the
28
28
  # standard library.
29
29
  #
30
+ # For the same reason, a namespace whose root segment names a gem in the
31
+ # bundle (`Flipper` for the `flipper` gem) is left alone: a project that
32
+ # reopens it (`module Flipper; module Adapters; ...`) makes it resolve in
33
+ # the index, while the members the gem itself defines stay invisible and
34
+ # would look like typos. Enabling `AllCops/ProjectIndexIncludesGems`
35
+ # indexes those sources and restores the check.
36
+ #
30
37
  # Names that appear as symbols or inside string literals in the same
31
38
  # file are never reported, since they usually belong to runtime
32
39
  # definitions the index cannot see (`stub_const`, `const_set`,
@@ -129,26 +136,61 @@ module RuboCop
129
136
  end
130
137
 
131
138
  def constant_typo_suggestion(node)
132
- return nil if literal_names.include?(node.short_name.to_s)
133
-
134
139
  namespace = resolve_constant_in_index(node.namespace)
135
140
  return nil unless namespace.is_a?(Rubydex::Namespace)
136
141
  return nil if namespace.find_member(node.short_name.to_s)
142
+ return nil unless complete_index_members?(namespace)
143
+ return nil if literal_names.include?(node.short_name.to_s)
137
144
 
138
145
  spell_check(node.short_name, constant_member_names(namespace))
139
146
  rescue StandardError
140
147
  nil
141
148
  end
142
149
 
150
+ # Whether the index can be trusted to list everything the namespace
151
+ # defines. A name missing from an incomplete member list is not
152
+ # evidence of a typo, so both gaps have to be ruled out first:
153
+ # an ancestor that does not resolve may contribute the name, and so
154
+ # may a gem that reopens the namespace.
155
+ def complete_index_members?(declaration)
156
+ fully_resolved_index_ancestry?(declaration) && !gem_owned_namespace?(declaration)
157
+ end
158
+
159
+ # Whether the namespace's root segment names a gem in the bundle,
160
+ # which is then free to define members the index never sees: gem
161
+ # sources are only indexed when `AllCops/ProjectIndexIncludesGems`
162
+ # is enabled. A project that reopens such a namespace to add its own
163
+ # members (`module Flipper; module Adapters; ...`) makes it resolve
164
+ # in the index while most of what it holds stays invisible.
165
+ def gem_owned_namespace?(declaration)
166
+ return false if config.for_all_cops['ProjectIndexIncludesGems']
167
+
168
+ root = declaration.name.to_s.split('::').first.to_s
169
+ bundled_gem_namespaces.include?(root.downcase)
170
+ end
171
+
172
+ # Gem names normalized towards the constant they conventionally
173
+ # provide: `flipper` for `Flipper`, `activerecord` for `ActiveRecord`.
174
+ def bundled_gem_namespaces
175
+ @bundled_gem_namespaces ||= bundled_gem_names.to_set { |name| name.delete('-_').downcase }
176
+ end
177
+
178
+ # The bundle's gems except those sourced from a local path, whose code
179
+ # lives in the project and is indexed like the rest of it — among them
180
+ # the project's own gem, which its lockfile lists in the `PATH` section.
181
+ def bundled_gem_names
182
+ (config.gem_versions_in_target || {}).keys - (config.path_sourced_gems_in_target || [])
183
+ end
184
+
143
185
  # Writers are indexed under the reader's name, so setter calls are
144
186
  # verified and spell-checked through the base name.
145
187
  def method_typo_suggestion(node)
146
188
  setter = setter_call?(node)
147
189
  base = setter ? node.method_name.to_s.delete_suffix('=') : node.method_name.to_s
148
- return nil if literal_names.include?(base)
149
190
 
150
191
  declaration = unknown_method_owner(node, base)
151
192
  return nil unless declaration
193
+ return nil if literal_names.include?(base)
152
194
 
153
195
  suggestion = spell_check(base, method_member_names(declaration))
154
196
  suggestion && setter ? "#{suggestion}=" : suggestion
@@ -156,13 +198,15 @@ module RuboCop
156
198
  nil
157
199
  end
158
200
 
159
- # The receiver's declaration when it resolves in the index, its whole
160
- # ancestry is resolved, and the method is not found — nil otherwise.
201
+ # The receiver's declaration when it resolves in the index, everything
202
+ # it can inherit or be reopened with is visible there, and the method
203
+ # is not found — nil otherwise.
161
204
  def unknown_method_owner(node, base)
162
205
  declaration = resolve_constant_in_index(node.receiver)
163
206
  return nil unless declaration.is_a?(Rubydex::Namespace)
164
207
  return nil if responds_in_index?(declaration, node.method_name.to_s, base)
165
208
  return nil unless fully_resolved_index_ancestry?(declaration)
209
+ return nil if gem_owned_namespace?(declaration)
166
210
 
167
211
  declaration
168
212
  end
@@ -191,7 +235,10 @@ module RuboCop
191
235
  if literal.sym_type?
192
236
  names << literal.value.to_s
193
237
  else
194
- literal.value.scan(LITERAL_IDENTIFIER_PATTERN) { |token| names << token }
238
+ # `scan` raises on binary string literals with invalid byte sequences.
239
+ value = literal.value
240
+ value = value.scrub unless value.valid_encoding?
241
+ value.scan(LITERAL_IDENTIFIER_PATTERN) { |token| names << token }
195
242
  end
196
243
  end
197
244
  end
@@ -53,6 +53,8 @@ module RuboCop
53
53
  end
54
54
 
55
55
  def after_send(node)
56
+ return if backrefs_unaffected?(node)
57
+
56
58
  @valid_ref = nil
57
59
 
58
60
  if regexp_first_argument?(node)
@@ -111,6 +113,19 @@ module RuboCop
111
113
  end
112
114
  end
113
115
 
116
+ # A call such as `hash[:key]` or `array[0]` cannot involve a regexp match:
117
+ # a non-string literal cannot act as a pattern (whereas `sub`, `gsub`,
118
+ # and `match` treat a string one as a pattern and do set backreferences),
119
+ # and `$~` is frame-local, so a user-defined method cannot change
120
+ # the caller's backreferences either.
121
+ def backrefs_unaffected?(send_node)
122
+ first_argument = send_node.first_argument
123
+ return false unless first_argument&.basic_literal?
124
+ return false if first_argument.str_type?
125
+
126
+ !regexp_receiver?(send_node)
127
+ end
128
+
114
129
  def regexp_first_argument?(send_node)
115
130
  send_node.first_argument&.regexp_type? \
116
131
  && REGEXP_ARGUMENT_METHODS.include?(send_node.method_name)
@@ -2,7 +2,7 @@
2
2
 
3
3
  # The Lint/RedundantCopDisableDirective cop needs to be disabled so as
4
4
  # to be able to provide a (bad) example of a redundant disable.
5
- # rubocop:disable Lint/RedundantCopDisableDirective
5
+ # rubocop:disable-next Lint/RedundantCopDisableDirective
6
6
  module RuboCop
7
7
  module Cop
8
8
  module Lint
@@ -104,17 +104,35 @@ module RuboCop
104
104
  end
105
105
 
106
106
  def each_redundant_disable(&block)
107
+ each_detached_next_directive(&block)
108
+
107
109
  cop_disabled_line_ranges.each do |cop, line_ranges|
110
+ # A pending cop that is not enabled in this run produces no
111
+ # offenses, so its directives cannot be judged - they typically
112
+ # prepare the code for the moment the cop gets enabled.
113
+ next if pending_cop_not_run?(cop)
114
+
108
115
  each_already_disabled(cop, line_ranges, &block)
109
116
  each_line_range(cop, line_ranges, &block)
110
117
  end
111
118
  end
112
119
 
120
+ def pending_cop_not_run?(cop)
121
+ # `Config#for_cop` corrects wrongly-namespaced names, but a name that
122
+ # is not registered as written never suppresses anything, so it is
123
+ # reported as unknown rather than exempted via the corrected cop.
124
+ return false unless all_cop_names.include?(cop)
125
+
126
+ cop_cfg = config.for_cop(cop)
127
+ cop_cfg['Enabled'] == 'pending' &&
128
+ !processed_source.registry.enabled_pending_cop?(cop_cfg, config, cop)
129
+ end
130
+
113
131
  def each_line_range(cop, line_ranges)
114
132
  line_ranges.each_with_index do |line_range, line_range_index|
115
133
  next if should_skip_line_range?(cop, line_range)
116
134
 
117
- comment = processed_source.comment_at_line(line_range.begin)
135
+ comment = directive_comment(line_range)
118
136
  next if skip_directive?(comment)
119
137
 
120
138
  next_range = line_ranges[line_range_index + 1]
@@ -127,6 +145,24 @@ module RuboCop
127
145
  ignore_offense?(line_range) || expected_final_disable?(cop, line_range)
128
146
  end
129
147
 
148
+ # A range opened by a `disable-next` directive starts at the statement,
149
+ # not at the directive comment above it, so ask the range itself.
150
+ def directive_comment(line_range)
151
+ if line_range.respond_to?(:directive)
152
+ line_range.directive.comment
153
+ else
154
+ processed_source.comment_at_line(line_range.begin)
155
+ end
156
+ end
157
+
158
+ # A `disable-next` with no statement attached (or misplaced at the end
159
+ # of a code line) suppresses nothing, so it is redundant by definition.
160
+ def each_detached_next_directive
161
+ processed_source.comment_config.detached_next_directives.each do |directive|
162
+ directive.raw_cop_names.each { |cop| yield directive.comment, cop }
163
+ end
164
+ end
165
+
130
166
  def skip_directive?(comment)
131
167
  directive = DirectiveComment.new(comment)
132
168
  directive.push? || directive.pop?
@@ -142,7 +178,7 @@ module RuboCop
142
178
  end
143
179
  end
144
180
 
145
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
181
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
146
182
  def each_already_disabled(cop, line_ranges)
147
183
  line_ranges.each_cons(2) do |previous_range, range|
148
184
  next if ignore_offense?(range)
@@ -152,7 +188,7 @@ module RuboCop
152
188
  # whether there are offenses or not.
153
189
  next unless followed_ranges?(previous_range, range)
154
190
 
155
- comment = processed_source.comment_at_line(range.begin)
191
+ comment = directive_comment(range)
156
192
 
157
193
  next unless comment
158
194
  # Comments disabling all cops don't count since it's reasonable
@@ -170,7 +206,6 @@ module RuboCop
170
206
  yield comment, redundant if redundant
171
207
  end
172
208
  end
173
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
174
209
 
175
210
  def find_redundant_cop(cop, range)
176
211
  cop_offenses = offenses_to_check.select { |offense| offense.cop_name == cop }
@@ -198,7 +233,11 @@ module RuboCop
198
233
  end
199
234
 
200
235
  def range_with_offense?(range, offenses = offenses_to_check)
201
- offenses.none? { |offense| range.cover?(offense.line) }
236
+ # An offense is covered when the disabled range overlaps any line of
237
+ # its span, mirroring how offenses are suppressed.
238
+ offenses.none? do |offense|
239
+ offense.line <= range.end && offense.location.last_line >= range.begin
240
+ end
202
241
  end
203
242
 
204
243
  def all_disabled?(comment)
@@ -244,14 +283,28 @@ module RuboCop
244
283
  location = DirectiveComment.new(comment).range
245
284
  cop_names = cops.sort.map { |c| describe(c) }.join(', ')
246
285
 
286
+ # An unknown cop may just not be loaded in this run (e.g. a custom
287
+ # cop whose configuration failed to load) - removing its directive
288
+ # would destroy something that cannot be restored, so only report.
289
+ # A misplaced EOL `disable-next` should be moved, not deleted.
290
+ if any_unknown_cop?(cops) || misplaced_next_directive?(comment)
291
+ return add_offense(location, message: message(cop_names))
292
+ end
293
+
247
294
  add_offense(location, message: message(cop_names)) do |corrector|
248
- range = comment_range_with_surrounding_space(location, comment.source_range)
295
+ remove_entire_comment(corrector, comment)
296
+ end
297
+ end
249
298
 
250
- if leave_free_comment?(comment, range)
251
- corrector.replace(range, ' # ')
252
- else
253
- corrector.remove(range)
254
- end
299
+ def remove_entire_comment(corrector, comment)
300
+ # Take the `--` reason with the directive; `leave_free_comment?` keeps anything else.
301
+ directive_range = DirectiveComment.new(comment).range_with_reason
302
+ range = comment_range_with_surrounding_space(directive_range, comment.source_range)
303
+
304
+ if leave_free_comment?(comment, range)
305
+ corrector.replace(range, ' # ')
306
+ else
307
+ corrector.remove(range)
255
308
  end
256
309
  end
257
310
 
@@ -261,14 +314,35 @@ module RuboCop
261
314
  ranges = cop_ranges.map { |_, r| r }
262
315
 
263
316
  cop_ranges.each do |cop, range|
264
- cop_name = describe(cop)
265
- add_offense(range, message: message(cop_name)) do |corrector|
266
- range = directive_range_in_list(range, ranges)
267
- corrector.remove(range)
268
- end
317
+ add_offense_for_cop_in_list(cop, range, ranges)
318
+ end
319
+ end
320
+
321
+ def add_offense_for_cop_in_list(cop, range, ranges)
322
+ cop_name = describe(cop)
323
+ return add_offense(range, message: message(cop_name)) if unknown_cop?(cop)
324
+
325
+ add_offense(range, message: message(cop_name)) do |corrector|
326
+ corrector.remove(directive_range_in_list(range, ranges))
269
327
  end
270
328
  end
271
329
 
330
+ def any_unknown_cop?(cops)
331
+ cops.any? { |cop| unknown_cop?(cop) }
332
+ end
333
+
334
+ def misplaced_next_directive?(comment)
335
+ directive = DirectiveComment.new(comment)
336
+ directive.disable_next? &&
337
+ !processed_source.comment_config.comment_only_line?(directive.line_number)
338
+ end
339
+
340
+ def unknown_cop?(cop)
341
+ return false if cop == 'all' || department_marker?(cop)
342
+
343
+ !all_cop_names.include?(cop)
344
+ end
345
+
272
346
  def leave_free_comment?(comment, range)
273
347
  free_comment = comment.text.gsub(range.source.strip, '')
274
348
 
@@ -303,7 +377,13 @@ module RuboCop
303
377
 
304
378
  SIMILAR_COP_NAMES_CACHE = Hash.new do |hash, cop_name|
305
379
  hash[:all_cop_names] = Registry.global.names unless hash.key?(:all_cop_names)
306
- hash[cop_name] = NameSimilarity.find_similar_name(cop_name, hash[:all_cop_names])
380
+ # A registered cop with the same base name is a better suggestion
381
+ # than anything spelling similarity can find - the most common
382
+ # mistake is qualifying a cop with the wrong department.
383
+ basename = "/#{cop_name.split('/').last}"
384
+ same_basename = hash[:all_cop_names].find { |name| name.end_with?(basename) }
385
+ hash[cop_name] =
386
+ same_basename || NameSimilarity.find_similar_name(cop_name, hash[:all_cop_names])
307
387
  end
308
388
  private_constant :SIMILAR_COP_NAMES_CACHE
309
389
 
@@ -344,4 +424,3 @@ module RuboCop
344
424
  end
345
425
  end
346
426
  end
347
- # rubocop:enable Lint/RedundantCopDisableDirective
@@ -30,21 +30,55 @@ module RuboCop
30
30
  # foo = "1"
31
31
  # # rubocop:enable all
32
32
  # baz
33
+ #
34
+ # # bad
35
+ # foo = 1
36
+ # # rubocop:pop
37
+ #
38
+ # # good
39
+ # # rubocop:push -Style/StringLiterals
40
+ # foo = "1"
41
+ # # rubocop:pop
33
42
  class RedundantCopEnableDirective < Base
34
43
  include RangeHelp
35
44
  include SurroundingSpace
36
45
  extend AutoCorrector
37
46
 
38
47
  MSG = 'Unnecessary enabling of %<cop>s.'
48
+ MSG_ORPHAN_POP = 'Unnecessary `rubocop:pop` without a matching `rubocop:push`.'
39
49
 
40
50
  def on_new_investigation
41
- return if processed_source.blank? || !processed_source.raw_source.include?('enable')
51
+ return if processed_source.blank?
42
52
 
53
+ source = processed_source.raw_source
54
+ check_extra_enables if source.include?('enable')
55
+ check_orphan_pops if source.include?('pop')
56
+ end
57
+
58
+ private
59
+
60
+ def check_extra_enables
43
61
  offenses = processed_source.comment_config.extra_enabled_comments
44
62
  offenses.each { |comment, cop_names| register_offense(comment, cop_names) }
45
63
  end
46
64
 
47
- private
65
+ def check_orphan_pops
66
+ push_depth = 0
67
+ processed_source.comments.each do |comment|
68
+ directive = DirectiveComment.new(comment)
69
+ if directive.push?
70
+ push_depth += 1
71
+ elsif directive.pop?
72
+ push_depth.zero? ? register_orphan_pop(directive) : push_depth -= 1
73
+ end
74
+ end
75
+ end
76
+
77
+ def register_orphan_pop(directive)
78
+ add_offense(directive.range, message: MSG_ORPHAN_POP) do |corrector|
79
+ corrector.remove(range_with_surrounding_space(directive.range, side: :right))
80
+ end
81
+ end
48
82
 
49
83
  def register_offense(comment, cop_names)
50
84
  directive = DirectiveComment.new(comment)
@@ -55,15 +89,21 @@ module RuboCop
55
89
  range_of_offense(comment, name),
56
90
  message: format(MSG, cop: all_or_name(name))
57
91
  ) do |corrector|
58
- if directive.match?(cop_names)
59
- corrector.remove(range_with_surrounding_space(directive.range, side: :right))
60
- else
61
- corrector.remove(range_with_comma(comment, name))
62
- end
92
+ corrector.remove(removal_range(directive, comment, cop_names, name))
63
93
  end
64
94
  end
65
95
  end
66
96
 
97
+ # When every cop on the directive is redundant the whole comment goes, `--` reason
98
+ # included; otherwise just the one cop is spliced out of the list.
99
+ def removal_range(directive, comment, cop_names, name)
100
+ if directive.match?(cop_names)
101
+ range_with_surrounding_space(directive.range_with_reason, side: :right)
102
+ else
103
+ range_with_comma(comment, name)
104
+ end
105
+ end
106
+
67
107
  def range_of_offense(comment, name)
68
108
  start_pos = comment_start(comment) + cop_name_indention(comment, name)
69
109
  range_between(start_pos, start_pos + name.size)
@@ -63,7 +63,7 @@ module RuboCop
63
63
 
64
64
  private
65
65
 
66
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
67
67
  def redundant_feature?(feature_name)
68
68
  feature_name == 'enumerator' ||
69
69
  (target_ruby_version >= 2.1 && feature_name == 'thread') ||
@@ -73,7 +73,6 @@ module RuboCop
73
73
  (target_ruby_version >= 3.2 && feature_name == 'set') ||
74
74
  (target_ruby_version >= 4.0 && feature_name == 'pathname')
75
75
  end
76
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
77
76
  end
78
77
  end
79
78
  end
@@ -208,7 +208,7 @@ module RuboCop
208
208
  add_offense(range) { |corrector| corrector.replace(range, '.') }
209
209
  end
210
210
 
211
- # rubocop:disable Metrics/AbcSize
211
+ # rubocop:disable-next Metrics/AbcSize
212
212
  def on_or(node)
213
213
  conversion_with_default?(node) do |send_node|
214
214
  range = send_node.loc.dot.begin.join(node.source_range.end)
@@ -221,7 +221,6 @@ module RuboCop
221
221
  end
222
222
  end
223
223
  end
224
- # rubocop:enable Metrics/AbcSize
225
224
 
226
225
  private
227
226
 
@@ -139,7 +139,7 @@ module RuboCop
139
139
  grandparent.array_type? && grandparent.children.size > 1
140
140
  end
141
141
 
142
- # rubocop:disable Metrics/AbcSize
142
+ # rubocop:disable-next Metrics/AbcSize
143
143
  def replacement_range_and_content(node)
144
144
  variable = node.children.first
145
145
  expression = node.source_range
@@ -157,7 +157,6 @@ module RuboCop
157
157
  [node.loc.operator, '']
158
158
  end
159
159
  end
160
- # rubocop:enable Metrics/AbcSize
161
160
 
162
161
  def array_splat?(node)
163
162
  node.children.first.array_type?
@@ -192,7 +192,7 @@ module RuboCop
192
192
  (hash (pair (sym :exception) false))
193
193
  PATTERN
194
194
 
195
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
195
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
196
196
  def on_send(node)
197
197
  return if node.arguments.any? || hash_or_set_with_block?(node)
198
198
 
@@ -208,7 +208,6 @@ module RuboCop
208
208
  corrector.remove(node.loc.dot.join(node.loc.end || node.loc.selector))
209
209
  end
210
210
  end
211
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
212
211
  alias on_csend on_send
213
212
 
214
213
  private
@@ -33,7 +33,7 @@ module RuboCop
33
33
  MSG_EACH_WITH_INDEX = 'Use `each` instead of `each_with_index`.'
34
34
  MSG_WITH_INDEX = 'Remove redundant `with_index`.'
35
35
 
36
- # rubocop:disable Metrics/AbcSize
36
+ # rubocop:disable-next Metrics/AbcSize
37
37
  def on_block(node)
38
38
  return unless node.receiver
39
39
  return if node.method?(:with_index) && !node.receiver.receiver
@@ -50,7 +50,6 @@ module RuboCop
50
50
  end
51
51
  end
52
52
  end
53
- # rubocop:enable Metrics/AbcSize
54
53
 
55
54
  alias on_numblock on_block
56
55
  alias on_itblock on_block
@@ -45,7 +45,7 @@ module RuboCop
45
45
  }
46
46
  PATTERN
47
47
 
48
- # rubocop:disable Metrics/AbcSize
48
+ # rubocop:disable-next Metrics/AbcSize
49
49
  def on_send(node)
50
50
  return unless require_safe_navigation?(node)
51
51
 
@@ -63,7 +63,6 @@ module RuboCop
63
63
  end
64
64
  end
65
65
  end
66
- # rubocop:enable Metrics/AbcSize
67
66
 
68
67
  private
69
68
 
@@ -85,7 +85,7 @@ module RuboCop
85
85
  end
86
86
  end
87
87
 
88
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
88
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
89
89
  def find_consistent_parts(grouped_operands)
90
90
  csend_in_and, csend_in_or, send_in_and, send_in_or = most_left_indices(grouped_operands)
91
91
 
@@ -99,7 +99,6 @@ module RuboCop
99
99
  ['.', csend_in_or]
100
100
  end
101
101
  end
102
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
103
102
 
104
103
  def already_appropriate_call?(operand, dot_op)
105
104
  return true if operand.safe_navigation? && dot_op == '&.'
@@ -126,7 +125,7 @@ module RuboCop
126
125
  end
127
126
  end
128
127
 
129
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
128
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
130
129
  def most_left_indices(grouped_operands)
131
130
  indices = { csend_in_and: nil, csend_in_or: nil, send_in_and: nil, send_in_or: nil }
132
131
 
@@ -139,7 +138,6 @@ module RuboCop
139
138
 
140
139
  indices.values
141
140
  end
142
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
143
141
 
144
142
  def operand_in_and?(node)
145
143
  return true if node.parent.and_type?