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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1d198a5503ecb56c2ee7fc8fc0a22f1bcf5b733cb2d1d22164fc3322da20390
4
- data.tar.gz: d698c0ca090ee2dd64879448b9a1e5fc9f51cc2b1816385f60e5c90f490aad99
3
+ metadata.gz: 82ffd9b6c03e12d770357a074e5f04f30536c85ce4afb9eff74987f03951d756
4
+ data.tar.gz: f9032e319d69366079bd51472893ef0e780e8405b2d3892ccde21c23deaf3fe0
5
5
  SHA512:
6
- metadata.gz: f31306b39a0d1daf1606fac0c7e812f04f9d0f50dacb37a22bcd31d3d848e1e66a33c8dc317f1d536c7a96cfb90a6736aacdb6aeacbc5670f9657eab390fe534
7
- data.tar.gz: d576dfbeaa9e6eb91c162b8c210054f3b5764a02bf4d8f2bcb8cc01c65441847ed287a040cb28e1e440e74d36ea77bef48c428142f8c34bd5940bc500f9c515f
6
+ metadata.gz: c00f0dac21282c6d8727c74ef585bab1d21f2818e29003b99e05d740ab2a142160d8cbfc9030e0654dbc6d08f83e811333d02bf41d4ea7bca3a033f03783efcc
7
+ data.tar.gz: 88f197ff630a6531ac0f702c46f68ccbe3d78315aa3f4ec57d0155b854d421bd0ef0abed6dcc7835cd90a869da14909ca99c606dd8d463a641c901a223dc6092
data/README.md CHANGED
@@ -51,7 +51,7 @@ To prevent an unwanted RuboCop update you might want to use a conservative versi
51
51
  in your `Gemfile`:
52
52
 
53
53
  ```rb
54
- gem 'rubocop', '~> 1.82', require: false
54
+ gem 'rubocop', '~> 1.92', require: false
55
55
  ```
56
56
 
57
57
  See [our versioning policy](https://docs.rubocop.org/rubocop/versioning.html) for further details.
data/config/default.yml CHANGED
@@ -4144,7 +4144,7 @@ Style/DisableCopsWithinSourceCodeDirective:
4144
4144
  Forbids disabling/enabling cops within source code.
4145
4145
  Enabled: false
4146
4146
  VersionAdded: '0.82'
4147
- VersionChanged: '1.90'
4147
+ VersionChanged: '1.92'
4148
4148
  # Cop or department names. A directive is exempt only when everything it
4149
4149
  # disables is listed here.
4150
4150
  AllowedCops: []
@@ -5231,7 +5231,9 @@ Style/NegativeArrayIndex:
5231
5231
  Also handles range patterns with length calculations. Recognizes preserving methods
5232
5232
  and their combinations, allowing safe replacement when the receiver matches.
5233
5233
  Enabled: pending
5234
+ Safe: false
5234
5235
  VersionAdded: '1.84'
5236
+ VersionChanged: '1.92'
5235
5237
 
5236
5238
  Style/NestedFileDirname:
5237
5239
  Description: 'Checks for nested `File.dirname`.'
@@ -35,16 +35,44 @@ module RuboCop
35
35
  cop_name: offense.cop_name,
36
36
  status: status || offense.status
37
37
  }
38
- # Only offenses suppressed by a directive can carry one, so keep the key out of the
39
- # common case rather than writing a null for every cached offense.
40
- hash[:justification] = offense.justification if offense.justification
38
+ add_justification(hash, offense)
39
+ add_corrections(hash, offense)
41
40
  hash
42
41
  end
43
42
 
44
43
  def message(offense)
