rubocop 1.91.0 → 1.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +3 -1
  4. data/lib/rubocop/cached_data.rb +47 -7
  5. data/lib/rubocop/changed_files.rb +35 -13
  6. data/lib/rubocop/cli/command/cop_names.rb +57 -0
  7. data/lib/rubocop/cli/command/execute_runner.rb +2 -2
  8. data/lib/rubocop/cli/command/explain.rb +34 -0
  9. data/lib/rubocop/cli/command/show_cops.rb +6 -0
  10. data/lib/rubocop/cli/command/show_docs_url.rb +6 -4
  11. data/lib/rubocop/cli.rb +3 -2
  12. data/lib/rubocop/comment_config.rb +17 -8
  13. data/lib/rubocop/config_loader.rb +6 -3
  14. data/lib/rubocop/config_loader_resolver.rb +37 -12
  15. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  16. data/lib/rubocop/config_validator.rb +11 -0
  17. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  18. data/lib/rubocop/cop/base.rb +42 -21
  19. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  20. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  21. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  22. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  23. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  24. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +22 -7
  25. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  26. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +23 -2
  27. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  28. data/lib/rubocop/cop/documentation.rb +1 -1
  29. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  30. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  31. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  32. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  33. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  34. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  35. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  36. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  37. data/lib/rubocop/cop/layout/class_structure.rb +9 -1
  38. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  39. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +14 -5
  40. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  41. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  42. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +1 -1
  43. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  44. data/lib/rubocop/cop/layout/end_alignment.rb +16 -10
  45. data/lib/rubocop/cop/layout/extra_spacing.rb +4 -0
  46. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -0
  47. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +5 -3
  48. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  49. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  50. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  51. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +2 -0
  52. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  53. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  54. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  55. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  56. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  57. data/lib/rubocop/cop/layout/line_length.rb +52 -18
  58. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  59. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  60. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +7 -0
  61. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  62. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +14 -0
  63. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  64. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  65. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +15 -2
  66. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  67. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  68. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  69. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  70. data/lib/rubocop/cop/layout/space_before_block_braces.rb +4 -1
  71. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  72. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +31 -8
  73. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  74. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  75. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  76. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  77. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  78. data/lib/rubocop/cop/lint/loop.rb +24 -2
  79. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  80. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  81. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  82. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +12 -1
  83. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  84. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  85. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  86. data/lib/rubocop/cop/lint/void.rb +8 -1
  87. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  88. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  89. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  90. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  91. data/lib/rubocop/cop/mixin/check_line_breakable.rb +17 -14
  92. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +5 -1
  93. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  94. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  95. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +69 -0
  96. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  97. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +15 -1
  98. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  99. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  100. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  101. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  102. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  103. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  104. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  105. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  106. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  107. data/lib/rubocop/cop/mixin/statement_modifier.rb +22 -2
  108. data/lib/rubocop/cop/mixin.rb +2 -0
  109. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  110. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  111. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  112. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  113. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  114. data/lib/rubocop/cop/offense.rb +59 -3
  115. data/lib/rubocop/cop/registry.rb +1 -1
  116. data/lib/rubocop/cop/security/json_load.rb +16 -4
  117. data/lib/rubocop/cop/style/access_modifier_declarations.rb +7 -0
  118. data/lib/rubocop/cop/style/alias.rb +18 -5
  119. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  120. data/lib/rubocop/cop/style/and_or.rb +4 -0
  121. data/lib/rubocop/cop/style/arguments_forwarding.rb +4 -1
  122. data/lib/rubocop/cop/style/attr.rb +3 -4
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  124. data/lib/rubocop/cop/style/block_delimiters.rb +29 -3
  125. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +54 -16
  127. data/lib/rubocop/cop/style/class_methods_definitions.rb +8 -0
  128. data/lib/rubocop/cop/style/combinable_loops.rb +85 -11
  129. data/lib/rubocop/cop/style/concat_array_literals.rb +46 -7
  130. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  131. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  132. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  133. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  134. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  135. data/lib/rubocop/cop/style/endless_method.rb +67 -6
  136. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  137. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  138. data/lib/rubocop/cop/style/float_division.rb +22 -2
  139. data/lib/rubocop/cop/style/for.rb +40 -2
  140. data/lib/rubocop/cop/style/format_string.rb +28 -6
  141. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  142. data/lib/rubocop/cop/style/guard_clause.rb +9 -1
  143. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  144. data/lib/rubocop/cop/style/hash_lookup_method.rb +32 -9
  145. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  146. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  147. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  148. data/lib/rubocop/cop/style/if_unless_modifier.rb +19 -20
  149. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  150. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  151. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  152. data/lib/rubocop/cop/style/it_block_parameter.rb +32 -3
  153. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  154. data/lib/rubocop/cop/style/lambda.rb +18 -0
  155. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  156. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  157. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +45 -10
  158. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +25 -2
  159. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  160. data/lib/rubocop/cop/style/method_def_parentheses.rb +29 -3
  161. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  162. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  163. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  164. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  165. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  166. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  167. data/lib/rubocop/cop/style/mutable_constant.rb +28 -1
  168. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  169. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  170. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  171. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  172. data/lib/rubocop/cop/style/next.rb +24 -10
  173. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  174. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  175. data/lib/rubocop/cop/style/not.rb +10 -5
  176. data/lib/rubocop/cop/style/numeric_predicate.rb +21 -5
  177. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  178. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  179. data/lib/rubocop/cop/style/reduce_to_hash.rb +17 -1
  180. data/lib/rubocop/cop/style/redundant_begin.rb +20 -3
  181. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  182. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  183. data/lib/rubocop/cop/style/redundant_exception.rb +15 -3
  184. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  185. data/lib/rubocop/cop/style/redundant_freeze.rb +10 -1
  186. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  187. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  188. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  189. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  190. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  191. data/lib/rubocop/cop/style/require_order.rb +9 -0
  192. data/lib/rubocop/cop/style/rescue_modifier.rb +5 -9
  193. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  194. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  195. data/lib/rubocop/cop/style/sample.rb +9 -8
  196. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  197. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  198. data/lib/rubocop/cop/style/single_line_methods.rb +16 -4
  199. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  200. data/lib/rubocop/cop/style/string_concatenation.rb +9 -3
  201. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  202. data/lib/rubocop/cop/style/symbol_proc.rb +23 -5
  203. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  204. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  205. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  206. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  207. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  208. data/lib/rubocop/cop/team.rb +10 -5
  209. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  210. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  211. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  212. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  213. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  214. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  215. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  216. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  217. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  218. data/lib/rubocop/lsp/runtime.rb +47 -11
  219. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  220. data/lib/rubocop/lsp.rb +5 -2
  221. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  222. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  223. data/lib/rubocop/mcp/scope.rb +65 -0
  224. data/lib/rubocop/mcp/server.rb +184 -76
  225. data/lib/rubocop/mcp/source_files.rb +35 -0
  226. data/lib/rubocop/offense_limit.rb +30 -0
  227. data/lib/rubocop/options.rb +45 -10
  228. data/lib/rubocop/plugin/configuration_integrator.rb +12 -5
  229. data/lib/rubocop/remote_config.rb +4 -1
  230. data/lib/rubocop/result_cache.rb +20 -13
  231. data/lib/rubocop/runner.rb +109 -41
  232. data/lib/rubocop/server/cache.rb +2 -2
  233. data/lib/rubocop/util.rb +69 -0
  234. data/lib/rubocop/version.rb +1 -1
  235. data/lib/rubocop.rb +5 -0
  236. metadata +16 -5
