rubocop 1.84.2 → 1.86.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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +91 -15
  3. data/config/obsoletion.yml +5 -0
  4. data/lib/rubocop/cache_config.rb +1 -1
  5. data/lib/rubocop/cli/command/auto_generate_config.rb +1 -1
  6. data/lib/rubocop/cli/command/mcp.rb +19 -0
  7. data/lib/rubocop/cli/command/show_cops.rb +2 -2
  8. data/lib/rubocop/cli/command/show_docs_url.rb +1 -1
  9. data/lib/rubocop/cli.rb +6 -3
  10. data/lib/rubocop/config.rb +14 -10
  11. data/lib/rubocop/config_finder.rb +1 -1
  12. data/lib/rubocop/config_loader_resolver.rb +2 -1
  13. data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
  14. data/lib/rubocop/config_store.rb +1 -1
  15. data/lib/rubocop/config_validator.rb +1 -1
  16. data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
  17. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
  18. data/lib/rubocop/cop/documentation.rb +2 -3
  19. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  20. data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
  21. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  22. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -2
  23. data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
  24. data/lib/rubocop/cop/layout/dot_position.rb +1 -1
  25. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +9 -2
  26. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +1 -1
  27. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  28. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
  29. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
  30. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
  31. data/lib/rubocop/cop/layout/end_alignment.rb +6 -3
  32. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +7 -1
  33. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  34. data/lib/rubocop/cop/layout/indentation_width.rb +1 -1
  35. data/lib/rubocop/cop/layout/line_length.rb +5 -3
  36. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
  37. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +28 -3
  38. data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
  39. data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
  40. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  41. data/lib/rubocop/cop/layout/space_around_keyword.rb +3 -1
  42. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +1 -0
  43. data/lib/rubocop/cop/lint/constant_reassignment.rb +59 -9
  44. data/lib/rubocop/cop/lint/constant_resolution.rb +1 -1
  45. data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
  46. data/lib/rubocop/cop/lint/duplicate_methods.rb +55 -8
  47. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  48. data/lib/rubocop/cop/lint/empty_conditional_body.rb +6 -1
  49. data/lib/rubocop/cop/lint/empty_in_pattern.rb +8 -1
  50. data/lib/rubocop/cop/lint/empty_when.rb +8 -1
  51. data/lib/rubocop/cop/lint/interpolation_check.rb +7 -2
  52. data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
  53. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +3 -1
  54. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  55. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +0 -9
  56. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +23 -6
  57. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +17 -0
  58. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
  59. data/lib/rubocop/cop/lint/syntax.rb +25 -1
  60. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +1 -0
  61. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
  62. data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
  63. data/lib/rubocop/cop/lint/unused_method_argument.rb +10 -0
  64. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  65. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +4 -4
  66. data/lib/rubocop/cop/lint/useless_default_value_argument.rb +2 -0
  67. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +22 -7
  68. data/lib/rubocop/cop/lint/void.rb +32 -12
  69. data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
  70. data/lib/rubocop/cop/migration/department_name.rb +12 -1
  71. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  72. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +2 -2
  73. data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
  74. data/lib/rubocop/cop/mixin/hash_transform_method.rb +10 -60
  75. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  76. data/lib/rubocop/cop/registry.rb +20 -13
  77. data/lib/rubocop/cop/security/eval.rb +15 -2
  78. data/lib/rubocop/cop/style/access_modifier_declarations.rb +14 -2
  79. data/lib/rubocop/cop/style/accessor_grouping.rb +4 -2
  80. data/lib/rubocop/cop/style/alias.rb +4 -1
  81. data/lib/rubocop/cop/style/and_or.rb +1 -0
  82. data/lib/rubocop/cop/style/arguments_forwarding.rb +25 -7
  83. data/lib/rubocop/cop/style/array_join.rb +4 -2
  84. data/lib/rubocop/cop/style/ascii_comments.rb +6 -3
  85. data/lib/rubocop/cop/style/attr.rb +5 -2
  86. data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
  87. data/lib/rubocop/cop/style/begin_block.rb +3 -1
  88. data/lib/rubocop/cop/style/block_delimiters.rb +25 -33
  89. data/lib/rubocop/cop/style/case_equality.rb +4 -0
  90. data/lib/rubocop/cop/style/class_and_module_children.rb +10 -2
  91. data/lib/rubocop/cop/style/collection_compact.rb +36 -16
  92. data/lib/rubocop/cop/style/colon_method_call.rb +3 -1
  93. data/lib/rubocop/cop/style/concat_array_literals.rb +2 -0
  94. data/lib/rubocop/cop/style/conditional_assignment.rb +0 -4
  95. data/lib/rubocop/cop/style/copyright.rb +1 -1
  96. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  97. data/lib/rubocop/cop/style/each_with_object.rb +2 -0
  98. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  99. data/lib/rubocop/cop/style/empty_class_definition.rb +43 -20
  100. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  101. data/lib/rubocop/cop/style/encoding.rb +7 -1
  102. data/lib/rubocop/cop/style/end_block.rb +3 -1
  103. data/lib/rubocop/cop/style/endless_method.rb +8 -3
  104. data/lib/rubocop/cop/style/file_open.rb +84 -0
  105. data/lib/rubocop/cop/style/for.rb +3 -0
  106. data/lib/rubocop/cop/style/format_string_token.rb +29 -2
  107. data/lib/rubocop/cop/style/global_vars.rb +5 -2
  108. data/lib/rubocop/cop/style/guard_clause.rb +9 -6
  109. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
  110. data/lib/rubocop/cop/style/hash_lookup_method.rb +7 -0
  111. data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
  112. data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
  113. data/lib/rubocop/cop/style/if_inside_else.rb +1 -5
  114. data/lib/rubocop/cop/style/if_unless_modifier.rb +14 -3
  115. data/lib/rubocop/cop/style/if_with_semicolon.rb +7 -5
  116. data/lib/rubocop/cop/style/inline_comment.rb +4 -1
  117. data/lib/rubocop/cop/style/ip_addresses.rb +1 -2
  118. data/lib/rubocop/cop/style/magic_comment_format.rb +2 -2
  119. data/lib/rubocop/cop/style/map_join.rb +123 -0
  120. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +5 -3
  121. data/lib/rubocop/cop/style/module_member_existence_check.rb +1 -11
  122. data/lib/rubocop/cop/style/multiline_if_then.rb +3 -1
  123. data/lib/rubocop/cop/style/mutable_constant.rb +1 -1
  124. data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
  125. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  126. data/lib/rubocop/cop/style/non_nil_check.rb +5 -11
  127. data/lib/rubocop/cop/style/not.rb +2 -0
  128. data/lib/rubocop/cop/style/numeric_literals.rb +3 -2
  129. data/lib/rubocop/cop/style/one_class_per_file.rb +115 -0
  130. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
  131. data/lib/rubocop/cop/style/parallel_assignment.rb +4 -0
  132. data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
  133. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  134. data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
  135. data/lib/rubocop/cop/style/proc.rb +3 -2
  136. data/lib/rubocop/cop/style/raise_args.rb +1 -1
  137. data/lib/rubocop/cop/style/reduce_to_hash.rb +184 -0
  138. data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
  139. data/lib/rubocop/cop/style/redundant_each.rb +3 -3
  140. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  141. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
  142. data/lib/rubocop/cop/style/redundant_line_continuation.rb +16 -0
  143. data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
  144. data/lib/rubocop/cop/style/redundant_parentheses.rb +25 -22
  145. data/lib/rubocop/cop/style/redundant_percent_q.rb +4 -1
  146. data/lib/rubocop/cop/style/redundant_return.rb +3 -1
  147. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +0 -5
  148. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +114 -0
  149. data/lib/rubocop/cop/style/safe_navigation.rb +7 -7
  150. data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
  151. data/lib/rubocop/cop/style/select_by_range.rb +197 -0
  152. data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
  153. data/lib/rubocop/cop/style/semicolon.rb +2 -0
  154. data/lib/rubocop/cop/style/single_line_block_params.rb +2 -2
  155. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -1
  156. data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
  157. data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
  158. data/lib/rubocop/cop/style/symbol_proc.rb +4 -3
  159. data/lib/rubocop/cop/style/tally_method.rb +181 -0
  160. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  161. data/lib/rubocop/cop/style/trailing_method_end_statement.rb +1 -0
  162. data/lib/rubocop/cop/style/yoda_expression.rb +1 -1
  163. data/lib/rubocop/cop/variable_force/branch.rb +2 -2
  164. data/lib/rubocop/directive_comment.rb +2 -1
  165. data/lib/rubocop/formatter/disabled_config_formatter.rb +1 -1
  166. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  167. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  168. data/lib/rubocop/formatter/simple_text_formatter.rb +0 -2
  169. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  170. data/lib/rubocop/formatter.rb +22 -21
  171. data/lib/rubocop/lsp/diagnostic.rb +1 -0
  172. data/lib/rubocop/lsp/routes.rb +10 -3
  173. data/lib/rubocop/mcp/server.rb +200 -0
  174. data/lib/rubocop/options.rb +10 -1
  175. data/lib/rubocop/path_util.rb +14 -2
  176. data/lib/rubocop/plugin/loader.rb +1 -1
  177. data/lib/rubocop/result_cache.rb +22 -10
  178. data/lib/rubocop/rspec/cop_helper.rb +8 -0
  179. data/lib/rubocop/rspec/shared_contexts.rb +11 -2
  180. data/lib/rubocop/runner.rb +8 -3
  181. data/lib/rubocop/server/cache.rb +5 -7
  182. data/lib/rubocop/server/core.rb +2 -0
  183. data/lib/rubocop/target_finder.rb +1 -1
  184. data/lib/rubocop/target_ruby.rb +18 -12
  185. data/lib/rubocop/version.rb +2 -2
  186. data/lib/rubocop.rb +14 -0
  187. metadata +22 -5
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for manual counting patterns that can be replaced by `Enumerable#tally`.
7
+ #
8
+ # The cop detects the following patterns:
9
+ #
10
+ # - `each_with_object(Hash.new(0)) { |item, counts| counts[item] += 1 }`
11
+ # - `group_by(&:itself).transform_values(&:count)`
12
+ # - `group_by { |x| x }.transform_values(&:size)`
13
+ # - `group_by { |x| x }.transform_values { |v| v.length }`
14
+ #
15
+ # @safety
16
+ # This cop is unsafe because it cannot guarantee that the receiver
17
+ # is an `Enumerable` by static analysis, so the correction may
18
+ # not be actually equivalent.
19
+ #
20
+ # @example
21
+ # # bad
22
+ # array.each_with_object(Hash.new(0)) { |item, counts| counts[item] += 1 }
23
+ #
24
+ # # bad
25
+ # array.group_by(&:itself).transform_values(&:count)
26
+ #
27
+ # # bad
28
+ # array.group_by { |item| item }.transform_values(&:size)
29
+ #
30
+ # # bad
31
+ # array.group_by { |item| item }.transform_values { |v| v.length }
32
+ #
33
+ # # good
34
+ # array.tally
35
+ #
36
+ class TallyMethod < Base
37
+ extend AutoCorrector
38
+ extend TargetRubyVersion
39
+ include RangeHelp
40
+
41
+ minimum_target_ruby_version 2.7
42
+
43
+ MSG_EACH_WITH_OBJECT = 'Use `tally` instead of `each_with_object`.'
44
+ MSG_GROUP_BY = 'Use `tally` instead of `group_by` and `transform_values`.'
45
+ RESTRICT_ON_SEND = %i[each_with_object transform_values].freeze
46
+ COUNTING_METHODS = %i[count size length].to_set.freeze
47
+
48
+ # Pattern 1: collection.each_with_object(Hash.new(0)) { |elem, hash| hash[elem] += 1 }
49
+ # @!method tally_each_with_object?(node)
50
+ def_node_matcher :tally_each_with_object?, <<~PATTERN
51
+ {
52
+ (block
53
+ (call _ :each_with_object
54
+ (send (const {nil? cbase} :Hash) :new (int 0)))
55
+ (args (arg _elem) (arg _hash))
56
+ (op_asgn
57
+ (send (lvar _hash) :[] (lvar _elem)) :+ (int 1)))
58
+ (numblock
59
+ (call _ :each_with_object
60
+ (send (const {nil? cbase} :Hash) :new (int 0)))
61
+ 2
62
+ (op_asgn
63
+ (send (lvar :_2) :[] (lvar :_1)) :+ (int 1)))
64
+ }
65
+ PATTERN
66
+
67
+ # Pattern 2: collection.group_by(&:itself).transform_values(&:count/size/length)
68
+ # @!method tally_group_by_symbol?(node)
69
+ def_node_matcher :tally_group_by_symbol?, <<~PATTERN
70
+ (call
71
+ (call _ :group_by (block_pass (sym :itself)))
72
+ :transform_values
73
+ (block_pass (sym %COUNTING_METHODS)))
74
+ PATTERN
75
+
76
+ # Pattern 3: collection.group_by { |x| x }.transform_values(&:count/size/length)
77
+ # @!method tally_group_by_identity_block?(node)
78
+ def_node_matcher :tally_group_by_identity_block?, <<~PATTERN
79
+ (call
80
+ {
81
+ (block (call _ :group_by) (args (arg _x)) (lvar _x))
82
+ (numblock (call _ :group_by) 1 (lvar :_1))
83
+ (itblock (call _ :group_by) :it (lvar :it))
84
+ }
85
+ :transform_values
86
+ (block_pass (sym %COUNTING_METHODS)))
87
+ PATTERN
88
+
89
+ # Pattern 4: collection.group_by(&:itself).transform_values { |v| v.count/size/length }
90
+ # collection.group_by { |x| x }.transform_values { |v| v.count/size/length }
91
+ # @!method tally_group_by_transform_block?(node)
92
+ def_node_matcher :tally_group_by_transform_block?, <<~PATTERN
93
+ {
94
+ (block
95
+ (call
96
+ {
97
+ (call _ :group_by (block_pass (sym :itself)))
98
+ (block (call _ :group_by) (args (arg _x)) (lvar _x))
99
+ (numblock (call _ :group_by) 1 (lvar :_1))
100
+ (itblock (call _ :group_by) :it (lvar :it))
101
+ }
102
+ :transform_values)
103
+ (args (arg _v))
104
+ (send (lvar _v) %COUNTING_METHODS))
105
+ (numblock
106
+ (call
107
+ {
108
+ (call _ :group_by (block_pass (sym :itself)))
109
+ (block (call _ :group_by) (args (arg _x)) (lvar _x))
110
+ (numblock (call _ :group_by) 1 (lvar :_1))
111
+ (itblock (call _ :group_by) :it (lvar :it))
112
+ }
113
+ :transform_values)
114
+ 1
115
+ (send (lvar :_1) %COUNTING_METHODS))
116
+ (itblock
117
+ (call
118
+ {
119
+ (call _ :group_by (block_pass (sym :itself)))
120
+ (block (call _ :group_by) (args (arg _x)) (lvar _x))
121
+ (numblock (call _ :group_by) 1 (lvar :_1))
122
+ (itblock (call _ :group_by) :it (lvar :it))
123
+ }
124
+ :transform_values)
125
+ :it
126
+ (send (lvar :it) %COUNTING_METHODS))
127
+ }
128
+ PATTERN
129
+ def on_send(node)
130
+ if node.method?(:each_with_object)
131
+ check_each_with_object(node)
132
+ elsif node.method?(:transform_values)
133
+ check_transform_values(node)
134
+ end
135
+ end
136
+ alias on_csend on_send
137
+
138
+ private
139
+
140
+ def check_each_with_object(node)
141
+ block_node = node.block_node
142
+ return unless block_node
143
+ return unless tally_each_with_object?(block_node)
144
+
145
+ add_offense(node.loc.selector, message: MSG_EACH_WITH_OBJECT) do |corrector|
146
+ corrector.replace(replacement_range(node, block_node), 'tally')
147
+ end
148
+ end
149
+
150
+ def check_transform_values(node)
151
+ if tally_group_by_symbol?(node) || tally_group_by_identity_block?(node)
152
+ register_group_by_offense(node, node)
153
+ elsif (block_node = node.block_node) && tally_group_by_transform_block?(block_node)
154
+ register_group_by_offense(node, block_node)
155
+ end
156
+ end
157
+
158
+ def register_group_by_offense(transform_node, end_node)
159
+ group_by_node = group_by_send_node(transform_node)
160
+
161
+ add_offense(group_by_node.loc.selector, message: MSG_GROUP_BY) do |corrector|
162
+ corrector.replace(replacement_range(group_by_node, end_node), 'tally')
163
+ end
164
+ end
165
+
166
+ def group_by_send_node(transform_node)
167
+ receiver = transform_node.receiver
168
+ if receiver.type?(:any_block)
169
+ receiver.send_node
170
+ else
171
+ receiver
172
+ end
173
+ end
174
+
175
+ def replacement_range(start_node, end_node)
176
+ range_between(start_node.loc.selector.begin_pos, end_node.source_range.end_pos)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -64,7 +64,7 @@ module RuboCop
64
64
 
