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
@@ -190,33 +190,58 @@ module RuboCop
190
190
 
191
191
  # Validates code style on class declaration.
192
192
  # Add offense when find a node out of expected order.
193
+ # A node is out of order when its category is expected earlier than
194
+ # the highest-priority category seen so far, so that a low-priority element
195
+ # (even an unmovable one) cannot mask disorder among the elements that follow it.
196
+ # Consecutive elements of the same category are reported only once,
197
+ # on the first element of the group.
193
198
  def on_class(class_node)
194
- previous = -1
195
- walk_over_nested_class_definition(class_node) do |node, category|
196
- index = expected_order.index(category)
197
- if index < previous
198
- message = format(MSG, category: category, previous: expected_order[previous])
199
- add_offense(node, message: message) { |corrector| autocorrect(corrector, node) }
199
+ # Corrections are registered in reverse source order because an insertion at
200
+ # a given position lands before any insertion already made there;
201
+ # this keeps the source order of nodes moved before the same anchor.
202
+ out_of_order_elements(class_node).reverse_each do |node, category, previous|
203
+ message = format(MSG, category: category, previous: previous)
204
+
205
+ add_offense(node, message: message) do |corrector|
206
+ autocorrect(corrector, node)
200
207
  end
201
- previous = index
202
208
  end
203
209
  end
204
210
  alias on_sclass on_class
205
211
 
206
212
  private
207
213
 
208
- # Autocorrect by swapping between two nodes autocorrecting them
209
- def autocorrect(corrector, node)
210
- previous = node.left_siblings.reverse.find do |sibling|
211
- !ignore_for_autocorrect?(node, sibling)
214
+ def out_of_order_elements(class_node)
215
+ out_of_order = []
216
+ max_index = -1
217
+ previous_category = nil
218
+ walk_over_nested_class_definition(class_node) do |node, category|
219
+ index = expected_order.index(category)
220
+ if index < max_index && category != previous_category
221
+ out_of_order << [node, category, expected_order[max_index]]
222
+ end
223
+ max_index = index if index > max_index
224
+ previous_category = category
212
225
  end
213
- return unless previous
214
-
215
- current_range = source_range_with_comment(node)
216
- previous_range = source_range_with_comment(previous)
226
+ out_of_order
227
+ end
217
228
 
218
- corrector.insert_before(previous_range, current_range.source)
219
- corrector.remove(current_range)
229
+ # Autocorrect by moving the node, together with the contiguous group of
230
+ # same-category elements that follows it, to its expected position.
231
+ def autocorrect(corrector, node)
232
+ return if dynamic_constant?(node)
233
+
234
+ anchor = insertion_anchor(node)
235
+ return unless anchor
236
+
237
+ anchor_range = source_range_with_comment(anchor)
238
+ # Reversed for the same reason offenses are registered in reverse source order:
239
+ # the last insertion at a position comes first.
240
+ movable_group(node).reverse_each do |group_node|
241
+ current_range = source_range_with_comment(group_node)
242
+ corrector.insert_before(anchor_range, current_range.source)
243
+ corrector.remove(current_range)
244
+ end
220
245
  end
221
246
 
222
247
  # Classifies a node to match with something in the {expected_order}
@@ -251,7 +276,7 @@ module RuboCop
251
276
  key = category || name
252
277
  visibility_key =
253
278
  if node.def_modifier?
254
- "#{name}_methods"
279
+ name.end_with?('_class_method') ? "#{name}s" : "#{name}_methods"
255
280
  else
256
281
  "#{node_visibility(node)}_#{key}"
257
282
  end
@@ -283,7 +308,7 @@ module RuboCop
283
308
  # Exploding such a node into its children would yield non-node values (e.g. a
284
309
  # method-name `Symbol` from a `csend`) and crash later checks.
285
310
  if class_def.type?(:begin, :kwbegin)
286
- class_def.children.compact
311
+ flatten_class_elements(class_def)
287
312
  else