@@ -13,19 +13,29 @@ module RuboCop
13
13
  def initialize(output, options = {})
14
14
  super
15
15
  @output_hash = { metadata: metadata_hash, files: [], summary: { offense_count: 0 } }
16
+ @problems = {}
16
17
  end
17
18
 
18
19
  def started(target_files)
19
20
  output_hash[:summary][:target_file_count] = target_files.count
20
21
  end
21
22
 
23
+ def file_problems(file, errors, warnings)
24
+ @problems[file] = hash_for_problems(file, errors, warnings)
25
+ end
26
+
22
27
  def file_finished(file, offenses)
23
- output_hash[:files] << hash_for_file(file, offenses)
28
+ entry = hash_for_file(file, offenses)
29
+ problems = @problems.delete(file)
30
+ entry.merge!(problems) if problems
31
+
32
+ output_hash[:files] << entry
24
33
  output_hash[:summary][:offense_count] += offenses.count
25
34
  end
26
35
 
27
36
  def finished(inspected_files)
28
37
  output_hash[:summary][:inspected_file_count] = inspected_files.count
38
+ output_hash[:summary].merge!(problem_counts(output_hash[:files]))
29
39
  output.write output_hash.to_json
30
40
  end
31
41
 
@@ -46,6 +56,32 @@ module RuboCop
46
56
  }
