rubocop 1.91.0 → 1.92.2

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 (278) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +4 -1
  4. data/lib/rubocop/cached_data.rb +47 -7
  5. data/lib/rubocop/changed_files.rb +35 -13
  6. data/lib/rubocop/cli/command/cop_names.rb +57 -0
  7. data/lib/rubocop/cli/command/execute_runner.rb +2 -2
  8. data/lib/rubocop/cli/command/explain.rb +34 -0
  9. data/lib/rubocop/cli/command/show_cops.rb +6 -0
  10. data/lib/rubocop/cli/command/show_docs_url.rb +6 -4
  11. data/lib/rubocop/cli.rb +3 -2
  12. data/lib/rubocop/comment_config.rb +17 -8
  13. data/lib/rubocop/config.rb +0 -4
  14. data/lib/rubocop/config_loader.rb +6 -3
  15. data/lib/rubocop/config_loader_resolver.rb +37 -12
  16. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  17. data/lib/rubocop/config_obsoletion.rb +9 -0
  18. data/lib/rubocop/config_validator.rb +11 -0
  19. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  20. data/lib/rubocop/cop/badge.rb +1 -1
  21. data/lib/rubocop/cop/base.rb +42 -21
  22. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  23. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  24. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  25. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  26. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  27. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +37 -7
  28. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  29. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +25 -4
  30. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +34 -0
  31. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  32. data/lib/rubocop/cop/documentation.rb +1 -1
  33. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  34. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  35. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  36. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  37. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  38. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  39. data/lib/rubocop/cop/layout/argument_alignment.rb +5 -1
  40. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  41. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  42. data/lib/rubocop/cop/layout/class_structure.rb +29 -7
  43. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  44. data/lib/rubocop/cop/layout/dot_position.rb +25 -15
  45. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +21 -8
  46. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  47. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  48. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  49. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +9 -3
  50. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  51. data/lib/rubocop/cop/layout/end_alignment.rb +21 -25
  52. data/lib/rubocop/cop/layout/extra_spacing.rb +21 -6
  53. data/lib/rubocop/cop/layout/first_argument_indentation.rb +27 -1
  54. data/lib/rubocop/cop/layout/first_array_element_line_break.rb +15 -0
  55. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +16 -8
  56. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  57. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  58. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  59. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +7 -3
  60. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  61. data/lib/rubocop/cop/layout/indentation_consistency.rb +22 -5
  62. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  63. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  64. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  65. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  66. data/lib/rubocop/cop/layout/line_length.rb +176 -26
  67. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  68. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  69. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +9 -0
  70. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  71. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +30 -5
  72. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  73. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  74. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +16 -3
  75. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  76. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  77. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  78. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  79. data/lib/rubocop/cop/layout/space_around_operators.rb +31 -4
  80. data/lib/rubocop/cop/layout/space_before_block_braces.rb +16 -1
  81. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  82. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +32 -9
  83. data/lib/rubocop/cop/layout/space_inside_range_literal.rb +29 -11
  84. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +2 -12
  85. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  86. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  87. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -1
  88. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  89. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +4 -0
  90. data/lib/rubocop/cop/lint/interpolation_check.rb +20 -17
  91. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  92. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  93. data/lib/rubocop/cop/lint/loop.rb +24 -2
  94. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  95. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  96. data/lib/rubocop/cop/lint/nested_percent_literal.rb +3 -0
  97. data/lib/rubocop/cop/lint/number_conversion.rb +3 -1
  98. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +22 -1
  99. data/lib/rubocop/cop/lint/percent_string_array.rb +75 -9
  100. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +39 -9
  101. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  102. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +11 -4
  103. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +47 -1
  104. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -0
  105. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  106. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  107. data/lib/rubocop/cop/lint/useless_setter_call.rb +7 -4
  108. data/lib/rubocop/cop/lint/void.rb +8 -1
  109. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  110. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  111. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  112. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  113. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -19
  114. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +8 -2
  115. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  116. data/lib/rubocop/cop/mixin/comments_help.rb +1 -1
  117. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  118. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +105 -0
  119. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  120. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +41 -6
  121. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  122. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  123. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  124. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  125. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  126. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  127. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  128. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  129. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  130. data/lib/rubocop/cop/mixin/statement_modifier.rb +39 -5
  131. data/lib/rubocop/cop/mixin/trailing_comma.rb +1 -1
  132. data/lib/rubocop/cop/mixin/visibility_help.rb +2 -1
  133. data/lib/rubocop/cop/mixin.rb +2 -0
  134. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  135. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  136. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  137. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  138. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  139. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  140. data/lib/rubocop/cop/offense.rb +59 -3
  141. data/lib/rubocop/cop/registry.rb +1 -1
  142. data/lib/rubocop/cop/security/json_load.rb +16 -4
  143. data/lib/rubocop/cop/style/access_modifier_declarations.rb +37 -2
  144. data/lib/rubocop/cop/style/alias.rb +18 -5
  145. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  146. data/lib/rubocop/cop/style/and_or.rb +37 -10
  147. data/lib/rubocop/cop/style/arguments_forwarding.rb +32 -3
  148. data/lib/rubocop/cop/style/attr.rb +3 -4
  149. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  150. data/lib/rubocop/cop/style/block_delimiters.rb +35 -3
  151. data/lib/rubocop/cop/style/case_equality.rb +14 -3
  152. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  153. data/lib/rubocop/cop/style/class_and_module_children.rb +57 -16
  154. data/lib/rubocop/cop/style/class_methods_definitions.rb +15 -0
  155. data/lib/rubocop/cop/style/collection_querying.rb +79 -46
  156. data/lib/rubocop/cop/style/combinable_loops.rb +89 -11
  157. data/lib/rubocop/cop/style/concat_array_literals.rb +58 -7
  158. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  159. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  160. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  161. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  162. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  163. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +53 -11
  164. data/lib/rubocop/cop/style/endless_method.rb +104 -11
  165. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  166. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  167. data/lib/rubocop/cop/style/float_division.rb +37 -2
  168. data/lib/rubocop/cop/style/for.rb +53 -2
  169. data/lib/rubocop/cop/style/format_string.rb +28 -6
  170. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  171. data/lib/rubocop/cop/style/guard_clause.rb +31 -22
  172. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +4 -0
  173. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  174. data/lib/rubocop/cop/style/hash_lookup_method.rb +42 -9
  175. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  176. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  177. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  178. data/lib/rubocop/cop/style/if_unless_modifier.rb +32 -26
  179. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  180. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  181. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  182. data/lib/rubocop/cop/style/it_block_parameter.rb +94 -4
  183. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  184. data/lib/rubocop/cop/style/lambda.rb +34 -1
  185. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  186. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  187. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +80 -13
  188. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +26 -2
  189. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  190. data/lib/rubocop/cop/style/method_def_parentheses.rb +38 -4
  191. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  192. data/lib/rubocop/cop/style/mixin_grouping.rb +12 -3
  193. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  194. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  195. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  196. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  197. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  198. data/lib/rubocop/cop/style/mutable_constant.rb +52 -7
  199. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  200. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  201. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  202. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  203. data/lib/rubocop/cop/style/next.rb +24 -10
  204. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  205. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  206. data/lib/rubocop/cop/style/not.rb +10 -5
  207. data/lib/rubocop/cop/style/numeric_predicate.rb +28 -5
  208. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  209. data/lib/rubocop/cop/style/parentheses_around_condition.rb +19 -1
  210. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  211. data/lib/rubocop/cop/style/quoted_symbols.rb +5 -7
  212. data/lib/rubocop/cop/style/raise_args.rb +80 -11
  213. data/lib/rubocop/cop/style/reduce_to_hash.rb +47 -33
  214. data/lib/rubocop/cop/style/redundant_begin.rb +45 -5
  215. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  216. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  217. data/lib/rubocop/cop/style/redundant_exception.rb +27 -11
  218. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  219. data/lib/rubocop/cop/style/redundant_freeze.rb +11 -3
  220. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +24 -1
  221. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  222. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  223. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  224. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  225. data/lib/rubocop/cop/style/redundant_self.rb +9 -1
  226. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +28 -0
  227. data/lib/rubocop/cop/style/redundant_sort.rb +38 -4
  228. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  229. data/lib/rubocop/cop/style/require_order.rb +25 -8
  230. data/lib/rubocop/cop/style/rescue_modifier.rb +11 -9
  231. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  232. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  233. data/lib/rubocop/cop/style/sample.rb +9 -8
  234. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  235. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  236. data/lib/rubocop/cop/style/send_with_literal_method_name.rb +4 -0
  237. data/lib/rubocop/cop/style/single_line_methods.rb +42 -4
  238. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  239. data/lib/rubocop/cop/style/string_concatenation.rb +21 -4
  240. data/lib/rubocop/cop/style/struct_inheritance.rb +6 -0
  241. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  242. data/lib/rubocop/cop/style/symbol_proc.rb +24 -6
  243. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  244. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  245. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  246. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  247. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  248. data/lib/rubocop/cop/team.rb +10 -5
  249. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  250. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  251. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  252. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  253. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  254. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  255. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  256. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  257. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  258. data/lib/rubocop/lsp/routes.rb +23 -23
  259. data/lib/rubocop/lsp/runtime.rb +47 -11
  260. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  261. data/lib/rubocop/lsp.rb +5 -2
  262. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  263. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  264. data/lib/rubocop/mcp/scope.rb +65 -0
  265. data/lib/rubocop/mcp/server.rb +184 -76
  266. data/lib/rubocop/mcp/source_files.rb +35 -0
  267. data/lib/rubocop/offense_limit.rb +30 -0
  268. data/lib/rubocop/options.rb +45 -10
  269. data/lib/rubocop/plugin/configuration_integrator.rb +36 -8
  270. data/lib/rubocop/remote_config.rb +4 -1
  271. data/lib/rubocop/result_cache.rb +20 -13
  272. data/lib/rubocop/runner.rb +110 -42
  273. data/lib/rubocop/server/cache.rb +6 -6
  274. data/lib/rubocop/target_ruby.rb +21 -9
  275. data/lib/rubocop/util.rb +69 -0
  276. data/lib/rubocop/version.rb +1 -1
  277. data/lib/rubocop.rb +5 -0
  278. metadata +16 -5
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module MCP
5
+ # What a request asks RuboCop to look at: which cops run, as `--only` and
6
+ # `--except` choose them, and, when checking files, whether to stick to the
7
+ # ones git says changed, as `--changed` does.
8
+ # @api private
9
+ class Scope
10
+ PROPERTIES = {
11
+ only: {
12
+ type: 'array', items: { type: 'string' },
13
+ description: 'Run only these cops or departments, ' \
14
+ 'such as `Style/StringLiterals` or `Lint`.'
15
+ },
16
+ except: {
17
+ type: 'array', items: { type: 'string' },
18
+ description: 'Run every cop except these cops or departments.'
19
+ },
20
+ changed: {
21
+ type: %w[boolean string],
22
+ description: 'Check only the files git says changed: `true` for changes since the ' \
23
+ 'last commit, or a revision to compare against, such as `main`.'
24
+ }
25
+ }.freeze
26
+
27
+ # An empty list would select no cops at all rather than leave the choice
28
+ # alone, so it counts as no list.
29
+ #
30
+ # @raise [RuboCop::Error] for a scope the command line would refuse
31
+ def initialize(inline:, only: nil, except: nil, changed: nil)
32
+ @only = only if only&.any?
33
+ @except = except if except&.any?
34
+ @changed = changed == true ? ChangedFiles::DEFAULT_REVISION : changed
35
+ validate(inline)
36
+ end
37
+
38
+ # @return [Hash] the cop selection, among the options the LSP runtime takes
39
+ def cop_options
40
+ { only: @only, except: @except }.compact
41
+ end
42
+
43
+ def select_files(files)
44
+ @changed ? ChangedFiles.new(@changed).filter(files) : files
45
+ end
46
+
47
+ private
48
+
49
+ # Refuses what the command line refuses for the same options. Cop names
50
+ # are left to the runner, which checks them the way `--only` does: short
51
+ # names allowed, and only once the configuration naming a plugin's cops
52
+ # has been loaded.
53
+ def validate(inline)
54
+ raise Error, '`changed` only applies when checking files.' if inline && @changed
55
+ raise Error, '`changed` takes `true` or a git revision.' if @changed == ''
56
+
57
+ validator = OptionsValidator.new(cop_options)
58
+ if validator.only_includes_redundant_disable?
59
+ raise Error, 'Lint/RedundantCopDisableDirective cannot be used with `only`.'
60
+ end
61
+ raise Error, 'Syntax checking cannot be turned off.' if validator.except_syntax?
62
+ end
63
+ end
64
+ end
65
+ end
@@ -19,7 +19,7 @@ rescue LoadError => e
19
19
  raise unless e.path == 'mcp'