45
- # JSON.dump will fail if the offense message contains text which is not
46
- # valid UTF-8
47
- offense.message.dup.force_encoding(::Encoding::UTF_8).scrub
44
+ encode_to_utf8(offense.message)
45
+ end
46
+
47
+ def encode_to_utf8(value)
48
+ string = value.to_s
49
+
50
+ if string.encoding == ::Encoding::BINARY
51
+ string.dup.force_encoding(::Encoding::UTF_8).scrub
52
+ else
53
+ string.encode(::Encoding::UTF_8, invalid: :replace, undef: :replace)
54
+ end
55
+ end
56
+
57
+ # Only offenses suppressed by a directive can carry one, so keep the key out of the
58
+ # common case rather than writing a null for every cached offense.
59
+ def add_justification(hash, offense)
60
+ return unless offense.justification
61
+
62
+ hash[:justification] = encode_to_utf8(offense.justification)
63
+ end
64
+
65
+ # A corrector cannot be serialized, so the edits it would make are cached
66
+ # separately; without this a cache hit would report no corrections.
67
+ def add_corrections(hash, offense)
68
+ return if offense.corrections.empty?
69
+
70
+ hash[:corrections] = offense.corrections.map do |correction|
71
+ { begin_pos: correction.begin_pos,
72
+ end_pos: correction.end_pos,
73
+ replacement: encode_to_utf8(correction.replacement) }
74
+ end
75
+ hash[:correction_safe] = offense.correction_safe
48
76
  end
49
77
 
50
78
  # Restore an offense object loaded from a JSON file.
@@ -52,7 +80,19 @@ module RuboCop
52
80
  offenses.map! do |o|
53
81
  location = location_from_source_buffer(o)
54
82
  Cop::Offense.new(o['severity'], location, o['message'], o['cop_name'], o['status'].to_sym,
55
- justification: o['justification'])
83
+ justification: o['justification'],
84
+ corrections: corrections_from_cache(o),
85
+ correction_safe: o.fetch('correction_safe', true))
86
+ end
87
+ end
88
+
89
+ def corrections_from_cache(offense)
90
+ cached = offense['corrections']
91
+ return [] unless cached
92
+
93
+ cached.map do |correction|
94
+ Cop::Offense::Correction.new(correction['begin_pos'], correction['end_pos'],
95
+ correction['replacement']).freeze
56
96
  end
57
97
  end
58
98
 
@@ -13,6 +13,15 @@ module RuboCop
13
13
  DEFAULT_REVISION = 'HEAD'
14
14
  FILESYSTEM_ENCODING = Encoding.find('filesystem')
15
15
 
16
+ # Whether git can resolve `name` to a commit in the current repository.
17
+ def self.revision?(name)
18
+ _stdout, _stderr, status =
19
+ Open3.capture3('git', 'rev-parse', '--verify', '--quiet', "#{name}^{commit}")
20
+ status.success?
21
+ rescue Errno::ENOENT
22
+ false
23
+ end
24
+
16
25
  def initialize(revision = nil)
17
26
  @revision = revision || DEFAULT_REVISION
18
27
  end
@@ -29,38 +38,51 @@ module RuboCop
29
38
  (modified_paths + untracked_paths).to_set { |path| File.expand_path(path, root) }
30
39
  end
31
40
 
41
+ # @return [Array<String>] the files among these that changed
42
+ def filter(files)
43
+ changed = paths
44
+ files.select { |file| changed.include?(file) }
45
+ end
46
+
32
47
  private
33
48
 
34
49
  def repository_root
35
50
  filesystem_path(git('rev-parse', '--show-toplevel').b.chomp)
36
51
  end
37
52
 
38
- def modified_paths
53
+ def modified_paths(revision = @revision)
39
54
  # `diff.relative` would make git report paths relative to the current
40
55
  # directory rather than to the repository root.
41
56
  split_paths(
42
- git('-c', 'diff.relative=false', 'diff', '--name-only', '--diff-filter=d', '-z', @revision)
57
+ git('-c', 'diff.relative=false', 'diff', '--name-only', '--diff-filter=d', '-z', revision)
43
58
  )
44
59
  rescue Error
45
- raise unless unborn_head?
60
+ raise unless revision == DEFAULT_REVISION && unborn_head?
46
61
 
