rubocop 1.88.2 → 1.90.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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +139 -16
  3. data/exe/rubocop +1 -1
  4. data/lib/rubocop/cached_data.rb +7 -2
  5. data/lib/rubocop/cli/command/execute_runner.rb +31 -0
  6. data/lib/rubocop/cli.rb +2 -4
  7. data/lib/rubocop/comment_config/directive_range.rb +32 -0
  8. data/lib/rubocop/comment_config/disable_next.rb +97 -0
  9. data/lib/rubocop/comment_config.rb +81 -49
  10. data/lib/rubocop/config.rb +76 -5
  11. data/lib/rubocop/config_loader.rb +1 -2
  12. data/lib/rubocop/config_loader_resolver.rb +1 -2
  13. data/lib/rubocop/config_validator.rb +39 -3
  14. data/lib/rubocop/cop/autocorrect_logic.rb +8 -0
  15. data/lib/rubocop/cop/badge.rb +8 -0
  16. data/lib/rubocop/cop/base.rb +40 -6
  17. data/lib/rubocop/cop/bundler.rb +19 -0
  18. data/lib/rubocop/cop/corrector.rb +4 -4
  19. data/lib/rubocop/cop/correctors/alignment_corrector.rb +55 -5
  20. data/lib/rubocop/cop/gemspec.rb +22 -0
  21. data/lib/rubocop/cop/generator/registration_injector.rb +188 -0
  22. data/lib/rubocop/cop/generator.rb +4 -0
  23. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +1 -2
  24. data/lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb +1 -2
  25. data/lib/rubocop/cop/layout/block_alignment.rb +1 -2
  26. data/lib/rubocop/cop/layout/class_structure.rb +108 -26
  27. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +1 -2
  28. data/lib/rubocop/cop/layout/empty_line_after_magic_comment.rb +63 -5
  29. data/lib/rubocop/cop/layout/end_alignment.rb +1 -2
  30. data/lib/rubocop/cop/layout/extra_spacing.rb +105 -3
  31. data/lib/rubocop/cop/layout/first_argument_indentation.rb +21 -1
  32. data/lib/rubocop/cop/layout/first_array_element_indentation.rb +1 -1
  33. data/lib/rubocop/cop/layout/indentation_width.rb +3 -1
  34. data/lib/rubocop/cop/layout/leading_comment_space.rb +27 -0
  35. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +1 -2
  36. data/lib/rubocop/cop/layout/line_length.rb +84 -12
  37. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +1 -2
  38. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +12 -2
  39. data/lib/rubocop/cop/layout/redundant_line_break.rb +77 -0
  40. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +1 -2
  41. data/lib/rubocop/cop/layout/space_around_operators.rb +7 -0
  42. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +31 -34
  43. data/lib/rubocop/cop/layout.rb +114 -0
  44. data/lib/rubocop/cop/lazy_loader.rb +41 -0
  45. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +9 -7
  46. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +71 -0
  47. data/lib/rubocop/cop/lint/ambiguous_range.rb +1 -2
  48. data/lib/rubocop/cop/lint/argument_mismatch.rb +89 -0
  49. data/lib/rubocop/cop/lint/constant_reassignment.rb +18 -21
  50. data/lib/rubocop/cop/lint/constant_resolution.rb +25 -0
  51. data/lib/rubocop/cop/lint/cop_directive_syntax.rb +84 -8
  52. data/lib/rubocop/cop/lint/deprecated_reference.rb +194 -0
  53. data/lib/rubocop/cop/lint/duplicate_methods.rb +300 -22
  54. data/lib/rubocop/cop/lint/implicit_string_concatenation.rb +1 -2
  55. data/lib/rubocop/cop/lint/ineffective_access_modifier.rb +1 -2
  56. data/lib/rubocop/cop/lint/inherit_exception.rb +53 -7
  57. data/lib/rubocop/cop/lint/interpolation_check.rb +1 -2
  58. data/lib/rubocop/cop/lint/literal_as_condition.rb +8 -6
  59. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +4 -7
  60. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +25 -12
  61. data/lib/rubocop/cop/lint/missing_super.rb +31 -1
  62. data/lib/rubocop/cop/lint/name_typo.rb +270 -0
  63. data/lib/rubocop/cop/lint/out_of_range_regexp_ref.rb +15 -4
  64. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +98 -19
  65. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +47 -7
  66. data/lib/rubocop/cop/lint/redundant_require_statement.rb +1 -2
  67. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +1 -2
  68. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +1 -2
  69. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +1 -2
  70. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -2
  71. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +8 -2
  72. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +2 -4
  73. data/lib/rubocop/cop/lint/super_argument_mismatch.rb +179 -0
  74. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +1 -2
  75. data/lib/rubocop/cop/lint/to_enum_arguments.rb +38 -3
  76. data/lib/rubocop/cop/lint/unreachable_code.rb +1 -2
  77. data/lib/rubocop/cop/lint/unused_private_method.rb +234 -0
  78. data/lib/rubocop/cop/lint/useless_access_modifier.rb +1 -2
  79. data/lib/rubocop/cop/lint/useless_assignment.rb +1 -2
  80. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +36 -0
  81. data/lib/rubocop/cop/lint/useless_method_definition.rb +1 -1
  82. data/lib/rubocop/cop/lint/useless_rescue.rb +1 -2
  83. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +2 -0
  84. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +61 -7
  85. data/lib/rubocop/cop/lint/void.rb +18 -8
  86. data/lib/rubocop/cop/lint.rb +178 -0
  87. data/lib/rubocop/cop/metrics/perceived_complexity.rb +1 -2
  88. data/lib/rubocop/cop/metrics/utils/code_length_calculator.rb +1 -1
  89. data/lib/rubocop/cop/metrics.rb +31 -0
  90. data/lib/rubocop/cop/migration/department_name.rb +1 -6
  91. data/lib/rubocop/cop/migration.rb +13 -0
  92. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -5
  93. data/lib/rubocop/cop/mixin/comments_help.rb +1 -2
  94. data/lib/rubocop/cop/mixin/configurable_enforced_style.rb +1 -2
  95. data/lib/rubocop/cop/mixin/endless_method_rewriter.rb +13 -5
  96. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +23 -6
  97. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +1 -2
  98. data/lib/rubocop/cop/mixin/hash_subset.rb +1 -2
  99. data/lib/rubocop/cop/mixin/indexed_method_arity.rb +94 -0
  100. data/lib/rubocop/cop/mixin/line_length_help.rb +7 -1
  101. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +51 -6
  102. data/lib/rubocop/cop/mixin/project_index_help.rb +140 -0
  103. data/lib/rubocop/cop/mixin/range_help.rb +1 -2
  104. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +171 -0
  105. data/lib/rubocop/cop/mixin/statement_modifier.rb +46 -4
  106. data/lib/rubocop/cop/mixin/trailing_comma.rb +2 -4
  107. data/lib/rubocop/cop/mixin.rb +82 -77
  108. data/lib/rubocop/cop/naming/accessor_method_name.rb +24 -0
  109. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +16 -0
  110. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +2 -4
  111. data/lib/rubocop/cop/naming/method_name.rb +5 -2
  112. data/lib/rubocop/cop/naming/predicate_prefix.rb +33 -5
  113. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -2
  114. data/lib/rubocop/cop/naming/variable_number.rb +3 -1
  115. data/lib/rubocop/cop/naming.rb +31 -0
  116. data/lib/rubocop/cop/offense.rb +37 -10
  117. data/lib/rubocop/cop/registry.rb +180 -46
  118. data/lib/rubocop/cop/security.rb +19 -0
  119. data/lib/rubocop/cop/style/accessor_grouping.rb +1 -2
  120. data/lib/rubocop/cop/style/arguments_forwarding.rb +3 -6
  121. data/lib/rubocop/cop/style/array_first_last.rb +1 -2
  122. data/lib/rubocop/cop/style/array_intersect.rb +24 -5
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +1 -2
  124. data/lib/rubocop/cop/style/block_delimiters.rb +3 -6
  125. data/lib/rubocop/cop/style/case_like_if.rb +2 -4
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +79 -20
  127. data/lib/rubocop/cop/style/combinable_loops.rb +1 -2
  128. data/lib/rubocop/cop/style/concat_array_literals.rb +1 -2
  129. data/lib/rubocop/cop/style/conditional_assignment.rb +23 -2
  130. data/lib/rubocop/cop/style/constant_visibility.rb +1 -2
  131. data/lib/rubocop/cop/style/copyright.rb +5 -1
  132. data/lib/rubocop/cop/style/directive_scope.rb +171 -0
  133. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +76 -12
  134. data/lib/rubocop/cop/style/documentation.rb +47 -0
  135. data/lib/rubocop/cop/style/empty_case_condition.rb +1 -2
  136. data/lib/rubocop/cop/style/endless_method.rb +0 -10
  137. data/lib/rubocop/cop/style/format_string_token.rb +1 -2
  138. data/lib/rubocop/cop/style/guard_clause.rb +1 -2
  139. data/lib/rubocop/cop/style/hash_each_methods.rb +1 -2
  140. data/lib/rubocop/cop/style/hash_syntax.rb +1 -2
  141. data/lib/rubocop/cop/style/identical_conditional_branches.rb +2 -4
  142. data/lib/rubocop/cop/style/if_inside_else.rb +1 -2
  143. data/lib/rubocop/cop/style/if_unless_modifier.rb +8 -50
  144. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +1 -2
  145. data/lib/rubocop/cop/style/lambda.rb +4 -0
  146. data/lib/rubocop/cop/style/map_to_hash.rb +1 -2
  147. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +22 -7
  148. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +1 -2
  149. data/lib/rubocop/cop/style/missing_respond_to_missing.rb +30 -0
  150. data/lib/rubocop/cop/style/multiline_if_then.rb +7 -1
  151. data/lib/rubocop/cop/style/multiline_method_signature.rb +1 -2
  152. data/lib/rubocop/cop/style/multiple_comparison.rb +33 -22
  153. data/lib/rubocop/cop/style/mutable_constant.rb +5 -0
  154. data/lib/rubocop/cop/style/negated_if_else_condition.rb +1 -2
  155. data/lib/rubocop/cop/style/negative_array_index.rb +3 -3
  156. data/lib/rubocop/cop/style/nil_comparison.rb +1 -2
  157. data/lib/rubocop/cop/style/operator_method_call.rb +1 -2
  158. data/lib/rubocop/cop/style/parallel_assignment.rb +1 -2
  159. data/lib/rubocop/cop/style/redundant_assignment.rb +12 -2
  160. data/lib/rubocop/cop/style/redundant_condition.rb +2 -4
  161. data/lib/rubocop/cop/style/redundant_constant_base.rb +37 -1
  162. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +1 -2
  163. data/lib/rubocop/cop/style/redundant_each.rb +1 -2
  164. data/lib/rubocop/cop/style/redundant_file_extension_in_require.rb +9 -0
  165. data/lib/rubocop/cop/style/redundant_format.rb +14 -7
  166. data/lib/rubocop/cop/style/redundant_line_continuation.rb +78 -135
  167. data/lib/rubocop/cop/style/redundant_parentheses.rb +65 -67
  168. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +1 -2
  169. data/lib/rubocop/cop/style/redundant_return.rb +1 -2
  170. data/lib/rubocop/cop/style/redundant_self_assignment.rb +1 -2
  171. data/lib/rubocop/cop/style/redundant_string_escape.rb +1 -2
  172. data/lib/rubocop/cop/style/rescue_modifier.rb +1 -2
  173. data/lib/rubocop/cop/style/safe_navigation.rb +11 -2
  174. data/lib/rubocop/cop/style/sample.rb +11 -6
  175. data/lib/rubocop/cop/style/select_by_regexp.rb +1 -2
  176. data/lib/rubocop/cop/style/semicolon.rb +2 -4
  177. data/lib/rubocop/cop/style/single_line_do_end_block.rb +1 -2
  178. data/lib/rubocop/cop/style/sole_nested_conditional.rb +21 -5
  179. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +2 -0
  180. data/lib/rubocop/cop/style/static_class.rb +21 -0
  181. data/lib/rubocop/cop/style/string_concatenation.rb +18 -5
  182. data/lib/rubocop/cop/style/super_arguments.rb +1 -2
  183. data/lib/rubocop/cop/style/symbol_proc.rb +1 -2
  184. data/lib/rubocop/cop/style/time_now.rb +41 -0
  185. data/lib/rubocop/cop/style/yoda_condition.rb +1 -2
  186. data/lib/rubocop/cop/style.rb +312 -0
  187. data/lib/rubocop/cop/team.rb +59 -6
  188. data/lib/rubocop/cop/util.rb +2 -4
  189. data/lib/rubocop/cop/variable_force/variable.rb +19 -8
  190. data/lib/rubocop/cops_documentation_generator.rb +4 -7
  191. data/lib/rubocop/directive_comment.rb +39 -3
  192. data/lib/rubocop/feature_loader.rb +0 -6
  193. data/lib/rubocop/formatter/emacs_style_formatter.rb +3 -1
  194. data/lib/rubocop/formatter/html_formatter.rb +2 -4
  195. data/lib/rubocop/formatter/json_formatter.rb +11 -6
  196. data/lib/rubocop/formatter/junit_formatter.rb +26 -9
  197. data/lib/rubocop/formatter/offense_count_formatter.rb +1 -2
  198. data/lib/rubocop/formatter/simple_text_formatter.rb +4 -3
  199. data/lib/rubocop/formatter/tap_formatter.rb +3 -1
  200. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -2
  201. data/lib/rubocop/lockfile.rb +18 -0
  202. data/lib/rubocop/lsp/diagnostic.rb +4 -8
  203. data/lib/rubocop/lsp/routes.rb +89 -22
  204. data/lib/rubocop/lsp/runtime.rb +4 -0
  205. data/lib/rubocop/lsp/server.rb +4 -0
  206. data/lib/rubocop/lsp/stdin_runner.rb +20 -0
  207. data/lib/rubocop/mcp/server.rb +1 -2
  208. data/lib/rubocop/options.rb +10 -6
  209. data/lib/rubocop/path_util.rb +1 -2
  210. data/lib/rubocop/pending_cops_reporter.rb +1 -6
  211. data/lib/rubocop/plugin/configuration_integrator.rb +6 -3
  212. data/lib/rubocop/project_index_loader.rb +16 -0
  213. data/lib/rubocop/rake_task.rb +1 -2
  214. data/lib/rubocop/result_cache.rb +6 -4
  215. data/lib/rubocop/rspec/expect_offense.rb +2 -4
  216. data/lib/rubocop/rspec/shared_contexts.rb +16 -2
  217. data/lib/rubocop/runner.rb +71 -43
  218. data/lib/rubocop/server/cache.rb +11 -7
  219. data/lib/rubocop/server/cli.rb +2 -4
  220. data/lib/rubocop/server.rb +14 -0
  221. data/lib/rubocop/target_finder.rb +4 -1
  222. data/lib/rubocop/target_ruby.rb +1 -1
  223. data/lib/rubocop/todo_audit.rb +111 -0
  224. data/lib/rubocop/version.rb +3 -5
  225. data/lib/rubocop.rb +20 -626
  226. data/lib/ruby_lsp/rubocop/addon.rb +1 -2
  227. metadata +28 -5
