rubocop 1.91.0 → 1.92.1

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 (263) 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_loader.rb +6 -3
  14. data/lib/rubocop/config_loader_resolver.rb +37 -12
  15. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  16. data/lib/rubocop/config_validator.rb +11 -0
  17. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  18. data/lib/rubocop/cop/badge.rb +1 -1
  19. data/lib/rubocop/cop/base.rb +42 -21
  20. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  21. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  22. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  23. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  24. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  25. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +37 -7
  26. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  27. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +25 -4
  28. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +34 -0
  29. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  30. data/lib/rubocop/cop/documentation.rb +1 -1
  31. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  32. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  33. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  34. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  35. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  36. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  37. data/lib/rubocop/cop/layout/argument_alignment.rb +5 -1
  38. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  39. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  40. data/lib/rubocop/cop/layout/class_structure.rb +29 -7
  41. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  42. data/lib/rubocop/cop/layout/dot_position.rb +25 -15
  43. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +21 -8
  44. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  45. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  46. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +9 -3
  47. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  48. data/lib/rubocop/cop/layout/end_alignment.rb +19 -17
  49. data/lib/rubocop/cop/layout/extra_spacing.rb +4 -0
  50. data/lib/rubocop/cop/layout/first_argument_indentation.rb +27 -1
  51. data/lib/rubocop/cop/layout/first_array_element_line_break.rb +15 -0
  52. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +16 -8
  53. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  54. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  55. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  56. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +7 -3
  57. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  58. data/lib/rubocop/cop/layout/indentation_consistency.rb +22 -5
  59. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  60. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  61. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  62. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  63. data/lib/rubocop/cop/layout/line_length.rb +153 -21
  64. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  65. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  66. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +9 -0
  67. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  68. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +30 -5
  69. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  70. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  71. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +15 -2
  72. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  73. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  74. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  75. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  76. data/lib/rubocop/cop/layout/space_before_block_braces.rb +4 -1
  77. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  78. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +31 -8
  79. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +2 -12
  80. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  81. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  82. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -1
  83. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  84. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +4 -0
  85. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  86. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  87. data/lib/rubocop/cop/lint/loop.rb +24 -2
  88. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  89. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  90. data/lib/rubocop/cop/lint/number_conversion.rb +3 -1
  91. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +12 -1
  92. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  93. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +47 -1
  94. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -0
  95. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  96. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  97. data/lib/rubocop/cop/lint/useless_setter_call.rb +7 -4
  98. data/lib/rubocop/cop/lint/void.rb +8 -1
  99. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  100. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  101. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  102. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  103. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -19
  104. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +8 -2
  105. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  106. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  107. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +105 -0
  108. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  109. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +24 -5
  110. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  111. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  112. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  113. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  114. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  115. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  116. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  117. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  118. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  119. data/lib/rubocop/cop/mixin/statement_modifier.rb +25 -4
  120. data/lib/rubocop/cop/mixin/visibility_help.rb +2 -1
  121. data/lib/rubocop/cop/mixin.rb +2 -0
  122. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  123. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  124. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  125. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  126. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  127. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  128. data/lib/rubocop/cop/offense.rb +59 -3
  129. data/lib/rubocop/cop/registry.rb +1 -1
  130. data/lib/rubocop/cop/security/json_load.rb +16 -4
  131. data/lib/rubocop/cop/style/access_modifier_declarations.rb +37 -2
  132. data/lib/rubocop/cop/style/alias.rb +18 -5
  133. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  134. data/lib/rubocop/cop/style/and_or.rb +6 -2
  135. data/lib/rubocop/cop/style/arguments_forwarding.rb +32 -3
  136. data/lib/rubocop/cop/style/attr.rb +3 -4
  137. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  138. data/lib/rubocop/cop/style/block_delimiters.rb +35 -3
  139. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  140. data/lib/rubocop/cop/style/class_and_module_children.rb +56 -16
  141. data/lib/rubocop/cop/style/class_methods_definitions.rb +15 -0
  142. data/lib/rubocop/cop/style/collection_querying.rb +79 -46
  143. data/lib/rubocop/cop/style/combinable_loops.rb +89 -11
  144. data/lib/rubocop/cop/style/concat_array_literals.rb +58 -7
  145. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  146. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  147. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  148. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  149. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  150. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +53 -11
  151. data/lib/rubocop/cop/style/endless_method.rb +71 -6
  152. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  153. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  154. data/lib/rubocop/cop/style/float_division.rb +22 -2
  155. data/lib/rubocop/cop/style/for.rb +53 -2
  156. data/lib/rubocop/cop/style/format_string.rb +28 -6
  157. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  158. data/lib/rubocop/cop/style/guard_clause.rb +31 -22
  159. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +4 -0
  160. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  161. data/lib/rubocop/cop/style/hash_lookup_method.rb +32 -9
  162. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  163. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  164. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  165. data/lib/rubocop/cop/style/if_unless_modifier.rb +32 -26
  166. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  167. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  168. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  169. data/lib/rubocop/cop/style/it_block_parameter.rb +94 -4
  170. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  171. data/lib/rubocop/cop/style/lambda.rb +34 -1
  172. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  173. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  174. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +80 -13
  175. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +25 -2
  176. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  177. data/lib/rubocop/cop/style/method_def_parentheses.rb +29 -3
  178. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  179. data/lib/rubocop/cop/style/mixin_grouping.rb +12 -3
  180. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  181. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  182. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  183. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  184. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  185. data/lib/rubocop/cop/style/mutable_constant.rb +52 -7
  186. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  187. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  188. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  189. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  190. data/lib/rubocop/cop/style/next.rb +24 -10
  191. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  192. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  193. data/lib/rubocop/cop/style/not.rb +10 -5
  194. data/lib/rubocop/cop/style/numeric_predicate.rb +21 -5
  195. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  196. data/lib/rubocop/cop/style/parentheses_around_condition.rb +1 -1
  197. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  198. data/lib/rubocop/cop/style/raise_args.rb +19 -9
  199. data/lib/rubocop/cop/style/reduce_to_hash.rb +17 -1
  200. data/lib/rubocop/cop/style/redundant_begin.rb +22 -4
  201. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  202. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  203. data/lib/rubocop/cop/style/redundant_exception.rb +15 -3
  204. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  205. data/lib/rubocop/cop/style/redundant_freeze.rb +11 -3
  206. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +24 -1
  207. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  208. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  209. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  210. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  211. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +28 -0
  212. data/lib/rubocop/cop/style/redundant_sort.rb +38 -4
  213. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  214. data/lib/rubocop/cop/style/require_order.rb +25 -8
  215. data/lib/rubocop/cop/style/rescue_modifier.rb +11 -9
  216. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  217. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  218. data/lib/rubocop/cop/style/sample.rb +9 -8
  219. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  220. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  221. data/lib/rubocop/cop/style/send_with_literal_method_name.rb +4 -0
  222. data/lib/rubocop/cop/style/single_line_methods.rb +21 -4
  223. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  224. data/lib/rubocop/cop/style/string_concatenation.rb +9 -3
  225. data/lib/rubocop/cop/style/struct_inheritance.rb +6 -0
  226. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  227. data/lib/rubocop/cop/style/symbol_proc.rb +24 -6
  228. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  229. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  230. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  231. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  232. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  233. data/lib/rubocop/cop/team.rb +10 -5
  234. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  235. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  236. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  237. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  238. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  239. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  240. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  241. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  242. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  243. data/lib/rubocop/lsp/routes.rb +23 -23
  244. data/lib/rubocop/lsp/runtime.rb +47 -11
  245. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  246. data/lib/rubocop/lsp.rb +5 -2
  247. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  248. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  249. data/lib/rubocop/mcp/scope.rb +65 -0
  250. data/lib/rubocop/mcp/server.rb +184 -76
  251. data/lib/rubocop/mcp/source_files.rb +35 -0
  252. data/lib/rubocop/offense_limit.rb +30 -0
  253. data/lib/rubocop/options.rb +45 -10
  254. data/lib/rubocop/plugin/configuration_integrator.rb +12 -5
  255. data/lib/rubocop/remote_config.rb +4 -1
  256. data/lib/rubocop/result_cache.rb +20 -13
  257. data/lib/rubocop/runner.rb +109 -41
  258. data/lib/rubocop/server/cache.rb +6 -6
  259. data/lib/rubocop/target_ruby.rb +21 -9
  260. data/lib/rubocop/util.rb +69 -0
  261. data/lib/rubocop/version.rb +1 -1
  262. data/lib/rubocop.rb +5 -0
  263. metadata +16 -5