20
20
 
21
21
  warn <<~MESSAGE
22
- Error: Unable to load `mcp` gem. Add `gem 'mcp', '~> 0.6'` to your Gemfile, or run `gem install mcp`.
22
+ Error: Unable to load `mcp` gem. Add `gem 'mcp'` to your Gemfile, or run `gem install mcp`.
23
23
  MESSAGE
24
24
 
25
25
  exit!
@@ -27,16 +27,59 @@ end
27
27
 
28
28
  require_relative '../lsp'
29
29
  require_relative '../lsp/runtime'
30
+ require_relative 'autocorrection_request'
31
+ require_relative 'explain_tool'
32
+ require_relative 'scope'
33
+ require_relative 'source_files'
30
34
 
31
35
  module RuboCop
32
36
  module MCP
33
37
  # RuboCop MCP Server.
34
38
  # @api private
35
39
  class Server
40
+ INSPECTION_DESCRIPTION =
41
+ 'Inspect Ruby code for offenses. ' \
42
+ 'Provide `source_code` to check inline code or `path` to check files. ' \
43
+ 'Either way the result lists offenses per file, with a summary, and the ' \
44
+ 'offenses use the `rubocop --format json` format, with 1-based lines. ' \
45
+ '`correctable` says whether `rubocop_autocorrection` fixes an offense, ' \
46
+ 'and one whose `correction` is not `safe` needs `safety` set to false. ' \
47
+ 'One with a `correction` that is not `correctable` is held back while code is ' \
48
+ 'being written, and only fixed with `contextual` set to true. ' \
49
+ '`max_offenses_per_cop` caps what each cop reports, and the ' \
50
+ 'summary\'s `unreported_offenses` counts what was left out. ' \
51
+ 'A cop that crashes is listed in its file\'s `errors`; the rest still report.'
52
+
53
+ AUTOCORRECTION_DESCRIPTION =
54
+ 'Autocorrect RuboCop offenses in Ruby code. ' \
55
+ 'Provide `source_code` to correct inline code, which comes back corrected as ' \
56
+ 'plain text, or `path` to correct files. ' \
57
+ 'Set `safety` to false to include unsafe corrections, and `contextual` to true ' \
58
+ 'to include the ones held back while code is being written, such as removing ' \
59
+ 'an unused variable. ' \
60
+ 'Set `dry_run` to true to preview: nothing is written, and each corrected file ' \
61
+ 'comes with its `diff`. ' \
62
+ 'For files, the result lists each file that was corrected or still has offenses, ' \
63
+ 'with the offenses left in the `rubocop_inspection` format, located in the ' \
64
+ 'corrected file. One whose `correction` is not `safe` needs `safety` set to false, ' \
65
+ 'and one with a `correction` that is not `correctable` needs `contextual` set to ' \
66
+ 'true. Each cop lists at most `max_offenses_per_cop` of them, 5 unless ' \
67
+ 'set, and the summary\'s `unreported_offenses` counts the rest. ' \
68
+ 'A cop that crashes is listed in its file\'s `errors`, and a file whose ' \
69
+ 'corrections loop is left unchanged.'
70
+
36
71
  def initialize(config_store)