@@ -12,6 +12,8 @@ module RuboCop
12
12
  # @api private
13
13
  LINT_SYNTAX_COP = "#{LINT_DEPARTMENT}/Syntax"
14
14
  # @api private
15
+ STYLE_DISABLE_COPS_DIRECTIVE_COP = 'Style/DisableCopsWithinSourceCodeDirective'
16
+ # @api private
15
17
  COP_NAME_PATTERN = '([A-Za-z]\w+/)*(?:[A-Za-z]\w+)'
16
18
  # @api private
17
19
  COP_NAME_PATTERN_NC = '(?:[A-Za-z]\w+/)*[A-Za-z]\w+'
@@ -24,13 +26,17 @@ module RuboCop
24
26
  # @api private
25
27
  PUSH_POP_ARGS_PATTERN = "([+\\-]#{COP_NAME_PATTERN_NC}(?:\\s+[+\\-]#{COP_NAME_PATTERN_NC})*)"
26
28
  # @api private
27
- AVAILABLE_MODES = %w[disable enable todo push pop].freeze
29
+ AVAILABLE_MODES = %w[disable enable todo push pop disable-next todo-next].freeze
30
+ # @api private
31
+ # Longest first, so a `-next` mode is not matched as its prefix
32
+ # (`-` is a word boundary).
33
+ MODES_PATTERN = AVAILABLE_MODES.sort_by { |mode| -mode.length }.join('|').freeze
28
34
  # @api private