65
65
  MSG = 'Useless trailing comma present in block arguments.'
66
66
 
67
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
67
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
68
68
  # lambda literal (`->`) never has block arguments.
69
69
  return if node.send_node.lambda_literal?
70
70
  return unless useless_trailing_comma?(node)
@@ -45,6 +45,7 @@ module RuboCop
45
45
  corrector.insert_before(node.loc.end, "\n#{' ' * node.loc.keyword.column}")
46
46
  end
47
47
  end
48
+ alias on_defs on_def
48
49
 
49
50
  private
50
51
 
@@ -76,7 +76,7 @@ module RuboCop
76
76
  end
77
77
 
78
78
  def supported_operators
79
- Array(cop_config['SupportedOperators'])
79
+ @supported_operators ||= Array(cop_config['SupportedOperators']).freeze
80
80
  end
81
81
 
82
82
  def offended_ancestor?(node)
@@ -346,8 +346,8 @@ module RuboCop
346
346
  end
347
347
  end
348
348
 
349
- CLASSES_BY_TYPE = Base.classes.each_with_object({}) do |klass, classes|
350
- classes[klass.type] = klass
349
+ CLASSES_BY_TYPE = Base.classes.to_h do |klass|
350
+ [klass.type, klass]
351
351
  end
352
352
  end