37
72
  @config_store = config_store
38
- @runtime = RuboCop::LSP::Runtime.new(@config_store)
73
+ # An agent isn't an editor, so offenses are located and worded the way
74
+ # the command line does it. Only contextual corrections are held back.
75
+ @runtime = RuboCop::LSP::Runtime.new(@config_store, lsp_mode: false)
76
+ # One cop crashing on one file should not cost an agent everything else
77
+ # in the request, so cop errors are collected and reported per file.
78
+ @runtime.raise_cop_error = false
39
79
  @options = {}
80
+ # Offenses are reported the way `--format json` reports them, so an agent
81
+ # sees the same 1-based locations and correction data either way.
82
+ @json_formatter = RuboCop::Formatter::JSONFormatter.new(nil)
40
83
  end
41
84
 
42
85
  def start
@@ -44,7 +87,7 @@ module RuboCop
44
87
  server = ::MCP::Server.new(
45
88
  name: 'rubocop_mcp_server',
46
89
  version: RuboCop::Version::STRING,
47
- tools: [inspection_tool, autocorrection_tool]
90
+ tools: [inspection_tool, autocorrection_tool, ExplainTool.new(@config_store).to_tool]
48
91
  )
49
92
 
50
93
  ::MCP::Server::Transports::StdioTransport.new(server).open
