rubocop 1.90.0 → 1.92.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 (303) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +241 -11
  4. data/config/obsoletion.yml +9 -0
  5. data/lib/rubocop/cached_data.rb +47 -7
  6. data/lib/rubocop/changed_files.rb +134 -0
  7. data/lib/rubocop/cli/command/cop_names.rb +57 -0
  8. data/lib/rubocop/cli/command/execute_runner.rb +23 -3
  9. data/lib/rubocop/cli/command/explain.rb +34 -0
  10. data/lib/rubocop/cli/command/show_cops.rb +6 -0
  11. data/lib/rubocop/cli/command/show_docs_url.rb +6 -4
  12. data/lib/rubocop/cli.rb +11 -4
  13. data/lib/rubocop/comment_config/directive_range.rb +3 -1
  14. data/lib/rubocop/comment_config/disable_next.rb +49 -3
  15. data/lib/rubocop/comment_config/push_pop.rb +55 -0
  16. data/lib/rubocop/comment_config.rb +32 -53
  17. data/lib/rubocop/config.rb +10 -0
  18. data/lib/rubocop/config_loader.rb +18 -7
  19. data/lib/rubocop/config_loader_resolver.rb +79 -12
  20. data/lib/rubocop/config_obsoletion/cop_rule.rb +2 -1
  21. data/lib/rubocop/config_obsoletion/removed_cop.rb +9 -1
  22. data/lib/rubocop/config_obsoletion.rb +1 -1
  23. data/lib/rubocop/config_validator.rb +29 -3
  24. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  25. data/lib/rubocop/cop/badge.rb +1 -1
  26. data/lib/rubocop/cop/base.rb +57 -24
  27. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  28. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  29. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  30. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  31. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  32. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +22 -7
  33. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  34. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +23 -2
  35. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  36. data/lib/rubocop/cop/correctors.rb +2 -2
  37. data/lib/rubocop/cop/documentation.rb +1 -1
  38. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  39. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  40. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  41. data/lib/rubocop/cop/generator.rb +1 -1
  42. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  43. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  44. data/lib/rubocop/cop/internal_affairs/on_send_without_on_csend.rb +1 -1
  45. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  46. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  47. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -0
  48. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  49. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  50. data/lib/rubocop/cop/layout/class_structure.rb +9 -1
  51. data/lib/rubocop/cop/layout/comment_indentation.rb +8 -5
  52. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  53. data/lib/rubocop/cop/layout/else_alignment.rb +1 -0
  54. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +14 -5
  55. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  56. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  57. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  58. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +1 -1
  59. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  60. data/lib/rubocop/cop/layout/end_alignment.rb +16 -10
  61. data/lib/rubocop/cop/layout/extra_spacing.rb +4 -0
  62. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -0
  63. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +17 -4
  64. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +5 -3
  65. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  66. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  67. data/lib/rubocop/cop/layout/first_parameter_indentation.rb +11 -0
  68. data/lib/rubocop/cop/layout/hash_alignment.rb +15 -10
  69. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +2 -0
  70. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  71. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  72. data/lib/rubocop/cop/layout/indentation_width.rb +4 -4
  73. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  74. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  75. data/lib/rubocop/cop/layout/line_length.rb +71 -19
  76. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  77. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  78. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +7 -0
  79. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  80. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +14 -0
  81. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  82. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  83. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +15 -2
  84. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  85. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  86. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +10 -4
  87. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  88. data/lib/rubocop/cop/layout/space_around_operators.rb +5 -1
  89. data/lib/rubocop/cop/layout/space_before_block_braces.rb +4 -1
  90. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  91. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +31 -8
  92. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  93. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  94. data/lib/rubocop/cop/lint/argument_mismatch.rb +14 -4
  95. data/lib/rubocop/cop/lint/assignment_in_condition.rb +2 -0
  96. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +88 -14
  97. data/lib/rubocop/cop/lint/duplicate_methods.rb +1 -1
  98. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  99. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  100. data/lib/rubocop/cop/lint/literal_as_condition.rb +23 -2
  101. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  102. data/lib/rubocop/cop/lint/loop.rb +24 -2
  103. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +245 -0
  104. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +19 -4
  105. data/lib/rubocop/cop/lint/nested_method_definition.rb +1 -1
  106. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +1 -1
  107. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  108. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +17 -1
  109. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +42 -17
  110. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  111. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  112. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  113. data/lib/rubocop/cop/lint/void.rb +8 -1
  114. data/lib/rubocop/cop/lint.rb +1 -0
  115. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  116. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  117. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  118. data/lib/rubocop/cop/migration/department_name.rb +1 -1
  119. data/lib/rubocop/cop/mixin/alignment.rb +5 -1
  120. data/lib/rubocop/cop/mixin/annotation_comment.rb +1 -1
  121. data/lib/rubocop/cop/mixin/check_line_breakable.rb +17 -14
  122. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +5 -1
  123. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  124. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  125. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +69 -0
  126. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  127. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +26 -2
  128. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +31 -3
  129. data/lib/rubocop/cop/mixin/hash_transform_method.rb +1 -1
  130. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  131. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  132. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  133. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  134. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  135. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +29 -3
  136. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  137. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  138. data/lib/rubocop/cop/mixin/statement_modifier.rb +22 -2
  139. data/lib/rubocop/cop/mixin.rb +4 -2
  140. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  141. data/lib/rubocop/cop/naming/block_forwarding.rb +4 -1
  142. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  143. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  144. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  145. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  146. data/lib/rubocop/cop/offense.rb +59 -3
  147. data/lib/rubocop/cop/registry.rb +10 -2
  148. data/lib/rubocop/cop/security/json_load.rb +16 -4
  149. data/lib/rubocop/cop/severity.rb +12 -0
  150. data/lib/rubocop/cop/style/access_modifier_declarations.rb +10 -2
  151. data/lib/rubocop/cop/style/alias.rb +18 -5
  152. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  153. data/lib/rubocop/cop/style/and_or.rb +4 -0
  154. data/lib/rubocop/cop/style/arguments_forwarding.rb +6 -3
  155. data/lib/rubocop/cop/style/attr.rb +3 -4
  156. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  157. data/lib/rubocop/cop/style/block_delimiters.rb +29 -3
  158. data/lib/rubocop/cop/style/case_like_if.rb +9 -6
  159. data/lib/rubocop/cop/style/class_and_module_children.rb +54 -16
  160. data/lib/rubocop/cop/style/class_methods_definitions.rb +8 -0
  161. data/lib/rubocop/cop/style/combinable_loops.rb +85 -11
  162. data/lib/rubocop/cop/style/concat_array_literals.rb +46 -7
  163. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  164. data/lib/rubocop/cop/style/constant_visibility.rb +6 -2
  165. data/lib/rubocop/cop/style/data_inheritance.rb +14 -0
  166. data/lib/rubocop/cop/style/directive_scope.rb +154 -21
  167. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +118 -18
  168. data/lib/rubocop/cop/style/documentation_method.rb +1 -9
  169. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  170. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  171. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  172. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  173. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  174. data/lib/rubocop/cop/style/empty_method.rb +15 -3
  175. data/lib/rubocop/cop/style/endless_method.rb +91 -11
  176. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  177. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  178. data/lib/rubocop/cop/style/file_null.rb +2 -2
  179. data/lib/rubocop/cop/style/float_division.rb +28 -14
  180. data/lib/rubocop/cop/style/for.rb +44 -1
  181. data/lib/rubocop/cop/style/format_string.rb +28 -6
  182. data/lib/rubocop/cop/style/format_string_token.rb +1 -1
  183. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  184. data/lib/rubocop/cop/style/guard_clause.rb +13 -1
  185. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  186. data/lib/rubocop/cop/style/hash_lookup_method.rb +35 -9
  187. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  188. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  189. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  190. data/lib/rubocop/cop/style/if_unless_modifier.rb +19 -20
  191. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  192. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  193. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  194. data/lib/rubocop/cop/style/it_block_parameter.rb +32 -3
  195. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  196. data/lib/rubocop/cop/style/lambda.rb +18 -0
  197. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  198. data/lib/rubocop/cop/style/magic_comment_format.rb +1 -1
  199. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  200. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +45 -10
  201. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +25 -2
  202. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  203. data/lib/rubocop/cop/style/method_def_parentheses.rb +29 -3
  204. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  205. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  206. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  207. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  208. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  209. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  210. data/lib/rubocop/cop/style/mutable_constant.rb +28 -1
  211. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  212. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  213. data/lib/rubocop/cop/style/nested_modifier.rb +36 -12
  214. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  215. data/lib/rubocop/cop/style/next.rb +24 -10
  216. data/lib/rubocop/cop/style/nil_comparison.rb +48 -8
  217. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  218. data/lib/rubocop/cop/style/non_nil_check.rb +18 -2
  219. data/lib/rubocop/cop/style/not.rb +10 -3
  220. data/lib/rubocop/cop/style/numeric_literal_prefix.rb +11 -1
  221. data/lib/rubocop/cop/style/numeric_predicate.rb +21 -5
  222. data/lib/rubocop/cop/style/one_line_conditional.rb +14 -3
  223. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -1
  224. data/lib/rubocop/cop/style/parentheses_around_condition.rb +6 -1
  225. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  226. data/lib/rubocop/cop/style/reduce_to_hash.rb +17 -1
  227. data/lib/rubocop/cop/style/redundant_begin.rb +20 -3
  228. data/lib/rubocop/cop/style/redundant_condition.rb +34 -17
  229. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +16 -1
  230. data/lib/rubocop/cop/style/redundant_exception.rb +15 -3
  231. data/lib/rubocop/cop/style/redundant_fetch_block.rb +10 -2
  232. data/lib/rubocop/cop/style/redundant_freeze.rb +10 -1
  233. data/lib/rubocop/cop/style/redundant_parentheses.rb +23 -2
  234. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  235. data/lib/rubocop/cop/style/redundant_regexp_character_class.rb +6 -5
  236. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +25 -5
  237. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  238. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  239. data/lib/rubocop/cop/style/require_order.rb +9 -0
  240. data/lib/rubocop/cop/style/rescue_modifier.rb +5 -9
  241. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  242. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  243. data/lib/rubocop/cop/style/sample.rb +9 -8
  244. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  245. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  246. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  247. data/lib/rubocop/cop/style/single_line_methods.rb +16 -4
  248. data/lib/rubocop/cop/style/special_global_vars.rb +1 -1
  249. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  250. data/lib/rubocop/cop/style/string_concatenation.rb +9 -3
  251. data/lib/rubocop/cop/style/struct_inheritance.rb +14 -0
  252. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  253. data/lib/rubocop/cop/style/symbol_proc.rb +23 -5
  254. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  255. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  256. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  257. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  258. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  259. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  260. data/lib/rubocop/cop/style.rb +0 -1
  261. data/lib/rubocop/cop/team.rb +10 -5
  262. data/lib/rubocop/cop/util.rb +1 -1
  263. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  264. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  265. data/lib/rubocop/cops_documentation_generator.rb +23 -2
  266. data/lib/rubocop/directive_comment.rb +37 -8
  267. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  268. data/lib/rubocop/formatter/disabled_config_formatter.rb +19 -1
  269. data/lib/rubocop/formatter/formatter_set.rb +6 -1
  270. data/lib/rubocop/formatter/github_actions_formatter.rb +6 -5
  271. data/lib/rubocop/formatter/html_formatter.rb +2 -2
  272. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  273. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  274. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  275. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  276. data/lib/rubocop/formatter/sarif_formatter.rb +186 -0
  277. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  278. data/lib/rubocop/formatter.rb +1 -0
  279. data/lib/rubocop/lsp/runtime.rb +47 -11
  280. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  281. data/lib/rubocop/lsp.rb +5 -2
  282. data/lib/rubocop/magic_comment.rb +27 -0
  283. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  284. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  285. data/lib/rubocop/mcp/scope.rb +65 -0
  286. data/lib/rubocop/mcp/server.rb +184 -76
  287. data/lib/rubocop/mcp/source_files.rb +35 -0
  288. data/lib/rubocop/offense_limit.rb +30 -0
  289. data/lib/rubocop/options.rb +97 -4
  290. data/lib/rubocop/path_util.rb +1 -1
  291. data/lib/rubocop/plugin/configuration_integrator.rb +12 -5
  292. data/lib/rubocop/plugin/loader.rb +1 -1
  293. data/lib/rubocop/rake_task.rb +1 -1
  294. data/lib/rubocop/remote_config.rb +8 -5
  295. data/lib/rubocop/result_cache.rb +36 -18
  296. data/lib/rubocop/runner.rb +160 -49
  297. data/lib/rubocop/server/cache.rb +2 -2
  298. data/lib/rubocop/unified_diff.rb +257 -0
  299. data/lib/rubocop/util.rb +69 -0
  300. data/lib/rubocop/version.rb +1 -1
  301. data/lib/rubocop.rb +9 -1
  302. metadata +21 -6
  303. data/lib/rubocop/cop/style/double_cop_disable_directive.rb +0 -46