29
35
  DIRECTIVE_MARKER_PATTERN = '# rubocop : '
30
36
  # @api private
31
37
  DIRECTIVE_MARKER_REGEXP = Regexp.new(DIRECTIVE_MARKER_PATTERN.gsub(' ', '\s*'))
32
38
  # @api private
33
- DIRECTIVE_HEADER_PATTERN = "#{DIRECTIVE_MARKER_PATTERN}((?:#{AVAILABLE_MODES.join('|')}))\\b"
39
+ DIRECTIVE_HEADER_PATTERN = "#{DIRECTIVE_MARKER_PATTERN}((?:#{MODES_PATTERN}))\\b"
34
40
  # @api private
35
41
  DIRECTIVE_COMMENT_REGEXP = Regexp.new(
36
42
  "#{DIRECTIVE_HEADER_PATTERN}(?:\\s+#{COPS_PATTERN}|\\s+#{PUSH_POP_ARGS_PATTERN})?"
@@ -78,6 +84,18 @@ module RuboCop
78
84
  MALFORMED_DIRECTIVE_WITHOUT_COP_NAME_REGEXP.match?(comment.text)
79
85
  end
80
86
 
87
+ # The text of the directive's optional `--` trailing comment, or `nil`
88
+ # when there is none.
89
+ def reason
90
+ return unless @match_data
91
+
92
+ tail = @match_data.post_match.lstrip
93
+ return unless tail.start_with?(TRAILING_COMMENT_MARKER)
94
+
95
+ reason = tail.delete_prefix(TRAILING_COMMENT_MARKER).strip
96
+ reason unless reason.empty?
97
+ end
98
+
81
99
  # Checks if this directive relates to single line
82
100
  def single_line?
83
101
  !comment.text.start_with?(DIRECTIVE_COMMENT_REGEXP)
@@ -96,6 +114,19 @@ module RuboCop
96
114
  )