47
57
  end
48
58
 
59
+ # A cop that crashes fails once per node it visits, so messages that
60
+ # differ only in their line and column are listed once, at the first place
61
+ # they happened. They name the file the way its entry does. The keys are
62
+ # absent when there's nothing to report, which keeps them additive for
63
+ # existing consumers.
64
+ def hash_for_problems(file, errors, warnings)
65
+ return {} if errors.empty? && warnings.empty?
66
+
67
+ path = smart_path(file)
68
+ location = /#{Regexp.escape(File.expand_path(file))}((?::\d+)*)/
69
+
70
+ { errors: errors, warnings: warnings }.filter_map do |key, messages|
71
+ next if messages.empty?
72
+
73
+ messages = messages.uniq { |message| message.gsub(location, '') }
74
+ [key, messages.map { |message| message.gsub(location) { path + Regexp.last_match(1) } }]
75
+ end.to_h
76
+ end
77
+
78
+ def problem_counts(files)
79
+ {
80
+ error_count: files.sum { |f| f[:errors]&.size.to_i },
81
+ warning_count: files.sum { |f| f[:warnings]&.size.to_i }
82
+ }.reject { |_, count| count.zero? }
83
+ end
84
+
49
85
  def hash_for_offense(offense)
50
86
  hash = {
51
87
  severity: offense.severity.name,
@@ -63,9 +99,43 @@ module RuboCop
63
99
  hash[:justification] = offense.justification
64
100
  end
65
101
 
102
+ # The edits autocorrection would make, so a consumer can apply or review
103
+ # them without running autocorrection itself. Additive, and absent when
104
+ # the offense has no correction.
105
+ hash[:correction] = correction_for(offense) unless offense.corrections.empty?
106
+
66
107
  hash
67
108
  end
68
109
 
110
+ def correction_for(offense)
111
+ { safe: offense.correction_safe, edits: edits_for(offense) }
112
+ end
113
+
114
+ def edits_for(offense)
115
+ buffer = offense.location.source_buffer
116
+
117
+ offense.corrections.map do |correction|
118
+ # Built as a range so the columns follow the same 1-based convention
119
+ # as `hash_for_location`, end-exclusive column included.
120
+ range = ::Parser::Source::Range.new(buffer, correction.begin_pos, correction.end_pos)
121
+
122
+ hash_for_edit_range(range).merge!(
123
+ begin_pos: correction.begin_pos,
124
+ end_pos: correction.end_pos,
125
+ replacement: correction.replacement
126
+ )
127
+ end
128
+ end
129
+
130
+ def hash_for_edit_range(range)
131
+ {
132
+ start_line: range.line,
133
+ start_column: range.column + 1,
134
+ last_line: range.last_line,
135
+ last_column: range.last_column.zero? ? 1 : range.last_column
136
+ }
137
+ end
138
+
69
139
  def hash_for_location(offense)
70
140
  {
71
141
  start_line: offense.line,
@@ -14,6 +14,8 @@ module RuboCop
14
14
  # --
15
15
  # 29 Total in 5 files
16
16
  class OffenseCountFormatter < BaseFormatter
17
+ include TextUtil
18
+
17
19
  attr_reader :offense_counts
18
20
 
19
21
  def started(target_files)
@@ -64,7 +66,7 @@ module RuboCop
64
66
  output.puts "#{count.to_s.ljust(column_width)}#{cop_information(cop_name)}"
65
67
  end
66
68
  output.puts '--'
67
- output.puts "#{total_count} Total in #{offending_files_count} files"
69
+ output.puts "#{total_count} Total in #{pluralize(offending_files_count, 'file')}"
68
70
 
69
71
  output.puts
70
72
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'io/console'
4
+
3
5
  module RuboCop
4
6
  module Formatter
5
7
  # This formatter prints a PACDOT per every file to be analyzed.
@@ -50,7 +52,7 @@ module RuboCop
50
52
 
51
53
  def cols
52
54
  @cols ||= begin
53
- _height, width = $stdout.winsize
55
+ _height, width = output.winsize if output.tty?
54
56
  width.nil? || width.zero? ? FALLBACK_TERMINAL_WIDTH : width
55
57
  end
56
58
  end
@@ -38,8 +38,9 @@ module RuboCop
38
38
  end
39
39
 
40
40
  def file_finished(file, offenses)
41
+ line_hash_counts = Hash.new(0)
41
42
  offenses.each do |offense|
42
- @results << result_for(file, offense)
43
+ @results << result_for(file, offense, line_hash_counts)
43
44
  end
44
45
  end
45
46
 
@@ -67,14 +68,14 @@ module RuboCop
67
68
  }
68
69
  end
69
70
 
70
- def result_for(file, offense)
71
+ def result_for(file, offense, line_hash_counts)
71
72
  result = {
72
73
  ruleId: offense.cop_name,
73
74
  ruleIndex: rule_index_for(file, offense),
74
75
  level: SEVERITY_LEVELS.fetch(offense.severity.name, 'warning'),
75
76
  message: { text: offense.message },
76
77
  locations: [location_for(file, offense)],
77
- partialFingerprints: fingerprints_for(offense)
78
+ partialFingerprints: fingerprints_for(offense, line_hash_counts)
78
79
  }
79
80
 
80
81
  # Suppressed offenses are only reported under `--display-suppressed`.
@@ -110,9 +111,13 @@ module RuboCop
110
111
 
111
112
  # GitHub uses these to match an alert across runs. Without them it falls
112
113
  # back to the file path, which makes alerts churn whenever a file moves.
113
- def fingerprints_for(offense)
114
- source_line = offense.location.source_line
115
- { primaryLocationLineHash: Digest::SHA256.hexdigest("#{offense.cop_name}#{source_line}") }
114
+ # Repeats of the same hash within a file get a `:N` suffix, like GitHub's
115
+ # own fingerprints, so two offenses on one line don't merge into one alert.
116
+ # The first occurrence keeps the bare hash to match earlier reports.
117
+ def fingerprints_for(offense, line_hash_counts)
118
+ line_hash = Digest::SHA256.hexdigest("#{offense.cop_name}#{offense.location.source_line}")
119
+ count = line_hash_counts[line_hash] += 1
120
+ { primaryLocationLineHash: count == 1 ? line_hash : "#{line_hash}:#{count}" }
116
121
  end
117
122
 
118
123
  def suppression_for(offense)
@@ -158,7 +163,7 @@ module RuboCop
158
163
 
159
164
  def default_level_for(file, offense, cop_class)
160
165
  severity = cop_config(file, offense)['Severity'] ||
161
- (cop_class&.lint? ? :warning : :convention)
166
+ Cop::Base::DEPARTMENT_SEVERITIES.fetch(cop_class&.department, :convention)
162
167
 
163
168
  SEVERITY_LEVELS.fetch(severity.to_sym, 'warning')
164
169
  end
@@ -14,6 +14,8 @@ module RuboCop
14
14
  # --
15
15
  # 29 Total in 2 files
16
16
  class WorstOffendersFormatter < BaseFormatter
17
+ include TextUtil
18
+
17
19
  attr_reader :offense_counts
18
20
 
19
21
  def started(target_files)
@@ -46,7 +48,7 @@ module RuboCop
46
48
  end
47
49
 
48
50
  output.puts '--'
49
- output.puts "#{total_count} Total in #{file_count} files"
51
+ output.puts "#{total_count} Total in #{pluralize(file_count, 'file')}"
50
52
 
51
53
  output.puts
52
54
  end
@@ -17,27 +17,31 @@ module RuboCop
17
17
  # Runtime for Language Server Protocol of RuboCop.
18
18
  # @api private
19
19
  class Runtime
20
- attr_writer :safe_autocorrect, :lint_mode, :layout_mode
20
+ attr_writer :safe_autocorrect, :lint_mode, :layout_mode, :raise_cop_error
21
21
 
22
- def initialize(config_store)
23
- RuboCop::LSP.enable
22
+ # Without `lsp_mode`, offenses keep the locations and messages the
23
+ # command line gives them, rather than the ones meant for an editor.
24
+ def initialize(config_store, lsp_mode: true)
25
+ RuboCop::LSP.enable if lsp_mode
24
26
 
25
27
  @runner = RuboCop::Lsp::StdinRunner.new(config_store)
26
28
 
27
29
  @safe_autocorrect = true
28
30
  @lint_mode = false
29
31
  @layout_mode = false
32
+ @raise_cop_error = true
30
33
  end
31
34
 
32
- def format(path, text, command:, prism_result: nil)
35
+ def format(path, text, command:, prism_result: nil, options: {})
33
36
  safe_autocorrect = if command
34
37
  command == 'rubocop.formatAutocorrects'
35
38
  else
36
39
  @safe_autocorrect
37
40
  end
38
41
 
39
- formatting_options = { autocorrect: true, safe_autocorrect: safe_autocorrect }
40
- formatting_options[:only] = config_only_options if @lint_mode || @layout_mode
42
+ formatting_options = {
43
+ autocorrect: true, safe_autocorrect: safe_autocorrect, raise_cop_error: @raise_cop_error
44
+ }.merge(call_options(options))
41
45
 
42
46
  @runner.run(path, text, formatting_options, prism_result: prism_result)
43
47
  @runner.formatted_source
@@ -48,17 +52,39 @@ module RuboCop
48
52
  end
49
53
 
50
54
  def offenses(path, text, document_encoding = nil, prism_result: nil)
51
- diagnostic_options = {}
52
- diagnostic_options[:only] = config_only_options if @lint_mode || @layout_mode
53
-
54
- @runner.run(path, text, diagnostic_options, prism_result: prism_result)
55
+ found = raw_offenses(path, text, prism_result: prism_result)
55
56
  processed_source = @runner.processed_source
56
57
  config = @runner.config_for_working_directory
57
- @runner.offenses.map do |offense|
58
+ found.map do |offense|
58
59
  build_diagnostic(offense, path, document_encoding, processed_source, config)
59
60
  end
60
61
  end
61
62
 
63
+ # The offenses before they are converted to LSP diagnostics, for callers
64
+ # that do not speak LSP.
65
+ def raw_offenses(path, text, prism_result: nil, options: {})
66
+ diagnostic_options = { raise_cop_error: @raise_cop_error }.merge(call_options(options))
67
+
68
+ @runner.run(path, text, diagnostic_options, prism_result: prism_result)
69
+ @runner.offenses
70
+ end
71
+
72
+ # The offenses the last `format` found but didn't correct, located in the
73
+ # source it returned.
74
+ def uncorrected_offenses
75
+ @runner.offenses.reject(&:corrected?)
76
+ end
77
+
78
+ # What went wrong in the last run. Cop errors only end up here when
79
+ # `raise_cop_error` is off; otherwise they are raised.
80
+ def errors
81
+ @runner.errors
82
+ end
83
+
84
+ def warnings
85
+ @runner.warnings
86
+ end
87
+
62
88
  private
63
89
 
64
90
  def build_diagnostic(offense, path, document_encoding, processed_source, config)
@@ -71,6 +97,16 @@ module RuboCop
71
97
  ).to_lsp_diagnostic(config)
