rubocop 1.84.2 → 1.85.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +83 -4
  3. data/config/obsoletion.yml +5 -0
  4. data/lib/rubocop/cli/command/mcp.rb +19 -0
  5. data/lib/rubocop/cli.rb +6 -3
  6. data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
  7. data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
  9. data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
  10. data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
  11. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  12. data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
  13. data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
  14. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
  15. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
  16. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
  17. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +7 -1
  18. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  19. data/lib/rubocop/cop/layout/indentation_width.rb +1 -1
  20. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
  21. data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
  22. data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
  23. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  24. data/lib/rubocop/cop/layout/space_around_keyword.rb +1 -1
  25. data/lib/rubocop/cop/lint/constant_resolution.rb +1 -1
  26. data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
  27. data/lib/rubocop/cop/lint/empty_block.rb +1 -1
  28. data/lib/rubocop/cop/lint/interpolation_check.rb +7 -2
  29. data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
  30. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +3 -1
  31. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +0 -9
  32. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +7 -6
  33. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
  34. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
  35. data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
  36. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
  37. data/lib/rubocop/cop/lint/void.rb +32 -12
  38. data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
  39. data/lib/rubocop/cop/migration/department_name.rb +12 -1
  40. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  41. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +1 -1
  42. data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
  43. data/lib/rubocop/cop/mixin/hash_transform_method.rb +10 -60
  44. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  45. data/lib/rubocop/cop/security/eval.rb +15 -2
  46. data/lib/rubocop/cop/style/accessor_grouping.rb +4 -2
  47. data/lib/rubocop/cop/style/alias.rb +4 -1
  48. data/lib/rubocop/cop/style/array_join.rb +4 -2
  49. data/lib/rubocop/cop/style/ascii_comments.rb +5 -2
  50. data/lib/rubocop/cop/style/attr.rb +5 -2
  51. data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
  52. data/lib/rubocop/cop/style/begin_block.rb +3 -1
  53. data/lib/rubocop/cop/style/block_delimiters.rb +2 -2
  54. data/lib/rubocop/cop/style/case_equality.rb +4 -0
  55. data/lib/rubocop/cop/style/class_and_module_children.rb +10 -2
  56. data/lib/rubocop/cop/style/colon_method_call.rb +3 -1
  57. data/lib/rubocop/cop/style/copyright.rb +1 -1
  58. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  59. data/lib/rubocop/cop/style/each_with_object.rb +2 -0
  60. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  61. data/lib/rubocop/cop/style/empty_class_definition.rb +21 -20
  62. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  63. data/lib/rubocop/cop/style/encoding.rb +7 -1
  64. data/lib/rubocop/cop/style/end_block.rb +3 -1
  65. data/lib/rubocop/cop/style/endless_method.rb +8 -3
  66. data/lib/rubocop/cop/style/file_open.rb +63 -0
  67. data/lib/rubocop/cop/style/for.rb +3 -0
  68. data/lib/rubocop/cop/style/format_string_token.rb +29 -2
  69. data/lib/rubocop/cop/style/global_vars.rb +4 -1
  70. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
  71. data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
  72. data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
  73. data/lib/rubocop/cop/style/if_unless_modifier.rb +3 -3
  74. data/lib/rubocop/cop/style/inline_comment.rb +4 -1
  75. data/lib/rubocop/cop/style/map_join.rb +123 -0
  76. data/lib/rubocop/cop/style/multiline_if_then.rb +3 -1
  77. data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
  78. data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
  79. data/lib/rubocop/cop/style/not.rb +2 -0
  80. data/lib/rubocop/cop/style/numeric_literals.rb +2 -1
  81. data/lib/rubocop/cop/style/one_class_per_file.rb +95 -0
  82. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
  83. data/lib/rubocop/cop/style/parallel_assignment.rb +4 -0
  84. data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
  85. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  86. data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
  87. data/lib/rubocop/cop/style/proc.rb +3 -2
  88. data/lib/rubocop/cop/style/reduce_to_hash.rb +169 -0
  89. data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
  90. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  91. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
  92. data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
  93. data/lib/rubocop/cop/style/redundant_parentheses.rb +6 -3
  94. data/lib/rubocop/cop/style/redundant_return.rb +3 -1
  95. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +104 -0
  96. data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
  97. data/lib/rubocop/cop/style/select_by_range.rb +197 -0
  98. data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
  99. data/lib/rubocop/cop/style/semicolon.rb +2 -0
  100. data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
  101. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -1
  102. data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
  103. data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
  104. data/lib/rubocop/cop/style/tally_method.rb +181 -0
  105. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  106. data/lib/rubocop/cop/variable_force/branch.rb +2 -2
  107. data/lib/rubocop/directive_comment.rb +2 -1
  108. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  109. data/lib/rubocop/lsp/diagnostic.rb +1 -0
  110. data/lib/rubocop/mcp/server.rb +174 -0
  111. data/lib/rubocop/options.rb +10 -1
  112. data/lib/rubocop/server/cache.rb +5 -7
  113. data/lib/rubocop/target_ruby.rb +18 -12
  114. data/lib/rubocop/version.rb +1 -1
  115. data/lib/rubocop.rb +14 -0
  116. metadata +34 -3