@@ -55,91 +98,158 @@ module RuboCop
55
98
  def inspection_tool
56
99
  build_tool(
57
100
  name: 'rubocop_inspection',
58
- description: 'Inspect Ruby code for offenses. ' \
59
- 'Provide `source_code` to check inline code or `path` to check files.',
101
+ description: INSPECTION_DESCRIPTION,
60
102
  title: "RuboCop's inspection",
61
103
  destructive_hint: false,
62
104
  idempotent_hint: true,
63
105
  read_only_hint: true,
64
- safety_required: false
65
- ) do |path, source_code|
66
- run_inspection(path, source_code)
106
+ properties: { max_offenses_per_cop: { type: 'integer', minimum: 1 } }
107
+ ) do |scope:, path: nil, source_code: nil, max_offenses_per_cop: nil|
108
+ run_inspection(path, source_code, max_offenses_per_cop, scope)
67
109
  end
68
110
  end
69
111
 
70
112
  def autocorrection_tool
71
113
  build_tool(
72
114
  name: 'rubocop_autocorrection',
73
- description: 'Autocorrect RuboCop offenses in Ruby code. ' \
74
- 'Provide `source_code` to correct inline code or `path` to correct files. ' \
75
- 'Set `safety` to false to include unsafe corrections.',
115
+ description: AUTOCORRECTION_DESCRIPTION,
76
116
  title: "RuboCop's autocorrection",
77
117
  destructive_hint: true,
78
118
  idempotent_hint: false,
79
119
  read_only_hint: false,
80
- safety_required: true
81
- ) do |path, source_code, safety|
82
- run_autocorrection(path, source_code, safety)
120
+ properties: AutocorrectionRequest::PROPERTIES,
121
+ required: ['safety']
122
+ ) do |scope:, path: nil, source_code: nil, **arguments|
123
+ run_autocorrection(path, source_code, AutocorrectionRequest.new(scope, **arguments))
83
124
  end
84
125
  end
85
126
 