288
313
  [class_def]
289
314
  end
@@ -296,13 +321,10 @@ module RuboCop
296
321
  private_constant?(node)
297
322
  end
298
323
 
299
- def ignore_for_autocorrect?(node, sibling)
300
- classification = classify(node)
301
- sibling_class = classify(sibling)
302
-
303
- ignore?(sibling, sibling_class) ||
304
- classification == sibling_class ||
305
- dynamic_constant?(node)
324
+ def flatten_class_elements(node)
325
+ node.children.compact.flat_map do |child|
326
+ child.kwbegin_type? ? flatten_class_elements(child) : [child]
327
+ end
306
328
  end
307
329
 
308
330
  def humanize_node(node)
@@ -322,6 +344,66 @@ module RuboCop
322
344
  !(expression.method?(:freeze) && expression.receiver&.recursive_basic_literal?)
323
345
  end
324
346
 
347
+ # The expected position of the node: the first left sibling within its movable span
348
+ # whose category is expected to appear after the node's.
349
+ # Requiring a strictly later category keeps the order of elements sharing a category stable.
350
+ def insertion_anchor(node)
351
+ index = expected_order.index(classify(node))
352
+
353
+ movable_span(node).find do |sibling|
354
+ classification = classify(sibling)
355
+
356
+ !ignore?(sibling, classification) && expected_order.index(classification) > index
357
+ end
358
+ end
359
+
360
+ # The node together with the contiguous same-category right siblings,
361
+ # so that the whole group moves in a single pass while the offense is
362
+ # reported only on its first element.
363
+ def movable_group(node)
364
+ classification = classify(node)
365
+ group = [node]
366
+
367
+ node.right_siblings.each do |sibling|
368
+ break unless classify(sibling) == classification
369
+ break if ignore?(sibling, classification) || dynamic_constant?(sibling)
370
+
371
+ group << sibling
372
+ end
373
+
374
+ group
375
+ end
376
+
377
+ # Left siblings the node may be reordered with: those after the last barrier.
378
+ # Ignored elements within the span are simply jumped over.
379
+ def movable_span(node)
380
+ left_siblings = node.left_siblings
381
+ barrier_index = left_siblings.rindex { |sibling| barrier?(node, sibling) }
382
+
383
+ barrier_index ? left_siblings[(barrier_index + 1)..] : left_siblings
384
+ end
385
+
386
+ # A dynamic constant blocks every element: the cop does not move such constants,
387
+ # and letting other elements jump over one would change execution order just the same.
388
+ # A bare visibility modifier blocks only the elements whose meaning depends on
389
+ # the visibility section they appear in.
390
+ def barrier?(node, sibling)
391
+ dynamic_constant?(sibling) || (visibility_dependent?(node) && visibility_block?(sibling))
392
+ end
393
+
394
+ # Whether moving the node across a bare visibility modifier would change its meaning.
395
+ # This is the case for `def` nodes and for macros whose category is classified by visibility
396
+ # (e.g. `attr_accessor` when the expected order lists `private_attribute_macros`).
397
+ # Inline visibility (`private def foo`, `def self.foo`) travels with the node.
398
+ def visibility_dependent?(node)
399
+ return true if node.def_type?
400
+ return false if !node.send_type? || node.def_modifier?
401
+
402
+ key = find_category(node.method_name) || node.method_name.to_s
403
+
404
+ VISIBILITY_SCOPES.any? { |visibility| expected_order.include?("#{visibility}_#{key}") }
405
+ end
406
+
325
407
  def private_constant?(node)
326
408
  return false unless node.casgn_type? && node.namespace.nil?
327
409
  return false unless (parent = node.parent)
@@ -161,7 +161,7 @@ module RuboCop
161
161
  next_sibling.if_type? && contains_guard_clause?(next_sibling)
162
162
  end
163
163
 