97
115
  end
98
116
 
117
+ # `#range` stops at the cop list, so a `--` reason sits outside it. The reason documents the
118
+ # directive and means nothing once the directive is gone, so removal has to cover both. Any
119
+ # other trailing text is an ordinary comment and is left alone.
120
+ def range_with_reason
121
+ directive_range = range
122
+ trailing = Parser::Source::Range.new(
123
+ comment.source_range.source_buffer, directive_range.end_pos, comment.source_range.end_pos
124
+ )
125
+ return directive_range unless trailing.source.lstrip.start_with?(TRAILING_COMMENT_MARKER)
126
+
127
+ directive_range.with(end_pos: comment.source_range.end_pos)
128
+ end
129
+
99
130
  # Returns match captures to directive comment pattern
100
131
  def match_captures
101
132
  @match_captures ||= @match_data && begin
@@ -109,7 +140,12 @@ module RuboCop
109
140
 
110
141
  # Checks if this directive disables cops
111
142
  def disabled?
112
- %w[disable todo].include?(mode)
143
+ %w[disable todo].include?(mode) || disable_next?
144
+ end
145
+
146
+ # Checks if this directive disables cops for the next statement only
147
+ def disable_next?
148
+ %w[disable-next todo-next].include?(mode)
113
149
  end
114
150
 
115
151
  # Checks if this directive enables cops
@@ -76,12 +76,6 @@ module RuboCop
76
76
  @feature.start_with?('.')
77
77
  end
78
78
 
79
- # @param [LoadError] error
80
- # @return [Boolean]
81
- def seems_cannot_load_such_file_error?(error)
82
- error.path == target
83
- end
84
-
85
79
  # @return [String]
86
80
  def target
87
81
  if relative?
@@ -23,7 +23,9 @@ module RuboCop
23
23
 
24
24
  def message(offense)
25
25
  message =
26
- if offense.corrected_with_todo?
26
+ if offense.disabled?
27
+ "[Suppressed] #{offense.message}"
28
+ elsif offense.corrected_with_todo?
27
29
  "[Todo] #{offense.message}"
28
30
  elsif offense.corrected?
29
31
  "[Corrected] #{offense.message}"
@@ -74,11 +74,10 @@ module RuboCop
74
74
  end
75
75
 
76
76
  # Make Kernel#binding public.