@@ -39,6 +39,9 @@ module RuboCop
39
39
  # array.reject { |x| x =~ /regexp/ }
40
40
  # array.reject { |x| /regexp/ =~ x }
41
41
  #
42
+ # # bad (negative form)
43
+ # array.reject { |x| !x.match? /regexp/ }
44
+ #
42
45
  # # good
43
46
  # array.grep(regexp)
44
47
  # array.grep_v(regexp)
@@ -48,18 +51,19 @@ module RuboCop
48
51
 
49
52
  MSG = 'Prefer `%<replacement>s` to `%<original_method>s` with a regexp match.'
50
53
  RESTRICT_ON_SEND = %i[select filter find_all reject].freeze
51
- REPLACEMENTS = { select: 'grep', filter: 'grep', find_all: 'grep', reject: 'grep_v' }.freeze
52
- OPPOSITE_REPLACEMENTS = {
53
- select: 'grep_v', filter: 'grep_v', find_all: 'grep_v', reject: 'grep'
54
- }.freeze
55
- REGEXP_METHODS = %i[match? =~ !~].to_set.freeze
54
+ SELECT_METHODS = %i[select filter find_all].freeze
55
+ REGEXP_METHODS = %i[match? =~].to_set.freeze
56
+ REGEXP_METHODS_NEGATED = %i[!~].to_set.freeze
56
57
 
57
58
  # @!method regexp_match?(node)
58
59
  def_node_matcher :regexp_match?, <<~PATTERN
59
60
  {
60
- (block call (args (arg $_)) ${(send _ %REGEXP_METHODS _) match-with-lvasgn})
61
- (numblock call $1 ${(send _ %REGEXP_METHODS _) match-with-lvasgn})
62
- (itblock call $_ ${(send _ %REGEXP_METHODS _) match-with-lvasgn})
61
+ (block call (args (arg $_)) ${(send _ %REGEXP_METHODS _) (send _ %REGEXP_METHODS_NEGATED _) match-with-lvasgn})
62
+ (block call (args (arg $_)) ${(send (send _ %REGEXP_METHODS _) :!) (send (begin (send _ %REGEXP_METHODS _)) :!) (send match-with-lvasgn :!) (send (begin match-with-lvasgn) :!)})
63
+ (numblock call $1 ${(send _ %REGEXP_METHODS _) (send _ %REGEXP_METHODS_NEGATED _) match-with-lvasgn})
64
+ (numblock call $1 ${(send (send _ %REGEXP_METHODS _) :!) (send (begin (send _ %REGEXP_METHODS _)) :!) (send match-with-lvasgn :!) (send (begin match-with-lvasgn) :!)})
65
+ (itblock call $_ ${(send _ %REGEXP_METHODS _) (send _ %REGEXP_METHODS_NEGATED _) match-with-lvasgn})
66
+ (itblock call $_ ${(send (send _ %REGEXP_METHODS _) :!) (send (begin (send _ %REGEXP_METHODS _)) :!) (send match-with-lvasgn :!) (send (begin match-with-lvasgn) :!)})
63
67
  }
64
68
  PATTERN
65
69
 
@@ -84,6 +88,12 @@ module RuboCop
84
88
  (send (lvar %1) ...)
85
89
  (send ... (lvar %1))
86
90
  (match-with-lvasgn regexp (lvar %1))
91
+ (send (send (lvar %1) ...) :!)
92
+ (send (send ... (lvar %1)) :!)
93
+ (send (match-with-lvasgn regexp (lvar %1)) :!)
94
+ (send (begin (send (lvar %1) ...)) :!)
95
+ (send (begin (send ... (lvar %1))) :!)
96
+ (send (begin (match-with-lvasgn regexp (lvar %1))) :!)
87
97
  }