@@ -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',
@@ -59,15 +59,22 @@ module RuboCop
59
59
 
60
60
  plugin_config.make_excludes_absolute
61
61
 
62
- # `unset_nil: false` keeps `AllCops` keys declared with a nil value by a plugin
63
- # (e.g. `TargetRailsVersion: ~` of rubocop-rails); `merge_all_cop_settings` above
64
- # carries such keys into each subsequent plugin's config, where `unset_nil: true`
65
- # would delete them from the combined configuration.
66
- ConfigLoader.merge_with_default(plugin_config, plugin_config_path, unset_nil: false)
62
+ merge_plugin_config_with_default(plugin_config, plugin_config_path)
67
63
  end
68
64
  end
69
65
  end
70
66
 
67
+ # `unset_nil: false` keeps `AllCops` keys declared with a nil value by a plugin
68
+ # (e.g. `TargetRailsVersion: ~` of rubocop-rails); `merge_all_cop_settings` carries
69
+ # such keys into each subsequent plugin's config, where `unset_nil: true` would
70
+ # delete them from the combined configuration. Preview is left to be applied when
71
+ # a project configuration is resolved against the defaults.
72
+ def merge_plugin_config_with_default(plugin_config, plugin_config_path)
73
+ ConfigLoader.merge_with_default(
74
+ plugin_config, plugin_config_path, unset_nil: false, resolve_preview: false
75
+ )
76
+ end
77
+
71
78
  def merge_plugin_config_into_all_cops!(rubocop_config, plugin_config)