77
- # rubocop:disable Lint/UselessMethodDefinition
77
+ # rubocop:disable-next Lint/UselessMethodDefinition
78
78
  def binding
79
79
  super
80
80
  end
81
- # rubocop:enable Lint/UselessMethodDefinition
82
81
 
83
82
  def decorated_message(offense)
84
83
  offense.message.gsub(/`(.+?)`/) { "<code>#{escape(Regexp.last_match(1))}</code>" }
@@ -144,11 +143,10 @@ module RuboCop
144
143
  }.freeze
145
144
 
146
145
  # Make Kernel#binding public.
147
- # rubocop:disable Lint/UselessMethodDefinition
146
+ # rubocop:disable-next Lint/UselessMethodDefinition
148
147
  def binding
149
148
  super
150
149
  end
151
- # rubocop:enable Lint/UselessMethodDefinition
152
150
  end
153
151
  end
154
152
  end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  end
48
48
 
49
49
  def hash_for_offense(offense)
50
- {
50
+ hash = {
51
51
  severity: offense.severity.name,
52
52
  message: offense.message,
53
53
  cop_name: offense.cop_name,
@@ -55,18 +55,23 @@ module RuboCop
55
55
  correctable: offense.correctable?,
56
56
  location: hash_for_location(offense)
57
57
  }
58
+
59
+ # Suppressed offenses appear only under `--display-suppressed`, so
60
+ # these keys are additive for existing consumers.
61
+ if offense.disabled?
62
+ hash[:suppressed] = true
63
+ hash[:justification] = offense.justification
64
+ end
65
+
66
+ hash
58
67
  end
59
68
 
60
- # TODO: Consider better solution for Offense#real_column.
61
- # The minimum value of `start_column: real_column` is 1.
62
- # So, the minimum value of `last_column` should be 1.
63
- # And non-zero value of `last_column` should be used as is.
64
69
  def hash_for_location(offense)
65
70
  {
66
71
  start_line: offense.line,
67
72
  start_column: offense.real_column,
68
73
  last_line: offense.last_line,
69
- last_column: offense.last_column.zero? ? 1 : offense.last_column,
74
+ last_column: offense.real_last_column,
70
75
  length: offense.location.length,
71
76
  # `line` and `column` exist for compatibility.
72
77
  # Use `start_line` and `start_column` instead.
@@ -25,21 +25,21 @@ module RuboCop
25
25
  super
26
26
 
27
27
  @test_case_elements = []
28
+ @config_store = nil
28
29
 
29
30
  reset_count
30
31
  end
31
32
 
33
+ def file_started(_file, options)
34
+ @config_store = options[:config_store]
35
+ end
36
+
32
37
  def file_finished(file, offenses)
33
38
  @inspected_file_count += 1
39
+ @offense_count += offenses.count
34
40
 
35
- # TODO: Returns all cops with the same behavior as
36
- # the original rubocop-junit-formatter.
37
- # https://github.com/mikian/rubocop-junit-formatter/blob/v0.1.4/lib/rubocop/formatter/junit_formatter.rb#L9
38
- #
39
- # In the future, it would be preferable to return only enabled cops.
40
- Cop::Registry.all.each do |cop|
41
+ cops_for(file).each do |cop|
41
42
  target_offenses = offenses_for_cop(offenses, cop)
42
- @offense_count += target_offenses.count
43
43
 
44
44
  next unless relevant_for_output?(options, target_offenses)
45
45
 
@@ -47,7 +47,7 @@ module RuboCop
47
47
  end
48
48
  end
49
49
 
50
- # rubocop:disable Layout/LineLength,Metrics/AbcSize,Metrics/MethodLength
50
+ # rubocop:disable-next Layout/LineLength,Metrics/AbcSize,Metrics/MethodLength
51
51
  def finished(_inspected_files)
52
52
  output.puts %(<?xml version='1.0'?>)
53
53
  output.puts %(<testsuites>)
@@ -70,10 +70,17 @@ module RuboCop
70
70
  output.puts %( </testsuite>)
71
71
  output.puts %(</testsuites>)
72
72
  end
73
- # rubocop:enable Layout/LineLength,Metrics/AbcSize,Metrics/MethodLength
74
73
 
75
74
  private
76
75
 
76
+ def cops_for(file)
77
+ # `file_started` is not called when this formatter is used directly through the API,
78
+ # in which case all cops are returned for compatibility.
79
+ return Cop::Registry.all if @config_store.nil?
80
+
81
+ registry.enabled(@config_store.for_file(file))
82
+ end
83
+
77
84
  def relevant_for_output?(options, target_offenses)
78
85
  !options[:display_only_failed] || target_offenses.any?
79
86
  end
@@ -91,6 +98,16 @@ module RuboCop
91
98
  end
92
99
  end
93
100
 
101
+ # Filtering by badge keeps lazy-loaded cops unloaded; `Registry#enabled` then loads only
102
+ # the enabled cops, the same set the runner mobilizes.
103
+ def registry
104
+ @registry ||= Cop::Registry.global.filter_by_badge(options) do |badge|
105
+ next false if badge.department == :Test || badge.match_name?(options[:except])
106
+
107
+ options[:only].nil? || badge.match_name?(options[:only])
108
+ end
109
+ end
110
+
94
111
  def classname_attribute_value(file)
95
112
  @classname_attribute_value_cache ||= Hash.new do |hash, key|
96
113
  hash[key] = key.delete_suffix('.rb').gsub("#{PathUtil.pwd}/", '').tr('/', '.')
@@ -52,7 +52,7 @@ module RuboCop
52
52
  report_summary(@offense_counts, @offending_files_count)
53
53
  end
54
54
 
55
- # rubocop:disable Metrics/AbcSize
55
+ # rubocop:disable-next Metrics/AbcSize
56
56
  def report_summary(offense_counts, offending_files_count)
57
57
  per_cop_counts = ordered_offense_counts(offense_counts)
58
58
  total_count = total_offense_count(offense_counts)
@@ -68,7 +68,6 @@ module RuboCop
68
68
 
69
69
  output.puts
70
70
  end
71
- # rubocop:enable Metrics/AbcSize
72
71
 
73
72
  def ordered_offense_counts(offense_counts)
74
73
  offense_counts.sort_by { |k, v| [-v, k] }.to_h
@@ -86,7 +86,9 @@ module RuboCop
86
86
 
87
87
  def message(offense)
88
88
  message =
89
- if offense.corrected_with_todo?
89
+ if offense.disabled?
90
+ magenta('[Suppressed] ')
91
+ elsif offense.corrected_with_todo?
90
92
  green('[Todo] ')
91
93
  elsif offense.corrected?
92
94
  green('[Corrected] ')
@@ -104,7 +106,7 @@ module RuboCop
104
106
  include Colorizable
105
107
  include TextUtil
106
108
 
107
- # rubocop:disable Metrics/ParameterLists
109
+ # rubocop:disable-next Metrics/ParameterLists
108
110
  def initialize(
109
111
  file_count, offense_count, correction_count, correctable_count, rainbow,
110
112
  safe_autocorrect: false
@@ -116,7 +118,6 @@ module RuboCop
116
118
  @rainbow = rainbow
117
119
  @safe_autocorrect = safe_autocorrect
118
120
  end
119
- # rubocop:enable Metrics/ParameterLists
120
121
 
121
122
  def summary
122
123
  if @correction_count.positive?
@@ -68,7 +68,9 @@ module RuboCop
68
68
 
69
69
  def message(offense)
70
70
  message =
71
- if offense.corrected_with_todo?
71
+ if offense.disabled?
72
+ '[Suppressed] '
73
+ elsif offense.corrected_with_todo?
72
74
  '[Todo] '
73
75
  elsif offense.corrected?
74
76
  '[Corrected] '
@@ -32,7 +32,7 @@ module RuboCop
32
32
  report_summary(@offense_counts)
33
33
  end
34
34
 
35
- # rubocop:disable Metrics/AbcSize
35
+ # rubocop:disable-next Metrics/AbcSize
36
36
  def report_summary(offense_counts)
37
37
  per_file_counts = ordered_offense_counts(offense_counts)
38
38
  total_count = total_offense_count(offense_counts)
@@ -50,7 +50,6 @@ module RuboCop
50
50
 
51
51
  output.puts
52
52
  end
53
- # rubocop:enable Metrics/AbcSize
54
53
 
55
54
  def ordered_offense_counts(offense_counts)
56
55
  offense_counts.sort_by { |k, v| [-v, k] }.to_h
@@ -59,6 +59,18 @@ module RuboCop
59
59
  end
60
60
  end
61
61
 
62
+ # Names of gems sourced from a local path, i.e. `path:` dependencies and the
63
+ # project's own gem when the `Gemfile` uses `gemspec`. Git sources are not
64
+ # included: their code lives outside the project, like a released gem's.
65
+ # @return [Array<String>]
66
+ def path_sourced_gem_names
67
+ return [] unless parser
68
+
69
+ parser.specs.filter_map do |spec|
70
+ spec.name if path_source?(spec.source)
71
+ end
72
+ end
73
+
62
74
  # Whether this lockfile includes the named gem, directly or indirectly.
63
75
  # @param [String] name
64
76
  # @return [Boolean]
@@ -68,6 +80,12 @@ module RuboCop
68
80
 
69
81
  private
70
82
 
83
+ # `Bundler::Source::Git` is a subclass of `Bundler::Source::Path`, but a git
84
+ # source is a remote one.
85
+ def path_source?(source)
86
+ source.is_a?(::Bundler::Source::Path) && !source.is_a?(::Bundler::Source::Git)
87
+ end
88
+
71
89
  # @return [Bundler::LockfileParser, nil]
72
90
  def parser
73
91
  return @parser if defined?(@parser)
@@ -35,7 +35,7 @@ module RuboCop
35
35
  code_actions
36
36
  end
37
37
 
38
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
38
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
39
39
  def to_lsp_diagnostic(config)
40
40
  highlighted = @offense.highlighted_area
41
41
 
@@ -61,7 +61,6 @@ module RuboCop
61
61
  }
62
62
  )
63
63
  end
64
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
65
64
 
66
65
  private
67
66
 
@@ -82,7 +81,7 @@ module RuboCop
82
81
  LanguageServer::Protocol::Interface::CodeDescription.new(href: doc_url)
83
82
  end
84
83
 
85
- # rubocop:disable Metrics/MethodLength
84
+ # rubocop:disable-next Metrics/MethodLength
86
85
  def autocorrect_action
87
86
  LanguageServer::Protocol::Interface::CodeAction.new(
88
87
  title: "Autocorrect #{@offense.cop_name}",
@@ -101,9 +100,8 @@ module RuboCop
101
100
  is_preferred: true
102
101
  )
103
102
  end
104
- # rubocop:enable Metrics/MethodLength
105
103
 
106
- # rubocop:disable Metrics/MethodLength
104
+ # rubocop:disable-next Metrics/MethodLength
107
105
  def offense_replacements
108
106
  @offense.corrector.as_replacements.map do |range, replacement|
109
107
  LanguageServer::Protocol::Interface::TextEdit.new(
@@ -121,9 +119,8 @@ module RuboCop
121
119
  )
122
120
  end
123
121
  end
124
- # rubocop:enable Metrics/MethodLength
125
122
 
126
- # rubocop:disable Metrics/MethodLength
123
+ # rubocop:disable-next Metrics/MethodLength
127
124
  def disable_line_action
128
125
  LanguageServer::Protocol::Interface::CodeAction.new(
129
126
  title: "Disable #{@offense.cop_name} for this line",
@@ -141,7 +138,6 @@ module RuboCop
141
138
  )
142
139
  )
