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.
- checksums.yaml +4 -4
- data/config/default.yml +83 -4
- data/config/obsoletion.yml +5 -0
- data/lib/rubocop/cli/command/mcp.rb +19 -0
- data/lib/rubocop/cli.rb +6 -3
- data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
- data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
- data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
- data/lib/rubocop/cop/gemspec/require_mfa.rb +1 -1
- data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
- data/lib/rubocop/cop/internal_affairs.rb +1 -0
- data/lib/rubocop/cop/layout/argument_alignment.rb +1 -1
- data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
- data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
- data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
- data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
- data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +7 -1
- data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
- data/lib/rubocop/cop/layout/indentation_width.rb +1 -1
- data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
- data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
- data/lib/rubocop/cop/layout/redundant_line_break.rb +1 -1
- data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
- data/lib/rubocop/cop/layout/space_around_keyword.rb +1 -1
- data/lib/rubocop/cop/lint/constant_resolution.rb +1 -1
- data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
- data/lib/rubocop/cop/lint/empty_block.rb +1 -1
- data/lib/rubocop/cop/lint/interpolation_check.rb +7 -2
- data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
- data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +3 -1
- data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +0 -9
- data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +7 -6
- data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
- data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
- data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
- data/lib/rubocop/cop/lint/useless_assignment.rb +1 -1
- data/lib/rubocop/cop/lint/void.rb +32 -12
- data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
- data/lib/rubocop/cop/migration/department_name.rb +12 -1
- data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
- data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +1 -1
- data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
- data/lib/rubocop/cop/mixin/hash_transform_method.rb +10 -60
- data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
- data/lib/rubocop/cop/security/eval.rb +15 -2
- data/lib/rubocop/cop/style/accessor_grouping.rb +4 -2
- data/lib/rubocop/cop/style/alias.rb +4 -1
- data/lib/rubocop/cop/style/array_join.rb +4 -2
- data/lib/rubocop/cop/style/ascii_comments.rb +5 -2
- data/lib/rubocop/cop/style/attr.rb +5 -2
- data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
- data/lib/rubocop/cop/style/begin_block.rb +3 -1
- data/lib/rubocop/cop/style/block_delimiters.rb +2 -2
- data/lib/rubocop/cop/style/case_equality.rb +4 -0
- data/lib/rubocop/cop/style/class_and_module_children.rb +10 -2
- data/lib/rubocop/cop/style/colon_method_call.rb +3 -1
- data/lib/rubocop/cop/style/copyright.rb +1 -1
- data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
- data/lib/rubocop/cop/style/each_with_object.rb +2 -0
- data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
- data/lib/rubocop/cop/style/empty_class_definition.rb +21 -20
- data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
- data/lib/rubocop/cop/style/encoding.rb +7 -1
- data/lib/rubocop/cop/style/end_block.rb +3 -1
- data/lib/rubocop/cop/style/endless_method.rb +8 -3
- data/lib/rubocop/cop/style/file_open.rb +63 -0
- data/lib/rubocop/cop/style/for.rb +3 -0
- data/lib/rubocop/cop/style/format_string_token.rb +29 -2
- data/lib/rubocop/cop/style/global_vars.rb +4 -1
- data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
- data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
- data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
- data/lib/rubocop/cop/style/if_unless_modifier.rb +3 -3
- data/lib/rubocop/cop/style/inline_comment.rb +4 -1
- data/lib/rubocop/cop/style/map_join.rb +123 -0
- data/lib/rubocop/cop/style/multiline_if_then.rb +3 -1
- data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
- data/lib/rubocop/cop/style/nil_lambda.rb +1 -1
- data/lib/rubocop/cop/style/not.rb +2 -0
- data/lib/rubocop/cop/style/numeric_literals.rb +2 -1
- data/lib/rubocop/cop/style/one_class_per_file.rb +95 -0
- data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
- data/lib/rubocop/cop/style/parallel_assignment.rb +4 -0
- data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
- data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
- data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
- data/lib/rubocop/cop/style/proc.rb +3 -2
- data/lib/rubocop/cop/style/reduce_to_hash.rb +169 -0
- data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
- data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
- data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
- data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
- data/lib/rubocop/cop/style/redundant_parentheses.rb +6 -3
- data/lib/rubocop/cop/style/redundant_return.rb +3 -1
- data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +104 -0
- data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
- data/lib/rubocop/cop/style/select_by_range.rb +197 -0
- data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
- data/lib/rubocop/cop/style/semicolon.rb +2 -0
- data/lib/rubocop/cop/style/single_line_block_params.rb +1 -1
- data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -1
- data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
- data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
- data/lib/rubocop/cop/style/tally_method.rb +181 -0
- data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
- data/lib/rubocop/cop/variable_force/branch.rb +2 -2
- data/lib/rubocop/directive_comment.rb +2 -1
- data/lib/rubocop/formatter/formatter_set.rb +1 -1
- data/lib/rubocop/lsp/diagnostic.rb +1 -0
- data/lib/rubocop/mcp/server.rb +174 -0
- data/lib/rubocop/options.rb +10 -1
- data/lib/rubocop/server/cache.rb +5 -7
- data/lib/rubocop/target_ruby.rb +18 -12
- data/lib/rubocop/version.rb +1 -1
- data/lib/rubocop.rb +14 -0
- 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
(
|
|
62
|
-
(
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
128
|
+
negated = negated?(regexp_method_send_node)
|
|
120
129
|
method_name = node.method_name
|
|
121
130
|
|
|
122
|
-
|
|
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
|
|
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
|
-
|
|
179
|
+
inner = unwrap_negation(node)
|
|
180
|
+
|
|
181
|
+
return inner.child_nodes.first if inner.match_with_lvasgn_type?
|
|
153
182
|
|
|
154
|
-
if
|
|
183
|
+
if inner.receiver.lvar_type? &&
|
|
155
184
|
(block.type?(:numblock, :itblock) ||
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
elsif
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -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
|
data/lib/rubocop/options.rb
CHANGED
|
@@ -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.'],
|
data/lib/rubocop/server/cache.rb
CHANGED
|
@@ -114,13 +114,11 @@ module RuboCop
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def acquire_lock
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:)
|