72
98
  end
73
99
 
100
+ # What a caller adds for one call, such as `only` and `except` to select
101
+ # the cops, or `editing` to hold back the corrections of cops with
102
+ # `AutoCorrect: contextual`, as LSP mode does. Without `only`, the cops
103
+ # are the departments a lint or layout mode limits the server to.
104
+ def call_options(options)
105
+ return options if options[:only] || !(@lint_mode || @layout_mode)
106
+
107
+ options.merge(only: config_only_options)
108
+ end
109
+
74
110
  def config_only_options
75
111
  only_options = []
76
112
  only_options << 'Lint' if @lint_mode
@@ -23,7 +23,6 @@ module RuboCop
23
23
  stderr: true,
24
24
  force_exclusion: true,
25
25
  formatters: ['RuboCop::Formatter::BaseFormatter'],
26
- raise_cop_error: true,
27
26
  todo_file: nil,
28
27
  todo_ignore_files: []
29
28
  }.freeze
@@ -41,6 +40,13 @@ module RuboCop
41
40
  end
42
41
 
43
42
  def run(path, contents, options, prism_result: nil)
43
+ # The cops mobilized for a configuration are cached, which only holds while
44
+ # every run asks for the same ones.
45
+ selection = options.values_at(:only, :except)
46
+ unless selection == @cop_selection
47
+ forget_mobilized_cops
48
+ @cop_selection = selection
49
+ end
44
50
  @options = options.merge(DEFAULT_RUBOCOP_OPTIONS)
