rubocop-callbacksystems 0.1.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 (182) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +78 -0
  4. data/config/default.yml +590 -0
  5. data/docs/cops.md +111 -0
  6. data/lib/rubocop/callbacksystems/autocorrection/batch.rb +135 -0
  7. data/lib/rubocop/callbacksystems/autocorrection/comment_rescue.rb +112 -0
  8. data/lib/rubocop/callbacksystems/autocorrection/rehearsal.rb +65 -0
  9. data/lib/rubocop/callbacksystems/autocorrection/rewrite_permit.rb +14 -0
  10. data/lib/rubocop/callbacksystems/class_structure/builder_assignment.rb +48 -0
  11. data/lib/rubocop/callbacksystems/class_structure/class_body.rb +50 -0
  12. data/lib/rubocop/callbacksystems/class_structure/divergence.rb +31 -0
  13. data/lib/rubocop/callbacksystems/class_structure/node_visibility.rb +82 -0
  14. data/lib/rubocop/callbacksystems/class_structure/reference_order.rb +36 -0
  15. data/lib/rubocop/callbacksystems/class_structure/statement_rank.rb +84 -0
  16. data/lib/rubocop/callbacksystems/class_structure/statement_run.rb +42 -0
  17. data/lib/rubocop/callbacksystems/class_structure/visibility.rb +84 -0
  18. data/lib/rubocop/callbacksystems/cops_document.rb +78 -0
  19. data/lib/rubocop/callbacksystems/execution/deferred_local_variables.rb +138 -0
  20. data/lib/rubocop/callbacksystems/execution/discarded_expression.rb +45 -0
  21. data/lib/rubocop/callbacksystems/execution/eager_evaluation_path.rb +73 -0
  22. data/lib/rubocop/callbacksystems/execution/immediate.rb +71 -0
  23. data/lib/rubocop/callbacksystems/execution/local_variable_occurrences.rb +112 -0
  24. data/lib/rubocop/callbacksystems/hashes/key_transform_detection.rb +108 -0
  25. data/lib/rubocop/callbacksystems/hashes/keyword_options.rb +37 -0
  26. data/lib/rubocop/callbacksystems/helpers/bodies.rb +150 -0
  27. data/lib/rubocop/callbacksystems/helpers/builders.rb +103 -0
  28. data/lib/rubocop/callbacksystems/helpers/comments.rb +56 -0
  29. data/lib/rubocop/callbacksystems/helpers/corrections.rb +15 -0
  30. data/lib/rubocop/callbacksystems/helpers/methods.rb +46 -0
  31. data/lib/rubocop/callbacksystems/helpers/names.rb +65 -0
  32. data/lib/rubocop/callbacksystems/helpers/node_types.rb +154 -0
  33. data/lib/rubocop/callbacksystems/helpers/rails.rb +52 -0
  34. data/lib/rubocop/callbacksystems/helpers/ranges.rb +107 -0
  35. data/lib/rubocop/callbacksystems/helpers/recursion.rb +29 -0
  36. data/lib/rubocop/callbacksystems/helpers/variables.rb +44 -0
  37. data/lib/rubocop/callbacksystems/helpers/visibility.rb +121 -0
  38. data/lib/rubocop/callbacksystems/helpers.rb +43 -0
  39. data/lib/rubocop/callbacksystems/methods/calls.rb +38 -0
  40. data/lib/rubocop/callbacksystems/methods/definitions.rb +218 -0
  41. data/lib/rubocop/callbacksystems/methods/delegate_macro.rb +144 -0
  42. data/lib/rubocop/callbacksystems/methods/delegation_target.rb +55 -0
  43. data/lib/rubocop/callbacksystems/methods/domain.rb +77 -0
  44. data/lib/rubocop/callbacksystems/methods/implicit_invocations.rb +17 -0
  45. data/lib/rubocop/callbacksystems/methods/macro_references.rb +238 -0
  46. data/lib/rubocop/callbacksystems/methods/reference_evidence.rb +74 -0
  47. data/lib/rubocop/callbacksystems/methods/siblings.rb +24 -0
  48. data/lib/rubocop/callbacksystems/methods/statement_definitions.rb +68 -0
  49. data/lib/rubocop/callbacksystems/offense.rb +13 -0
  50. data/lib/rubocop/callbacksystems/plugin.rb +27 -0
  51. data/lib/rubocop/callbacksystems/project_index/cache.rb +16 -0
  52. data/lib/rubocop/callbacksystems/project_index/confinement_cache.rb +40 -0
  53. data/lib/rubocop/callbacksystems/project_index/constant_references.rb +74 -0
  54. data/lib/rubocop/callbacksystems/project_index/corrections.rb +35 -0
  55. data/lib/rubocop/callbacksystems/project_index/coverage.rb +56 -0
  56. data/lib/rubocop/callbacksystems/project_index/diagnostics.rb +60 -0
  57. data/lib/rubocop/callbacksystems/project_index/freshness.rb +104 -0
  58. data/lib/rubocop/callbacksystems/project_index/loader.rb +9 -0
  59. data/lib/rubocop/callbacksystems/project_index/location.rb +29 -0
  60. data/lib/rubocop/callbacksystems/project_index/nested_class_confinement.rb +297 -0
  61. data/lib/rubocop/callbacksystems/project_index/nested_class_method_dispatch.rb +50 -0
  62. data/lib/rubocop/callbacksystems/project_index/reading.rb +129 -0
  63. data/lib/rubocop/callbacksystems/project_index/source_file.rb +59 -0
  64. data/lib/rubocop/callbacksystems/project_index/source_file_match.rb +57 -0
  65. data/lib/rubocop/callbacksystems/project_index/support.rb +46 -0
  66. data/lib/rubocop/callbacksystems/source/comment_blocks.rb +43 -0
  67. data/lib/rubocop/callbacksystems/source/comments.rb +141 -0
  68. data/lib/rubocop/callbacksystems/source/file_ast.rb +15 -0
  69. data/lib/rubocop/callbacksystems/source/files_checksum.rb +43 -0
  70. data/lib/rubocop/callbacksystems/source/single_line_call.rb +181 -0
  71. data/lib/rubocop/callbacksystems/source/statement_with_comments.rb +131 -0
  72. data/lib/rubocop/callbacksystems/testing/cop_helpers.rb +42 -0
  73. data/lib/rubocop/callbacksystems/testing/fixture_call.rb +30 -0
  74. data/lib/rubocop/callbacksystems/testing/fixture_names.rb +62 -0
  75. data/lib/rubocop/callbacksystems/testing/path_mapping.rb +104 -0
  76. data/lib/rubocop/cop/callbacksystems/application_controller_method_definition.rb +44 -0
  77. data/lib/rubocop/cop/callbacksystems/assert_with_expected_argument.rb +60 -0
  78. data/lib/rubocop/cop/callbacksystems/base.rb +134 -0
  79. data/lib/rubocop/cop/callbacksystems/body_order.rb +478 -0
  80. data/lib/rubocop/cop/callbacksystems/builder_method_parentheses.rb +74 -0
  81. data/lib/rubocop/cop/callbacksystems/collapse_multiline_expression.rb +140 -0
  82. data/lib/rubocop/cop/callbacksystems/comment_block_length.rb +137 -0
  83. data/lib/rubocop/cop/callbacksystems/complex_conditional.rb +75 -0
  84. data/lib/rubocop/cop/callbacksystems/controller_test_response_assertion.rb +105 -0
  85. data/lib/rubocop/cop/callbacksystems/controller_test_response_first_assertion.rb +123 -0
  86. data/lib/rubocop/cop/callbacksystems/data_clump.rb +339 -0
  87. data/lib/rubocop/cop/callbacksystems/declarative_method_naming.rb +350 -0
  88. data/lib/rubocop/cop/callbacksystems/delegate_requires_private_option.rb +81 -0
  89. data/lib/rubocop/cop/callbacksystems/eager_loading_scope_naming.rb +386 -0
  90. data/lib/rubocop/cop/callbacksystems/early_return.rb +215 -0
  91. data/lib/rubocop/cop/callbacksystems/empty_line_before_method.rb +168 -0
  92. data/lib/rubocop/cop/callbacksystems/empty_line_between_sections.rb +169 -0
  93. data/lib/rubocop/cop/callbacksystems/empty_private_section.rb +107 -0
  94. data/lib/rubocop/cop/callbacksystems/expanded_empty_class_or_module.rb +71 -0
  95. data/lib/rubocop/cop/callbacksystems/inline_memoized_computation.rb +110 -0
  96. data/lib/rubocop/cop/callbacksystems/job_enqueue_methods.rb +93 -0
  97. data/lib/rubocop/cop/callbacksystems/max_safe_navigation_depth.rb +118 -0
  98. data/lib/rubocop/cop/callbacksystems/method_invocation_order.rb +261 -0
  99. data/lib/rubocop/cop/callbacksystems/nested_class_order.rb +255 -0
  100. data/lib/rubocop/cop/callbacksystems/no_abbreviations.rb +197 -0
  101. data/lib/rubocop/cop/callbacksystems/no_anemic_delegation.rb +101 -0
  102. data/lib/rubocop/cop/callbacksystems/no_anemic_record.rb +309 -0
  103. data/lib/rubocop/cop/callbacksystems/no_assert_select.rb +31 -0
  104. data/lib/rubocop/cop/callbacksystems/no_bang_method_without_counterpart.rb +451 -0
  105. data/lib/rubocop/cop/callbacksystems/no_call_method.rb +73 -0
  106. data/lib/rubocop/cop/callbacksystems/no_comments_in_test_class_body.rb +159 -0
  107. data/lib/rubocop/cop/callbacksystems/no_consecutive_appends.rb +261 -0
  108. data/lib/rubocop/cop/callbacksystems/no_custom_route_actions.rb +124 -0
  109. data/lib/rubocop/cop/callbacksystems/no_direct_eager_loading.rb +90 -0
  110. data/lib/rubocop/cop/callbacksystems/no_each_with_early_return.rb +58 -0
  111. data/lib/rubocop/cop/callbacksystems/no_explicit_public_modifier.rb +81 -0
  112. data/lib/rubocop/cop/callbacksystems/no_hash_with_default_block.rb +68 -0
  113. data/lib/rubocop/cop/callbacksystems/no_local_variable_return.rb +173 -0
  114. data/lib/rubocop/cop/callbacksystems/no_manual_accumulation.rb +188 -0
  115. data/lib/rubocop/cop/callbacksystems/no_mixed_memoization.rb +145 -0
  116. data/lib/rubocop/cop/callbacksystems/no_nested_modules_in_classes.rb +78 -0
  117. data/lib/rubocop/cop/callbacksystems/no_nil_at_end_of_method.rb +74 -0
  118. data/lib/rubocop/cop/callbacksystems/no_redundant_validation_name.rb +37 -0
  119. data/lib/rubocop/cop/callbacksystems/no_redundant_wrapper_method.rb +161 -0
  120. data/lib/rubocop/cop/callbacksystems/no_section_divider_comments.rb +105 -0
  121. data/lib/rubocop/cop/callbacksystems/no_send_in_tests.rb +75 -0
  122. data/lib/rubocop/cop/callbacksystems/no_service_objects.rb +64 -0
  123. data/lib/rubocop/cop/callbacksystems/no_single_letter_names.rb +47 -0
  124. data/lib/rubocop/cop/callbacksystems/no_singleton_class_in_modules.rb +43 -0
  125. data/lib/rubocop/cop/callbacksystems/no_tap_collection_building.rb +173 -0
  126. data/lib/rubocop/cop/callbacksystems/no_turbo_stream_in_controllers.rb +52 -0
  127. data/lib/rubocop/cop/callbacksystems/no_typographic_clutter.rb +159 -0
  128. data/lib/rubocop/cop/callbacksystems/no_writes_in_get_actions.rb +149 -0
  129. data/lib/rubocop/cop/callbacksystems/ordered_mixin_arguments.rb +78 -0
  130. data/lib/rubocop/cop/callbacksystems/plural_controller_names.rb +103 -0
  131. data/lib/rubocop/cop/callbacksystems/prefer_attr_reader_over_instance_variable.rb +86 -0
  132. data/lib/rubocop/cop/callbacksystems/prefer_backslash_continuation.rb +478 -0
  133. data/lib/rubocop/cop/callbacksystems/prefer_class_for_state.rb +532 -0
  134. data/lib/rubocop/cop/callbacksystems/prefer_class_new_for_empty_nested_class.rb +247 -0
  135. data/lib/rubocop/cop/callbacksystems/prefer_class_over_builder_block.rb +124 -0
  136. data/lib/rubocop/cop/callbacksystems/prefer_default_argument.rb +225 -0
  137. data/lib/rubocop/cop/callbacksystems/prefer_delegate.rb +242 -0
  138. data/lib/rubocop/cop/callbacksystems/prefer_delegate_over_instance_variable_assignment.rb +175 -0
  139. data/lib/rubocop/cop/callbacksystems/prefer_excluding.rb +204 -0
  140. data/lib/rubocop/cop/callbacksystems/prefer_extract_associated.rb +54 -0
  141. data/lib/rubocop/cop/callbacksystems/prefer_it_block_parameter.rb +156 -0
  142. data/lib/rubocop/cop/callbacksystems/prefer_many.rb +57 -0
  143. data/lib/rubocop/cop/callbacksystems/prefer_method_over_lambda.rb +211 -0
  144. data/lib/rubocop/cop/callbacksystems/prefer_method_over_variable.rb +172 -0
  145. data/lib/rubocop/cop/callbacksystems/prefer_module_for_static_class.rb +286 -0
  146. data/lib/rubocop/cop/callbacksystems/prefer_ordinal_array_access.rb +136 -0
  147. data/lib/rubocop/cop/callbacksystems/prefer_positive_wrap.rb +265 -0
  148. data/lib/rubocop/cop/callbacksystems/prefer_squish.rb +47 -0
  149. data/lib/rubocop/cop/callbacksystems/prefer_stringify_keys.rb +24 -0
  150. data/lib/rubocop/cop/callbacksystems/prefer_symbolize_keys.rb +24 -0
  151. data/lib/rubocop/cop/callbacksystems/prefer_turbo_page_refresh_broadcasts.rb +40 -0
  152. data/lib/rubocop/cop/callbacksystems/private_constants_in_private_section.rb +160 -0
  153. data/lib/rubocop/cop/callbacksystems/private_delegate_placement.rb +342 -0
  154. data/lib/rubocop/cop/callbacksystems/private_methods_in_private_section.rb +70 -0
  155. data/lib/rubocop/cop/callbacksystems/private_nested_class_method_visibility.rb +269 -0
  156. data/lib/rubocop/cop/callbacksystems/private_nested_classes.rb +94 -0
  157. data/lib/rubocop/cop/callbacksystems/public_methods_must_have_tests.rb +190 -0
  158. data/lib/rubocop/cop/callbacksystems/redundant_concern_extend.rb +157 -0
  159. data/lib/rubocop/cop/callbacksystems/redundant_constant_freeze.rb +57 -0
  160. data/lib/rubocop/cop/callbacksystems/redundant_constant_namespace.rb +134 -0
  161. data/lib/rubocop/cop/callbacksystems/reflow_comments.rb +133 -0
  162. data/lib/rubocop/cop/callbacksystems/repeated_fixture_in_tests.rb +106 -0
  163. data/lib/rubocop/cop/callbacksystems/restricted_controller_actions.rb +85 -0
  164. data/lib/rubocop/cop/callbacksystems/routes_module_scope.rb +336 -0
  165. data/lib/rubocop/cop/callbacksystems/single_line_setup_block.rb +107 -0
  166. data/lib/rubocop/cop/callbacksystems/single_singleton_class.rb +49 -0
  167. data/lib/rubocop/cop/callbacksystems/single_top_level_class_or_module.rb +160 -0
  168. data/lib/rubocop/cop/callbacksystems/single_use_fixture_variable.rb +211 -0
  169. data/lib/rubocop/cop/callbacksystems/single_use_setup_variable.rb +684 -0
  170. data/lib/rubocop/cop/callbacksystems/singular_scope_names.rb +47 -0
  171. data/lib/rubocop/cop/callbacksystems/space_inside_percent_literal.rb +73 -0
  172. data/lib/rubocop/cop/callbacksystems/system_test_browser_driver.rb +30 -0
  173. data/lib/rubocop/cop/callbacksystems/test_method_definition.rb +86 -0
  174. data/lib/rubocop/cop/callbacksystems/test_method_order.rb +319 -0
  175. data/lib/rubocop/cop/callbacksystems/too_many_instance_variables.rb +78 -0
  176. data/lib/rubocop/cop/callbacksystems/too_many_local_variables.rb +77 -0
  177. data/lib/rubocop/cop/callbacksystems/too_many_validation_guards.rb +84 -0
  178. data/lib/rubocop/cop/callbacksystems/unnecessary_local_variable.rb +176 -0
  179. data/lib/rubocop/cop/callbacksystems/unused_private_method_in_nested_class.rb +203 -0
  180. data/lib/rubocop-callbacksystems.rb +21 -0
  181. data/rubocop.yml +685 -0
  182. metadata +337 -0
