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
@@ -35,7 +35,7 @@ module RuboCop
35
35
  code_actions
36
36
  end
37
37
 
38
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
38
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
39
39
  def to_lsp_diagnostic(config)
40
40
  highlighted = @offense.highlighted_area
41
41
 
@@ -61,7 +61,6 @@ module RuboCop
61
61
  }
62
62
  )
63
63
  end
64
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
65
64
 
66
65
  private
67
66
 
@@ -82,7 +81,7 @@ module RuboCop
82
81
  LanguageServer::Protocol::Interface::CodeDescription.new(href: doc_url)
83
82
  end
84
83
 
85
- # rubocop:disable Metrics/MethodLength
84
+ # rubocop:disable-next Metrics/MethodLength
86
85
  def autocorrect_action
87
86
  LanguageServer::Protocol::Interface::CodeAction.new(
88
87
  title: "Autocorrect #{@offense.cop_name}",
@@ -101,9 +100,8 @@ module RuboCop
101
100
  is_preferred: true
102
101
  )
103
102
  end
104
- # rubocop:enable Metrics/MethodLength
105
103
 
106
- # rubocop:disable Metrics/MethodLength
104
+ # rubocop:disable-next Metrics/MethodLength
107
105
  def offense_replacements
108
106
  @offense.corrector.as_replacements.map do |range, replacement|
109
107
  LanguageServer::Protocol::Interface::TextEdit.new(
@@ -121,9 +119,8 @@ module RuboCop
121
119
  )
122
120
  end
123
121
  end
124
- # rubocop:enable Metrics/MethodLength
125
122
 
126
- # rubocop:disable Metrics/MethodLength
123
+ # rubocop:disable-next Metrics/MethodLength
127
124
  def disable_line_action
128
125
  LanguageServer::Protocol::Interface::CodeAction.new(
129
126
  title: "Disable #{@offense.cop_name} for this line",
@@ -141,7 +138,6 @@ module RuboCop
141
138
  )
142
139
  )
143
140
  end
144
- # rubocop:enable Metrics/MethodLength
145
141
 
146
142
  def line_disable_comment
147
143
  DisableCommentEdits.new(
@@ -151,25 +151,13 @@ module RuboCop
151
151
  return
152
152
  end
153
153
 
154
- uri = request[:params][:arguments][0][:uri]
155
- formatted = nil
156
-
157
- # The `workspace/executeCommand` is an LSP method triggered by intentional user actions,
158
- # so the user's intention for autocorrection is respected.
159
- LSP.disable { formatted = format_file(uri, command: command) }
160
-
161
- @server.write(
162
- id: request[:id],
163
- method: 'workspace/applyEdit',
164
- params: {
165
- label: label,
166
- edit: {
167
- changes: {
168
- uri => formatted
169
- }
170
- }
171
- }
172
- )
154
+ # Clients such as Eglot can invoke a command without arguments, so respond with
155
+ # an error instead of letting the server crash on a missing URI.
156
+ if (uri = document_uri_from(request))
157
+ write_formatting_edits(request, uri: uri, command: command, label: label)
158
+ else
159
+ write_invalid_arguments_error(request, command)
160
+ end
173
161
  end
174
162
 
175
163
  handle 'textDocument/willSave' do |_request|
@@ -219,6 +207,47 @@ module RuboCop
219
207
  }
220
208
  end
221
209
 