86
- def run_inspection(path, source_code)
87
- if source_code
88
- offenses = @runtime.offenses(path || 'example.rb', source_code, source_code.encoding)
89
- offenses.to_json
90
- else
91
- process_files(path, filter_empty: true) do |file, source|
92
- offenses = @runtime.offenses(file, source, source.encoding)
93
-
94
- { path: PathUtil.relative_path(file), offenses: offenses }
127
+ # A limit counts across the files of one request, so every request starts
128
+ # from zero.
129
+ def run_inspection(path, source_code, max_offenses_per_cop, scope)
130
+ limit = OffenseLimit.new(max_offenses_per_cop) if max_offenses_per_cop
131
+ result =
132
+ if source_code
133
+ file = path || 'example.rb'
134
+ build_result([file], [inspect_source(file, source_code, limit, scope)])
135
+ else
136
+ process_files(path, scope) { |file, src| inspect_source(file, src, limit, scope) }
95
137
  end
138
+ result_json(result, limit)
139
+ end
140
+
141
+ def inspect_source(file, source, limit, scope)
142
+ options = { **scope.cop_options, editing: true }
143
+ offenses = @runtime.raw_offenses(file, source, options: options)
144
+ offense_entry(file, offenses, limit).merge(problems_of_last_run(file))
145
+ end
146
+
147
+ # The offenses of a file the way `--format json` lists them, capped across
148
+ # the files of a request.
149
+ def offense_entry(file, offenses, limit)
150
+ offenses = limit.filter(offenses) if limit
151
+ @json_formatter.hash_for_file(file, offenses)
152
+ end
153
+
154
+ def run_autocorrection(path, source_code, request)
155
+ return correct_inline(path, source_code, request) if source_code
156
+
157
+ result = process_files(path, request.scope) do |file, source|
158
+ correct_file(file, source, request)
96
159
  end
160
+ result[:summary][:corrected_file_count] = result[:files].count { |file| file[:corrected] }
161
+ result_json(result, request.limit)
97
162
  end
98
163
 
99
- def run_autocorrection(path, source_code, safety)
100
- command = safety ? 'rubocop.formatAutocorrects' : 'rubocop.formatAutocorrectsAll'
164
+ # The agent never sees stderr, so the summary says what the limit left out.
165
+ def result_json(result, limit)
166
+ unreported = limit&.elided_per_cop
167
+ result[:summary][:unreported_offenses] = unreported if unreported&.any?
168
+ result.to_json
169
+ end
101
170
 
102
- if source_code
103
- @runtime.format(path || 'example.rb', source_code, command: command).tap do |corrected|
104
- write_file(path, corrected) if path
105
- end
171
+ # Cops that keep undoing each other's corrections leave the file half
172
+ # corrected, so it is left alone and the loop is reported instead. A file
173
+ # with nothing to correct is left alone too, so its modification time
174
+ # doesn't tell editors and agents it changed. The offenses left over are
175
+ # the ones an agent has to fix itself, which saves it inspecting again.
176
+ def correct_file(file, source, request)
177
+ corrected = @runtime.format(file, source, **request.format_options)
178
+ problems = problems_of_last_run(file)
179
+ looped = @runtime.errors.any?(Runner::InfiniteCorrectionLoop)
180
+ changed = !looped && source != corrected
181
+ leftovers = leftover_offenses(file, source, corrected, changed, request)
182
+ entry = offense_entry(file, leftovers, request.limit)
183
+ diff = write_or_diff(file, entry[:path], source, corrected, request) if changed
184
+
185
+ { path: entry[:path], corrected: changed, diff: diff, **entry, **problems }.compact
186
+ end
187
+
188
+ # A dry run writes nothing and returns the diff a real run would have
189
+ # applied, line endings included.
190
+ def write_or_diff(file, path, source, corrected, request)
191
+ if request.dry_run?
192
+ UnifiedDiff.new(path, source, Util.emulate_write_read_cycle(corrected)).to_s
106
193
  else
107
- process_files(path) do |file, source|
108
- @runtime.format(file, source, command: command).then do |corrected|
109
- write_file(file, corrected)
194
+ SourceFiles.write(file, corrected)
195
+ nil
196
+ end
197
+ end
110
198
 
111
- { path: PathUtil.relative_path(file), corrected: source != corrected }
112
- end
113
- end
199
+ # What's left is in the file as written, or as a dry run would have
200
+ # written it, or as it was when its corrections loop. The runner checks
201
+ # corrected code with LF line endings, which writing turns into CRLF on
202
+ # Windows, so there the file is checked again as written.
203
+ def leftover_offenses(file, source, corrected, changed, request)
204
+ on_disk = changed ? Util.emulate_write_read_cycle(corrected) : source
205
+ return @runtime.uncorrected_offenses if on_disk == corrected
206
+
207
+ @runtime.raw_offenses(file, on_disk, options: request.runtime_options)
208
+ end
209
+
210
+ # Corrected inline code comes back as plain text with nowhere to mention
211
+ # a cop that crashed, so a crash fails the call instead of going unseen.
212
+ def correct_inline(path, source_code, request)
213
+ file = path || 'example.rb'
214
+ corrected = @runtime.format(file, source_code, **request.format_options)
215
+ errors = problems_of_last_run(file)[:errors]
216
+ raise RuboCop::Error, errors.join("\n") if errors
217
+
218
+ SourceFiles.write(path, corrected) if path && !request.dry_run?
219
+ corrected
220
+ end
221
+
222
+ # Collapsed the way `--format json` collapses them, which also folds the
223
+ # repeats from each autocorrection pass a crashing cop fails on.
224
+ def problems_of_last_run(file)
225
+ errors, warnings = [@runtime.errors, @runtime.warnings].map do |problems|
226
+ problems.map { |problem| Rainbow::StringUtils.uncolor(problem.to_s) }
114
227
  end