143
140
  end
144
- # rubocop:enable Metrics/MethodLength
145
141
 
146
142
  def line_disable_comment
147
143
  DisableCommentEdits.new(
@@ -21,6 +21,10 @@ module RuboCop
21
21
  RuboCop::CLI::Command::AutoGenerateConfig::AUTO_GENERATED_FILE
22
22
  ].freeze
23
23
 
24
+ # Commands handled by `workspace/executeCommand`. Advertised to the client
25
+ # via `executeCommandProvider` so it knows which commands it can invoke.
26
+ EXECUTE_COMMANDS = %w[rubocop.formatAutocorrects rubocop.formatAutocorrectsAll].freeze
27
+
24
28
  def self.handle(name, &block)
25
29
  define_method(:"handle_#{name}", &block)
26
30
  end
@@ -50,6 +54,12 @@ module RuboCop
50
54
  result: LanguageServer::Protocol::Interface::InitializeResult.new(
51
55
  capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
52
56
  document_formatting_provider: true,
57
+ code_action_provider: LanguageServer::Protocol::Interface::CodeActionOptions.new(
58
+ code_action_kinds: [LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX]
59
+ ),
60
+ execute_command_provider: LanguageServer::Protocol::Interface::ExecuteCommandOptions.new(
61
+ commands: EXECUTE_COMMANDS
62
+ ),
53
63
  text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
54
64
  change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::INCREMENTAL,
55
65
  open_close: true
@@ -107,18 +117,26 @@ module RuboCop
107
117
  @server.write(id: request[:id], result: format_file(uri))
108
118
  end
109
119
 
120
+ handle 'textDocument/codeAction' do |request|
121
+ @server.write(id: request[:id], result: code_actions_for(request[:params]))
122
+ end
123
+
110
124
  handle 'workspace/didChangeConfiguration' do |_request|
111
125
  Logger.log('Ignoring workspace/didChangeConfiguration')
112
126
  end
113
127
 
114
128
  handle 'workspace/didChangeWatchedFiles' do |request|
115
- changed = request[:params][:changes].any? do |change|
129
+ config_changed = request[:params][:changes].any? do |change|
116
130
  CONFIGURATION_FILE_PATTERNS.any? { |path| change[:uri].end_with?(path) }
117
131
  end
118
132
 
119
- if changed
133
+ if config_changed
120
134
  Logger.log('Configuration file changed; restart required')
121
135
  @server.stop
136
+ else
137
+ # A watched source file changed on disk, so the project index may be
138
+ # stale; drop it so the next diagnostics run rebuilds it.
139
+ @server.reset_project_index
122
140
  end
123
141
  end
124
142
 
@@ -133,25 +151,13 @@ module RuboCop
133
151
  return
134
152
  end
135
153
 
136
- uri = request[:params][:arguments][0][:uri]
137
- formatted = nil
138
-
139
- # The `workspace/executeCommand` is an LSP method triggered by intentional user actions,
140
- # so the user's intention for autocorrection is respected.
141
- LSP.disable { formatted = format_file(uri, command: command) }
142
-
143
- @server.write(
144
- id: request[:id],
145
- method: 'workspace/applyEdit',
146
- params: {
147
- label: label,
148
- edit: {
149
- changes: {
150
- uri => formatted
151
- }
152
- }
153
- }
154
- )
154
+ # Clients such as Eglot can invoke a command without arguments, so respond with
155
+ # an error instead of letting the server crash on a missing URI.
156
+ if (uri = document_uri_from(request))
157
+ write_formatting_edits(request, uri: uri, command: command, label: label)
158
+ else
159
+ write_invalid_arguments_error(request, command)
160
+ end
155
161
  end
156
162
 
157
163
  handle 'textDocument/willSave' do |_request|
@@ -159,7 +165,9 @@ module RuboCop
159
165
  end
160
166
 
161
167
  handle 'textDocument/didSave' do |_request|
162
- # Nothing to do
168
+ # The buffer's contents are now on disk, so the project index may be
169
+ # stale; drop it so the next diagnostics run rebuilds it.
170
+ @server.reset_project_index
163
171
  end
164
172
 
165
173
  handle '$/cancelRequest' do |_request|
@@ -199,6 +207,47 @@ module RuboCop
199
207
  }
