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,93 @@
1
+ # Ensures methods that enqueue jobs follow the `_later` naming convention.
2
+ # This cop only flags when there's a corresponding method that does the actual work.
3
+ #
4
+ # The convention is to have paired methods:
5
+ # - `process` - does the actual work
6
+ # - `process_later` - enqueues a job that calls `process`
7
+ #
8
+ # @example
9
+ # # bad - work method exists, job method should be named process_later
10
+ # def enqueue_process
11
+ # ProcessJob.perform_later(self)
12
+ # end
13
+ #
14
+ # def process
15
+ # # actual work
16
+ # end
17
+ #
18
+ # # good - follows naming convention
19
+ # def process_later
20
+ # ProcessJob.perform_later(self)
21
+ # end
22
+ #
23
+ # def process
24
+ # # actual work
25
+ # end
26
+ #
27
+ # # good - no corresponding work method, so no flag
28
+ # def notify
29
+ # NotificationJob.perform_later(self)
30
+ # end
31
+ #
32
+ class RuboCop::Cop::Callbacksystems::JobEnqueueMethods < RuboCop::Cop::Callbacksystems::Base
33
+ def on_new_investigation
34
+ @sibling_methods = RuboCop::Callbacksystems::Methods::Siblings.new
35
+ end
36
+
37
+ def on_def(node)
38
+ report EnqueueMethod.new(node, sibling_methods:) unless node.method_name.to_s.end_with?("_later")
39
+ end
40
+
41
+ alias on_defs on_def
42
+
43
+ private
44
+ attr_reader :sibling_methods
45
+
46
+ # A method enqueuing a job, read for the work method its job is named after.
47
+ class EnqueueMethod
48
+ include RuboCop::Callbacksystems::Helpers
49
+
50
+ MESSAGE = "Rename `%<name>s` to `%<expected>s` to follow the _later convention."
51
+
52
+ def initialize(node, sibling_methods:)
53
+ @node = node
54
+ @sibling_methods = sibling_methods
55
+ end
56
+
57
+ def offense
58
+ RuboCop::Callbacksystems::Offense.new(node, message) if pairs_with_work_method?
59
+ end
60
+
61
+ private
62
+ attr_reader :node, :sibling_methods
63
+
64
+ def pairs_with_work_method?
65
+ base_name && sibling_methods.named(base_name, beside: node).any?
66
+ end
67
+
68
+ def base_name
69
+ @base_name ||= job_class_name.delete_suffix("Job").underscore.to_sym if job_class_name.end_with?("Job")
70
+ end
71
+
72
+ def job_class_name
73
+ @job_class_name ||= class_name_of(perform_later_call&.receiver).to_s
74
+ end
75
+
76
+ def class_name_of(receiver)
77
+ case receiver&.type
78
+ when :const then receiver.short_name
79
+ when :send then receiver.method_name
80
+ end
81
+ end
82
+
83
+ def perform_later_call
84
+ RuboCop::Callbacksystems::Execution::Immediate.new(node.body).find do |candidate|
85
+ candidate.call_type? && candidate.method?(:perform_later)
86
+ end
87
+ end
88
+
89
+ def message
90
+ format(MESSAGE, name: node.method_name, expected: :"#{base_name}_later")
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,118 @@
1
+ # Deeply chained safe navigation (`a&.b&.c&.d&.e`) signals deep coupling or
2
+ # poor data modeling. Refactor: pre-validate, restructure the object, or
3
+ # extract an intermediate.
4
+ #
5
+ # @example MaxDepth: 3 (default)
6
+ # # bad - four safe-navigation links
7
+ # value = account&.owner&.address&.city
8
+ #
9
+ # # good - extract an intermediate
10
+ # owner = account&.owner
11
+ # value = owner&.address&.city
12
+ #
13
+ class RuboCop::Cop::Callbacksystems::MaxSafeNavigationDepth < RuboCop::Cop::Callbacksystems::Base
14
+ def on_new_investigation
15
+ @navigation_context = NavigationContext.new
16
+ end
17
+
18
+ def on_csend(node)
19
+ report NavigationChain.new(node, navigation_context, max_depth: cop_config["MaxDepth"])
20
+ end
21
+
22
+ private
23
+ attr_reader :navigation_context
24
+
25
+ # The chain a safe-navigation call sits in, measured once from its outermost link so every chain is reported once.
26
+ class NavigationChain
27
+ include RuboCop::Callbacksystems::Helpers
28
+
29
+ MESSAGE = "Safe-navigation depth %<depth>d exceeds maximum %<maximum>d. Restructure or extract intermediates."
30
+
31
+ def initialize(node, navigation_context, max_depth:)
32
+ @node = node
33
+ @navigation_context = navigation_context
34
+ @max_depth = max_depth
35
+ end
36
+
37
+ def offense
38
+ RuboCop::Callbacksystems::Offense.new(node, message) if outermost?(node) && depth > max_depth
39
+ end
40
+
41
+ private
42
+ attr_reader :node, :navigation_context, :max_depth
43
+ delegate :outermost?, :top_of, to: :navigation_context, private: true
44
+
45
+ # Every link in the chain counts, not just a consecutive run (`a&.b.c&.d` is depth 2), matching the JS rule.
46
+ def depth
47
+ @depth ||= links_down_from(top_of(node)).count(&:csend_type?)
48
+ end
49
+
50
+ def links_down_from(link)
51
+ if block_given?
52
+ while link&.call_type?
53
+ yield link
54
+ link = link.receiver
55
+ end
56
+ else
57
+ to_enum(__method__, link)
58
+ end
59
+ end
60
+
61
+ def message
62
+ format(MESSAGE, depth:, maximum: max_depth)
63
+ end
64
+ end
65
+
66
+ # Receiver-chain ancestry shared by every safe link in one investigation, with iterative path compression.
67
+ class NavigationContext
68
+ Facts = Data.define(:top, :safe_link)
69
+ NO_FACTS = Facts.new(nil, nil)
70
+
71
+ def initialize
72
+ @facts_by_link = {}.compare_by_identity
73
+ end
74
+
75
+ def outermost?(node)
76
+ safe_link_at_or_above(chained_parent_of(node)).nil?
77
+ end
78
+
79
+ def top_of(node)
80
+ facts_for(node).top
81
+ end
82
+
83
+ private
84
+ attr_reader :facts_by_link
85
+
86
+ def safe_link_at_or_above(node)
87
+ node&.then { facts_for(it).safe_link }
88
+ end
89
+
90
+ def facts_for(node)
91
+ path = uncached_path_from(node)
92
+ facts = inherited_facts_of(path)
93
+ path.reverse_each do |link|
94
+ facts = Facts.new(facts.top || link, link.csend_type? ? link : facts.safe_link)
95
+ facts_by_link[link] = facts
96
+ end
97
+ facts_by_link.fetch(node)
98
+ end
99
+
100
+ def uncached_path_from(node)
101
+ [].then do |path|
102
+ while node && !facts_by_link.key?(node)
103
+ path << node
104
+ node = chained_parent_of(node)
105
+ end
106
+ path
107
+ end
108
+ end
109
+
110
+ def chained_parent_of(link)
111
+ link.parent&.then { it if it.call_type? && it.receiver.equal?(link) }
112
+ end
113
+
114
+ def inherited_facts_of(path)
115
+ path.last&.then { facts_by_link.fetch(chained_parent_of(it), NO_FACTS) } || NO_FACTS
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,261 @@
1
+ # Ensures methods are ordered by invocation: callers before callees, and a
2
+ # caller's callees in the order it first invokes them (depth-first). Public
3
+ # methods are ordered before private ones. A macro, block, or lambda that names a
4
+ # method (a callback, a delegate target, a validation) calls it too, and it reads
5
+ # at the top of the class, so a method reached only that way leads its visibility
6
+ # group in the order the macros mention it (a guard before its action, since the
7
+ # guard is evaluated first). A method a real method body calls follows that caller
8
+ # instead.
9
+ #
10
+ # The actions of a controller are the exception: `Rails/ActionOrder` owns their
11
+ # order, so they lead their group in the order that cop asks for and the walk
12
+ # places everything else. A macro naming an action (a `rate_limit :create`) would
13
+ # otherwise pull it to the top and the two cops would undo each other forever.
14
+ #
15
+ # @example
16
+ # # bad - called method defined before caller
17
+ # def helper
18
+ # end
19
+ #
20
+ # def process
21
+ # helper
22
+ # end
23
+ #
24
+ # # bad - sibling callees defined out of invocation order
25
+ # def process
26
+ # helper_b
27
+ # helper_a
28
+ # end
29
+ #
30
+ # def helper_a
31
+ # end
32
+ #
33
+ # def helper_b
34
+ # end
35
+ #
36
+ # # good - caller before callee, callees in first-invocation order
37
+ # def process
38
+ # helper_a
39
+ # helper_b
40
+ # end
41
+ #
42
+ # def helper_a
43
+ # end
44
+ #
45
+ # def helper_b
46
+ # end
47
+ #
48
+ # # good - with private section
49
+ # def process
50
+ # helper
51
+ # end
52
+ #
53
+ # private
54
+ # def helper
55
+ # end
56
+ #
57
+ # # good - a callback method leads: its macro calls it and reads at the top
58
+ # after_commit :notify_later
59
+ #
60
+ # def notify_later
61
+ # NotifyJob.perform_later(self)
62
+ # end
63
+ #
64
+ # def process
65
+ # compute
66
+ # end
67
+ #
68
+ # def compute
69
+ # end
70
+ #
71
+ # # good - `Rails/ActionOrder` places the actions, whatever a macro names
72
+ # rate_limit :create, to: 10, within: 30.minutes
73
+ #
74
+ # def new
75
+ # end
76
+ #
77
+ # def create
78
+ # end
79
+ #
80
+ class RuboCop::Cop::Callbacksystems::MethodInvocationOrder < RuboCop::Cop::Callbacksystems::Base
81
+ extend RuboCop::Cop::AutoCorrector
82
+
83
+ def on_class(node)
84
+ analyze(node, direct_definitions_in(node.body, :def), macro_references: true, actions: actions_of(node))
85
+ analyze(node, direct_definitions_in(node.body, :defs), macro_references: false, actions: [])
86
+ end
87
+
88
+ alias on_module on_class
89
+
90
+ def on_sclass(node)
91
+ analyze(node, direct_definitions_in(node.body, :def), macro_references: true, actions: [])
92
+ end
93
+
94
+ private
95
+ def analyze(node, methods, macro_references:, actions:)
96
+ report_each InvocationOrder.new(processed_source, node, methods, macro_references:, actions:)
97
+ end
98
+
99
+ # Read from `Rails/ActionOrder`, since two cops ordering the same methods by different rules undo each other.
100
+ def actions_of(node)
101
+ controller_class?(node) ? expected_action_order : []
102
+ end
103
+
104
+ def expected_action_order
105
+ if config.cop_enabled?("Rails/ActionOrder")
106
+ Array(config.for_cop("Rails/ActionOrder")["ExpectedOrder"]).map(&:to_sym)
107
+ else
108
+ []
109
+ end
110
+ end
111
+
112
+ class InvocationOrder
113
+ include RuboCop::Callbacksystems::Helpers
114
+
115
+ MESSAGE = "Method `%<expected>s` should be defined before `%<actual>s` to keep callers before callees."
116
+
117
+ def initialize(processed_source, node, methods, macro_references:, actions:)
118
+ @processed_source = processed_source
119
+ @node = node
120
+ @methods = methods
121
+ @macro_references = macro_references
122
+ @actions = actions
123
+ end
124
+
125
+ def each_offense
126
+ yield offense if analyzable? && divergence.found?
127
+ end
128
+
129
+ private
130
+ attr_reader :processed_source, :node, :methods, :macro_references, :actions
131
+
132
+ def analyzable?
133
+ node.body && methods.size >= 2 && method_names.uniq.size == method_names.size
134
+ end
135
+
136
+ def method_names
137
+ @method_names ||= methods.map(&:method_name)
138
+ end
139
+
140
+ def divergence
141
+ @divergence ||= RuboCop::Callbacksystems::ClassStructure::Divergence.new(method_names, from: canonical_order)
142
+ end
143
+
144
+ def canonical_order
145
+ @canonical_order ||= LEVELS.flat_map { ordered_by_visibility(it) }
146
+ end
147
+
148
+ def ordered_by_visibility(visibility)
149
+ leading_first(full_order.select { visibilities[it] == visibility })
150
+ end
151
+
152
+ def leading_first(names)
153
+ leading = leading_order.select { names.include?(it) }
154
+
155
+ leading + (names - leading)
156
+ end
157
+
158
+ def leading_order
159
+ [ :initialize, *actions ]
160
+ end
161
+
162
+ def full_order
163
+ @full_order ||= RuboCop::Callbacksystems::ClassStructure::ReferenceOrder.new(ordering_seeds, call_graph).to_a
164
+ end
165
+
166
+ def ordering_seeds
167
+ macro_led + real_roots + method_names
168
+ end
169
+
170
+ def macro_led
171
+ if macro_references
172
+ reference_order.reject { called_methods.include?(it) }
173
+ else
174
+ []
175
+ end
176
+ end
177
+
178
+ def reference_order
179
+ @reference_order ||= macro_methods.select { known_method_names.include?(it) }
180
+ end
181
+
182
+ def macro_methods
183
+ @macro_methods ||= RuboCop::Callbacksystems::Methods::MacroReferences.new(node.body)
184
+ end
185
+
186
+ def known_method_names
187
+ @known_method_names ||= Set.new(method_names)
188
+ end
189
+
190
+ def called_methods
191
+ @called_methods ||= Set.new(call_graph.values.flatten)
192
+ end
193
+
194
+ def call_graph
195
+ @call_graph ||= methods.to_h { [ it.method_name, calls_from(it) ] }
196
+ end
197
+
198
+ def calls_from(method)
199
+ if method.body
200
+ receiverless_method_names_in(method.body).uniq.select { places?(method.method_name, it) }
201
+ else
202
+ []
203
+ end
204
+ end
205
+
206
+ # A private caller places no public callee, since the public methods lead whatever reads them from below.
207
+ def places?(caller_name, callee_name)
208
+ known_method_names.include?(callee_name) && level_of(callee_name) >= level_of(caller_name)
209
+ end
210
+
211
+ def level_of(name)
212
+ LEVELS.index(visibilities[name])
213
+ end
214
+
215
+ def visibilities
216
+ @visibilities ||= methods.to_h { [ it.method_name, visibility_of(it) ] }
217
+ end
218
+
219
+ def real_roots
220
+ method_names.reject { called_methods.include?(it) }
221
+ end
222
+
223
+ def offense
224
+ RuboCop::Callbacksystems::Offense.new(methods_by_name[divergence.expected], message,
225
+ correcting: method_runs.any?(&:reorderable?)) { reorder(it) }
226
+ end
227
+
228
+ def methods_by_name
229
+ @methods_by_name ||= methods.index_by(&:method_name)
230
+ end
231
+
232
+ def message
233
+ format(MESSAGE, expected: divergence.expected, actual: divergence.actual)
234
+ end
235
+
236
+ def method_runs
237
+ @method_runs ||= runs_in(node.body) { methods.include?(it) }.filter_map { run_of(it) }
238
+ end
239
+
240
+ def run_of(members)
241
+ statements = statements_of(members)
242
+
243
+ if uninterrupted?(statements)
244
+ RuboCop::Callbacksystems::ClassStructure::StatementRun.new(statements, order: canonical_order) { it.node.method_name }
245
+ end
246
+ end
247
+
248
+ def statements_of(members)
249
+ members.map { RuboCop::Callbacksystems::Source::StatementWithComments.new(it, processed_source) }
250
+ end
251
+
252
+ # A comment unattached to either method is an intentional boundary, not source for a sortable run.
253
+ def uninterrupted?(statements)
254
+ statements.each_cons(2).none? { |above, below| above.range.end.join(below.range.begin).source.strip.present? }
255
+ end
256
+
257
+ def reorder(corrector)
258
+ method_runs.each { it.rewrite(corrector) }
259
+ end
260
+ end
261
+ end