@@ -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,9 @@ module RuboCop
33
33
 
34
34
  define_options.parse!(args)
35
35
 
36
+ return_path_given_to_changed(args)
37
+ imply_autocorrect_for_diff
38
+
36
39
  @validator.validate_compatibility
37
40
 
38
41
  if @options[:stdin]
@@ -51,6 +54,21 @@ module RuboCop
51
54
 
52
55
  private
53
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
+
63
+ # `--diff` is a dry run of autocorrection, so it turns autocorrection on
64
+ # unless the user already picked a mode with `-a`, `-A` or `-x`.
65
+ def imply_autocorrect_for_diff
66
+ return unless @options[:diff] && !@options[:autocorrect]
67
+
68
+ @options[:safe_autocorrect] = true
69
+ @options[:autocorrect] = true
70
+ end
71
+
54
72
  # rubocop:disable-next Metrics/AbcSize
55
73
  def define_options
56
74
  OptionParser.new do |opts|
@@ -92,12 +110,20 @@ module RuboCop
92
110
  option(opts, '--enable-pending-cops')
93
111
  option(opts, '--disable-all-cops')
94
112
  option(opts, '--enable-all-cops')
113
+ option(opts, '--[no-]preview')
95
114
  option(opts, '--ignore-disable-comments')
96
115
  option(opts, '--force-exclusion')