228
+ @json_formatter.hash_for_problems(file, errors, warnings)
115
229
  end
116
230
 
117
- def process_files(path, filter_empty: false)
231
+ def process_files(path, scope)
118
232
  target_finder = RuboCop::TargetFinder.new(@config_store, @options)
119
- target_files = target_finder.find(path ? [path] : [], :only_recognized_file_types)
120
- all_files = target_files.map { |file| yield(file, read_file(file)) }
121
- files = filter_empty ? all_files.reject { |f| f[:offenses]&.empty? } : all_files
233
+ target_files = scope.select_files(
234
+ target_finder.find(path ? [path] : [], :only_recognized_file_types)
235
+ )
236
+ all_files = target_files.map do |file|
237
+ yield(file, SourceFiles.read(file, @config_store.for_file(file)))
238
+ end
122
239
 
123
- { files: files, summary: build_summary(target_files, all_files) }.to_json
240
+ build_result(target_files, all_files)
124
241
  end
125
242
 
126
- def read_file(file)
127
- config = @config_store.for_file(file)
128
- RuboCop::ProcessedSource.from_file(
129
- file, config.target_ruby_version, parser_engine: config.parser_engine
130
- ).raw_source
131
- rescue Errno::ENOENT
132
- raise RuboCop::Error, "No such file or directory: #{file}"
243
+ # Inline code is reported as a single file, so an agent reads one shape
244
+ # whichever way it asked. Files with nothing to report are only counted.
245
+ def build_result(target_files, all_files)
246
+ files = all_files.reject { |f| nothing_to_report?(f) }
247
+
248
+ { files: files, summary: build_summary(target_files, all_files) }
133
249
  end
134
250
 
135
- def write_file(file, content)
136
- File.write(file, content)
137
- rescue Errno::EACCES
138
- raise RuboCop::Error, "Permission denied: #{file}"
139
- rescue Errno::ENOSPC
140
- raise RuboCop::Error, "No space left on device: #{file}"
141
- rescue Errno::EROFS
142
- raise RuboCop::Error, "Read-only file system: #{file}"
251
+ def nothing_to_report?(file)
252
+ file[:offenses].empty? && !file[:corrected] && !file[:errors] && !file[:warnings]
143
253
  end
144
254
 
145
255
  # NOTE: It is useful for RuboCop's result summary to be shown in the LLM's responses
@@ -147,28 +257,16 @@ module RuboCop
147
257
  # to reason about. Since LLM execution is non-deterministic, it is also sensible to
148
258
  # compute the summary deterministically at this stage.
149
259
  def build_summary(target_files, files)
150
- summary = { target_file_count: target_files.count }
151
- if files.first&.key?(:offenses)
152
- summary[:offense_count] = files.sum { |f| f[:offenses].size }
153
- else
154
- summary[:corrected_file_count] = files.count { |f| f[:corrected] }
155
- end
156
- summary
260
+ offense_count = files.sum { |f| f[:offenses].size }
261
+ { target_file_count: target_files.count, offense_count: offense_count }
262
+ .merge(@json_formatter.problem_counts(files))
157
263
  end
158
264
 
159
265
  # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
160
266
  def build_tool(
161
267
  name:, description:,
162
- title:, destructive_hint:, idempotent_hint:, read_only_hint:, safety_required:
268
+ title:, destructive_hint:, idempotent_hint:, read_only_hint:, properties:, required: nil
163
269
  )
164
- if safety_required
165
- safety_property = { safety: { type: 'boolean' } }
166
- required = ['safety']
167
- else
168
- safety_property = {}
169
- required = nil
170
- end
171
-
172
270
  ::MCP::Tool.define(
173
271
  name: name,
174
272
  description: description,
@@ -176,8 +274,11 @@ module RuboCop
176
274
  properties: {
177
275
  path: { type: 'string' },
178
276
  source_code: { type: 'string' }
179
- }.merge(safety_property),
180
- required: required
277
+ }.merge(properties, Scope::PROPERTIES),
278
+ required: required,
279
+ # The gem checks arguments against the schema, so one the tool
280
+ # doesn't take is refused by name rather than failing the call.
281
+ additionalProperties: false
181
282
  }.compact,