@@ -0,0 +1,47 @@
1
+ # Detects `str.strip.gsub(/\s+/, " ")` patterns that should use `squish`. The
2
+ # method names the whole transformation, where the chain spells out a regexp
3
+ # the reader has to decode.
4
+ #
5
+ # @example
6
+ # # bad
7
+ # text.strip.gsub(/\s+/, " ")
8
+ # name.strip.gsub(/[[:space:]]+/, " ")
9
+ #
10
+ # # good
11
+ # text.squish
12
+ # name.squish
13
+ #
14
+ class RuboCop::Cop::Callbacksystems::PreferSquish < RuboCop::Cop::Callbacksystems::Base
15
+ extend RuboCop::Cop::AutoCorrector
16
+
17
+ MESSAGE = 'Use `squish` instead of `strip.gsub(/\s+/, " ")`.'
18
+ WHITESPACE_PATTERNS = %w[ \s+ [[:space:]]+ ]
19
+
20
+ # @!method strip_gsub_pattern(node)
21
+ def_node_matcher :strip_gsub_pattern, <<~PATTERN
22
+ (call $(call _ :strip) :gsub (regexp (str $_pattern) (regopt)) (str " "))
23
+ PATTERN
24
+
25
+ def on_send(node)
26
+ strip_gsub_pattern(node) do |strip_call, pattern|
27
+ next if WHITESPACE_PATTERNS.exclude?(pattern)
28
+
29
+ add_squish_offense(node, strip_call:)
30
+ end
31
+ end
32
+
33
+ alias on_csend on_send
34
+
35
+ private
36
+ def add_squish_offense(node, strip_call:)
37
+ if source_comments.any_within?(node)
38
+ add_offense(node, message: MESSAGE)
39
+ else
40
+ add_offense(node, message: MESSAGE) { it.replace(node, replacement_for(strip_call)) }
41
+ end
42
+ end
43
+
44
+ def replacement_for(call)
45
+ "#{call_prefix_of(call)}squish"
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ # Detects `hash.transform_keys(&:to_s)` patterns that should use `stringify_keys`.
2
+ # The Active Support method names what the keys become, where the transform
3
+ # makes the reader work it out from the block.
4
+ #
5
+ # @example
6
+ # # bad
7
+ # hash.transform_keys(&:to_s)
8
+ # hash.transform_keys { |k| k.to_s }
9
+ #
10
+ # # good
11
+ # hash.stringify_keys
12
+ #
13
+ class RuboCop::Cop::Callbacksystems::PreferStringifyKeys < RuboCop::Cop::Callbacksystems::Base
14
+ include RuboCop::Callbacksystems::Hashes::KeyTransformDetection
15
+
16
+ private
17
+ def source_method
18
+ :to_s
19
+ end
20
+
21
+ def preferred_method
22
+ :stringify_keys
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # Detects `hash.transform_keys(&:to_sym)` patterns that should use `symbolize_keys`.
2
+ # The Active Support method names what the keys become, where the transform
3
+ # makes the reader work it out from the block.
4
+ #
5
+ # @example
6
+ # # bad
7
+ # hash.transform_keys(&:to_sym)
8
+ # hash.transform_keys { |k| k.to_sym }
9
+ #
10
+ # # good
11
+ # hash.symbolize_keys
12
+ #
13
+ class RuboCop::Cop::Callbacksystems::PreferSymbolizeKeys < RuboCop::Cop::Callbacksystems::Base
14
+ include RuboCop::Callbacksystems::Hashes::KeyTransformDetection
15
+
16
+ private
17
+ def source_method
18
+ :to_sym
19
+ end
20
+
21
+ def preferred_method
22
+ :symbolize_keys
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # Enforces Turbo page refreshes over targeted stream broadcasts.
2
+ #
3
+ # Targeted broadcasts (append, prepend, replace, remove, and friends) and the
4
+ # macros that generate them tie the server to specific partials and DOM targets.
5
+ # Page refresh broadcasts let the client morph the current page instead, which
6
+ # keeps the broadcasting code declarative and the views as the single source of
7
+ # truth.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # broadcasts_to :board
12
+ # message.broadcast_append_to :messages
13
+ # broadcast_replace_later_to room
14
+ #
15
+ # # good - macro
16
+ # broadcasts_refreshes
17
+ # broadcasts_refreshes_to :board
18
+ #
19
+ # # good - granular, manual refresh
20
+ # broadcast_refresh_to :board
21
+ # broadcast_refresh_later_to room
22
+ #
23
+ class RuboCop::Cop::Callbacksystems::PreferTurboPageRefreshBroadcasts < RuboCop::Cop::Callbacksystems::Base
24
+ MESSAGE = "Prefer Turbo page refreshes over `%<name>s`."
25
+
26
+ def on_send(node)
27
+ add_offense(node, message: format(MESSAGE, name: node.method_name)) if targeted_broadcast?(node)
28
+ end
29
+
30
+ alias on_csend on_send
31
+
32
+ private
33
+ MACROS = %i[ broadcasts broadcasts_to ]
34
+ TARGETED_BROADCAST =
35
+ /\Abroadcast_(append|prepend|replace|update|remove|before|after|action|render)(_later)?(_to)?\z/
36
+
37
+ def targeted_broadcast?(node)
38
+ (bare_send?(node) && MACROS.include?(node.method_name)) || TARGETED_BROADCAST.match?(node.method_name)
39
+ end
40
+ end
@@ -0,0 +1,160 @@
1
+ # A constant that only serves the implementation belongs in the private
2
+ # section, where every other implementation detail already lives. Marking it
3
+ # with `private_constant` from the top of the class says the same thing twice
4
+ # and leaves the declaration among the public ones.
5
+ #
6
+ # The marker earns its place when class-level code above the private section
7
+ # reads the constant, since the declaration cannot move below the code that
8
+ # reads it. Class-level code inside the section moves down with it.
9
+ #
10
+ # @example
11
+ # # bad - only method bodies read it
12
+ # class Backend
13
+ # Route = Data.define(:host, :matcher)
14
+ # private_constant :Route
15
+ #
16
+ # def route_for(host)
17
+ # Route.new(host, nil)
18
+ # end
19
+ # end
20
+ #
21
+ # # good - declared where the implementation lives
22
+ # class Backend
23
+ # def route_for(host)
24
+ # Route.new(host, nil)
25
+ # end
26
+ #
27
+ # private
28
+ # Route = Data.define(:host, :matcher)
29
+ # end
30
+ #
31
+ # # bad - the reader is private too, so both move
32
+ # class Backend
33
+ # MEMBERS = [ :host, :matcher ]
34
+ # private_constant :MEMBERS
35
+ #
36
+ # private
37
+ # Route = Data.define(*MEMBERS)
38
+ # end
39
+ #
40
+ # # good
41
+ # class Backend
42
+ # private
43
+ # MEMBERS = [ :host, :matcher ]
44
+ # Route = Data.define(*MEMBERS)
45
+ # end
46
+ #
47
+ # # good - class-level code above reads it, so it has to stay there
48
+ # class Backend
49
+ # FORMATS = [ :json ]
50
+ # private_constant :FORMATS
51
+ #
52
+ # validates :format, inclusion: { in: FORMATS }
53
+ # end
54
+ #
55
+ class RuboCop::Cop::Callbacksystems::PrivateConstantsInPrivateSection < RuboCop::Cop::Callbacksystems::Base
56
+ def on_send(node)
57
+ report_each PrivateConstantMarker.new(node)
58
+ end
59
+
60
+ alias on_csend on_send
61
+
62
+ private
63
+ DEFERRED_MACROS = %i[ test setup teardown ]
64
+
65
+ class PrivateConstantMarker
66
+ include RuboCop::Callbacksystems::Helpers
67
+
68
+ MESSAGE = "Declare `%<name>s` in the private section instead of marking it with `private_constant`."
69
+ REDUNDANT_MESSAGE = "Remove `private_constant` from `%<name>s`, which is already declared in the private section."
70
+
71
+ def initialize(node)
72
+ @node = node
73
+ end
74
+
75
+ def each_offense
76
+ movable_arguments.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
77
+ end
78
+
79
+ private
80
+ attr_reader :node
81
+
82
+ def movable_arguments
83
+ enclosing_body ? names_marked_by(node, macro: :private_constant).reject { pinned?(it) } : []
84
+ end
85
+
86
+ def enclosing_body
87
+ enclosing_class_or_module_of(node)&.body
88
+ end
89
+
90
+ def pinned?(argument)
91
+ pinning_names.include?(argument.value.to_s)
92
+ end
93
+
94
+ def pinning_names
95
+ @pinning_names ||= constant_reads.select(&:pins_declaration?).map(&:name)
96
+ end
97
+
98
+ def constant_reads
99
+ constant_nodes.map { ConstantRead.new(it, enclosing_body) }
100
+ end
101
+
102
+ def constant_nodes
103
+ nodes_in(enclosing_body, :const)
104
+ end
105
+
106
+ def message_for(argument)
107
+ format(template_for(argument.value.to_sym), name: argument.value)
108
+ end
109
+
110
+ def template_for(name)
111
+ declared_privately?(declaration_of(name)) ? REDUNDANT_MESSAGE : MESSAGE
112
+ end
113
+
114
+ def declared_privately?(declaration)
115
+ declaration && in_private_section?(declaration, enclosing_body)
116
+ end
117
+
118
+ def declaration_of(name)
119
+ statements_in(enclosing_body).find { declared_name_of(it) == name }
120
+ end
121
+ end
122
+
123
+ # A read that would break if the declaration moved, made by class-level code above the private section.
124
+ class ConstantRead
125
+ include RuboCop::Callbacksystems::Helpers
126
+
127
+ def initialize(node, body)
128
+ @node = node
129
+ @body = body
130
+ end
131
+
132
+ def pins_declaration?
133
+ own_constant? && class_level? && !in_private_section?(node, body)
134
+ end
135
+
136
+ def name
137
+ node.short_name.to_s
138
+ end
139
+
140
+ private
141
+ attr_reader :node, :body
142
+
143
+ # `Other::Namespace::NAME` reads a constant of its own that happens to end in the same name.
144
+ def own_constant?
145
+ node.namespace.nil?
146
+ end
147
+
148
+ def class_level?
149
+ !runs_later? && !definition_identifier?(node)
150
+ end
151
+
152
+ def runs_later?
153
+ enclosing_method_of(node).present? || inside_deferred_block?
154
+ end
155
+
156
+ def inside_deferred_block?
157
+ node.each_ancestor(:any_block).any? { DEFERRED_MACROS.include?(it.method_name) }
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,342 @@
1
+ # `delegate ... private: true` declares private methods, so it belongs in the
2
+ # private section next to the other private declarations. Left above, it hides
3
+ # private methods among the public API; left below the section's methods, it
4
+ # hides a declaration among behavior.
5
+ #
6
+ # @example
7
+ # # bad
8
+ # class Report
9
+ # delegate :total, to: :order, private: true
10
+ #
11
+ # def to_s
12
+ # total.to_s
13
+ # end
14
+ #
15
+ # private
16
+ # attr_reader :order
17
+ # end
18
+ #
19
+ # # bad - below the methods of its section
20
+ # class Report
21
+ # private
22
+ # def formatted_total
23
+ # total.to_s
24
+ # end
25
+ #
26
+ # delegate :total, to: :order, private: true
27
+ # end
28
+ #
29
+ # # good
30
+ # class Report
31
+ # def to_s
32
+ # total.to_s
33
+ # end
34
+ #
35
+ # private
36
+ # attr_reader :order
37
+ # delegate :total, to: :order, private: true
38
+ # end
39
+ #
40
+ class RuboCop::Cop::Callbacksystems::PrivateDelegatePlacement < RuboCop::Cop::Callbacksystems::Base
41
+ extend RuboCop::Cop::AutoCorrector
42
+
43
+ def on_send(node)
44
+ report Delegate.new(node, processed_source)
45
+ end
46
+
47
+ alias on_csend on_send
48
+
49
+ private
50
+ class Delegate
51
+ include RuboCop::Callbacksystems::Helpers
52
+
53
+ MESSAGE = "Move this `delegate` into the private section; it declares private methods."
54
+ STRAYED_MESSAGE = "Move this `delegate` up beside the other private declarations."
55
+
56
+ def initialize(node, processed_source)
57
+ @node = node
58
+ @processed_source = processed_source
59
+ end
60
+
61
+ def offense
62
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) } if misplaced?
63
+ end
64
+
65
+ private
66
+ attr_reader :node, :processed_source
67
+
68
+ def misplaced?
69
+ private_delegate? && statements.include?(node) && !placed_with_declarations?
70
+ end
71
+
72
+ def private_delegate?
73
+ macro.macro? && macro.private?
74
+ end
75
+
76
+ def macro
77
+ @macro ||= RuboCop::Callbacksystems::Methods::DelegateMacro.new(node)
78
+ end
79
+
80
+ def statements
81
+ @statements ||= statements_in(enclosing_body)
82
+ end
83
+
84
+ def enclosing_body
85
+ @enclosing_body ||= enclosing_definition_of(node)&.body
86
+ end
87
+
88
+ def placed_with_declarations?
89
+ declarations_after_private_modifier.include?(node)
90
+ end
91
+
92
+ def declarations_after_private_modifier
93
+ statements_after_private_modifier.take_while { declaration?(it) }
94
+ end
95
+
96
+ def statements_after_private_modifier
97
+ private_modifier ? statements.drop(statements.index(private_modifier) + 1) : []
98
+ end
99
+
100
+ def private_modifier
101
+ @private_modifier ||= private_modifier_in(enclosing_body)
102
+ end
103
+
104
+ def declaration?(statement)
105
+ statement.casgn_type? || declaration_macro?(statement)
106
+ end
107
+
108
+ def message
109
+ outside_private_section? ? MESSAGE : STRAYED_MESSAGE
110
+ end
111
+
112
+ def outside_private_section?
113
+ visibility_at(node, enclosing_body) != :private
114
+ end
115
+
116
+ def correctable?
117
+ movement_safe? && relocation_safe?
118
+ end
119
+
120
+ def movement_safe?
121
+ !blocked_by_constant? && !blocked_by_method_definition? && !blocked_by_comment? &&
122
+ !blocked_by_executable_statement? && tooling_scope_kept?
123
+ end
124
+
125
+ # Moving up over the assignment of a constant the delegate reads would break the class at load time.
126
+ def blocked_by_constant?
127
+ referenced_constant_names.intersect?(constant_names_crossed_moving_up)
128
+ end
129
+
130
+ def referenced_constant_names
131
+ node.arguments.flat_map { it.each_node(:const).map(&:short_name) }
132
+ end
133
+
134
+ def constant_names_crossed_moving_up
135
+ if anchor_index && anchor_index < node_index
136
+ statements[(anchor_index + 1)...node_index].select(&:casgn_type?).map(&:name)
137
+ else
138
+ []
139
+ end
140
+ end
141
+
142
+ def anchor_index
143
+ statements.index(anchor)
144
+ end
145
+
146
+ def anchor
147
+ declarations_after_private_modifier.last || private_modifier || statements.excluding(node).last
148
+ end
149
+
150
+ def node_index
151
+ statements.index(node)
152
+ end
153
+
154
+ # Swapping two declarations of the same method would change which implementation the class keeps.
155
+ def blocked_by_method_definition?
156
+ names = macro.defined_method_names&.to_set
157
+ crossed_names = method_names_defined_by(crossed_statements)
158
+
159
+ names.nil? || crossed_names.nil? || names.intersect?(crossed_names)
160
+ end
161
+
162
+ def method_names_defined_by(found)
163
+ names = found.map { RuboCop::Callbacksystems::Methods::StatementDefinitions.new(it).names }
164
+
165
+ names.flat_map(&:to_a).to_set if names.none?(&:nil?)
166
+ end
167
+
168
+ def crossed_statements
169
+ if anchor_index
170
+ if node_index < anchor_index
171
+ statements[(node_index + 1)..anchor_index]
172
+ else
173
+ statements[(anchor_index + 1)...node_index]
174
+ end
175
+ else
176
+ []
177
+ end
178
+ end
179
+
180
+ def blocked_by_comment?
181
+ movement_blocks.each_cons(2).any? do |above, below|
182
+ above.range.end.join(below.range.begin).source.strip.present?
183
+ end
184
+ end
185
+
186
+ def movement_blocks
187
+ movement_statements.map { RuboCop::Callbacksystems::Source::StatementWithComments.new(it, processed_source) }
188
+ end
189
+
190
+ def movement_statements
191
+ if anchor_index
192
+ first, last = [ node_index, anchor_index ].minmax
193
+ statements[first..last]
194
+ else
195
+ [ node ]
196
+ end
197
+ end
198
+
199
+ # Method bodies do not run while the class loads. Unknown statements do, so their relative order is kept.
200
+ def blocked_by_executable_statement?
201
+ crossed_statements.any? { !movable_across?(it) }
202
+ end
203
+
204
+ def movable_across?(statement)
205
+ statement.type?(:def, :defs) || visibility_modifier_of(statement) || declaration_macro?(statement)
206
+ end
207
+
208
+ def tooling_scope_kept?
209
+ movement_blocks.none?(&:contains_tooling_comment?)
210
+ end
211
+
212
+ def relocation_safe?
213
+ relocation_ranges_proven? && relocated_source.possible?
214
+ end
215
+
216
+ def relocation_ranges_proven?
217
+ statement_owns_line?(node) && (!anchor || statement_owns_line?(anchor))
218
+ end
219
+
220
+ def statement_owns_line?(statement)
221
+ StatementLine.new(statement).owned?
222
+ end
223
+
224
+ def relocated_source
225
+ @relocated_source ||= RelocatedSource.new(block, to: section_indentation)
226
+ end
227
+
228
+ def block
229
+ @block ||= RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source)
230
+ end
231
+
232
+ def section_indentation
233
+ statements_after_private_modifier.first&.then { indentation_of(it) } ||
234
+ "#{body_indentation}#{indentation_step}"
235
+ end
236
+
237
+ def body_indentation
238
+ indentation_of(statements.first)
239
+ end
240
+
241
+ def indentation_step
242
+ " " * (statements.first.source_range.column - enclosing_definition_of(node).source_range.column)
243
+ end
244
+
245
+ def correct(corrector)
246
+ if anchor
247
+ corrector.insert_after(anchor_block.range, relocated_delegate)
248
+ corrector.remove(statement_removal_range_of(block.range))
249
+ else
250
+ corrector.replace(block.range, private_section)
251
+ end
252
+ end
253
+
254
+ def anchor_block
255
+ @anchor_block ||= RuboCop::Callbacksystems::Source::StatementWithComments.new(anchor, processed_source)
256
+ end
257
+
258
+ def relocated_delegate
259
+ private_modifier ? indented_delegate : "\n\n#{body_indentation}private#{indented_delegate}"
260
+ end
261
+
262
+ def indented_delegate
263
+ "\n#{relocated_source.source}"
264
+ end
265
+
266
+ def private_section
267
+ "#{body_indentation}private#{indented_delegate}"
268
+ end
269
+ end
270
+
271
+ # Whether a statement range can be moved without consuming code that shares either edge line.
272
+ class StatementLine
273
+ def initialize(node)
274
+ @node = node
275
+ end
276
+
277
+ def owned?
278
+ code_before.strip.empty? && (code_after.strip.empty? || code_after.lstrip.start_with?("#"))
279
+ end
280
+
281
+ private
282
+ attr_reader :node
283
+
284
+ def code_before
285
+ node.source_range.source_line[0...node.source_range.column]
286
+ end
287
+
288
+ def code_after
289
+ node.source_range.end.source_line[node.source_range.end.column..]
290
+ end
291
+ end
292
+
293
+ # Source shifted by one indentation delta, with relative whitespace and heredoc contents left intact.
294
+ class RelocatedSource
295
+ def initialize(statement, to:)
296
+ @statement = statement
297
+ @target_indentation = to
298
+ end
299
+
300
+ def possible?
301
+ indentation_removable? && heredocs_preserved?
302
+ end
303
+
304
+ def source
305
+ lines.map { reindented(it) }.join
306
+ end
307
+
308
+ private
309
+ attr_reader :statement, :target_indentation
310
+
311
+ def indentation_removable?
312
+ delta >= 0 || lines.all? { it.strip.empty? || it.start_with?(removed_indentation) }
313
+ end
314
+
315
+ def delta
316
+ target_indentation.length - statement.node.source_range.column
317
+ end
318
+
319
+ def lines
320
+ @lines ||= statement.source.lines
321
+ end
322
+
323
+ def removed_indentation
324
+ " " * -delta
325
+ end
326
+
327
+ def heredocs_preserved?
328
+ delta.zero? || heredocs.all? { it.source.start_with?("<<~") }
329
+ end
330
+
331
+ def heredocs
332
+ statement.node.each_node(:str, :dstr, :xstr).select { it.loc?(:heredoc_body) }
333
+ end
334
+
335
+ def reindented(line)
336
+ if line.strip.empty? then line
337
+ elsif delta.positive? then "#{" " * delta}#{line}"
338
+ else line.delete_prefix(removed_indentation)
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,70 @@
1
+ # A method you define goes in the private section, not above it with a
2
+ # `private :name` afterwards. The section already says what the marker repeats.
3
+ #
4
+ # The marker earns its place when the method is not defined here, since a
5
+ # section cannot reach a method that arrives from a superclass or a macro.
6
+ # `Data.define` generating readers you would rather hide is the usual case.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class Report
11
+ # def total
12
+ # end
13
+ # private :total
14
+ # end
15
+ #
16
+ # # good
17
+ # class Report
18
+ # private
19
+ # def total
20
+ # end
21
+ # end
22
+ #
23
+ # # good - the reader comes from the superclass, so nothing here can hold it
24
+ # class Request < Data.define(:lock_held)
25
+ # alias lock_held? lock_held
26
+ #
27
+ # private :lock_held
28
+ # end
29
+ #
30
+ class RuboCop::Cop::Callbacksystems::PrivateMethodsInPrivateSection < RuboCop::Cop::Callbacksystems::Base
31
+ def on_send(node)
32
+ report_each PrivateCall.new(node)
33
+ end
34
+
35
+ alias on_csend on_send
36
+
37
+ private
38
+ class PrivateCall
39
+ include RuboCop::Callbacksystems::Helpers
40
+
41
+ MESSAGE = "Declare `%<name>s` in the private section instead of marking it with `private :%<name>s`."
42
+
43
+ def initialize(node)
44
+ @node = node
45
+ end
46
+
47
+ def each_offense
48
+ movable_arguments.each { yield RuboCop::Callbacksystems::Offense.new(it, format(MESSAGE, name: it.value)) }
49
+ end
50
+
51
+ private
52
+ attr_reader :node
53
+
54
+ def movable_arguments
55
+ names_marked_by(node, macro: :private).select { defined_here?(it) }
56
+ end
57
+
58
+ def defined_here?(argument)
59
+ method_names.include?(argument.value.to_sym)
60
+ end
61
+
62
+ def method_names
63
+ @method_names ||= direct_definitions_in(enclosing_body, :def).map(&:method_name)
64
+ end
65
+
66
+ def enclosing_body
67
+ enclosing_definition_of(node)&.body
68
+ end
69
+ end
70
+ end