164
- # rubocop:disable Metrics/CyclomaticComplexity
164
+ # rubocop:disable-next Metrics/CyclomaticComplexity
165
165
  def last_heredoc_argument(node)
166
166
  n = last_heredoc_argument_node(node)
167
167
  n = n.children.first while n.respond_to?(:begin_type?) && n.begin_type?
@@ -176,7 +176,6 @@ module RuboCop
176
176
 
177
177
  last_heredoc_argument(n.receiver) if n.respond_to?(:receiver)
178
178
  end
179
- # rubocop:enable Metrics/CyclomaticComplexity
180
179
 
181
180
  def last_heredoc_argument_node(node)
182
181
  return node unless node.respond_to?(:if_branch)
@@ -5,6 +5,14 @@ module RuboCop
5
5
  module Layout
6
6
  # Checks for a newline after the final magic comment.
7
7
  #
8
+ # `NumberOfEmptyLines` configures the minimum number of empty lines required. Set it
9
+ # to `2` when using YARD, which otherwise treats the magic comments as documentation
10
+ # for the first module or class in the file.
11
+ #
12
+ # NOTE: `Layout/EmptyLines` has to be disabled for values greater than `1`, as it
13
+ # removes the extra empty lines this cop adds, and autocorrecting with both enabled
14
+ # loops between them.
15
+ #
8
16
  # @example
9
17
  # # good
10
18
  # # frozen_string_literal: true
@@ -20,30 +28,80 @@ module RuboCop
20
28
  # class Person
21
29
  # # Some code
22
30
  # end
31
+ #
32
+ # @example NumberOfEmptyLines: 1 (default)
33
+ # # good
34
+ # # frozen_string_literal: true
35
+ #
36
+ # class Person
37
+ # end
38
+ #
39
+ # @example NumberOfEmptyLines: 2
40
+ # # bad
41
+ # # frozen_string_literal: true
42
+ #
43
+ # class Person
44
+ # end
45
+ #
46
+ # # good
47
+ # # frozen_string_literal: true
48
+ #
49
+ #
50
+ # class Person
51
+ # end
23
52
  class EmptyLineAfterMagicComment < Base
24
53
  include RangeHelp
25
54
  extend AutoCorrector
26
55
 
27
- MSG = 'Add an empty line after magic comments.'
56
+ MSG = 'Expected at least %<expected>d empty %<lines>s after magic comments; found ' \
57
+ '%<actual>d.'
58
+
59
+ def validate_config
60
+ return if number_of_empty_lines.is_a?(Integer) && number_of_empty_lines.positive?
61
+
62
+ raise ValidationError,
63
+ 'The `Layout/EmptyLineAfterMagicComment` cop only accepts a positive ' \
64
+ 'integer for its `NumberOfEmptyLines` configuration parameter, but ' \
65
+ "`#{number_of_empty_lines.inspect}` was given."
66
+ end
28
67
 
29
68
  def on_new_investigation
30
69
  return unless (last_magic_comment = last_magic_comment(processed_source))
31
- return unless (next_line = processed_source[last_magic_comment.loc.line])
32
- return if next_line.strip.empty?
70
+
71
+ actual = empty_lines_after(last_magic_comment)
72
+ # Trailing empty lines are `Layout/TrailingEmptyLines`' responsibility.
73
+ return unless processed_source[last_magic_comment.loc.line + actual]
74
+ return if actual >= number_of_empty_lines
33
75
 
34
76
  offending_range = offending_range(last_magic_comment)
35
77
 
36
- add_offense(offending_range) do |corrector|
37
- corrector.insert_before(offending_range, "\n")
78
+ add_offense(offending_range, message: message(actual)) do |corrector|
79
+ corrector.insert_before(offending_range, "\n" * (number_of_empty_lines - actual))
38
80
  end
39
81
  end
40
82
 
41
83
  private
42
84
 