97
116
  option(opts, '--only-recognized-file-types')
98
117
  option(opts, '--ignore-parent-exclusion')
99
118
  option(opts, '--ignore-unrecognized-cops')
100
119
  option(opts, '--force-default-config')
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
125
+ @options[:changed] = revision || ChangedFiles::DEFAULT_REVISION
126
+ end
101
127
  option(opts, '-s', '--stdin FILE')
102
128
  option(opts, '--editor-mode')
103
129
  option(opts, '-P', '--[no-]parallel')
@@ -132,10 +158,13 @@ module RuboCop
132
158
  option(opts, '--display-only-correctable')
133
159
  option(opts, '--display-only-safe-correctable')
134
160
  option(opts, '--display-suppressed')
161
+ option(opts, '--max-offenses-per-cop COUNT') do
162
+ @validator.validate_max_offenses_per_cop_option
163
+ end
135
164
  end
136
165
  end
137
166
 
138
- # rubocop:todo Naming/InclusiveLanguage
167
+ # rubocop:todo Naming/InclusiveLanguage -- `--auto-correct` is the deprecated flag name
139
168
  # the autocorrect command-line arguments map to the autocorrect @options values like so:
140
169
  # :fix_layout :autocorrect :safe_autocorrect :autocorrect_all
141
170
  # -x, --fix-layout true true - -