72
79
  rubocop_config['AllCops'].merge!(plugin_config['AllCops'])
73
80
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
3
  require 'time'
4
+ require 'uri'
5
5
 
6
6
  module RuboCop
7
7
  # Common methods and behaviors for dealing with remote config files.
@@ -45,6 +45,9 @@ module RuboCop
45
45
  def request(uri = @uri, limit = 10, &block)
46
46
  raise ArgumentError, 'HTTP redirect too deep' if limit.zero?
47
47
 
48
+ # Loaded here rather than at the top, since most runs never fetch a remote config.
49
+ require 'net/http'
50
+
48
51
  http = Net::HTTP.new(uri.hostname, uri.port)
49
52
  http.use_ssl = uri.instance_of?(URI::HTTPS)
50
53
 
@@ -59,9 +59,9 @@ module RuboCop
59
59
  puts "Removing the #{remove_count} oldest files from #{rubocop_cache_dir}" if verbose
60
60
  sorted = files.sort_by { |path| File.mtime(path) }
61
61
  remove_files(sorted, remove_count)
62
- rescue Errno::ENOENT
62
+ rescue Errno::ENOENT, Errno::EACCES, Errno::EPERM, Errno::EROFS
63
63
  # This can happen if parallel RuboCop invocations try to remove the
64
- # same files. No problem.
64
+ # same files, or if the cache can't be written to. No problem.
65
65
  puts $ERROR_INFO if verbose
66
66
  end
67
67
 
@@ -138,26 +138,17 @@ module RuboCop
138
138
 
139
139
  begin
140
140
  FileUtils.mkdir_p(dir)
141
- rescue Errno::EACCES, Errno::EROFS => e
141
+ rescue Errno::EACCES, Errno::EPERM, Errno::EROFS => e
142
142
  warn "Couldn't create cache directory. Continuing without cache.\n #{e.message}"
143
143
  return
144
144
  end
145
145
 
146
- preliminary_path = "#{@path}_#{rand(1_000_000_000)}"
147
146
  # RuboCop must be in control of where its cached data is stored. A
148
147
  # symbolic link anywhere in the cache directory tree can be an
149
148
  # indication that a symlink attack is being waged.
150
149
  return if symlink_protection_triggered?(dir)
151
150
 
152
- File.open(preliminary_path, 'w', encoding: Encoding::UTF_8) do |f|
153
- f.write(@cached_data.to_json(offenses))
154
- end
155
- # The preliminary path is used so that if there are multiple RuboCop
156
- # processes trying to save data for the same inspected file
157
- # simultaneously, the only problem we run in to is a competition who gets
158
- # to write to the final file. The contents are the same, so no corruption
159
- # of data should occur.
160
- FileUtils.mv(preliminary_path, @path)
151
+ write_cache_file(offenses)
161
152
  end
162
153
 
163
154
  private
@@ -177,6 +168,22 @@ module RuboCop
177
168
  false
178
169
  end
179
170
 
171
+ def write_cache_file(offenses)
172
+ preliminary_path = "#{@path}_#{rand(1_000_000_000)}"
173
+ File.open(preliminary_path, 'w', encoding: Encoding::UTF_8) do |f|
174
+ f.write(@cached_data.to_json(offenses))
175
+ end
176
+ # The preliminary path is used so that if there are multiple RuboCop
177
+ # processes trying to save data for the same inspected file
178
+ # simultaneously, the only problem we run in to is a competition who gets
179
+ # to write to the final file. The contents are the same, so no corruption
180
+ # of data should occur.
181
+ FileUtils.mv(preliminary_path, @path)
182
+ rescue Errno::EACCES, Errno::EPERM, Errno::EROFS => e
183
+ FileUtils.rm_f(preliminary_path)
184
+ warn "Couldn't write cache file. Continuing without cache.\n #{e.message}"
185
+ end
186
+
180
187
  def file_checksum(file, config_store)
181
188
  stat = File.stat(file)
182
189
  return unavailable_checksum unless stat.file?