200
208
  end
201
209
 
210
+ def document_uri_from(request)
211
+ argument = request.dig(:params, :arguments, 0)
212
+
213
+ argument[:uri] if argument.is_a?(Hash)
214
+ end
215
+
216
+ def write_formatting_edits(request, uri:, command:, label:)
217
+ formatted = nil
218
+
219
+ # The `workspace/executeCommand` is an LSP method triggered by intentional user actions,
220
+ # so the user's intention for autocorrection is respected.
221
+ LSP.disable { formatted = format_file(uri, command: command) }
222
+
223
+ @server.write(
224
+ id: request[:id],
225
+ method: 'workspace/applyEdit',
226
+ params: {
227
+ label: label,
228
+ edit: {
229
+ changes: {
230
+ uri => formatted
231
+ }
232
+ }
233
+ }
234
+ )
235
+ end
236
+
237
+ def write_invalid_arguments_error(request, command)
238
+ message = "Missing document URI in arguments for #{command}"
239
+
240
+ @server.write(
241
+ id: request[:id],
242
+ error: LanguageServer::Protocol::Interface::ResponseError.new(
243
+ code: LanguageServer::Protocol::Constant::ErrorCodes::INVALID_PARAMS,
244
+ message: message
245
+ )
246
+ )
247
+
248
+ Logger.log(message)
249
+ end
250
+
202
251
  def format_file(file_uri, command: nil)