@@ -161,6 +190,8 @@ module RuboCop
161
190
  end
162
191
 
163
192
  option(opts, '--disable-uncorrectable')
193
+
194
+ option(opts, '--diff')
164
195
  end
165
196
  end
166
197
  # rubocop:enable Naming/InclusiveLanguage
@@ -249,6 +280,9 @@ module RuboCop
249
280
  option(opts, '--show-docs-url [COP1,COP2,...]') do |list|
250
281
  @options[:show_docs_url] = list.nil? ? [] : list.split(',')
251
282
  end
283
+ option(opts, '--explain COP1,COP2,...') do |list|
284
+ @options[:explain] = list.split(',')
285
+ end
252
286
  end
253
287
  end
254
288
 
@@ -400,6 +434,8 @@ module RuboCop
400
434
  validate_display_only_failed_and_display_only_correctable
401
435
  validate_display_only_correctable_and_autocorrect
402
436
  validate_lsp_and_editor_mode
437
+ validate_diff
438
+ validate_changed_and_stdin
403
439
  validate_enable_all_cops_and_disable_all_cops
404
440
  disable_parallel_when_invalid_option_combo
405
441
 
@@ -447,6 +483,20 @@ module RuboCop
447
483
  '--display-only-failed cannot be used together with other display options.'
448
484
  end
449
485
 
486
+ # `--diff` promises not to write anything, and `--auto-gen-config` exists to
487
+ # write a file.
488
+ def validate_diff
489
+ return unless @options.key?(:diff) && @options.key?(:auto_gen_config)
490
+
491
+ raise OptionArgumentError, '--diff cannot be used with --auto-gen-config.'
492
+ end
493
+
494
+ def validate_changed_and_stdin
495
+ return unless @options.key?(:changed) && @options.key?(:stdin)
496
+
497
+ raise OptionArgumentError, '--changed cannot be used with --stdin.'
498
+ end
499
+
450
500
  def validate_lsp_and_editor_mode
451
501
  return if !@options.key?(:lsp) || !@options.key?(:editor_mode)
452
502
 
@@ -475,7 +525,7 @@ module RuboCop
475
525
  end
476
526
 
477
527
  def disable_parallel_when_invalid_option_combo