47
- # A repository without commits has nothing to diff against, and
48
- # everything in it is untracked - which is what the caller wants to hear
49
- # rather than an error about `HEAD` not resolving.
50
- []
62
+ # A repository without commits has no `HEAD` to diff against, but files
63
+ # can already be staged in it. Against the empty tree every one of them
64
+ # counts as changed, which is what the caller wants to hear rather than
65
+ # an error about `HEAD` not resolving.
66
+ modified_paths(empty_tree)
51
67
  end
52
68
 
53
69
  def unborn_head?
54
- return false unless @revision == DEFAULT_REVISION
70
+ !self.class.revision?(DEFAULT_REVISION)
71
+ end
55
72
 
56
- _stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--verify', '--quiet', 'HEAD')
57
- !status.success?
73
+ # Hashed rather than hardcoded, since its id depends on the repository's
74
+ # object format (SHA-1 or SHA-256).
75
+ def empty_tree
76
+ git('hash-object', '-t', 'tree', '--stdin').chomp
58
77
  end
59
78
 
60
79
  def untracked_paths
61
- # `--full-name` anchors the paths to the repository root, the way the
62
- # paths from `git diff` already are.
63
- split_paths(git('ls-files', '--others', '--exclude-standard', '--full-name', '-z'))
80
+ # Unlike `git diff`, `ls-files` only looks under the current directory, so
81
+ # the `:/` pathspec widens it to the whole repository, and `--full-name`
82
+ # anchors the paths to the repository root the way `git diff` does.
83
+ split_paths(
84
+ git('ls-files', '--others', '--exclude-standard', '--full-name', '-z', '--', ':/')
85
+ )
64
86
  end
65
87
 
66
88
  # The `-z` above is what makes these usable: without it git wraps any path
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ class CLI
5
+ module Command
6
+ # Shared handling for commands that take a list of cop names.
7
+ #
8
+ # Validation happens here rather than during option parsing because the
9
+ # registry is not complete until the configuration has been loaded, which
10
+ # is also why `--only` and `--except` are validated from the runner.
11
+ # @api private
12
+ module CopNames
13
+ private
14
+
15
+ # The cops among `names` that exist, so a command can report on what it
16
+ # understood before {#validate_cop_names!} objects to the rest.
17
+ def known_cop_classes(names)
18
+ names.filter_map { |name| cop_class_for(name) }
19
+ end
20
+
21
+ # @raise [IncorrectCopNameError] if any name is not a cop
22
+ def validate_cop_names!(names)
23
+ unknown = names.reject { |name| cop_class_for(name) }
24
+ return if unknown.empty?
25
+
26
+ raise IncorrectCopNameError, unknown.map { |name| unknown_cop_message(name) }.join("\n")
27
+ end
28
+
29
+ # Not memoized: the MCP server lives long enough for a plugin to register
30
+ # a cop after a lookup has missed it.
31
+ def cop_class_for(name)
32
+ Cop::Registry.global.find_by_cop_name(name)
33
+ end
34
+
35
+ def unknown_cop_message(name)
36
+ return department_message(name) if department?(name)
37
+
38
+ message = "Unrecognized cop: #{name}."
39
+ similar = NameSimilarity.find_similar_names(name, Cop::Registry.global.names)
40
+ return message if similar.empty?
41
+
42
+ "#{message}\nDid you mean? #{similar.join(', ')}"
43
+ end
44
+
45
+ def department?(name)
46
+ Cop::Registry.global.departments.any? { |department| department.to_s == name }
47
+ end
48
+
49
+ def department_message(name)
50
+ example = Cop::Registry.global.names.find { |cop| cop.start_with?("#{name}/") }
51
+
52
+ "#{name} is a department, not a cop. Name one of its cops, such as #{example}."
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -9,7 +9,7 @@ module RuboCop
9
9
  include Formatter::TextUtil
10
10
 
11
11
  # Combination of short and long formatter names.
