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
@@ -216,14 +216,8 @@ module RuboCop
216
216
  message = find_message(range_to_pass, message)
217
217
 
218
218
  status, corrector = enabled_lines?(range) ? correct(range, &block) : :disabled
219
- justification = suppression_reason(range) if status == :disabled
220
219
 
221
- # Since this range may be generated from Ruby code embedded in some
222
- # template file, we convert it to location info in the original file.
223
- range = range_for_original(range)
224
-
225
- current_offenses << Offense.new(severity, range, message, name, status, corrector,
226
- justification: justification)
220
+ current_offenses << new_offense(range, severity, message, status, corrector)
227
221
  end
228
222
 
229
223
  # This method should be overridden when a cop's behavior depends
@@ -271,10 +265,21 @@ module RuboCop
271
265
  @config.target_ruby_version
272
266
  end
273
267
 
274
- # Returns a gems locked versions (i.e. from Gemfile.lock or gems.locked)
275
- # @returns [Gem::Version | nil] The locked gem version, or nil if the gem is not present.
268
+ # Returns the version of a gem in the target, by default its locked version
269
+ # (i.e. from Gemfile.lock or gems.locked).
270
+ #
271
+ # The requirements declared with `requires_gem` are checked against the version
272
+ # this method returns, so an extension that lets users state the version of its
273
+ # gem in `AllCops` (e.g. `TargetFrameworkVersion`) can override it for that gem,
274
+ # returning a `Gem::Version`, and fall back to `super` for any other gem.
275
+ #
276
+ # @param [String] gem_name
277
+ # @return [Gem::Version, nil] The gem version, or nil if the gem is not present.
278
+ #
279
+ # @api public
276
280
  def target_gem_version(gem_name)
277
- @config.gem_versions_in_target && @config.gem_versions_in_target[gem_name]
281
+ gem_versions = @config.gem_versions_in_target
282
+ gem_versions && gem_versions[gem_name]
278
283
  end
279
284
 
280
285
  def parser_engine
@@ -382,6 +387,12 @@ module RuboCop
382
387
  cop_config.fetch('AutoCorrect', 'always') == 'contextual'
383
388
  end
384
389
 
390
+ # @api private
391
+ def disabled_autocorrect?
392
+ # `false` is the same as `'disabled'` for backward compatibility.
393
+ ['disabled', false].include?(cop_config['AutoCorrect'])
394
+ end
395
+
385
396
  def inspect # :nodoc:
386
397
  "#<#{self.class.name}:#{object_id} @config=#{@config} @options=#{@options}>"
387
398
  end
@@ -394,6 +405,22 @@ module RuboCop
394
405
  @restrict_on_send ||= self::RESTRICT_ON_SEND.to_a.freeze
395
406
  end
396
407
 
408
+ # @return [Offense]
409
+ def new_offense(range, severity, message, status, corrector)
410
+ justification = suppression_reason(range) if status == :disabled
411
+
412
+ # Since this range may be generated from Ruby code embedded in some
413
+ # template file, we convert it to location info in the original file.
414
+ # A cop configured with `AutoCorrect: false` reports no edits, since
415
+ # nothing that honors the configuration applies them. The corrector
416
+ # stays, so an editor can still offer it as a quick fix to choose.
417
+ corrections = [] if corrector && disabled_autocorrect?
418
+
419
+ Offense.new(severity, range_for_original(range), message, name, status, corrector,
420
+ justification: justification, correction_safe: safe_autocorrect?,
421
+ corrections: corrections)
422
+ end
423
+
397
424
  ### Reserved for Cop::Cop
398
425
 
399
426
  def callback_argument(range)
@@ -465,7 +492,8 @@ module RuboCop
465
492
  # The unsafe correction will not run, so the offense is
466
493
  # uncorrectable for this run and gets a todo comment.
467
494
  attempt_correction(range, nil)
468
- elsif corrector && (always_autocorrect? || (contextual_autocorrect? && !LSP.enabled?))
495
+ elsif corrector && (always_autocorrect? ||
496
+ (contextual_autocorrect? && !contextual_autocorrect_held_back?))
469
497
  :uncorrected
470
498
  else
471
499
  :unsupported
@@ -601,17 +629,10 @@ module RuboCop
601
629
  end
602
630
 
603
631
  def target_satisfies_all_gem_version_requirements?