210
+ def document_uri_from(request)
211
+ argument = request.dig(:params, :arguments, 0)
212
+
213
+ argument[:uri] if argument.is_a?(Hash)
214
+ end
215
+
216
+ def write_formatting_edits(request, uri:, command:, label:)
217
+ formatted = nil
218
+
219
+ # The `workspace/executeCommand` is an LSP method triggered by intentional user actions,
220
+ # so the user's intention for autocorrection is respected.
221
+ LSP.disable { formatted = format_file(uri, command: command) }
222
+
223
+ @server.write(
224
+ id: request[:id],
225
+ method: 'workspace/applyEdit',
226
+ params: {
227
+ label: label,
228
+ edit: {
229
+ changes: {
230
+ uri => formatted
231
+ }
232
+ }
233
+ }
234
+ )
235
+ end
236
+
237
+ def write_invalid_arguments_error(request, command)
238
+ message = "Missing document URI in arguments for #{command}"
239
+
240
+ @server.write(
241
+ id: request[:id],
242
+ error: LanguageServer::Protocol::Interface::ResponseError.new(
243
+ code: LanguageServer::Protocol::Constant::ErrorCodes::INVALID_PARAMS,
244
+ message: message
245
+ )
246
+ )
247
+
248
+ Logger.log(message)
249
+ end
250
+
222
251
  def format_file(file_uri, command: nil)
223
252
  unless (text = @text_cache[file_uri])
224
253
  Logger.log("Format request arrived before text synchronized; skipping: `#{file_uri}'")
@@ -156,7 +156,7 @@ module RuboCop
156
156
  summary
157
157
  end
158
158
 