45
51
  @options[:stdin] = contents
46
52
 
@@ -86,10 +92,10 @@ module RuboCop
86
92
  @offenses = offenses
87
93
  end
88
94
 
89
- def do_inspection_loop(file)
90
- source, offenses = super
91
- @processed_source = source
92
- [source, offenses]
95
+ def do_inspection_loop(file, source = nil)
96
+ processed_source, offenses = super
97
+ @processed_source = processed_source
98
+ [processed_source, offenses]
93
99
  end
94
100
  end
95
101
  end
data/lib/rubocop/lsp.rb CHANGED
@@ -26,8 +26,11 @@ module RuboCop
26
26
  if block
27
27
  original = @enabled
28
28
  @enabled = false
29
- yield
30
- @enabled = original
29
+ begin
30
+ yield
31
+ ensure
32
+ @enabled = original
33
+ end
31
34
  else
32
35
  @enabled = false
33
36
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module MCP
5
+ # What an autocorrection request asks for, shared by every file it corrects.
6
+ # The keywords are the tool's arguments, so an unknown one is refused.
7
+ # @api private
8
+ class AutocorrectionRequest
9
+ PROPERTIES = {
10
+ safety: { type: 'boolean' },
11
+ contextual: { type: 'boolean' },
12
+ dry_run: { type: 'boolean' },
13
+ max_offenses_per_cop: { type: 'integer', minimum: 1 }
14
+ }.freeze
15
+
16
+ # The offenses left over come back after the files are written, so they're
17
+ # capped unless the agent asks otherwise: a result too big for the client
18
+ # would hide what was done.
19
+ DEFAULT_MAX_OFFENSES_PER_COP = 5
20
+
21
+ attr_reader :scope, :limit
22
+
23
+ def initialize(scope, safety: true, contextual: false, dry_run: false,
24
+ max_offenses_per_cop: DEFAULT_MAX_OFFENSES_PER_COP)
25
+ @scope = scope
26
+ @safety = safety
27
+ @contextual = contextual
28
+ @dry_run = dry_run
29
+ @limit = OffenseLimit.new(max_offenses_per_cop)
30
+ end
31
+
32
+ def dry_run?
33
+ @dry_run
34
+ end
35
+
36
+ # The options `LSP::Runtime#format` takes to apply the corrections
37
+ # `safety` and `contextual` allow, from the cops the scope selects.
38
+ def format_options
39
+ command = @safety ? 'rubocop.formatAutocorrects' : 'rubocop.formatAutocorrectsAll'
40
+ { command: command, options: runtime_options }
41
+ end
42
+
43
+ # The options the LSP runtime takes for each call the request makes.
44
+ # Cops with `AutoCorrect: contextual` hold back corrections that get in
45
+ # the way of code still being written, such as removing a variable that
46
+ # is about to be used, so an agent that is done editing has to ask for them.
47
+ def runtime_options
48
+ { **@scope.cop_options, editing: !@contextual }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module MCP
5
+ # The `rubocop_explain` tool: what cops do, as `--explain` prints it.
6
+ # @api private
7
+ class ExplainTool
8
+ include RuboCop::CLI::Command::CopNames
9
+
10
+ DESCRIPTION =
11
+ 'Explain what cops do: the problem each one targets, bad and good examples, ' \
12
+ 'its configuration as this project resolves it, and whether it autocorrects. ' \
13
+ 'Takes cop names as offenses report them, such as `Style/StringLiterals`. ' \
14
+ 'Pass the `path` of a file to see the configuration that applies to it.'
15
+ INPUT_SCHEMA = {
16
+ properties: {
17
+ cop_names: { type: 'array', items: { type: 'string' }, minItems: 1 },
18
+ path: { type: 'string' }
19
+ },
20
+ required: ['cop_names'],
21
+ additionalProperties: false
22
+ }.freeze
23
+ ANNOTATIONS = {
24
+ title: "RuboCop's cop explanation", destructive_hint: false, idempotent_hint: true,
25
+ open_world_hint: false, read_only_hint: true
26
+ }.freeze
27
+
28
+ def initialize(config_store)
29
+ @config_store = config_store
30
+ end
31
+
32
+ # The tool's block does not run in this instance, so it reaches
33
+ # `explain` through a method object it closes over.
34
+ def to_tool
35
+ explain = method(:explain)
36
+
37
+ ::MCP::Tool.define(
38
+ name: 'rubocop_explain',
39
+ description: DESCRIPTION,
40
+ input_schema: INPUT_SCHEMA,
41
+ annotations: ANNOTATIONS
42
+ ) do |cop_names:, path: nil|
43
+ ::MCP::Tool::Response.new([{ type: 'text', text: explain.call(cop_names, path) }])
44
+ rescue RuboCop::Error => e
45
+ ::MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Explains the cops it recognizes and then names the ones it does not, as
52
+ # `--explain` does, so a typo does not cost the explanations asked for
53
+ # alongside it. The configuration is loaded first, since that is what
54
+ # registers the cops of any plugins it names.
55
+ def explain(cop_names, path)
56
+ config = path ? @config_store.for_file(path) : @config_store.for_pwd
57
+ explanations = known_cop_classes(cop_names).map do |cop_class|
58
+ Cop::CopExplanation.new(cop_class, config).to_s
59
+ end
60
+ validate_cop_names!(cop_names)
61
+
62
+ explanations.join("\n\n")
63
+ rescue IncorrectCopNameError => e
64
+ raise RuboCop::Error, [*explanations, e.message].join("\n\n")
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module MCP
5
+ # What a request asks RuboCop to look at: which cops run, as `--only` and
6
+ # `--except` choose them, and, when checking files, whether to stick to the
7
+ # ones git says changed, as `--changed` does.
8
+ # @api private
9
+ class Scope
10
+ PROPERTIES = {
11
+ only: {
12
+ type: 'array', items: { type: 'string' },
13
+ description: 'Run only these cops or departments, ' \
14
+ 'such as `Style/StringLiterals` or `Lint`.'
15
+ },
16
+ except: {
17
+ type: 'array', items: { type: 'string' },
18
+ description: 'Run every cop except these cops or departments.'
19
+ },
20
+ changed: {
21
+ type: %w[boolean string],
22
+ description: 'Check only the files git says changed: `true` for changes since the ' \
23
+ 'last commit, or a revision to compare against, such as `main`.'
24
+ }
25
+ }.freeze
26
+
27
+ # An empty list would select no cops at all rather than leave the choice
28
+ # alone, so it counts as no list.
29
+ #
30
+ # @raise [RuboCop::Error] for a scope the command line would refuse
31
+ def initialize(inline:, only: nil, except: nil, changed: nil)
32
+ @only = only if only&.any?
33
+ @except = except if except&.any?
34
+ @changed = changed == true ? ChangedFiles::DEFAULT_REVISION : changed
35
+ validate(inline)
36
+ end
37
+
38
+ # @return [Hash] the cop selection, among the options the LSP runtime takes
39
+ def cop_options
40
+ { only: @only, except: @except }.compact
41
+ end
42
+
43
+ def select_files(files)
44
+ @changed ? ChangedFiles.new(@changed).filter(files) : files
45
+ end
46
+
47
+ private
48
+
49
+ # Refuses what the command line refuses for the same options. Cop names
50
+ # are left to the runner, which checks them the way `--only` does: short
51
+ # names allowed, and only once the configuration naming a plugin's cops
52
+ # has been loaded.
53
+ def validate(inline)
54
+ raise Error, '`changed` only applies when checking files.' if inline && @changed
55
+ raise Error, '`changed` takes `true` or a git revision.' if @changed == ''
56
+
57
+ validator = OptionsValidator.new(cop_options)
58
+ if validator.only_includes_redundant_disable?
59
+ raise Error, 'Lint/RedundantCopDisableDirective cannot be used with `only`.'
60
+ end
61
+ raise Error, 'Syntax checking cannot be turned off.' if validator.except_syntax?
62
+ end
63
+ end
64
+ end
65
+ end