88
98
  PATTERN
89
99
 
@@ -97,7 +107,7 @@ module RuboCop
97
107
  return if match_predicate_without_receiver?(regexp_method_send_node)
98
108
 
99
109
  replacement = replacement(regexp_method_send_node, node)
100
- return if target_ruby_version <= 2.2 && replacement == 'grep_v'
110
+ return if target_ruby_version <= 2.2 && replacement.include?('grep_v')
101
111
 
102
112
  regexp = find_regexp(regexp_method_send_node, block_node)
103
113
 
@@ -115,11 +125,14 @@ module RuboCop
115
125
  end
116
126
 
117
127
  def replacement(regexp_method_send_node, node)
118
- opposite = opposite?(regexp_method_send_node)
119
-
128
+ negated = negated?(regexp_method_send_node)
120
129
  method_name = node.method_name
121
130
 
122
- opposite ? OPPOSITE_REPLACEMENTS[method_name] : REPLACEMENTS[method_name]
131
+ if SELECT_METHODS.include?(method_name)
132
+ negated ? 'grep_v' : 'grep'
133
+ else # reject
134
+ negated ? 'grep' : 'grep_v'
135
+ end
123
136
  end
124
137
 
125
138
  def register_offense(node, block_node, regexp, replacement)
@@ -138,30 +151,47 @@ module RuboCop
138
151
  return unless (block_arg_name, regexp_method_send_node = regexp_match?(block_node))
139
152
 
140
153
  block_arg_name = :"_#{block_arg_name}" if block_node.numblock_type?
154
+ block_arg_name = :it if block_node.type?(:itblock)
141
155
 
142
156
  return unless calls_lvar?(regexp_method_send_node, block_arg_name)
143
157
 
144
158
  regexp_method_send_node
145
159
  end
146
160
 
147
- def opposite?(regexp_method_send_node)
148
- regexp_method_send_node.send_type? && regexp_method_send_node.method?(:!~)
161
+ def negated?(regexp_method_send_node)
162
+ return true if regexp_method_send_node.send_type? && regexp_method_send_node.method?(:!)
163
+
164
+ inner = unwrap_negation(regexp_method_send_node)
165
+ inner.send_type? && inner.method?(:!~)
166
+ end
167
+
168
+ def unwrap_negation(node)
169
+ if node.send_type? && node.method?(:!)
170
+ receiver = node.receiver
171
+ receiver = receiver.children.first if receiver.begin_type?
172
+ receiver
173
+ else
174
+ node
175
+ end
149
176
  end
150
177
 
151
178
  def find_regexp(node, block)
152
- return node.child_nodes.first if node.match_with_lvasgn_type?
179
+ inner = unwrap_negation(node)
180
+
181
+ return inner.child_nodes.first if inner.match_with_lvasgn_type?
153
182
 
154
- if node.receiver.lvar_type? &&
183
+ if inner.receiver.lvar_type? &&
155
184
  (block.type?(:numblock, :itblock) ||
156
- node.receiver.source == block.first_argument.source)
157
- node.first_argument
158
- elsif node.first_argument.lvar_type?
159
- node.receiver
185
+ inner.receiver.source == block.first_argument.source)
186
+ inner.first_argument
187
+ elsif inner.first_argument&.lvar_type?
188
+ inner.receiver
160
189
  end
161
190
  end
162
191
 
163
192
  def match_predicate_without_receiver?(node)
164
- node.send_type? && node.method?(:match?) && node.receiver.nil?
193
+ inner = unwrap_negation(node)
194
+ inner.send_type? && inner.method?(:match?) && inner.receiver.nil?
165
195
  end
166
196
  end
167
197
  end
@@ -5,6 +5,8 @@ module RuboCop
5
5
  module Style
6
6
  # Checks for multiple expressions placed on the same line.
7
7
  # It also checks for lines terminated with a semicolon.
8
+ # In idiomatic Ruby, each expression should be on its own line
9
+ # for readability.
8
10
  #
9
11
  # This cop has `AllowAsExpressionSeparator` configuration option.
10
12
  # It allows `;` to separate several expressions on the same line.
@@ -33,7 +33,7 @@ module RuboCop
33
33
 
34
34
  MSG = 'Name `%<method>s` block params `|%<params>s|`.'
35
35
 
36
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
36
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
37
37
  return unless node.single_line?
38
38
 
39
39
  return unless eligible_method?(node)