159
- # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
159
+ # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
160
160
  def build_tool(
161
161
  name:, description:,
162
162
  title:, destructive_hint:, idempotent_hint:, read_only_hint:, safety_required:
@@ -194,7 +194,6 @@ module RuboCop
194
194
  ::MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
195
195
  end
196
196
  end
197
- # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
198
197
  end
199
198
  end
200
199
  end
@@ -51,7 +51,7 @@ module RuboCop
51
51
 
52
52
  private
53
53
 
54
- # rubocop:disable Metrics/AbcSize
54
+ # rubocop:disable-next Metrics/AbcSize
55
55
  def define_options
56
56
  OptionParser.new do |opts|
57
57
  opts.banner = rainbow.wrap('Usage: rubocop [options] [file1, file2, ...]').bright
@@ -71,7 +71,6 @@ module RuboCop
71
71
  add_profile_options(opts) if RUBY_ENGINE == 'ruby' && !Platform.windows?
72
72
  end
73
73
  end
74
- # rubocop:enable Metrics/AbcSize
75
74
 
76
75
  def add_check_options(opts) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
77
76
  section(opts, 'Basic Options') do # rubocop:disable Metrics/BlockLength
@@ -132,6 +131,7 @@ module RuboCop
132
131
  option(opts, '--display-only-fail-level-offenses')
133
132
  option(opts, '--display-only-correctable')
134
133
  option(opts, '--display-only-safe-correctable')
134
+ option(opts, '--display-suppressed')
135
135
  end
136
136
  end
137
137
 
@@ -173,6 +173,8 @@ module RuboCop
173
173
  @options.replace(ConfigRegeneration.new.options.merge(@options))
174
174
  end
175
175
 
176
+ option(opts, '--report-unused-todo-entries')
177
+
176
178
  option(opts, '--exclude-limit COUNT') { @validator.validate_exclude_limit_option }
177
179
  option(opts, '--no-exclude-limit')
178
180
 
@@ -382,7 +384,7 @@ module RuboCop
382
384
  %i[only except].each { |opt| OptionsValidator.validate_cop_list(@options[opt]) }
383
385
  end
384
386
 
385
- # rubocop:disable Metrics/AbcSize
387
+ # rubocop:disable-next Metrics/AbcSize
386
388
  def validate_compatibility # rubocop:disable Metrics/MethodLength
387
389
  if only_includes_redundant_disable?
388
390
  raise OptionArgumentError, 'Lint/RedundantCopDisableDirective cannot be used with --only.'
@@ -405,7 +407,6 @@ module RuboCop
405
407
 
406
408
  raise OptionArgumentError, "Incompatible cli options: #{incompatible_options.inspect}"
407
409
  end
408
- # rubocop:enable Metrics/AbcSize
409
410
 
410
411
  def validate_auto_gen_config
411
412
  return if @options.key?(:auto_gen_config)
@@ -527,7 +528,7 @@ module RuboCop
527
528
 
528
529
  # This module contains help texts for command line options.
529
530
  # @api private
530
- # rubocop:disable Metrics/ModuleLength
531
+ # rubocop:disable-next Metrics/ModuleLength
531
532
  module OptionsHelp
532
533
  MAX_EXCL = RuboCop::Options::DEFAULT_MAXIMUM_EXCLUSION_ITEMS.to_s
533
534
  FORMATTER_OPTION_LIST = RuboCop::Formatter::FormatterSet::BUILTIN_FORMATTERS_FOR_KEYS.keys
@@ -545,6 +546,8 @@ module RuboCop
545
546
  regenerate_todo: ['Regenerate the TODO configuration file using',
546
547
  'the last configuration. If there is no existing',
547
548
  'TODO file, acts like --auto-gen-config.'],
549
+ report_unused_todo_entries: ['Also report TODO configuration file entries that',
550
+ 'are no longer needed, and fail if any are found.'],
548
551
  offense_counts: ['Include offense counts in configuration',
549
552
  'file generated by --auto-gen-config.',
550
553
  'Default is true.'],
@@ -611,6 +614,8 @@ module RuboCop
611
614
  display_only_correctable: ['Only output correctable offense messages.'],
612
615
  display_only_safe_correctable: ['Only output safe-correctable offense messages',
613
616
  'when combined with --display-only-correctable.'],
617
+ display_suppressed: ['Also output offenses suppressed by directive',
618
+ 'comments. They do not affect the exit code.'],
614
619
  show_cops: ['Show the given cops, or all cops by',
615
620
  'default, and their configurations for the',
616
621
  'current directory.',
@@ -689,5 +694,4 @@ module RuboCop
689
694
  memory: 'Profile rubocop memory usage.'
690
695
  }.freeze
691
696
  end
692
- # rubocop:enable Metrics/ModuleLength
693
697
  end
@@ -63,7 +63,7 @@ module RuboCop
63
63
  end
64
64
  end
65
65
 
66
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
+ # rubocop:disable-next Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
67
67
  def match_path?(pattern, path)
68
68
  case pattern
69
69
  when String
@@ -89,7 +89,6 @@ module RuboCop
89
89
  end
90
90
  end
91
91
  end
92
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
93
92
 
94
93
  # Returns true for an absolute Unix or Windows path.
95
94
  def absolute?(path)
@@ -59,7 +59,11 @@ module RuboCop
59
59
 
60
60
  plugin_config.make_excludes_absolute
61
61
 
62
- ConfigLoader.merge_with_default(plugin_config, plugin_config_path)
62
+ # `unset_nil: false` keeps `AllCops` keys declared with a nil value by a plugin
63
+ # (e.g. `TargetRailsVersion: ~` of rubocop-rails); `merge_all_cop_settings` above
64
+ # carries such keys into each subsequent plugin's config, where `unset_nil: true`
65
+ # would delete them from the combined configuration.
66
+ ConfigLoader.merge_with_default(plugin_config, plugin_config_path, unset_nil: false)
63
67
  end
64
68
  end
65
69
  end
@@ -88,7 +92,7 @@ module RuboCop
88
92
  result
89
93
  end
90
94
 
91
- # rubocop:disable Metrics/AbcSize
95
+ # rubocop:disable-next Metrics/AbcSize
92
96
  def load_plugin_rubocop_config(plugin, runner_context)
93
97
  rules = plugin.rules(runner_context)
94
98
 
@@ -105,7 +109,6 @@ module RuboCop
105
109
  raise "Plugin `#{plugin_name}' failed to load with error: #{error_message}"
106
110
  end
107
111
  end
108
- # rubocop:enable Metrics/AbcSize
109
112
 
110
113
  # This is how we ensure "first-in wins": plugins can override AllCops settings that are
111
114
  # set by RuboCop's default configuration, but once a plugin sets an AllCop setting, they
@@ -74,7 +74,7 @@ module RuboCop
74
74
 
75
75
  def setup_subtasks(name, *args, &task_block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
76
76
  namespace(name) do
77
- # rubocop:todo Naming/InclusiveLanguage
77
+ # rubocop:todo-next Naming/InclusiveLanguage
78
78
  task(:auto_correct, *args) do |_, task_args|
79
79
  require 'rainbow'
80
80
  warn Rainbow(
@@ -86,7 +86,6 @@ module RuboCop
86
86
  perform('--autocorrect')
87
87
  end
88
88
  end
89
- # rubocop:enable Naming/InclusiveLanguage
90
89
 
91
90
  desc "Autocorrect RuboCop offenses (only when it's safe)."
92
91
  task(:autocorrect, *args) do |_, task_args|
@@ -117,7 +117,7 @@ module RuboCop
117
117
  source
118
118
  end
119
119
 
120
- # rubocop:disable Metrics/AbcSize
120
+ # rubocop:disable-next Metrics/AbcSize
121
121
  def expect_offense(source, file = nil, severity: nil, chomp: false, **replacements)
122
122
  expected_annotations = parse_annotations(source, **replacements)
123
123
  source = expected_annotations.plain_source
@@ -138,9 +138,8 @@ module RuboCop
138
138
 
139
139
  @offenses
140
140
  end
141
- # rubocop:enable Metrics/AbcSize
142
141
 
143
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
142
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
144
143
  def expect_correction(correction, loop: true, source: nil)
145
144
  if source
146
145
  expected_annotations = parse_annotations(source, raise_error: false)
@@ -177,7 +176,6 @@ module RuboCop
177
176
  expect(new_source).to eq(correction)
178
177
  expect(@processed_source).to be_valid_syntax, 'Expected correction to be valid syntax'
179
178
  end
180
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
181
179
 
182
180
  def expect_no_corrections
183
181
  raise '`expect_no_corrections` must follow `expect_offense`' unless @processed_source
@@ -6,7 +6,7 @@ require 'tmpdir'
6
6
  # or stubbing Dir.pwd get a fresh value.
7
7
  RSpec.configure { |c| c.before { RuboCop::PathUtil.reset_pwd } }
8
8
 
9
- # rubocop:disable Metrics/BlockLength
9
+ # rubocop:disable-next Metrics/BlockLength
10
10
  RSpec.shared_context 'isolated environment' do
11
11
  around do |example|
12
12
  Dir.mktmpdir do |tmpdir|
@@ -69,7 +69,6 @@ RSpec.shared_context 'isolated environment' do
69
69
  end
70
70
  end
71
71
  end
72
- # rubocop:enable Metrics/BlockLength
73
72
 
74
73
  # Workaround for https://github.com/rubocop/rubocop/issues/12978,
75
74
  # there should already be no gemfile in the temp directory
@@ -136,6 +135,8 @@ RSpec.shared_context 'config' do # rubocop:disable Metrics/BlockLength
136
135
 
137
136
  let(:gem_versions) { {} }
138
137
 
138
+ let(:path_sourced_gems) { [] }
139
+
139
140
  ### Utilities
140
141
 
141
142
  def source_range(range, buffer: source_buffer)
@@ -180,6 +181,7 @@ RSpec.shared_context 'config' do # rubocop:disable Metrics/BlockLength
180
181
  **gem_versions.transform_values { |value| Gem::Version.new(value) }
181
182
  }
182
183
  )
184
+ allow(config).to receive(:path_sourced_gems_in_target).and_return(path_sourced_gems)
183
185
 
184
186
  config
185
187
  end
@@ -67,6 +67,8 @@ module RuboCop
67
67
  end
68
68
 
69
69
  def run(paths)
70
+ @inspection_team_by_config = {}.compare_by_identity
71
+
70
72
  # Compute the cache source checksum once to avoid potential
71
73
  # inconsistencies between workers.
72
74
  ResultCache.source_checksum
@@ -264,7 +266,8 @@ module RuboCop
264
266
  file_offense_cache(file) do
265
267
  source, offenses = do_inspection_loop(file)
266
268
  offenses = add_redundant_disables(file, offenses.compact.sort, source)
267
- offenses.sort.reject(&:disabled?).freeze
269
+ offenses = offenses.reject(&:disabled?) unless @options[:display_suppressed]
270
+ offenses.sort.freeze
268
271
  end
269
272
  end
270
273
 
@@ -321,7 +324,13 @@ module RuboCop
321
324
  end
322
325
 
323
326
  def check_for_redundant_disables?(source)
324
- return false if source.disabled_line_ranges.empty? || except_redundant_cop_disable_directive?
327
+ return false if except_redundant_cop_disable_directive?
328
+ # Detached `disable-next` directives produce no disabled ranges but must
329
+ # still be reported as redundant.
330
+ if source.disabled_line_ranges.empty? &&
331
+ source.comment_config.detached_next_directives.empty?
332
+ return false
333
+ end
325
334
 
326
335
  !@options[:only]
327
336
  end
@@ -496,15 +505,8 @@ module RuboCop
496
505
 
497
506
  def mobilize_team(processed_source)
498
507
  config = @config_store.for_file(processed_source.path)
499
- team = Cop::Team.mobilize(mobilized_cop_classes(config), config, @options)
500
-
501
- if @project_index
502
- team.cops.each do |cop|
503
- cop.project_index = @project_index
504
- end
505
- end
506
-
507
- team
508
+ @inspection_team_by_config ||= {}.compare_by_identity
509
+ @inspection_team_by_config[config] ||= assemble_team(config)
508
510
  end
509
511
 
510
512
  def mobilized_cop_classes(config)
@@ -609,7 +611,7 @@ module RuboCop
609
611
  end
610
612
  end
611
613
 
612
- # rubocop:disable Metrics/MethodLength
614
+ # rubocop:disable-next Metrics/MethodLength
613
615
  def get_processed_source(file, prism_result, source: nil)
614
616
  config = @config_store.for_file(file)
615
617
  ruby_version = config.target_ruby_version
@@ -643,25 +645,26 @@ module RuboCop
643
645
  processed_source.registry = mobilized_cop_classes(config)
644
646
  processed_source
645
647
  end
646
- # rubocop:enable Metrics/MethodLength
647
648
 
648
649
  # A Cop::Team instance is stateful and may change when inspecting.
649
650
  # The "standby" team for a given config is an initialized but
650
651
  # otherwise dormant team that can be used for config- and option-
651
652
  # level caching in ResultCache.
652
653
  def standby_team(config)
653
- @team_by_config ||= {}.compare_by_identity
654
- @team_by_config[config] ||= begin
655
- team = Cop::Team.mobilize(mobilized_cop_classes(config), config, @options)
656
-
657
- if @project_index
658
- team.cops.each do |cop|
659
- cop.project_index = @project_index
660
- end
661
- end
654
+ @standby_team_by_config ||= {}.compare_by_identity
655
+ @standby_team_by_config[config] ||= assemble_team(config)
656
+ end
662
657
 
663
- team
658
+ def assemble_team(config)
659
+ team = Cop::Team.mobilize(mobilized_cop_classes(config), config, @options)
660
+
661
+ if @project_index
662
+ team.cops.each do |cop|
663
+ cop.project_index = @project_index
664
+ end
664
665
  end
666
+
667
+ team
665
668
  end
666
669
  end
667
670
  end
@@ -45,7 +45,7 @@ module RuboCop
45
45
  @project_dir_cache_key ||= project_dir[1..].tr('/', '+')
46
46
  end
47
47
 
48
- # rubocop:disable Metrics/AbcSize
48
+ # rubocop:disable-next Metrics/AbcSize
49
49
  def restart_key(args_config_file_path: nil)
50
50
  lockfile_path = LOCKFILE_NAMES.map do |lockfile_name|
51
51
  Pathname(project_dir).join(lockfile_name)
@@ -61,7 +61,6 @@ module RuboCop
61
61
 
62
62
  Digest::SHA1.hexdigest(version_data + config_data + inherit_from_data + require_data)
63
63
  end
64
- # rubocop:enable Metrics/AbcSize
65
64
 
66
65
  def dir
67
66
  dir_path.tap do |d|
@@ -55,7 +55,7 @@ module RuboCop
55
55
 
56
56
  private
57
57
 
58
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
58
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
59
59
  def process_arguments(argv)
60
60
  server_arguments = delete_server_argument_from(argv)
61
61
 
@@ -84,9 +84,8 @@ module RuboCop
84
84
 
85
85
  STATUS_SUCCESS
86
86
  end
87
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
88
87
 
89
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
88
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength
90
89
  def run_command(server_command, detach:)
91
90
  case server_command
92
91
  when '--server'
@@ -107,7 +106,6 @@ module RuboCop
107
106
  Server::ClientCommand::Status.new.run
108
107
  end
109
108
  end
110
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
111
109
 
112
110
  def fetch_cache_root_path_from(arguments)
113
111
  cache_root = arguments.detect { |argument| argument.start_with?('--cache-root') }
@@ -17,6 +17,11 @@ module RuboCop
17
17
  module Server
18
18
  TIMEOUT = 20
19
19
 
20
+ # Options that start a long-lived protocol server on stdio in the current process.
21
+ # They must never be forwarded to the server process, which would run them as
22
+ # a lint request and silently produce no protocol response.
23
+ IN_PROCESS_PROTOCOL_OPTIONS = %w[--lsp --mcp].freeze
24
+
20
25
  autoload :CLI, 'rubocop/server/cli'
21
26
  autoload :Cache, 'rubocop/server/cache'
22
27
  autoload :ClientCommand, 'rubocop/server/client_command'
@@ -36,6 +41,15 @@ module RuboCop
36
41
  Cache.pid_running?
37
42
  end
38
43
 
44
+ # Whether the invocation should be forwarded to the running server process.
45
+ # `--lsp` and `--mcp` always run in the current process even when the server is running:
46
+ # they serve a protocol on stdio, which the server's `exec` command cannot do.
47
+ def forward_to_server?(argv = ARGV)
48
+ return false unless running?
49
+
50
+ (IN_PROCESS_PROTOCOL_OPTIONS & argv).empty?
51
+ end
52
+
39
53
  def wait_for_running_status!(expected)
40
54
  start_time = Time.now
41
55
  while Server.running? != expected
@@ -21,7 +21,10 @@ module RuboCop
21
21
 
22
22
  args.uniq.each do |arg|
23
23
  files += if File.directory?(arg)
24
- target_files_in_dir(arg.chomp(File::SEPARATOR))
24
+ # Expand the directory so that the traversal can prune directories the
25
+ # config excludes - the exclude patterns are absolute paths, and glued
26
+ # onto a relative base (`rubocop .`) they never match anything.
27
+ target_files_in_dir(File.expand_path(arg.chomp(File::SEPARATOR)))
25
28
  else
26
29
  process_explicit_path(arg, mode)
27
30
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # Audits `.rubocop_todo.yml` for `Exclude` entries that are no longer
5
+ # needed: files a cop would not flag anymore, or files that no longer
6
+ # exist. Used by the `--report-unused-todo-entries` option.
7
+ # @api private
8
+ class TodoAudit
9
+ Entry = Struct.new(:cop_name, :path)
10
+
11
+ def initialize(config_store, options)
12
+ @config_store = config_store
13
+ @options = options
14
+ end
15
+
16
+ # @return [Array<Entry>, nil] the unused entries, or `nil` when there is
17
+ # no todo file to audit
18
+ def unused_entries
19
+ return nil unless todo_path
20
+
21
+ entries = todo_exclude_entries
22
+ return [] if entries.empty?
23
+
24
+ offending_cops = offending_cops_without_todo(entries)
25
+ entries.reject { |entry| offending_cops[absolute(entry.path)]&.include?(entry.cop_name) }
26
+ end
27
+
28
+ def todo_file
29
+ CLI::Command::AutoGenerateConfig::AUTO_GENERATED_FILE
30
+ end
31
+
32
+ private
33
+
34
+ def todo_path
35
+ return @todo_path if defined?(@todo_path)
36
+
37
+ path = File.expand_path(todo_file)
38
+ @todo_path = File.exist?(path) ? path : nil
39
+ end
40
+
41
+ def todo_exclude_entries
42
+ yaml = YAML.safe_load_file(todo_path, permitted_classes: [Regexp, Symbol], aliases: true)
43
+ return [] unless yaml.is_a?(Hash)
44
+
45
+ yaml.flat_map do |cop_name, cop_config|
46
+ next [] unless auditable_cop?(cop_name, cop_config)
47
+
48
+ cop_config['Exclude'].grep(String).map { |path| Entry.new(cop_name, path) }
49
+ end
50
+ end
51
+
52
+ # Only registered cops with an `Exclude` list can be audited - for an
53
+ # unknown cop (e.g. from an extension that is not loaded in this run)
54
+ # the absence of offenses proves nothing.
55
+ def auditable_cop?(cop_name, cop_config)
56
+ cop_name.include?('/') &&
57
+ cop_config.is_a?(Hash) &&
58
+ cop_config['Exclude'].is_a?(Array) &&
59
+ Cop::Registry.global.contains_cop_matching?([cop_name])
60
+ end
61
+
62
+ # Inspects every file the todo entries mention, with the todo exclusions
63
+ # subtracted from the configuration, and returns the names of the cops
64
+ # that still report offenses, keyed by absolute file path.
65
+ def offending_cops_without_todo(entries)
66
+ files = entries.map { |entry| absolute(entry.path) }.uniq.select { |file| File.file?(file) }
67
+
68
+ files.to_h do |file|
69
+ offenses = inspect_file(file, entries)
70
+ [file, offenses.to_set(&:cop_name)]
71
+ end
72
+ end
73
+
74
+ def inspect_file(file, entries)
75
+ config = audit_config(@config_store.for_file(file), entries)
76
+ team = Cop::Team.mobilize(Cop::Registry.global, config, @options.merge(autocorrect: false))
77
+
78
+ processed_source = ProcessedSource.from_file(
79
+ file, config.target_ruby_version, parser_engine: config.parser_engine
80
+ )
81
+ processed_source.config = config
82
+ processed_source.registry = Cop::Registry.global
83
+
84
+ team.investigate(processed_source).offenses.reject(&:disabled?)
85
+ end
86
+
87
+ # A copy of the configuration with the todo exclusions removed for the
88
+ # audited cops, so their offenses in the listed files become visible.
89
+ def audit_config(config, entries)
90
+ hash = config.to_hash.dup
91
+
92
+ entries.group_by(&:cop_name).each do |cop_name, cop_entries|
93
+ cop_config = hash[cop_name]
94
+ next unless cop_config.is_a?(Hash) && cop_config['Exclude'].is_a?(Array)
95
+
96
+ hash[cop_name] = subtract_excludes(cop_config, cop_entries)
97
+ end
98
+
99
+ Config.create(hash, config.loaded_path, check: false)
100
+ end
101
+
102
+ def subtract_excludes(cop_config, cop_entries)
103
+ removals = cop_entries.map { |entry| absolute(entry.path) }
104
+ cop_config.merge('Exclude' => cop_config['Exclude'] - removals)
105
+ end
106
+
107
+ def absolute(path)
108
+ File.expand_path(path, File.dirname(todo_path))
109
+ end
110
+ end
111
+ end