604
- gem_requirements = self.class.gem_requirements
605
- return true if gem_requirements.empty?
606
-
607
- gem_requirements.all? do |gem_name, version_req|
608
- all_gem_versions_in_target = @config.gem_versions_in_target
609
- next false unless all_gem_versions_in_target
610
-
611
- gem_version_in_target = all_gem_versions_in_target[gem_name]
612
- next false unless gem_version_in_target
632
+ self.class.gem_requirements.all? do |gem_name, version_requirement|
633
+ gem_version = target_gem_version(gem_name)
613
634
 
614
- version_req.satisfied_by?(gem_version_in_target)
635
+ gem_version && version_requirement.satisfied_by?(gem_version)
615
636
  end
616
637
  end
617
638
  end
@@ -81,7 +81,6 @@ module RuboCop
81
81
  # gem 'bar', '< 2.1'
82
82
  #
83
83
  class GemComment < Base
84
- include DefNode
85
84
  include GemDeclaration
86
85
 
87
86
  MSG = 'Missing gem description comment.'
@@ -51,14 +51,6 @@ module RuboCop
51
51
  end
52
52
  end
53
53
 
54
- private
55
-
56
- def previous_declaration(node)
57
- declarations = gem_declarations(processed_source.ast)
58
- node_index = declarations.map(&:location).find_index(node.location)
59
- declarations.to_a[node_index - 1]
60
- end
61
-
62
54
  # @!method gem_declarations(node)
63
55
  def_node_search :gem_declarations, <<~PATTERN