85
+ def message(actual)
86
+ format(MSG, expected: number_of_empty_lines, actual: actual,
87
+ lines: number_of_empty_lines == 1 ? 'line' : 'lines')
88
+ end
89
+
90
+ def empty_lines_after(last_magic_comment)
91
+ first_line = last_magic_comment.loc.line
92
+ count = 0
93
+ count += 1 while (line = processed_source[first_line + count]) && line.strip.empty?
94
+ count
95
+ end
96
+
43
97
  def offending_range(last_magic_comment)
44
98
  source_range(processed_source.buffer, last_magic_comment.loc.line + 1, 0)
45
99
  end
46
100
 
101
+ def number_of_empty_lines
102
+ cop_config.fetch('NumberOfEmptyLines', 1)
103
+ end
104
+
47
105
  # Find the last magic comment in the source file.
48
106
  #
49
107
  # Take all comments that precede the first line of code (or just take
@@ -123,7 +123,7 @@ module RuboCop
123
123
  AlignmentCorrector.align_end(corrector, processed_source, node, alignment_node(node))
124
124
  end
125
125
 
126
- # rubocop:disable Metrics/CyclomaticComplexity
126
+ # rubocop:disable-next Metrics/CyclomaticComplexity
127
127
  def check_assignment(node, rhs)
128
128
  # If there are method calls chained to the right hand side of the
129
129
  # assignment, we let rhs be the receiver of those method calls before
@@ -139,7 +139,6 @@ module RuboCop
139
139
 
140
140
  check_asgn_alignment(node, rhs)
141
141
  end
142
- # rubocop:enable Metrics/CyclomaticComplexity
143
142
 
144
143
  def check_asgn_alignment(outer_node, inner_node)