478
- return unless @options.key?(:parallel)
528
+ return unless @options[:parallel]
479
529
 
480
530
  invalid_flags = invalid_arguments_for_parallel
481
531
 
@@ -483,7 +533,9 @@ module RuboCop
483
533
 
484
534
  @options.delete(:parallel)
485
535
 
486
- 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 ' \
487
539
  "it is not compatible with #{invalid_flags.join(', ')}."
488
540
  end
489
541
 
@@ -511,6 +563,25 @@ module RuboCop
511
563
  @incompatible_options ||= @options.keys & Options::EXITING_OPTIONS
512
564
  end
513
565
 
566
+ # OptionParser hands `--changed lib/` the path as the revision, which is a
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)
573
+
574
+ raise OptionArgumentError,
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'
583
+ end
584
+
514
585
  def validate_exclude_limit_option
515
586
  return if /^\d+$/.match?(@options[:exclude_limit])
516
587
 
@@ -566,12 +637,21 @@ module RuboCop
566
637
  'when running --auto-gen-config, except if the',
567
638
  'number of files with offenses is bigger than',
568
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.'],
569
645
  exclude_limit: ['Set the limit for how many files to explicitly exclude.',
570
646
  'If there are more files than the limit, the cop will',
571
647
  "be disabled instead. Default is #{MAX_EXCL}."],
572
648
  disable_uncorrectable: ['Used with --autocorrect to annotate any',
573
649
  'offenses that do not support autocorrect',
574
650
  'with `rubocop:todo` comments.'],
651
+ diff: ['Print a unified diff of what autocorrection',
652
+ 'would change, without writing any files.',
653
+ 'Turns on safe autocorrection unless a mode',
654
+ 'was already given with -a, -A or -x.'],
575
655
  no_exclude_limit: ['Do not set the limit for how many files to exclude.'],
576
656
  force_exclusion: ['Any files excluded by `Exclude` in configuration',
577
657
  'files will be excluded, even if given explicitly',
@@ -584,6 +664,10 @@ module RuboCop
584
664
  ignore_parent_exclusion: ['Prevent from inheriting `AllCops/Exclude` from',
585
665
  'parent folders.'],
586
666
  ignore_unrecognized_cops: ['Ignore unrecognized cops or departments in the config.'],
667
+ changed: ['Inspect only the files that differ from a git',
668
+ 'revision, defaulting to HEAD. Untracked files',
669
+ 'count as changed. Pass a revision with',
670
+ '`--changed=REVISION`.'],
587
671
  force_default_config: ['Use default configuration even if configuration',
588
672
  'files are present in the directory tree.'],
589
673
  format: ['Choose an output formatter. This option',
@@ -598,6 +682,7 @@ module RuboCop
598
682
  'specified --format, or the default format',
599
683
  'if no format is specified.'],
600
684
  fail_level: ['Minimum severity for exit with error code.',
685
+ 'Overrides `AllCops: FailLevel` in the configuration.',
601
686
  ' [A] autocorrect',
602
687
  ' [I] info',
603
688
  ' [R] refactor',
@@ -620,6 +705,10 @@ module RuboCop
620
705
  'default, and their configurations for the',
621
706
  'current directory.',
622
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.'],
623
712
  show_docs_url: ['Display url to documentation for the given',
624
713
  'cops, or base url by default.'],
625
714
  fail_fast: ['Inspect files in order of modification',
@@ -642,6 +731,10 @@ module RuboCop
642
731
  '`AllCops/DisabledByDefault` in config files.'],
643
732
  display_style_guide: 'Display style guide URLs in offense messages.',
644
733
  enable_pending_cops: 'Run with pending cops.',
734
+ preview: ['Opt in to unstable behavior: cops that are',
735
+ '`Enabled: preview`, and changes to existing',
736
+ 'cops that are not the default yet.',
737
+ 'Overrides `AllCops: Preview`.'],
645
738
  enable_all_cops: ['Run with all cops enabled, including those',
646
739
  'disabled by default. Overrides',
647
740
  '`AllCops/EnabledByDefault` and',
@@ -44,7 +44,7 @@ module RuboCop
44
44
  uri.start_with?('http://', 'https://')
45
45
  end
46
46
 
47
- SMART_PATH_CACHE = {} # rubocop:disable Style/MutableConstant
47
+ SMART_PATH_CACHE = {} # rubocop:disable Style/MutableConstant -- a cache, written to at runtime
48
48
  private_constant :SMART_PATH_CACHE
49
49
 
50
50
  def smart_path(path)