64
56
  (:send nil? :gem str ...)
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Reads the documentation comment a cop carries above its class definition
6
+ # and splits it into the sections worth showing on their own.
7
+ #
8
+ # YARD is a development dependency, so the comment is read straight from the
9
+ # source rather than through a doclet.
10
+ # @api private
11
+ class CopDocumentation
12
+ TAG = /^@(\w+)\s*(.*)$/.freeze
13
+
14
+ def initialize(cop_class)
15
+ @cop_class = cop_class
16
+ end
17
+
18
+ # @return [Array<Array(String, Array<String>)>] title and body per section
19
+ def sections
20
+ comment = comment_lines
21
+ return [] if comment.empty?
22
+
23
+ prose, tagged = split_on_first_tag(comment)
24
+
25
+ [['Details', strip_blank(prose)], *tagged_sections(tagged)].reject { |_, b| b.empty? }
26
+ end
27
+
28
+ private
29
+
30
+ def comment_lines
31
+ path, line = source_location
32
+ return [] unless path && File.file?(path)
33
+
34
+ lines = File.readlines(path, chomp: true)
35
+ collect_comment_above(lines, line - 2)
36
+ end
37
+
38
+ def source_location
39
+ name = @cop_class.name
40
+ return [] unless name
41
+
42
+ Module.const_source_location(name) || []
43
+ rescue NameError
44
+ []
45
+ end
46
+
47
+ # Walks up from the class definition for as long as the lines are comments.
48
+ def collect_comment_above(lines, index)
49
+ block = []
50
+ while index >= 0 && lines[index].match?(/^\s*#/)
51
+ block.unshift(lines[index].sub(/^\s*# ?/, ''))
52
+ index -= 1
53
+ end
54
+ block
55
+ end
56
+
57
+ def split_on_first_tag(comment)
58
+ first_tag = comment.index { |line| line.match?(TAG) }
59
+ return [comment, []] unless first_tag
60
+
61
+ [comment[0...first_tag], comment[first_tag..]]
62
+ end
63
+
64
+ def tagged_sections(lines)
65
+ lines.slice_before { |line| line.match?(TAG) }.map do |(head, *body)|
66
+ tag, argument = head.match(TAG).captures
67
+ [section_title(tag, argument), strip_blank(body)]
68
+ end
69
+ end
70
+
71
+ def section_title(tag, argument)
72
+ title = tag.capitalize
73
+ argument.empty? ? title : "#{title}: #{argument}"
74
+ end
75
+
76
+ def strip_blank(lines)
77
+ lines.drop_while(&:empty?).reverse.drop_while(&:empty?).reverse
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Explains what a single cop does: its description, how it is configured,
6
+ # whether it corrects, and the examples from its own documentation.
7
+ #
8
+ # Rendered as plain text, which is what `--explain` prints and what the MCP
9
+ # server hands to an agent.
10
+ # @api private
11
+ class CopExplanation
12
+ # Keys that describe the cop to RuboCop itself rather than something a
13
+ # user would set, so they are reported in their own section or not at all.
14
+ METADATA_KEYS = %w[
15
+ Description StyleGuide Reference References Enabled Safe SafeAutoCorrect
16
+ AutoCorrect VersionAdded VersionChanged Include Exclude Details Preview
17
+ ].freeze
18
+
19
+ def initialize(cop_class, config)
20
+ @cop_class = cop_class
21
+ @config = config
22
+ @cop_config = config.for_cop(cop_class.cop_name)
23
+ end
24
+
25
+ def to_s
26
+ lines.join("\n")
27
+ end
28
+
29
+ private
30
+
31
+ def lines
32
+ description = description_lines
33
+
34
+ [
35
+ @cop_class.cop_name,
36
+ *section(nil, description),
37
+ *section('Properties', property_lines),
38
+ *documentation_sections(description).flat_map { |title, body| section(title, body) },
39
+ *section('Configuration', configuration_lines),
40
+ *section('References', reference_lines)
41
+ ]
42
+ end
43
+
44
+ def section(title, body)
45
+ return [] if body.empty?
46
+
47
+ heading = title ? [title, ''] : []
48
+ ['', *heading, *body.map { |line| line.empty? ? '' : " #{line}" }]
49
+ end
50
+
51
+ def description_lines
52
+ Array(@cop_config['Description']).flat_map { |line| line.split("\n") }
53
+ end
54
+
55
+ def property_lines
56
+ [
57
+ "Enabled: #{@cop_config.fetch('Enabled', true)}",
58
+ "Safe: #{@cop_config.fetch('Safe', true)}",
59
+ "Autocorrect: #{autocorrect_description}",
60
+ *version_lines,
61
+ *scope_lines
62
+ ]
63
+ end
64
+
65
+ # Which files the cop looks at. A cop that only runs on Gemfiles
66
+ # explains an absent offense better than anything else here does.
67
+ def scope_lines
68
+ [
69
+ *patterns_line('Applies to: ', @cop_config['Include']),
70
+ *patterns_line('Excludes: ', @cop_config['Exclude'])
71
+ ]
72
+ end
73
+
74
+ def patterns_line(label, patterns)
75
+ patterns = Array(patterns).compact
76
+ return [] if patterns.empty?
77
+
78
+ ["#{label}#{patterns.map { |pattern| readable_pattern(pattern) }.join(', ')}"]
79
+ end
80
+
81
+ # `Exclude` patterns are absolutized against the config that set them,
82
+ # which makes for a wall of identical prefixes. A user config can also
83
+ # hold a regexp rather than a glob.
84
+ def readable_pattern(pattern)
85
+ pattern.is_a?(String) ? PathUtil.smart_path(pattern) : pattern.inspect
86
+ end
87
+
88
+ def autocorrect_description
89
+ return 'not supported' unless @cop_class.support_autocorrect?
90
+
91
+ # Asks the cop, so `AutoCorrect` is read the way the cop reads it.
92
+ cop = @cop_class.new(@config)
93
+ return 'disabled' unless cop.always_autocorrect? || cop.contextual_autocorrect?
94
+
95
+ description = cop.safe_autocorrect? ? 'safe, applied by -a' : 'unsafe, applied by -A only'
96
+ # Contextual corrections wait until the code is finished, which an
97
+ # editor or an agent mid-edit cannot promise, so they have to ask.
98
+ description += ', but through LSP or MCP only on request' if cop.contextual_autocorrect?
99
+ description
100
+ end
101
+
102
+ def version_lines
103
+ lines = []
104
+ lines << "Added: #{@cop_config['VersionAdded']}" if @cop_config['VersionAdded']
105
+ lines << "Changed: #{@cop_config['VersionChanged']}" if @cop_config['VersionChanged']
106
+ lines
107
+ end
108
+
109
+ def configuration_lines
110
+ (@cop_config.keys - METADATA_KEYS).sort.map do |key|
111
+ "#{key}: #{format_value(@cop_config[key])}"
112
+ end
113
+ end
114
+
115
+ def format_value(value)
116
+ value.is_a?(Array) ? value.join(', ') : value.to_s
117
+ end
118
+
119
+ def reference_lines
120
+ annotator = MessageAnnotator.new(@config, @cop_class.cop_name, @cop_config, {})
121
+
122
+ [*annotator.urls, Documentation.url_for(@cop_class, @config)].compact
123
+ end
124
+
125
+ # The comment above the class opens by restating the description, which
126
+ # is already printed above, so that opening is dropped.
127
+ def documentation_sections(description)
128
+ CopDocumentation.new(@cop_class).sections.filter_map do |title, body|
129
+ body -= description if title == 'Details'
130
+ body = strip_blank(body)
131
+ [title, body] unless body.empty?
132
+ end
133
+ end
134
+
135
+ def strip_blank(lines)
136
+ lines.drop_while(&:empty?).reverse.drop_while(&:empty?).reverse
137
+ end
138
+ end
139
+ end
140
+ end
@@ -109,8 +109,20 @@ module RuboCop
109
109
  end
110
110
 
111
111
  def inside_string_ranges(node)
112
- return [] unless node.is_a?(Parser::AST::Node)
112
+ return source_string_ranges unless node.is_a?(Parser::AST::Node)
113
113
 
114
+ string_ranges(node)
115
+ end
116
+
117
+ def source_string_ranges
118
+ return [] unless processed_source.ast
119
+ return @source_string_ranges if @source_string_ranges_source.equal?(processed_source)
120
+
121
+ @source_string_ranges_source = processed_source
122
+ @source_string_ranges = string_ranges(processed_source.ast)
123
+ end
124
+
125
+ def string_ranges(node)
114
126
  node.each_node(:any_str).filter_map { |n| inside_string_range(n) }
115
127
  end
116
128
 
@@ -17,6 +17,7 @@ module RuboCop
17
17
 
18
18
  def call(corrector)
19
19
  corrector.replace(offending_range, correction)
20
+ corrector.replace(block_node.loc.end, closing_keyword) if block_node.braces?
20
21
  end
21
22
 
22
23
  private
@@ -24,13 +25,27 @@ module RuboCop
24
25
  attr_reader :block_node, :collection_node, :argument_node
25
26
 
26
27
  def correction
27
- if block_node.arguments?
28
- format(CORRECTION_WITH_ARGUMENTS,
29
- collection: collection_node.source,
30
- variables: argument_node.children.map(&:source).join(', '))
31
- else
32
- format(CORRECTION_WITHOUT_ARGUMENTS, enumerable: collection_node.source)
33
- end
28
+ keyword = if block_node.arguments?
29
+ format(
30
+ CORRECTION_WITH_ARGUMENTS,
31
+ collection: collection_node.source,
32
+ variables: argument_node.children.map(&:source).join(', ')
33
+ )
34
+ else
35
+ format(CORRECTION_WITHOUT_ARGUMENTS, enumerable: collection_node.source)
36
+ end
37
+
38
+ padding_needed?(offending_range.end_pos) ? "#{keyword} " : keyword
39
+ end
40
+
41
+ def closing_keyword
42
+ padding_needed?(block_node.loc.end.begin_pos - 1) ? ' end' : 'end'
43
+ end
44
+
45
+ def padding_needed?(position)
46
+ character = block_node.source_range.source_buffer.source[position]
47
+
48
+ !character.nil? && !character.match?(/\s/)
34
49
  end
35
50
 
36
51
  def offending_range
@@ -12,7 +12,7 @@ module RuboCop
12
12
  when :no_empty_lines
13
13
  corrector.remove(range)
14
14
  when :empty_lines
15
- corrector.insert_before(range, "\n")
15
+ corrector.insert_before(range.begin, "\n")
16
16
  end
17
17
  end
18
18
 
@@ -29,6 +29,10 @@ module RuboCop
29
29
  insert_arguments(corrector)
30
30
  end
31
31
 
32
+ def replace_delimiters?
33
+ !block_node.braces? && arg_to_unparenthesized_call?
34
+ end
35
+
32
36
  private
33
37
 
34
38
  attr_reader :block_node, :method, :arguments
@@ -77,7 +81,7 @@ module RuboCop
77
81
  end
78
82
 
79
83
  def replace_delimiters(corrector)
80
- return if block_node.braces? || !arg_to_unparenthesized_call?
84
+ return unless replace_delimiters?
81
85
 
82
86
  corrector.insert_after(block_begin, ' ') unless separating_space?
83
87
 
@@ -90,7 +94,7 @@ module RuboCop
90
94
  # `;`; joining everything with `,` would turn them into extra parameters
91
95
  # and change the lambda's arity.
92
96
  regular, shadow = arguments.children.partition { |arg| !arg.shadowarg_type? }
93
- arg_string = regular.map(&:source).join(', ')
97
+ arg_string = regular.map { |arg| argument_source(arg) }.join(', ')
94
98
  arg_string += "; #{shadow.map(&:source).join(', ')}" unless shadow.empty?
95
99
  arg_string
96
100
  end
@@ -140,6 +144,23 @@ module RuboCop
140
144
  def separating_space?
141
145
  block_begin.source_buffer.source[block_begin.begin_pos + 2].match?(/\s/)
142
146
  end
147
+
148
+ def argument_source(argument)
149
+ return argument.source unless argument.type?(:optarg, :kwoptarg)
150
+ return argument.source unless operator_expression?(argument.default_value)
151
+
152
+ default_value = argument.default_value
153
+ name = argument.source_range.begin.join(default_value.source_range.begin).source
154
+
155
+ "#{name}(#{default_value.source})"
156
+ end
157
+
158
+ def operator_expression?(node)
159
+ return true if node.type?(:range, :if, :and, :or)
160
+ return false unless node.call_type?
161
+
162
+ node.operator_method? && !node.method?(:[])
163
+ end
143
164
  end
144
165
  end
145
166
  end
@@ -21,14 +21,55 @@ module RuboCop
21
21
  corrector.insert_after(node.loc.end, ' ')
22
22
  end
23
23
 
24
+ def heredoc_strands_code_after_close_paren?(node)
25
+ return false unless heredoc_body_before_close_paren?(node)
26
+
27
+ after_paren = text_after_close_paren(node)
28
+
29
+ !after_paren.empty? && !after_paren.start_with?('.', '&.', ',')
30
+ end
31
+
24
32
  private
25
33
 
26
34
  # When the line above `)` ends with a comment and a chained call follows `)`,
27
35
  # crossing the newline would pull the chain into the comment. Preserve the newline.
36
+ # When a heredoc body comes before `)`, a chained call has to start on the line
37
+ # after the terminator instead, since the terminator must stay alone on its line.
28
38
  def remove_close_paren(corrector, node, buffer)
29
- newlines = !comment_above_close_paren_swallows_chain?(node, buffer)
30
- corrector.remove(range_with_surrounding_space(range: node.loc.end, buffer: buffer,
31
- side: :left, newlines: newlines))
39
+ if heredoc_above_close_paren_swallows_chain?(node)
40
+ range = range_with_surrounding_space(range: node.loc.end, buffer: buffer, side: :left)
41
+
42
+ corrector.replace(range, "\n")
43
+ else
44
+ newlines = !comment_above_close_paren_swallows_chain?(node, buffer)
45
+ range = range_with_surrounding_space(
46
+ range: node.loc.end, buffer: buffer, side: :left, newlines: newlines
47
+ )
48
+
49
+ corrector.remove(range)
50
+ end
51
+ end
52
+
53
+ def heredoc_above_close_paren_swallows_chain?(node)
54
+ return false unless heredoc_body_before_close_paren?(node)
55
+
56
+ text_after_close_paren(node).start_with?('.', '&.')
57
+ end
58
+
59
+ def heredoc_body_before_close_paren?(node)
60
+ return false unless (last_child = node.children.last)
61
+
62
+ close_paren_pos = node.loc.end.begin_pos
63
+
64
+ last_child.each_node(:any_str).any? do |str_node|
65
+ str_node.heredoc? && str_node.loc.heredoc_end.end_pos <= close_paren_pos
66
+ end
67
+ end
68
+
69
+ def text_after_close_paren(node)
70
+ close_paren = node.loc.end
71
+
72
+ close_paren.source_line[(close_paren.column + 1)..].to_s.lstrip
32
73
  end
33
74
 
34
75
  def comment_above_close_paren_swallows_chain?(node, buffer)
@@ -46,12 +87,8 @@ module RuboCop
46
87
  end
47
88
 
48
89
  def chained_after_close_paren?(node)
49
- close_paren = node.loc.end
50
- line_text = close_paren.source_line
51
- after_paren = line_text[(close_paren.column + 1)..]
52
- return false if after_paren.nil?
90
+ trimmed = text_after_close_paren(node)
53
91
 
54
- trimmed = after_paren.lstrip
55
92
  !trimmed.empty? && !trimmed.start_with?('#')
56
93
  end
57
94
 
@@ -14,7 +14,7 @@ module RuboCop
14
14
  # @api private
15
15
  def url_for(cop_class, config = nil)
16
16
  base = department_to_basename(cop_class.department)
17
- fragment = cop_class.cop_name.downcase.gsub(/[^a-z]/, '')
17
+ fragment = cop_class.cop_name.downcase.delete('/')
18
18
  base_url = base_url_for(cop_class, config)
19
19
  extension = extension_for(cop_class, config)
20
20
 
@@ -109,12 +109,6 @@ module RuboCop
109
109
  end
110
110
  end
111
111
 
112
- def match_block_variable_name?(receiver_name)
113
- gem_specification(processed_source.ast) do |block_variable_name|
114
- return block_variable_name == receiver_name
115
- end
116
- end
117
-
118
112
  def add_dependency_method?(method_name)
119
113
  ADD_DEPENDENCY_METHODS.include?(method_name)
120
114
  end
@@ -80,12 +80,6 @@ module RuboCop
80
80
 
81
81
  private
82
82
 
83
- def previous_declaration(node)
84
- declarations = dependency_declarations(processed_source.ast)
85
- node_index = declarations.find_index(node)
86
- declarations.to_a[node_index - 1]
87
- end
88
-
89
83
  def get_dependency_name(node)
90
84
  node.method_name
91
85
  end
@@ -201,11 +201,8 @@ module RuboCop
201
201
  def add_newline?(node)
202
202
  # Determine if a blank line should be inserted before the new directive
203
203
  # in order to spread out pattern matchers
204
- return false if node.sibling_index&.zero?
205
- return false unless node.parent
206
-
207
- prev_sibling = node.parent.child_nodes[node.sibling_index - 1]
208
- return false unless prev_sibling && pattern_matcher?(prev_sibling)
204
+ prev_sibling = node.left_sibling
205
+ return false unless prev_sibling.is_a?(AST::Node) && pattern_matcher?(prev_sibling)
209
206
 
210
207
  node.loc.line == last_line(prev_sibling) + 1
211
208
  end
@@ -46,13 +46,6 @@ module RuboCop
46
46
 
47
47
  private
48
48
 
49
- def arguments_range(node)
50
- range_between(
51
- node.first_argument.source_range.begin_pos,
52
- node.last_argument.source_range.end_pos
53
- )
54
- end
55
-
56
49
  def group_satisfied?(group_types, symbol_args)
57
50
  group_types.all? { |type| symbol_args.any? { |arg| arg.value == type } }
58
51
  end
@@ -31,15 +31,9 @@ module RuboCop
31
31
  dup_arguments = arguments.dup
32
32
  dup_arguments.delete(config_node)
33
33
 
34
- corrector.replace(offense_range(arguments), dup_arguments.map(&:source).join(', '))
34
+ corrector.replace(arguments_range(node), dup_arguments.map(&:source).join(', '))
35
35
  end
36
36
  end
37
-
38
- private
39
-
40
- def offense_range(arguments)
41
- range_between(arguments.first.source_range.begin_pos, arguments.last.source_range.end_pos)
42
- end
43
37
  end
44
38
  end
45
39
  end
@@ -109,7 +109,7 @@ module RuboCop
109
109
  end
110
110
 
111
111
  def on_if(node)
112
- traverse_condition(node.condition) do |cond|
112
+ node.condition.each_node(:send) do |cond|
113
113
  add_offense(cond, message: MSG_FOR_CONDITIONAL_USE) if style_detected_api_used?(cond)
114
114
  end
115
115
  end
@@ -134,12 +134,6 @@ module RuboCop
134
134
  no_acceptable_style_check(node) ||
135
135
  style_detected_check(node)
136
136
  end
137
-
138
- def traverse_condition(condition, &block)
139
- yield condition if condition.send_type?
140
-
141
- condition.each_child_node { |child| traverse_condition(child, &block) }
142
- end
143
137
  end
144
138
  end
145
139
  end