182
283
  annotations: {
183
284
  title: title,
@@ -186,11 +287,18 @@ module RuboCop
186
287
  open_world_hint: false,
187
288
  read_only_hint: read_only_hint
188
289
  }
189
- ) do |path: nil, source_code: nil, safety: true|
190
- result = yield(path, source_code, safety)
290
+ ) do |**arguments|
291
+ # Taking any keywords makes the gem pass a `server_context` as well.
292
+ # Each tool's own block names the arguments it accepts.
293
+ arguments.delete(:server_context)
294
+ selection = Scope::PROPERTIES.keys.to_h { |key| [key, arguments.delete(key)] }
295
+ scope = Scope.new(inline: !arguments[:source_code].nil?, **selection)
296
+ result = yield(**arguments, scope: scope)
191
297
 
192
298
  ::MCP::Tool::Response.new([{ type: 'text', text: result }])
193
- rescue RuboCop::Error => e
299
+ rescue RuboCop::Error, IncorrectCopNameError => e
300
+ # The runner raises `IncorrectCopNameError` for a name in `only` or
301
+ # `except` that it does not know, with suggestions.
194
302
  ::MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
195
303
  end
196
304
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module MCP
5
+ # Reads and writes the files the tools work on. A file that can't be read
6
+ # or written fails the request with an error the agent can act on.
7
+ # @api private
8
+ module SourceFiles
9
+ module_function
10
+
11
+ def read(file, config)
12
+ RuboCop::ProcessedSource.from_file(
13
+ file, config.target_ruby_version, parser_engine: config.parser_engine
14
+ ).raw_source
15
+ rescue Errno::ENOENT
16
+ raise RuboCop::Error, "No such file or directory: #{file}"
17
+ end
18
+
19
+ # Writing what a file already holds would only bump its modification time.
20
+ def write(file, content)
21
+ return if File.file?(file) && File.binread(file) == content.b
22
+
23
+ Util.replace_file_contents(file, content)
24
+ rescue Errno::EACCES
25
+ raise RuboCop::Error, "Permission denied: #{file}"
26
+ rescue Errno::EPERM
27
+ raise RuboCop::Error, "Operation not permitted: #{file}"
28
+ rescue Errno::ENOSPC
29
+ raise RuboCop::Error, "No space left on device: #{file}"
30
+ rescue Errno::EROFS
31
+ raise RuboCop::Error, "Read-only file system: #{file}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # Caps how many offenses each cop gets to report.
5
+ #
6
+ # Counted across a whole run rather than per file, since a cop that fires a
7
+ # handful of times in every file is exactly the one worth capping. Offenses
8
+ # have to be offered in a stable order (files in the order they are
9
+ # reported) for the same ones to survive every time.
10
+ # @api private
11
+ class OffenseLimit
12
+ attr_reader :max
13
+
14
+ def initialize(max)
15
+ @max = max
16
+ @offered = Hash.new(0)
17
+ end
18
+
19
+ # @return [Array<Cop::Offense>] the offenses still within their cop's limit
20
+ def filter(offenses)
21
+ offenses.select { |offense| (@offered[offense.cop_name] += 1) <= @max }
22
+ end
23
+
24
+ # @return [Hash{String => Integer}] how many offenses each cop had left out,
25
+ # sorted by cop name
26
+ def elided_per_cop
27
+ @offered.filter_map { |cop, count| [cop, count - @max] if count > @max }.sort.to_h
28
+ end
29
+ end
30
+ end
@@ -17,7 +17,7 @@ module RuboCop
17
17
  'cops are enabled (via eg. Include/Exclude), and so that certain cops ' \
18
18
  'like Naming/FileName can be checked.'
19
19
  EXITING_OPTIONS = %i[
20
- version verbose_version show_cops list_enabled_cops_for show_docs_url lsp mcp
20
+ version verbose_version show_cops list_enabled_cops_for show_docs_url explain lsp mcp
21
21
  ].freeze
22
22
  DEFAULT_MAXIMUM_EXCLUSION_ITEMS = 15
23
23
 
@@ -33,6 +33,7 @@ module RuboCop
33
33
 
34
34
  define_options.parse!(args)
35
35
 
36
+ return_path_given_to_changed(args)
36
37
  imply_autocorrect_for_diff
37
38
 
38
39
  @validator.validate_compatibility
@@ -53,6 +54,12 @@ module RuboCop
53
54
 
54
55
  private
55
56
 
57
+ # `--changed lib` hands the path to `--changed` as its optional argument,
58
+ # so it goes back among the paths to inspect.
59
+ def return_path_given_to_changed(args)
60
+ args.unshift(@path_given_to_changed) if @path_given_to_changed
61
+ end
62
+
56
63
  # `--diff` is a dry run of autocorrection, so it turns autocorrection on
57
64
  # unless the user already picked a mode with `-a`, `-A` or `-x`.
58
65
  def imply_autocorrect_for_diff
@@ -111,8 +118,11 @@ module RuboCop
111
118
  option(opts, '--ignore-unrecognized-cops')