12
- INTEGRATION_FORMATTERS = %w[h html j json ju junit].freeze
12
+ INTEGRATION_FORMATTERS = %w[h html j json ju junit sa sarif].freeze
13
13
 
14
14
  self.command_name = :execute_runner
15
15
 
@@ -141,7 +141,7 @@ module RuboCop
141
141
 
142
142
  def maybe_print_corrected_source
143
143
  # Integration tools (like RubyMine) expect to have only the JSON result
144
- # when specifying JSON format. Similar HTML and JUnit are targeted as well.
144
+ # when specifying JSON format. Similar HTML, JUnit and SARIF are targeted as well.
145
145
  # See: https://github.com/rubocop/rubocop/issues/8673
146
146
  return if INTEGRATION_FORMATTERS.include?(@options[:format])
147
147
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ class CLI
5
+ module Command
6
+ # Explains what cops do, as described by {Cop::CopExplanation}.
7
+ # @api private
8
+ class Explain < Base
9
+ include CopNames
10
+
11
+ self.command_name = :explain
12
+
13
+ def initialize(env)
14
+ super
15
+
16
+ @config = @config_store.for(PathUtil.pwd)
17
+ end
18
+
19
+ def run
20
+ names = @options[:explain]
21
+
22
+ known_cop_classes(names).each_with_index do |cop_class, index|
23
+ puts if index.positive?
24
+ puts Cop::CopExplanation.new(cop_class, @config)
25
+ end
26
+
27
+ # Checked after printing, so a typo in a list does not cost you the
28
+ # explanations you asked for alongside it.
29
+ validate_cop_names!(names)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -7,6 +7,8 @@ module RuboCop
7
7
  # for the current directory.
8
8
  # @api private
9
9
  class ShowCops < Base
10
+ include CopNames
11
+
10
12
  self.command_name = :show_cops
11
13
 
12
14
  ExactMatcher = Struct.new(:pattern) do
@@ -38,6 +40,10 @@ module RuboCop
38
40
 
39
41
  def run
40
42
  print_available_cops
43
+ # Checked after printing, so a typo alongside good names still shows
44
+ # what was asked for. Wildcards are patterns, not names, so a pattern
45
+ # that matches nothing is not an error.
46
+ validate_cop_names!(@options[:show_cops].grep_v(/\*/))
41
47
  end
42
48
 
43
49
  private
@@ -7,6 +7,8 @@ module RuboCop
7
7
  # or documentation base url by default.
8
8
  # @api private
9
9
  class ShowDocsUrl < Base
10
+ include CopNames
11
+
10
12
  self.command_name = :show_docs_url
11
13
 
12
14
  def initialize(env)
@@ -17,6 +19,9 @@ module RuboCop
17
19
 
18
20
  def run
19
21
  print_documentation_url
22
+ # Checked after printing, so a typo alongside good names still gives
23
+ # you the urls you asked for.
24
+ validate_cop_names!(cops_array)
20
25
  end
21
26
 
22
27
  private
@@ -24,10 +29,7 @@ module RuboCop
24
29
  def print_documentation_url
25
30
  puts Cop::Documentation.default_base_url if cops_array.empty?
26
31
 
27
- cops_array.each do |cop_name|
28
- cop = Cop::Registry.global.find_by_cop_name(cop_name)
29
- next unless cop
30
-
32
+ known_cop_classes(cops_array).each do |cop|
31
33
  url = Cop::Documentation.url_for(cop, @config)
32
34
  puts url if url
33
35
  end
data/lib/rubocop/cli.rb CHANGED
@@ -13,7 +13,8 @@ module RuboCop
13
13
  DEFAULT_PARALLEL_OPTIONS = %i[
14
14
  color config debug display_style_guide display_time display_only_fail_level_offenses
15
15
  display_only_failed editor_mode except extra_details fail_level fix_layout format formatters
16
- ignore_disable_comments lint only only_guide_cops out preview require safe
16
+ ignore_disable_comments lint max_offenses_per_cop only only_guide_cops out preview require
17
+ safe
17
18
  autocorrect safe_autocorrect autocorrect_all
18
19
  ].freeze