145
144
  align_with = {
@@ -41,6 +41,7 @@ module RuboCop
41
41
 
42
42
  @aligned_comments = aligned_locations(processed_source.comments.map(&:loc))
43
43
  @corrected = Set.new if force_equal_sign_alignment?
44
+ prepare_alignment_data
44
45
 
45
46
  processed_source.tokens.each_cons(2) do |token1, token2|
46
47
  check_tokens(processed_source.ast, token1, token2)
@@ -95,19 +96,120 @@ module RuboCop
95
96
  end_pos = token2.begin_pos - 1
96
97
  return if end_pos <= start_pos
97
98
 
98
- return if allow_for_alignment? && aligned_tok?(token2)
99
+ return if allow_for_alignment? && aligned_tok?(token1, token2)
99
100
 
100
101
  yield range_between(start_pos, end_pos)
101
102
  end
102
103
 
103
- def aligned_tok?(token)
104
+ def aligned_tok?(previous_token, token)
104
105
  if token.comment?
105
106
  @aligned_comments.include?(token.line)
106
107
  else
107
- aligned_with_something?(token.pos)
108
+ aligned_with_something?(token.pos) &&
109
+ (ASSIGNMENT_OR_COMPARISON_TOKENS.include?(token.type) ||
110
+ spacing_varies?(previous_token, token))
108
111
  end
109
112
  end
110
113
 
114
+ def spacing_varies?(previous_token, token)
115
+ by_column, typed = group_profile(token.line)
116
+ column = token.pos.column
117
+ lines, spacings = typed[[column, token.type]]
118
+ entries = by_column[column]
119
+
120
+ if lines.size > 1
121
+ uniform_alignment_allowed?(token, lines, spacings)
122
+ elsif entries.size > 1
123
+ distinct_spacings(entries).size > 1
124
+ else
125
+ nearest_spacing_varies?(previous_token, token)
126
+ end
127
+ end
128
+
129
+ def uniform_alignment_allowed?(token, lines, spacings)
130
+ return @alignment_verdicts[lines] if @alignment_verdicts.key?(lines)
131
+
132
+ @alignment_verdicts[lines] =
133
+ spacings.uniq.size > 1 || aligned_on_other_column?(token, lines)
134
+ end
135
+
136
+ def aligned_on_other_column?(token, lines)
137
+ line_set = Set.new(lines)
138
+ by_column, = group_profile(token.line)
139
+
140
+ by_column.any? do |column, entries|
141
+ next false if column == token.pos.column
142
+
143
+ shared = entries.filter_map { |line, spacing| spacing if line_set.include?(line) }
144
+ shared.uniq.size > 1
145
+ end
146
+ end
147
+
148
+ def distinct_spacings(entries)
149
+ entries.map(&:last).uniq
150
+ end
151
+
152
+ def group_profile(line_number)
153
+ group = alignment_lines(line_number)
154
+
155
+ @group_profiles[group] ||= build_group_profile(group)
156
+ end
157
+
158
+ def build_group_profile(group)
159
+ by_column = Hash.new { |hash, key| hash[key] = [] }
160
+ typed = Hash.new { |hash, key| hash[key] = [[], []] }
161
+
162
+ group.each { |line| record_line_spacings(by_column, typed, line) }
163
+
164
+ [by_column, typed]
165
+ end
166
+
167
+ def record_line_spacings(by_column, typed, line)
168
+ @tokens_by_line[line]&.each_cons(2) do |previous_token, token|
169
+ spacing = token.begin_pos - previous_token.end_pos
170
+ by_column[token.pos.column] << [line, spacing]
171
+ lines, spacings = typed[[token.pos.column, token.type]]
172
+ lines << line
173
+ spacings << spacing
174
+ end
175
+ end
176
+
177
+ def nearest_spacing_varies?(previous_token, token)
178
+ spacing = token.begin_pos - previous_token.end_pos
179
+ line_ranges = [(token.line - 1).downto(1),
180
+ (token.line + 1).upto(processed_source.lines.length)]
181
+
182
+ line_ranges.any? do |line_numbers|
183
+ nearest_spacing_differs?(line_numbers, token, spacing)
184
+ end
185
+ end
186
+
187
+ def nearest_spacing_differs?(line_numbers, token, spacing)
188
+ line_numbers.each do |line|
189
+ break if definition_boundary_lines.include?(line)
190
+ next if aligned_comment_lines.include?(line)
191
+
192
+ other_spacing = spacing_before_token_at(line, token.pos.column)
193
+ return other_spacing != spacing if other_spacing
194
+ end
195
+
196
+ false
197
+ end
198
+
199
+ def spacing_before_token_at(line_number, column)
200
+ tokens = @tokens_by_line[line_number]
201
+ index = tokens&.index { |token| token.pos.column == column }
202
+ return unless index&.positive?
203
+
204
+ tokens[index].begin_pos - tokens[index - 1].end_pos
205
+ end
206
+
207
+ def prepare_alignment_data
208
+ @group_profiles = {}.compare_by_identity
209
+ @alignment_verdicts = {}.compare_by_identity
210
+ @tokens_by_line = processed_source.tokens.group_by(&:line)
211
+ end
212
+
111
213
  def ignored_range?(ast, start_pos)
112
214
  ignored_ranges(ast).any? { |r| r.include?(start_pos) }
113
215
  end
@@ -156,7 +156,8 @@ module RuboCop
156
156
  return unless should_check?(node)
157
157
  return if same_line?(node, node.first_argument)
158
158
  return if enforce_first_argument_with_fixed_indentation? &&
159
- !enable_layout_first_method_argument_line_break?
159
+ (!enable_layout_first_method_argument_line_break? ||
160
+ conflicts_with_fixed_indentation_alignment?(node))
160
161
 
161
162
  indent = base_indentation(node) + configured_indentation_width
162
163
 
@@ -309,6 +310,25 @@ module RuboCop
309
310
  def enable_layout_first_method_argument_line_break?
310
311
  config.cop_enabled?('Layout/FirstMethodArgumentLineBreak')
311
312
  end
313
+
314
+ def conflicts_with_fixed_indentation_alignment?(node)
315
+ return false unless special_inner_call_indentation?(node)
316
+ return false unless argument_alignment_applies?(node)
317
+
318
+ base_indentation(node) != indentation_of_method_line(node)
319
+ end
320
+
321
+ def argument_alignment_applies?(node)
322
+ return false if !node.call_type? || node.method?(:[]=)
323
+
324
+ node.arguments.size >= 2 ||
325
+ (node.first_argument.hash_type? && node.first_argument.pairs.count >= 2)
326
+ end
327
+
328
+ def indentation_of_method_line(node)
329
+ lineno = node.loc.selector ? node.loc.selector.line : node.loc.begin.line
330
+ processed_source.lines[lineno - 1] =~ /\S/
331
+ end
312
332
  end
313
333
  end
314
334
  end
@@ -172,7 +172,7 @@ module RuboCop
172
172
  'Indent the right bracket the same as the first position ' \
173
173
  'after the preceding left parenthesis.'
174
174
  when :parent_hash_key
175
- 'Indent the right bracket the same as the parent hash key.' \
175
+ 'Indent the right bracket the same as the parent hash key.'
176
176
  else
177
177
  'Indent the right bracket the same as the start of the line ' \
178
178
  'where the left bracket is.'
@@ -206,7 +206,9 @@ module RuboCop
206
206
  def autocorrect(corrector, node)
207
207
  return unless node
208
208
 
209
- AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
209
+ AlignmentCorrector.correct(
210
+ corrector, processed_source, node, @column_delta, tab_indentation: true
211
+ )
210
212
  end
211
213
 
212
214
  # Returns a range at the first non-space column of the line the opening parenthesis is on,
@@ -98,6 +98,24 @@ module RuboCop
98
98
  #
99
99
  # name = 'John' #: String
100
100
  #
101
+ # @example AllowYARDCommentBlockSeparator: false (default)
102
+ #
103
+ # # bad
104
+ #
105
+ # # Copyright (c) Example Corp
106
+ # #-
107
+ # class Client
108
+ # end
109
+ #
110
+ # @example AllowYARDCommentBlockSeparator: true
111
+ #
112
+ # # good
113
+ #
114
+ # # Copyright (c) Example Corp
115
+ # #-
116
+ # class Client
117
+ # end
118
+ #
101
119
  class LeadingCommentSpace < Base
102
120
  include RangeHelp
103
121
  extend AutoCorrector
@@ -113,6 +131,7 @@ module RuboCop
113
131
  next if gemfile_ruby_comment?(comment)
114
132
  next if rbs_inline_annotation?(comment)
115
133
  next if steep_annotation?(comment)
134
+ next if yard_comment_block_separator?(comment)
116
135
 
117
136
  add_offense(comment) do |corrector|
118
137
  expr = comment.source_range
@@ -197,6 +216,14 @@ module RuboCop
197
216
  def steep_annotation?(comment)
198
217
  allow_steep_annotation? && comment.text.start_with?(/#[$:]/)
199
218
  end
219
+
220
+ def allow_yard_comment_block_separator?
221
+ cop_config['AllowYARDCommentBlockSeparator']
222
+ end
223
+
224
+ def yard_comment_block_separator?(comment)
225
+ allow_yard_comment_block_separator? && comment.text.match?(/\A#-\s*\z/)
226
+ end
200
227
  end
201
228
  end
202
229
  end
@@ -87,7 +87,7 @@ module RuboCop
87
87
  corrector.replace(range, correction)
88
88
  end
89
89
 
90
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
91
91
  def ignored_literal_ranges(ast)
92
92
  # which lines start inside a string literal?
93
93
  return [] if ast.nil?
@@ -106,7 +106,6 @@ module RuboCop
106
106
  end
107
107
  end
108
108
  end
109
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
110
109
 
111
110
  def comment_ranges(comments)
112
111
  comments.map(&:source_range)