353
353
  end
@@ -52,7 +52,8 @@ module RuboCop
52
52
  def initialize(comment, cop_registry = Cop::Registry.global)
53
53
  @comment = comment
54
54
  @cop_registry = cop_registry
55
- @match_data = comment.text.match(DIRECTIVE_COMMENT_REGEXP)
55
+ match_data = comment.text.match(DIRECTIVE_COMMENT_REGEXP)
56
+ @match_data = match_data&.pre_match&.match?(/\A#\s*\z/) ? nil : match_data
56
57
  @mode, @cops = match_captures
57
58
  end
58
59
 
@@ -233,7 +233,7 @@ module RuboCop
233
233
 
234
234
  def output_exclude_list(output_buffer, offending_files, cop_name)
235
235
  require 'pathname'
236
- parent = Pathname.new(Dir.pwd)
236
+ parent = Pathname.new(PathUtil.pwd)
237
237
 
238
238
  output_buffer.puts ' Exclude:'
239
239
  excludes(offending_files, cop_name, parent).each do |exclude_path|
@@ -57,7 +57,7 @@ module RuboCop
57
57
  if output_path
58
58
  dir_path = File.dirname(output_path)
59
59
  FileUtils.mkdir_p(dir_path)
60
- output = File.open(output_path, 'w')
60
+ output = File.open(output_path, 'w') # rubocop:disable Style/FileOpen
61
61
  else
62
62
  output = $stdout
63
63
  end
@@ -93,7 +93,7 @@ module RuboCop
93
93
 
94
94
  def classname_attribute_value(file)
95
95
  @classname_attribute_value_cache ||= Hash.new do |hash, key|
96
- hash[key] = key.delete_suffix('.rb').gsub("#{Dir.pwd}/", '').tr('/', '.')
96
+ hash[key] = key.delete_suffix('.rb').gsub("#{PathUtil.pwd}/", '').tr('/', '.')
97
97
  end
98
98
  @classname_attribute_value_cache[file]
99
99
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'colorizable'
4
-
5
3
  module RuboCop
6
4
  module Formatter
7
5
  # A basic formatter that displays only files with offenses.
@@ -24,7 +24,7 @@ module RuboCop
24
24
  def file_finished(file, offenses)
25
25
  return if offenses.empty?
26
26
 
27
- path = Pathname.new(file).relative_path_from(Pathname.new(Dir.pwd))
27
+ path = Pathname.new(file).relative_path_from(Pathname.new(PathUtil.pwd))
28
28
  @offense_counts[path] = offenses.size
29
29
  end
30
30
 
@@ -3,32 +3,33 @@
3
3
  module RuboCop
4
4
  # The bootstrap module for formatter.
5
5
  module Formatter
6
- require_relative 'formatter/text_util'
6
+ autoload :Colorizable, 'rubocop/formatter/colorizable'
7
+ autoload :TextUtil, 'rubocop/formatter/text_util'
7
8
 
8
- require_relative 'formatter/base_formatter'
9
- require_relative 'formatter/simple_text_formatter'
9
+ autoload :BaseFormatter, 'rubocop/formatter/base_formatter'
10
+ autoload :SimpleTextFormatter, 'rubocop/formatter/simple_text_formatter'
10
11
 
11
12
  # relies on simple text
12
- require_relative 'formatter/clang_style_formatter'
13
- require_relative 'formatter/disabled_config_formatter'
14
- require_relative 'formatter/emacs_style_formatter'
15
- require_relative 'formatter/file_list_formatter'
16
- require_relative 'formatter/fuubar_style_formatter'
17
- require_relative 'formatter/github_actions_formatter'
18
- require_relative 'formatter/html_formatter'
19
- require_relative 'formatter/json_formatter'
20
- require_relative 'formatter/junit_formatter'
21
- require_relative 'formatter/markdown_formatter'
22
- require_relative 'formatter/offense_count_formatter'
23
- require_relative 'formatter/pacman_formatter'
24
- require_relative 'formatter/progress_formatter'
25
- require_relative 'formatter/quiet_formatter'
26
- require_relative 'formatter/tap_formatter'
27
- require_relative 'formatter/worst_offenders_formatter'
13
+ autoload :ClangStyleFormatter, 'rubocop/formatter/clang_style_formatter'
14
+ autoload :DisabledConfigFormatter, 'rubocop/formatter/disabled_config_formatter'
15
+ autoload :EmacsStyleFormatter, 'rubocop/formatter/emacs_style_formatter'
16
+ autoload :FileListFormatter, 'rubocop/formatter/file_list_formatter'
17
+ autoload :FuubarStyleFormatter, 'rubocop/formatter/fuubar_style_formatter'
18
+ autoload :GitHubActionsFormatter, 'rubocop/formatter/github_actions_formatter'
19
+ autoload :HTMLFormatter, 'rubocop/formatter/html_formatter'
20
+ autoload :JSONFormatter, 'rubocop/formatter/json_formatter'
21
+ autoload :JUnitFormatter, 'rubocop/formatter/junit_formatter'
22
+ autoload :MarkdownFormatter, 'rubocop/formatter/markdown_formatter'
23
+ autoload :OffenseCountFormatter, 'rubocop/formatter/offense_count_formatter'
24
+ autoload :PacmanFormatter, 'rubocop/formatter/pacman_formatter'
25
+ autoload :ProgressFormatter, 'rubocop/formatter/progress_formatter'
26
+ autoload :QuietFormatter, 'rubocop/formatter/quiet_formatter'
27
+ autoload :TapFormatter, 'rubocop/formatter/tap_formatter'
28
+ autoload :WorstOffendersFormatter, 'rubocop/formatter/worst_offenders_formatter'
28
29
 
29
30
  # relies on progress formatter
30
- require_relative 'formatter/auto_gen_config_formatter'
31
+ autoload :AutoGenConfigFormatter, 'rubocop/formatter/auto_gen_config_formatter'
31
32
 
32
- require_relative 'formatter/formatter_set'
33
+ autoload :FormatterSet, 'rubocop/formatter/formatter_set'
33
34
  end
34
35
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'language_server-protocol'
3
4
  require_relative 'disable_comment_edits'
4
5
  require_relative 'severity'
5
6
 
@@ -61,9 +61,16 @@ module RuboCop
61
61
 
62
62
  handle 'initialized' do |_request|
63
63
  version = RuboCop::Version::STRING
64
- yjit = Object.const_defined?('RubyVM::YJIT') && RubyVM::YJIT.enabled? ? ' +YJIT' : ''
65
-
66
- Logger.log("RuboCop #{version} language server#{yjit} initialized, PID #{Process.pid}")
64
+ # Only one JIT can be enabled at the same time, since YJIT and ZJIT are mutually exclusive.
65
+ jit = if Object.const_defined?('RubyVM::YJIT') && RubyVM::YJIT.enabled?
66
+ '+YJIT'
67
+ elsif Object.const_defined?('RubyVM::ZJIT') && RubyVM::ZJIT.enabled?
68
+ '+ZJIT'
69
+ else
70
+ ''
71
+ end
72
+
73
+ Logger.log("RuboCop #{version} language server#{jit} initialized, PID #{Process.pid}")
67
74
  end
68
75
 
69
76
  handle 'shutdown' do |request|
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'mcp'
5
+
6
+ required_mcp_version = '0.6.0'
7
+
8
+ if Gem::Version.new(required_mcp_version) > Gem::Version.new(MCP::VERSION)
9
+ # While `mcp` is not a runtime dependency, users may have an outdated version installed.
10
+ warn <<~MESSAGE
11
+ Error: `mcp` gem version #{MCP::VERSION} was loaded, but `rubocop --mcp` requires #{required_mcp_version}.
12
+ - If you're using Bundler and don't yet have `gem 'mcp'` as a dependency, add it now.
13
+ - If you're using Bundler and already have `gem 'mcp'` as a dependency, update it to the most recent version.
14
+ - If you don't use Bundler, run `gem update mcp`.
15
+ MESSAGE
16
+ exit!
17
+ end
18
+ rescue LoadError => e
19
+ raise unless e.path == 'mcp'
20
+
21
+ warn <<~MESSAGE
22
+ Error: Unable to load `mcp` gem. Add `gem 'mcp', '~> 0.6'` to your Gemfile, or run `gem install mcp`.
23
+ MESSAGE
24
+
25
+ exit!
26
+ end
27
+
28
+ require_relative '../lsp'
29
+ require_relative '../lsp/runtime'
30
+
31
+ module RuboCop
32
+ module MCP
33
+ # RuboCop MCP Server.
34
+ # @api private
35
+ class Server
36
+ def initialize(config_store)
37
+ @config_store = config_store
38
+ @runtime = RuboCop::LSP::Runtime.new(@config_store)
39
+ @options = {}
40
+ end
41
+
42
+ def start
43
+ # No `protocol_version` is specified because draft feature by default can be used.
44
+ server = ::MCP::Server.new(
45
+ name: 'rubocop_mcp_server',
46
+ version: RuboCop::Version::STRING,
47
+ tools: [inspection_tool, autocorrection_tool]
48
+ )
49
+
50
+ ::MCP::Server::Transports::StdioTransport.new(server).open
51
+ end
52
+
53
+ private
54
+
55
+ def inspection_tool
56
+ build_tool(
57
+ name: 'rubocop_inspection',
58
+ description: 'Inspect Ruby code for offenses. ' \
59
+ 'Provide `source_code` to check inline code or `path` to check files.',
60
+ title: "RuboCop's inspection",
61
+ destructive_hint: false,
62
+ idempotent_hint: true,
63
+ read_only_hint: true,
64
+ safety_required: false
65
+ ) do |path, source_code|
66
+ run_inspection(path, source_code)
67
+ end
68
+ end
69
+
70
+ def autocorrection_tool
71
+ build_tool(
72
+ 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.',
76
+ title: "RuboCop's autocorrection",
77
+ destructive_hint: true,
78
+ idempotent_hint: false,
79
+ read_only_hint: false,
80
+ safety_required: true
81
+ ) do |path, source_code, safety|
82
+ run_autocorrection(path, source_code, safety)
83
+ end
84
+ end
85
+
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 }
95
+ end
96
+ end
97
+ end
98
+
99
+ def run_autocorrection(path, source_code, safety)
100
+ command = safety ? 'rubocop.formatAutocorrects' : 'rubocop.formatAutocorrectsAll'
101
+
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
106
+ else
107
+ process_files(path) do |file, source|
108
+ @runtime.format(file, source, command: command).then do |corrected|
109
+ write_file(file, corrected)
110
+
111
+ { path: PathUtil.relative_path(file), corrected: source != corrected }
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def process_files(path, filter_empty: false)
118
+ 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
122
+
123
+ { files: files, summary: build_summary(target_files, all_files) }.to_json
124
+ end
125
+
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}"
133
+ end
134
+
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}"
143
+ end
144
+
145
+ # NOTE: It is useful for RuboCop's result summary to be shown in the LLM's responses
146
+ # during interactions, so the summary is returned in a form that is easy for the LLM
147
+ # to reason about. Since LLM execution is non-deterministic, it is also sensible to
148
+ # compute the summary deterministically at this stage.
149
+ 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
157
+ end
158
+
159
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
160
+ def build_tool(
161
+ name:, description:,
162
+ title:, destructive_hint:, idempotent_hint:, read_only_hint:, safety_required:
163
+ )
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
+ ::MCP::Tool.define(
173
+ name: name,
174
+ description: description,
175
+ input_schema: {
176
+ properties: {
177
+ path: { type: 'string' },
178
+ source_code: { type: 'string' }
179
+ }.merge(safety_property),
180
+ required: required
181
+ }.compact,
182
+ annotations: {
183
+ title: title,
184
+ destructive_hint: destructive_hint,
185
+ idempotent_hint: idempotent_hint,
186
+ open_world_hint: false,
187
+ read_only_hint: read_only_hint
188
+ }
189
+ ) do |path: nil, source_code: nil, safety: true|
190
+ result = yield(path, source_code, safety)
191
+
192
+ ::MCP::Tool::Response.new([{ type: 'text', text: result }])
193
+ rescue RuboCop::Error => e
194
+ ::MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
195
+ end
196
+ end
197
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
198
+ end
199
+ end
200
+ end
@@ -16,7 +16,7 @@ module RuboCop
16
16
  'root of the project. RuboCop will use this path to determine which ' \