19
20
 
@@ -210,7 +211,7 @@ module RuboCop
210
211
  return unless Options::EXITING_OPTIONS.any? { |o| @options.key? o }
211
212
 
212
213
  run_command(:version) if @options[:version] || @options[:verbose_version]
213
- %i[show_cops list_enabled_cops_for show_docs_url lsp mcp].each do |name|
214
+ %i[show_cops list_enabled_cops_for show_docs_url explain lsp mcp].each do |name|
214
215
  run_command(name) if @options[name]
215
216
  end
216
217
  raise Finished
@@ -78,20 +78,17 @@ module RuboCop
78
78
 
79
79
  # The names of the cops that are opted in by an `enable` comment directive
80
80
  # or a `+` argument of a `push`/`next` directive, used to mobilize cops
81
- # disabled in the config on demand.
81
+ # disabled in the config on demand. Departments are expanded for the
82
+ # opt-in forms only: a plain `enable` naming a department usually just
83
+ # closes a `disable` of it, and expanding it would switch on every
84
+ # config-disabled cop of the department for the rest of the file.
82
85
  #
83
86
  # @api private
84
87
  # @return [Set<String>]
85
88
  def opt_in_cops
86
89
  @opt_in_cops ||= begin
87
90
  cops = Set.new
88
- each_directive do |directive|
89
- if directive.enabled?
90
- cops.merge(directive.raw_cop_names) unless directive.all_cops?
91
- elsif directive.push? || directive.next?
92
- cops.merge(directive.signed_args.fetch('+', []))
93
- end
94
- end
91
+ each_directive { |directive| cops.merge(opted_in_by(directive)) }
95
92
  cops
96
93
  end
97
94
  end
@@ -113,6 +110,18 @@ module RuboCop
113
110
  extras
114
111
  end
115
112
 
113
+ def opted_in_by(directive)
114
+ if directive.push? || directive.next?
115
+ directive.signed_args.fetch('+', []).flat_map { |name| expand_cop_name(name) }
116
+ elsif !directive.enabled? || directive.all_cops?
117
+ []
118
+ elsif directive.enable_next?
119
+ directive.cop_names
120
+ else
121
+ directive.raw_cop_names
122
+ end
123
+ end
124
+
116
125
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
117
126
  def analyze
118
127
  return {} if @no_directives
@@ -173,7 +173,8 @@ module RuboCop
173
173
  hash = ConfigLoader.load_yaml_configuration(path)
174
174
  config = Config.new(hash, path).tap(&:make_excludes_absolute)
175
175
 
176
- @default_configuration = ConfigLoader.merge_with_default(config, path)
176
+ @default_configuration =
177
+ ConfigLoader.merge_with_default(config, path, resolve_preview: false)
177
178
  end
178
179
 
179
180
  # Returns the path RuboCop inferred as the root of the project. No file
@@ -189,8 +190,10 @@ module RuboCop
189
190
  end
190
191
 
191
192
  # Merges the given configuration with the default one.
192
- def merge_with_default(config, config_file, unset_nil: true)
193
- resolver.merge_with_default(config, config_file, unset_nil: unset_nil)
193
+ def merge_with_default(config, config_file, unset_nil: true, resolve_preview: true)
194
+ resolver.merge_with_default(
195
+ config, config_file, unset_nil: unset_nil, resolve_preview: resolve_preview
196
+ )
194
197
  end
195
198
 
196
199
  # Applies CLI overrides for `AllCops/EnabledByDefault` and
@@ -37,8 +37,9 @@ module RuboCop
37
37
 
38
38
  def resolve_inheritance(path, hash, file, debug) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
39
39
  inherited_files = Array(hash['inherit_from'])
40
- base_configs(path, inherited_files, file)
41
- .each_with_index.reverse_each do |base_config, index|
40
+ base_configs = base_configs(path, inherited_files, file)
41
+ preview = inherited_preview?(hash, base_configs)
42
+ base_configs.each_with_index.reverse_each do |base_config, index|
42
43
  override_department_setting_for_cops(base_config, hash)