@@ -38,7 +38,7 @@ module RuboCop
38
38
 
39
39
  # rubocop:disable Metrics/AbcSize
40
40
  def on_block(node)
41
- return if !node.single_line? || node.braces?
41
+ return if node.multiline? || node.braces?
42
42
  return if single_line_blocks_preferred? && suitable_as_single_line?(node)
43
43
 
44
44
  add_offense(node) do |corrector|
@@ -4,7 +4,9 @@ module RuboCop
4
4
  module Cop
5
5
  module Style
6
6
  # Checks for single-line method definitions that contain a body.
7
- # It will accept single-line methods with no body.
7
+ # Single-line methods with a body are harder to read and debug
8
+ # than their multi-line equivalents. It will accept single-line
9
+ # methods with no body.
8
10
  #
9
11
  # Endless methods added in Ruby 3.0 are also accepted by this cop.
10
12
  #
@@ -4,7 +4,12 @@ module RuboCop
4
4
  module Cop
5
5
  module Style
6
6
  # Looks for uses of Perl-style global variables.
7
- # Correcting to global variables in the 'English' library
7
+ # Perl-style global variables like `$;` or `$/` are cryptic
8
+ # and hard to understand without consulting documentation.
9
+ # The `English` library provides descriptive aliases like
10
+ # `$FIELD_SEPARATOR` and `$INPUT_RECORD_SEPARATOR`.
11
+ #
12
+ # Correcting to global variables in the `English` library
8
13
  # will add a require statement to the top of the file if
9
14
  # enabled by RequireEnglish config.
10
15
  #
@@ -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)
@@ -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
 