17
17
  'cops are enabled (via eg. Include/Exclude), and so that certain cops ' \
18
18
  'like Naming/FileName can be checked.'
19
- EXITING_OPTIONS = %i[version verbose_version show_cops show_docs_url lsp].freeze
19
+ EXITING_OPTIONS = %i[version verbose_version show_cops show_docs_url lsp mcp].freeze
20
20
  DEFAULT_MAXIMUM_EXCLUSION_ITEMS = 15
21
21
 
22
22
  def initialize
@@ -57,6 +57,7 @@ module RuboCop
57
57
  add_check_options(opts)
58
58
  add_cache_options(opts)
59
59
  add_lsp_option(opts)
60
+ add_mcp_option(opts)
60
61
  add_server_options(opts)
61
62
  add_output_options(opts)
62
63
  add_autocorrection_options(opts)
@@ -215,6 +216,12 @@ module RuboCop
215
216
  end
216
217
  end
217
218
 
219
+ def add_mcp_option(opts)
220
+ section(opts, 'MCP Option') do
221
+ option(opts, '--mcp')
222
+ end
223
+ end
224
+
218
225
  def add_server_options(opts)
219
226
  section(opts, 'Server Options') do
220
227
  option(opts, '--[no-]server')
@@ -651,6 +658,8 @@ module RuboCop
651
658
  server_status: 'Show server status.',