43
44
  override_enabled_for_disabled_departments(base_config, hash)
44
45
 
@@ -50,7 +51,7 @@ module RuboCop
50
51
  v = merge(v, hash[k],
51
52
  cop_name: k, file: file, debug: debug,
52
53
  inherited_file: inherited_files[index],
53
- inherit_mode: determine_inherit_mode(hash, k))
54
+ inherit_mode: determine_inherit_mode(hash, k, preview))
54
55
  end
55
56
  hash[k] = v
56
57
  fix_include_paths(base_config.loaded_path, hash, path, k, v) if only_base_has_include
@@ -94,8 +95,13 @@ module RuboCop
94
95
  # only cops explicitly disabled in user configuration are disabled.
95
96
  # When the `--disable-all-cops` or `--enable-all-cops` CLI option is given,
96
97
  # it takes precedence over the configuration values.
97
- def merge_with_default(config, config_file, unset_nil:)
98
- base_defaults = apply_preview_defaults(ConfigLoader.default_configuration, preview?(config))
98
+ #
99
+ # With `resolve_preview: false` the `Preview` sections of the default
100
+ # configuration are left in place and preview is treated as off. That is
101
+ # what extending the default configuration itself needs: preview gets
102
+ # applied later, when a project configuration is resolved against it.
103
+ def merge_with_default(config, config_file, unset_nil:, resolve_preview: true)
104
+ base_defaults, preview = defaults_with_preview(config, resolve_preview)
99
105
  default_configuration = base_defaults
100
106
  disabled_by_default, enabled_by_default = resolve_default_overrides(config)
101
107
 
@@ -110,7 +116,7 @@ module RuboCop
110
116
  end
111
117
  override_enabled_for_disabled_departments(default_configuration, config)
112
118
 
113
- opts = { inherit_mode: inherit_mode_for_default(config), unset_nil: unset_nil }
119
+ opts = { inherit_mode: inherit_mode_for_default(config, preview), unset_nil: unset_nil }
114
120
  Config.new(merge(default_configuration, config, **opts), config_file)
115
121
  end
116
122
 
@@ -185,16 +191,24 @@ module RuboCop
185
191
 
186
192
  private
187
193
 
188
- def inherit_mode_for_default(config)
189
- with_preview_exclude_merge(config['inherit_mode'] || {}, config)
194
+ def defaults_with_preview(config, resolve_preview)
195
+ defaults = ConfigLoader.default_configuration
196
+ return [defaults, false] unless resolve_preview
197
+
198
+ preview = preview?(config)
199
+ [apply_preview_defaults(defaults, preview), preview]
200
+ end
201
+
202
+ def inherit_mode_for_default(config, preview)
203
+ with_preview_exclude_merge(config['inherit_mode'] || {}, preview)
190
204
  end
191
205
 
192
206
  # Under `Preview`, `Exclude` is merged rather than replaced, so that excluding
193
207
  # one directory does not silently drop the excludes it would have inherited -
194
208
  # from the default configuration or from a file named in `inherit_from`. An
195
209
  # explicit `inherit_mode` still wins, in either direction.
196
- def with_preview_exclude_merge(mode, config)
197
- return mode unless preview?(config)
210
+ def with_preview_exclude_merge(mode, preview)
211
+ return mode unless preview
198
212
  return mode if Array(mode['override']).include?('Exclude')
199
213
  return mode if Array(mode['merge']).include?('Exclude')
200
214
 
@@ -249,10 +263,21 @@ module RuboCop
249
263
  "the same parameter in #{opts[:inherited_file]}"
250
264
  end
251
265
 