112
119
  option(opts, '--force-default-config')
113
120
  option(opts, '--changed [REVISION]') do |revision|
121
+ if @validator.path_given_to_changed?(revision)
122
+ @path_given_to_changed = revision
123
+ revision = nil
124
+ end
114
125
  @options[:changed] = revision || ChangedFiles::DEFAULT_REVISION
115
- @validator.validate_changed_revision(revision)
116
126
  end
117
127
  option(opts, '-s', '--stdin FILE')
118
128
  option(opts, '--editor-mode')
@@ -148,6 +158,9 @@ module RuboCop
148
158
  option(opts, '--display-only-correctable')
149
159
  option(opts, '--display-only-safe-correctable')
150
160
  option(opts, '--display-suppressed')
161
+ option(opts, '--max-offenses-per-cop COUNT') do
162
+ @validator.validate_max_offenses_per_cop_option
163
+ end
151
164
  end
152
165
  end
153
166
 
@@ -267,6 +280,9 @@ module RuboCop
267
280
  option(opts, '--show-docs-url [COP1,COP2,...]') do |list|
268
281
  @options[:show_docs_url] = list.nil? ? [] : list.split(',')
269
282
  end
283
+ option(opts, '--explain COP1,COP2,...') do |list|
284
+ @options[:explain] = list.split(',')
285
+ end
270
286
  end
271
287
  end
272
288
 
@@ -509,7 +525,7 @@ module RuboCop
509
525
  end
510
526
 
511
527
  def disable_parallel_when_invalid_option_combo
512
- return unless @options.key?(:parallel)
528
+ return unless @options[:parallel]
513
529
 
514
530
  invalid_flags = invalid_arguments_for_parallel
515
531
 
@@ -517,7 +533,9 @@ module RuboCop
517
533
 
518
534
  @options.delete(:parallel)
519
535
 
520
- puts '-P/--parallel is being ignored because ' \
536
+ # On stderr, so that it can't end up in the middle of a formatter's
537
+ # output, such as `--format json`.
538
+ warn '-P/--parallel is being ignored because ' \
521
539
  "it is not compatible with #{invalid_flags.join(', ')}."
522
540
  end
523
541
 
@@ -546,14 +564,22 @@ module RuboCop
546
564
  end
547
565
 
548
566
  # OptionParser hands `--changed lib/` the path as the revision, which is a
549
- # natural thing to type, so point at the fix rather than at git's error.
550
- def validate_changed_revision(revision)
551
- return unless revision && File.exist?(revision)
567
+ # natural thing to type, so an argument that names an existing path is
568
+ # taken as one. When it also names a revision there is no telling which
569
+ # was meant, and git itself refuses such an argument as ambiguous.
570
+ def path_given_to_changed?(revision)
571
+ return false unless revision && File.exist?(revision)
572
+ return true unless ChangedFiles.revision?(revision)
552
573
 
553
574
  raise OptionArgumentError,
554
- "--changed takes a git revision, but `#{revision}` is a path. Write the revision " \
555
- "as `--changed=#{revision}` if that is really what you meant, or drop it to " \
556
- 'compare against HEAD.'
575
+ "--changed got `#{revision}`, which is both a path and a git revision. Write " \
576
+ "`./#{revision}` for the path, or `--changed=#{revision}^0` for the revision."
577
+ end
578
+
579
+ def validate_max_offenses_per_cop_option
580
+ return if /^[1-9]\d*$/.match?(@options[:max_offenses_per_cop])
581
+
582
+ raise OptionParser::InvalidArgument, '--max-offenses-per-cop must be a positive integer'
557
583
  end
558
584
 
559
585
  def validate_exclude_limit_option
@@ -611,6 +637,11 @@ module RuboCop
611
637
  'when running --auto-gen-config, except if the',
612
638
  'number of files with offenses is bigger than',
613
639
  'exclude-limit. Default is false.'],
640
+ max_offenses_per_cop: ['Report at most COUNT offenses per cop across the',
641
+ 'whole run, so a single noisy cop cannot bury the',
642
+ 'rest. Offenses beyond the limit are not reported',
643
+ 'and do not count towards the totals, the same way',
644
+ 'the other display filters work.'],
614
645
  exclude_limit: ['Set the limit for how many files to explicitly exclude.',
615
646
  'If there are more files than the limit, the cop will',
616
647
  "be disabled instead. Default is #{MAX_EXCL}."],
@@ -674,6 +705,10 @@ module RuboCop
674
705
  'default, and their configurations for the',
675
706
  'current directory.',
676
707
  'You can use `*` as a wildcard.'],
708
+ explain: ['Explain what the given cops do: their',
709
+ 'description, configuration, whether they',
710
+ 'correct, and the examples from their own',
711
+ 'documentation.'],
677
712
  show_docs_url: ['Display url to documentation for the given',
678
713
  'cops, or base url by default.'],
679
714
  fail_fast: ['Inspect files in order of modification',