652
659
  no_detach: 'Run the server process in the foreground.',
653
660
  lsp: 'Start a language server listening on STDIN.',
661
+ mcp: ['Start an MCP (Model Context Protocol) server that',
662
+ 'communicates over stdio.'],
654
663
  raise_cop_error: ['Raise cop-related errors with cause and location.',
655
664
  'This is used to prevent cops from failing silently.',
656
665
  'Default is false.'],
@@ -10,7 +10,19 @@ module RuboCop
10
10
 
11
11
  module_function
12
12
 
13
- def relative_path(path, base_dir = Dir.pwd)
13
+ # Returns the current working directory, cached for the duration of a run.
14
+ # Dir.pwd is a syscall; caching it avoids repeated overhead since RuboCop
15
+ # never changes the working directory during a run.
16
+ def pwd
17
+ @pwd ||= Dir.pwd
18
+ end
19
+
20
+ # Reset the cached pwd. Only needed in tests that use Dir.chdir.
21
+ def reset_pwd
22
+ @pwd = nil
23
+ end
24
+
25
+ def relative_path(path, base_dir = PathUtil.pwd)
14
26
  PathUtil.relative_paths_cache[base_dir][path] ||=
15
27
  # Optimization for the common case where path begins with the base
16
28
  # dir. Just cut off the first part.
@@ -41,7 +53,7 @@ module RuboCop
41
53
  path.uri.to_s
42
54
  else
43
55
  # Ideally, we calculate this relative to the project root.
44
- base_dir = Dir.pwd
56
+ base_dir = PathUtil.pwd
45
57
 
46
58
  if path.start_with? base_dir
47
59
  relative_path(path, base_dir)
@@ -84,7 +84,7 @@ module RuboCop
84
84
  end
85
85
 
86
86
  def require_plugin(require_path)
87
- FeatureLoader.load(config_directory_path: Dir.pwd, feature: require_path)
87
+ FeatureLoader.load(config_directory_path: PathUtil.pwd, feature: require_path)
88
88
  end
89
89
 
90
90
  def constantize_plugin_from_gemspec_metadata(plugin_name)