252
- def determine_inherit_mode(hash, key)
266
+ # The `AllCops: Preview` in effect once inheritance is resolved: the file
267
+ # itself wins, then the files it inherits from, last one first. It has to be
268
+ # known before merging, since the keys are merged in whatever order they
269
+ # appear and `AllCops` may well come after the cops whose `Exclude` it affects.
270
+ def inherited_preview?(hash, base_configs)
271
+ all_cops = [hash, *base_configs.reverse].map { |config| config['AllCops'] }.find do |params|
272
+ params.is_a?(Hash) && params.key?('Preview')
273
+ end
274
+ preview?('AllCops' => all_cops)
275
+ end
276
+
277
+ def determine_inherit_mode(hash, key, preview)
253
278
  cop_cfg = hash[key]
254
279
  local_inherit = cop_cfg['inherit_mode'] if cop_cfg.is_a?(Hash)
255
- with_preview_exclude_merge(local_inherit || hash['inherit_mode'] || {}, hash)
280
+ with_preview_exclude_merge(local_inherit || hash['inherit_mode'] || {}, preview)
256
281
  end
257
282
 
258
283
  def should_union?(derived_hash, base_hash, root_mode, key)
@@ -20,7 +20,7 @@ module RuboCop
20
20
 
21
21
  if reason
22
22
  "#{base} since #{reason.chomp}."
23
- elsif alternatives
23
+ elsif alternatives.any?
24
24
  "#{base}. Please use #{to_sentence(alternatives, connector: 'and/or')} instead."
25
25
  else
26
26
  "#{base}."
@@ -173,6 +173,7 @@ module RuboCop
173
173
  def validate_all_cops_parameters
174
174
  validate_new_cops_parameter
175
175
  validate_fail_level_parameter
176
+ validate_preview_parameter
176
177
  end
177
178
 
178
179
  def validate_fail_level_parameter
@@ -185,6 +186,16 @@ module RuboCop
185
186
  raise ValidationError, message
186
187
  end
187
188
 
189
+ def validate_preview_parameter
190
+ preview = @config.for_all_cops['Preview']
191
+ return if [nil, true, false].include?(preview)
192
+
193
+ message = "invalid #{preview} for `Preview` found in #{smart_loaded_path}\n" \
194
+ 'Valid choices are: true, false'
195
+
196
+ raise ValidationError, message
197
+ end
198
+
188
199
  def validate_all_cops_new_cops_parameter
189
200
  new_cop_parameter = @config.for_all_cops['NewCops']
190
201
  return if new_cop_parameter.nil? || NEW_COPS_VALUES.include?(new_cop_parameter)
@@ -40,12 +40,9 @@ module RuboCop
40
40
  # allow turning off autocorrect on a cop by cop basis
41
41
  return true unless cop_config
42
42
 
43
- # `false` is the same as `disabled` for backward compatibility.
44
- return false if ['disabled', false].include?(cop_config['AutoCorrect'])
43
+ return false if disabled_autocorrect?
45
44
 
46
- # When LSP is enabled or the `--editor-mode` option is on, it is considered as editing
47
- # source code, and autocorrection with `AutoCorrect: contextual` will not be performed.
48
- return false if contextual_autocorrect? && LSP.enabled?
45
+ return false if contextual_autocorrect_held_back?
49
46
 
50
47
  # :safe_autocorrect is a derived option based on several command-line
51
48
  # arguments - see RuboCop::Options#add_autocorrection_options
@@ -56,6 +53,14 @@ module RuboCop
56
53
 
57
54
  private
58
55
 
56
+ # When LSP is enabled or the `--editor-mode` option is on, it is considered as editing
57
+ # source code, and autocorrection with `AutoCorrect: contextual` will not be performed.
58
+ # The `editing` option says the same without the rest of LSP mode, which
59
+ # is about showing offenses in an editor.
60
+ def contextual_autocorrect_held_back?
61
+ contextual_autocorrect? && (LSP.enabled? || @options.fetch(:editing, false))
62
+ end
63
+
59
64
  def disable_offense(offense_range)
60
65
  unbreakable_range = multiline_ranges(offense_range)&.find do |range|
61
66
  offense_range.overlaps?(range) &&