@@ -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
@@ -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
 
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+ require_relative '../lsp'
5
+ require_relative '../lsp/runtime'
6
+
7
+ module RuboCop
8
+ module MCP
9
+ # RuboCop MCP Server.
10
+ # @api private
11
+ class Server
12
+ def initialize(config_store)
13
+ @config_store = config_store
14
+ @runtime = RuboCop::LSP::Runtime.new(@config_store)
15
+ @options = {}
16
+ end
17
+
18
+ def start
19
+ # No `protocol_version` is specified because draft feature by default can be used.
20
+ server = ::MCP::Server.new(
21
+ name: 'rubocop_mcp_server',
22
+ version: RuboCop::Version::STRING,
23
+ tools: [inspection_tool, autocorrection_tool]
24
+ )
25
+
26
+ ::MCP::Server::Transports::StdioTransport.new(server).open
27
+ end
28
+
29
+ private
30
+
31
+ def inspection_tool
32
+ build_tool(
33
+ name: 'rubocop_inspection',
34
+ description: 'Inspect Ruby code for offenses. ' \
35
+ 'Provide `source_code` to check inline code or `path` to check files.',
36
+ title: "RuboCop's inspection",
37
+ destructive_hint: false,
38
+ idempotent_hint: true,
39
+ read_only_hint: true,
40
+ safety_required: false
41
+ ) do |path, source_code|
42
+ run_inspection(path, source_code)
43
+ end
44
+ end
45
+
46
+ def autocorrection_tool
47
+ build_tool(
48
+ name: 'rubocop_autocorrection',
49
+ description: 'Autocorrect RuboCop offenses in Ruby code. ' \
50
+ 'Provide `source_code` to correct inline code or `path` to correct files. ' \
51
+ 'Set `safety` to false to include unsafe corrections.',
52
+ title: "RuboCop's autocorrection",
53
+ destructive_hint: true,
54
+ idempotent_hint: false,
55
+ read_only_hint: false,
56
+ safety_required: true
57
+ ) do |path, source_code, safety|
58
+ run_autocorrection(path, source_code, safety)
59
+ end
60
+ end
61
+
62
+ def run_inspection(path, source_code)
63
+ if source_code
64
+ offenses = @runtime.offenses(path || 'example.rb', source_code, source_code.encoding)
65
+ offenses.to_json
66
+ else
67
+ process_files(path, filter_empty: true) do |file, source|
68
+ offenses = @runtime.offenses(file, source, source.encoding)
69
+
70
+ { path: PathUtil.relative_path(file), offenses: offenses }
71
+ end
72
+ end
73
+ end
74
+
75
+ def run_autocorrection(path, source_code, safety)
76
+ command = safety ? 'rubocop.formatAutocorrects' : 'rubocop.formatAutocorrectsAll'
77
+
78
+ if source_code
79
+ @runtime.format(path || 'example.rb', source_code, command: command).tap do |corrected|
80
+ write_file(path, corrected) if path
81
+ end
82
+ else
83
+ process_files(path) do |file, source|
84
+ @runtime.format(file, source, command: command).then do |corrected|
85
+ write_file(file, corrected)
86
+
87
+ { path: PathUtil.relative_path(file), corrected: source != corrected }
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def process_files(path, filter_empty: false)
94
+ target_finder = RuboCop::TargetFinder.new(@config_store, @options)
95
+ target_files = target_finder.find(path ? [path] : [], :only_recognized_file_types)
96
+ all_files = target_files.map { |file| yield(file, read_file(file)) }
97
+ files = filter_empty ? all_files.reject { |f| f[:offenses]&.empty? } : all_files
98
+
99
+ { files: files, summary: build_summary(target_files, all_files) }.to_json
100
+ end
101
+
102
+ def read_file(file)
103
+ config = @config_store.for_file(file)
104
+ RuboCop::ProcessedSource.from_file(
105
+ file, config.target_ruby_version, parser_engine: config.parser_engine
106
+ ).raw_source
107
+ rescue Errno::ENOENT
108
+ raise RuboCop::Error, "No such file or directory: #{file}"
109
+ end
110
+
111
+ def write_file(file, content)
112
+ File.write(file, content)
113
+ rescue Errno::EACCES
114
+ raise RuboCop::Error, "Permission denied: #{file}"
115
+ rescue Errno::ENOSPC
116
+ raise RuboCop::Error, "No space left on device: #{file}"
117
+ rescue Errno::EROFS
118
+ raise RuboCop::Error, "Read-only file system: #{file}"
119
+ end
120
+
121
+ # NOTE: It is useful for RuboCop's result summary to be shown in the LLM's responses
122
+ # during interactions, so the summary is returned in a form that is easy for the LLM
123
+ # to reason about. Since LLM execution is non-deterministic, it is also sensible to
124
+ # compute the summary deterministically at this stage.
125
+ def build_summary(target_files, files)
126
+ summary = { target_file_count: target_files.count }
127
+ if files.first&.key?(:offenses)
128
+ summary[:offense_count] = files.sum { |f| f[:offenses].size }
129
+ else
130
+ summary[:corrected_file_count] = files.count { |f| f[:corrected] }
131
+ end
132
+ summary
133
+ end
134
+
135
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
136
+ def build_tool(
137
+ name:, description:,
138
+ title:, destructive_hint:, idempotent_hint:, read_only_hint:, safety_required:
139
+ )
140
+ if safety_required
141
+ safety_property = { safety: { type: 'boolean' } }
142
+ required = ['safety']
143
+ else
144
+ safety_property = {}
145
+ required = nil
146
+ end
147
+
148
+ ::MCP::Tool.define(
149
+ name: name,
150
+ description: description,
151
+ input_schema: {
152
+ properties: {
153
+ path: { type: 'string' },
154
+ source_code: { type: 'string' }
155
+ }.merge(safety_property),
156
+ required: required
157
+ }.compact,
158
+ annotations: {
159
+ title: title,
160
+ destructive_hint: destructive_hint,
161
+ idempotent_hint: idempotent_hint,
162
+ open_world_hint: false,
163
+ read_only_hint: read_only_hint
164
+ }
165
+ ) do |path: nil, source_code: nil, safety: true|
166
+ result = yield(path, source_code, safety)
167
+
168
+ ::MCP::Tool::Response.new([{ type: 'text', text: result }])
169
+ end
170
+ end
171
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
172
+ end
173
+ end
174
+ 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.'],
@@ -114,13 +114,11 @@ module RuboCop
114
114
  end
115
115
 
116
116
  def acquire_lock
117
- lock_file = File.open(lock_path, File::CREAT)
118
- # flock returns 0 if successful, and false if not.
119
- flock_result = lock_file.flock(File::LOCK_EX | File::LOCK_NB)
120
- yield flock_result != false
121
- ensure
122
- lock_file.flock(File::LOCK_UN)
123
- lock_file.close
117
+ File.open(lock_path, File::CREAT) do |lock_file|
118
+ # flock returns 0 if successful, and false if not.
119
+ flock_result = lock_file.flock(File::LOCK_EX | File::LOCK_NB)
120
+ yield flock_result != false
121
+ end
124
122
  end
125
123
 
126
124
  def write_port_and_token_files(port:, token:)