203
252
  unless (text = @text_cache[file_uri])
204
253
  Logger.log("Format request arrived before text synchronized; skipping: `#{file_uri}'")
@@ -219,6 +268,24 @@ module RuboCop
219
268
  }]
220
269
  end
221
270
 
271
+ # Returns the quickfix code actions for a `textDocument/codeAction` request.
272
+ #
273
+ # RuboCop attaches each offense's autocorrect and disable-line actions to
274
+ # the diagnostic it publishes, under the diagnostic's `data` (see
275
+ # `Diagnostic#to_lsp_diagnostic`). The LSP spec preserves that `data`
276
+ # between `textDocument/publishDiagnostics` and `textDocument/codeAction`,
277
+ # so the client hands the diagnostics back in the request's context and we
278
+ # can surface their actions without re-analyzing the file. This makes the
279
+ # actions available to clients that request code actions rather than
280
+ # reading them off the diagnostic (Eglot, Helix, Flycheck, ...).
281
+ def code_actions_for(params)
282
+ diagnostics = params.dig(:context, :diagnostics) || []
283
+ only = params.dig(:context, :only)
284
+
285
+ diagnostics.flat_map { |diagnostic| diagnostic.dig(:data, :code_actions) || [] }
286
+ .select { |action| only.nil? || only.include?(action[:kind]) }
287
+ end
288
+
222
289
  def diagnostic(file_uri, text)
223
290
  @text_cache[file_uri] = text
224
291
 
@@ -43,6 +43,10 @@ module RuboCop
43
43
  @runner.formatted_source
44
44
  end
45
45
 
46
+ def reset_project_index
47
+ @runner.reset_project_index
48
+ end
49
+
46
50
  def offenses(path, text, document_encoding = nil, prism_result: nil)
47
51
  diagnostic_options = {}
48
52
  diagnostic_options[:only] = config_only_options if @lint_mode || @layout_mode
@@ -61,6 +61,10 @@ module RuboCop
61
61
  @runtime.layout_mode = options[:layout_mode]
62
62
  end
63
63
 
64
+ def reset_project_index
65
+ @runtime.reset_project_index
66
+ end
67
+
64
68
  def stop(&block)
65
69
  at_exit(&block) if block
66
70
  exit
@@ -60,8 +60,28 @@ module RuboCop
60
60
  @options[:stdin]
61
61
  end
62
62
 
63
+ # Drop the cached project index so the next run rebuilds it. Called when a
64
+ # file is saved or a watched file changes, i.e. when the on-disk state the
65
+ # index is derived from may have changed.
66
+ def reset_project_index
67
+ @project_index = nil
68
+ @project_index_built = false
69
+ end
70
+
63
71
  private
64
72
 
73
+ # The project index is built from files on disk, not from the in-memory
74
+ # buffer, so it does not change while the user is typing. Build it once and
75
+ # reuse it across runs instead of re-walking and re-indexing the whole
76
+ # project on every request; `reset_project_index` drops the cache when the
77
+ # on-disk state changes.
78
+ def build_project_index(target_files)
79
+ return if @project_index_built
80
+
81
+ super
82
+ @project_index_built = true
83
+ end
84
+
65
85
  def file_finished(_file, offenses)
66
86
  @offenses = offenses
67
87
  end
@@ -156,7 +156,7 @@ module RuboCop
156
156
  summary
157
157
  end
158
158
 
159
- # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
159
+ # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
160
160
  def build_tool(
161
161
  name:, description:,
162
162
  title:, destructive_hint:, idempotent_hint:, read_only_hint:, safety_required:
@@ -194,7 +194,6 @@ module RuboCop
194
194
  ::MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
195
195
  end
196
196
  end
197
- # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
198
197
  end
199
198
  end
200
199
  end