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,81 @@
1
+ # `delegate` does not inherit the visibility of the surrounding section.
2
+ # A `delegate` placed under a `private` keyword still defines public methods
3
+ # unless `private: true` is passed explicitly.
4
+ #
5
+ # @example
6
+ # # bad - `body` ends up public despite the surrounding `private`
7
+ # class Foo
8
+ # def initialize(node)
9
+ # @node = node
10
+ # end
11
+ #
12
+ # private
13
+ # attr_reader :node
14
+ # delegate :body, to: :node
15
+ # end
16
+ #
17
+ # # good - opts into private visibility explicitly
18
+ # class Foo
19
+ # def initialize(node)
20
+ # @node = node
21
+ # end
22
+ #
23
+ # private
24
+ # attr_reader :node
25
+ # delegate :body, to: :node, private: true
26
+ # end
27
+ #
28
+ class RuboCop::Cop::Callbacksystems::DelegateRequiresPrivateOption < RuboCop::Cop::Callbacksystems::Base
29
+ extend RuboCop::Cop::AutoCorrector
30
+
31
+ def on_send(node)
32
+ report DelegateCall.new(node)
33
+ end
34
+
35
+ alias on_csend on_send
36
+
37
+ private
38
+ class DelegateCall
39
+ include RuboCop::Callbacksystems::Helpers
40
+
41
+ MESSAGE = "`delegate` under a `private` section must pass `private: true`; visibility is not inherited " \
42
+ "automatically."
43
+
44
+ def initialize(node)
45
+ @node = node
46
+ end
47
+
48
+ def offense
49
+ if missing_private_option?
50
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: macro.private_option_known?) { correct(it) }
51
+ end
52
+ end
53
+
54
+ private
55
+ attr_reader :node
56
+
57
+ def missing_private_option?
58
+ macro.macro? && !macro.private? && inside_private_section?
59
+ end
60
+
61
+ def macro
62
+ @macro ||= RuboCop::Callbacksystems::Methods::DelegateMacro.new(node)
63
+ end
64
+
65
+ def inside_private_section?
66
+ enclosing_body && visibility_at(node, enclosing_body) == :private
67
+ end
68
+
69
+ def enclosing_body
70
+ @enclosing_body ||= enclosing_body_for(node)
71
+ end
72
+
73
+ def correct(corrector)
74
+ if macro.private_option
75
+ replace_expression(corrector, macro.private_option.value, with: "true")
76
+ else
77
+ corrector.insert_after(macro.last_option, ", private: true")
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,386 @@
1
+ # Ensures eager loading scopes follow naming conventions. A scope named for the
2
+ # associations it loads (`with_posts`) reads at the call site as what the query
3
+ # carries, while one named for the page that uses it (`with_show_data`) ties the
4
+ # model to a controller, and one named for a generic term (`with_associations`)
5
+ # says nothing about what gets loaded.
6
+ # The `with_*` prefix is reserved for eager loading. A scope whose body only
7
+ # filters or orders records needs a name describing that condition instead.
8
+ # Composed scopes and dynamic query builders stay out when their eager loading
9
+ # cannot be determined from the body.
10
+ # Default scopes may come from ancestors or concerns; unreadable ancestry leaves
11
+ # the reverse naming check undecided while explicit eager loading is still read.
12
+ # - Must use `with_*` prefix for any of includes, preload, or eager_load
13
+ # - Must not contain controller action names (index, show, etc.)
14
+ # - Must not contain generic terms (associations, relations)
15
+ #
16
+ # @example
17
+ # # bad - missing with_ prefix
18
+ # scope :including_posts, -> { includes(:posts) }
19
+ #
20
+ # # bad - contains controller action
21
+ # scope :with_show_data, -> { includes(:items) }
22
+ #
23
+ # # bad - contains generic term
24
+ # scope :with_associations, -> { includes(:items) }
25
+ #
26
+ # # bad - with_ promises eager loading, not a filter
27
+ # scope :with_active, -> { where(active: true) }
28
+ #
29
+ # # good
30
+ # scope :with_posts, -> { includes(:posts) }
31
+ # scope :with_line_items, -> { includes(:line_items) }
32
+ # scope :active, -> { where(active: true) }
33
+ #
34
+ class RuboCop::Cop::Callbacksystems::EagerLoadingScopeNaming < RuboCop::Cop::Callbacksystems::Base
35
+ include RuboCop::Callbacksystems::ProjectIndex::Support
36
+
37
+ def on_new_investigation
38
+ @default_scopes = if project_index.nil? || project_index_reliable?
39
+ DefaultScopes.new(processed_source, project_index:)
40
+ end
41
+ end
42
+
43
+ def on_send(node)
44
+ report ScopeName.new(node, default_scopes:) if scope_definition?(node)
45
+ end
46
+
47
+ alias on_csend on_send
48
+
49
+ private
50
+ attr_reader :default_scopes
51
+
52
+ # The name a `scope` declares, read against the association its body eager loads.
53
+ class ScopeName
54
+ include RuboCop::Callbacksystems::Helpers
55
+
56
+ PREFIX_MESSAGE = "Eager loading scopes should follow `with_*` convention. Rename `%<name>s` to " \
57
+ "`with_%<association>s`."
58
+ ACTION_MESSAGE = "Scope name `%<name>s` contains controller action `%<action>s`. Use a more descriptive name."
59
+ GENERIC_MESSAGE = "Scope name `%<name>s` contains generic term `%<term>s`. Use a more specific name like " \
60
+ "`with_%<association>s`."
61
+ RESERVED_PREFIX_MESSAGE = "Reserve `with_*` for eager loading scopes. Rename `%<name>s` to describe its query."
62
+ GENERIC_TERMS = %w[ association associations relation relations ]
63
+
64
+ def initialize(node, default_scopes:)
65
+ @node = node
66
+ @default_scopes = default_scopes
67
+ end
68
+
69
+ def offense
70
+ if messages.any?
71
+ RuboCop::Callbacksystems::Offense.new(node.first_argument, messages.join(" "))
72
+ end
73
+ end
74
+
75
+ private
76
+ attr_reader :node, :default_scopes
77
+
78
+ def messages
79
+ if association
80
+ [ prefix_message, action_message, generic_message ].compact
81
+ elsif name.start_with?("with_") && scope_body.without_eager_loading?
82
+ [ format(RESERVED_PREFIX_MESSAGE, name:) ]
83
+ else
84
+ []
85
+ end
86
+ end
87
+
88
+ def association
89
+ @association ||= scope_body.eager_loading_association
90
+ end
91
+
92
+ def scope_body
93
+ @scope_body ||= ScopeBody.new(node.arguments.second, default_scopes:)
94
+ end
95
+
96
+ def prefix_message
97
+ format(PREFIX_MESSAGE, name:, association:) unless name.start_with?("with_")
98
+ end
99
+
100
+ def name
101
+ node.first_argument.value.to_s
102
+ end
103
+
104
+ def action_message
105
+ format(ACTION_MESSAGE, name:, action: controller_action) if controller_action
106
+ end
107
+
108
+ def controller_action
109
+ STANDARD_CONTROLLER_ACTIONS.find { name_parts.include?(it.to_s) }
110
+ end
111
+
112
+ def name_parts
113
+ @name_parts ||= name.split("_")
114
+ end
115
+
116
+ def generic_message
117
+ format(GENERIC_MESSAGE, name:, term: generic_term, association:) if generic_term
118
+ end
119
+
120
+ def generic_term
121
+ GENERIC_TERMS.find { name_parts.include?(it) }
122
+ end
123
+ end
124
+
125
+ class ScopeBody
126
+ include RuboCop::Callbacksystems::Helpers
127
+
128
+ QUERY_METHODS = %i[
129
+ all distinct group having invert_where joins left_joins left_outer_joins limit none offset order
130
+ readonly references reorder reselect reverse_order rewhere select strict_loading unscope where
131
+ ]
132
+
133
+ def initialize(body, default_scopes:)
134
+ @body = body
135
+ @default_scopes = default_scopes
136
+ end
137
+
138
+ def eager_loading_association
139
+ association_name_for(eager_load_call) if eager_load_call
140
+ end
141
+
142
+ def without_eager_loading?
143
+ deferred_callable_block?(body) && eager_load_call.nil? && !default_scope? && plain_query?(scope_body.body)
144
+ end
145
+
146
+ private
147
+ attr_reader :body, :default_scopes
148
+
149
+ def eager_load_call
150
+ return @eager_load_call if defined?(@eager_load_call)
151
+
152
+ @eager_load_call = RuboCop::Callbacksystems::Execution::Immediate.new(scope_body&.body).find do |candidate|
153
+ candidate.call_type? && EAGER_LOADING_METHODS.include?(candidate.method_name)
154
+ end
155
+ end
156
+
157
+ def scope_body
158
+ body if any_block_type?(body)
159
+ end
160
+
161
+ def association_name_for(eager_load_node)
162
+ names = eager_load_node.arguments.flat_map { AssociationNames.new(it).to_a }
163
+ names.join("_and_") if names.any?
164
+ end
165
+
166
+ def default_scope?
167
+ default_scopes.nil? || default_scopes.possible_for?(body)
168
+ end
169
+
170
+ # A named scope, a merge, or a dynamic builder can carry eager loading from another declaration.
171
+ def plain_query?(expression)
172
+ expression&.call_type? && query_method?(expression) &&
173
+ (call_on_self?(expression) || plain_query?(expression.receiver))
174
+ end
175
+
176
+ def query_method?(expression)
177
+ QUERY_METHODS.include?(expression.method_name) || where_negation?(expression)
178
+ end
179
+
180
+ def where_negation?(expression)
181
+ expression.method?(:not) && expression.receiver.call_type? && expression.receiver.method?(:where) &&
182
+ expression.receiver.arguments.empty?
183
+ end
184
+ end
185
+
186
+ # Absence is trusted only in readable declarations and their known ancestry; a model's defaults may live elsewhere.
187
+ class DefaultScopes
188
+ include RuboCop::Callbacksystems::Helpers
189
+ include RuboCop::Cop::ProjectIndexHelp
190
+
191
+ def initialize(processed_source, project_index:)
192
+ @processed_source = processed_source
193
+ @project_index = project_index
194
+ @sources = {}
195
+ @possibilities = {}.compare_by_identity
196
+ end
197
+
198
+ def possible_for?(node)
199
+ owner = enclosing_class_or_module_of(node)
200
+
201
+ owner.nil? || node.each_ancestor.take_while { !it.equal?(owner) }.any? { any_block_type?(it) } ||
202
+ possibilities.fetch(owner) { possibilities[owner] = possible_in?(owner) }
203
+ end
204
+
205
+ private
206
+ attr_reader :processed_source, :project_index, :sources, :possibilities
207
+
208
+ def possible_in?(owner)
209
+ return true if owner.module_type?
210
+
211
+ declarations = if project_index
212
+ indexed_declarations_for(owner)
213
+ else
214
+ local_declarations_for(owner)
215
+ end
216
+
217
+ declarations.nil? || declarations.any? { it.default_scope? || !it.known_ancestry? }
218
+ end
219
+
220
+ def indexed_declarations_for(owner)
221
+ namespace = resolve_constant_in_index(owner.identifier)
222
+
223
+ if namespace.is_a?(Rubydex::Namespace) && namespace.definitions.any?
224
+ definitions_for(namespace).map { declaration_for(it) }.then do |declarations|
225
+ declarations.presence if declarations.exclude?(nil)
226
+ end
227
+ end
228
+ end
229
+
230
+ def definitions_for(namespace)
231
+ ancestors = [ *namespace.ancestors, *namespace.singleton_class.ancestors ].map do |ancestor|
232
+ ancestor.is_a?(Rubydex::SingletonClass) ? ancestor.attached_class : ancestor
233
+ end
234
+
235
+ ancestors.uniq(&:name).flat_map { it.definitions.to_a }.select do |definition|
236
+ definition.location.uri.start_with?(FILE_URI_PREFIX)
237
+ end
238
+ end
239
+
240
+ def declaration_for(definition)
241
+ source_for(definition.location)&.declaration_at(definition.location)
242
+ end
243
+
244
+ def source_for(location)
245
+ path = location.to_file_path
246
+
247
+ sources.fetch(path) do
248
+ source = if File.expand_path(path) == File.expand_path(processed_source.file_path)
249
+ processed_source
250
+ else
251
+ RuboCop::Callbacksystems::Source::FileAst
252
+ .processed_source(path, ruby_version: processed_source.ruby_version)
253
+ end
254
+ sources[path] = Source.new(source, project_index:) if source
255
+ end
256
+ end
257
+
258
+ def local_declarations_for(owner)
259
+ candidates = nodes_in(processed_source.ast, :class, :module).select do |candidate|
260
+ candidate.identifier.short_name == owner.identifier.short_name
261
+ end
262
+
263
+ candidates.map { Declaration.new(it, project_index:) }
264
+ end
265
+
266
+ class Source
267
+ include RuboCop::Callbacksystems::Helpers
268
+
269
+ def initialize(processed_source, project_index:)
270
+ @declarations = nodes_in(processed_source.ast, :class, :module).to_h do |node|
271
+ location = RuboCop::Callbacksystems::ProjectIndex::Location.for_ast \
272
+ node.source_range, uri: RuboCop::Callbacksystems::ProjectIndex::SourceFile.new(processed_source.file_path).uri
273
+ [ location, Declaration.new(node, project_index:) ]
274
+ end
275
+ end
276
+
277
+ def declaration_at(location)
278
+ declarations[RuboCop::Callbacksystems::ProjectIndex::Location.for_rubydex(location)]
279
+ end
280
+
281
+ private
282
+ attr_reader :declarations
283
+ end
284
+
285
+ class Declaration
286
+ include RuboCop::Callbacksystems::Helpers
287
+
288
+ MIXIN_METHODS = %i[ include prepend extend ]
289
+
290
+ def initialize(node, project_index:)
291
+ @node = node
292
+ @project_index = project_index
293
+ end
294
+
295
+ def default_scope?
296
+ members.any? do |member|
297
+ member.method?(:default_scope) && (member.any_def_type? || call_on_self?(member))
298
+ end
299
+ end
300
+
301
+ def known_ancestry?
302
+ ancestors.all? { Ancestor.new(it, project_index:).known? }
303
+ end
304
+
305
+ private
306
+ attr_reader :node, :project_index
307
+
308
+ def members
309
+ @members ||= nodes_in(node.body, :send, :csend, :def, :defs).select do |member|
310
+ enclosing_class_or_module_of(member).equal?(node)
311
+ end
312
+ end
313
+
314
+ def ancestors
315
+ superclass = node.parent_class if node.class_type?
316
+ mixins = members.select do |member|
317
+ member.call_type? && call_on_self?(member) && MIXIN_METHODS.include?(member.method_name)
318
+ end
319
+
320
+ [ superclass, *mixins.flat_map(&:arguments) ].compact
321
+ end
322
+ end
323
+
324
+ class Ancestor
325
+ include RuboCop::Callbacksystems::Helpers
326
+ include RuboCop::Cop::ProjectIndexHelp
327
+
328
+ FRAMEWORK_ANCESTORS = %w[ ActiveRecord::Base ActiveSupport::Concern ]
329
+
330
+ def initialize(node, project_index:)
331
+ @node = node
332
+ @project_index = project_index
333
+ end
334
+
335
+ def known?
336
+ framework? || (project_index && node.const_type? &&
337
+ resolve_constant_in_index(node).is_a?(Rubydex::Namespace))
338
+ end
339
+
340
+ private
341
+ attr_reader :node, :project_index
342
+
343
+ def framework?
344
+ FRAMEWORK_ANCESTORS.include?(constant_name_of(node)&.delete_prefix("::")) &&
345
+ (node.absolute? || lexical_nesting_of(node).empty?)
346
+ end
347
+ end
348
+ end
349
+
350
+ class AssociationNames
351
+ include Enumerable
352
+
353
+ def initialize(argument)
354
+ @argument = argument
355
+ end
356
+
357
+ def each
358
+ if block_given?
359
+ @pending = [ argument ]
360
+ until pending.empty?
361
+ advance
362
+ visit { yield it }
363
+ end
364
+ else
365
+ to_enum(__method__)
366
+ end
367
+ end
368
+
369
+ private
370
+ attr_reader :argument, :pending, :current
371
+
372
+ def advance
373
+ @current = pending.pop
374
+ end
375
+
376
+ def visit
377
+ if current.type?(:sym, :str)
378
+ yield current.value.to_s
379
+ elsif current.hash_type?
380
+ pending.concat(current.pairs.reverse.map(&:key))
381
+ elsif current.array_type?
382
+ pending.concat(current.children.reverse)
383
+ end
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,215 @@
1
+ # Prohibits early exits except for a single guard clause on the first line.
2
+ # Applies to `return` in methods and `next`/`break` in blocks. A body that exits
3
+ # in several places has to be read with every exit in mind, and the guard at the
4
+ # top is the one shape a reader takes in at a glance. Past it, a condition reads
5
+ # better as a branch wrapping what it guards.
6
+ #
7
+ # @example
8
+ # # bad - return in the middle of method
9
+ # def process(user)
10
+ # data = fetch_data
11
+ # return if data.empty?
12
+ # process_data(data)
13
+ # end
14
+ #
15
+ # # bad - multiple early returns
16
+ # def process(user)
17
+ # return unless user
18
+ # return if user.inactive?
19
+ # do_something
20
+ # end
21
+ #
22
+ # # bad - next in the middle of a block
23
+ # items.each do |item|
24
+ # process(item)
25
+ # next if item.done?
26
+ # finalize(item)
27
+ # end
28
+ #
29
+ # # bad - break in the middle of a block
30
+ # items.each do |item|
31
+ # process(item)
32
+ # break if item.last?
33
+ # end
34
+ #
35
+ # # good - single guard clause on first line
36
+ # def process(user)
37
+ # return unless user
38
+ # do_something(user)
39
+ # end
40
+ #
41
+ # # good - single next guard on first line
42
+ # items.each do |item|
43
+ # next if item.nil?
44
+ # process(item)
45
+ # end
46
+ #
47
+ class RuboCop::Cop::Callbacksystems::EarlyReturn < RuboCop::Cop::Callbacksystems::Base
48
+ def on_def(node)
49
+ report_each IllegalExits.new(node.body, :return) if node.body
50
+ end
51
+
52
+ alias on_defs on_def
53
+
54
+ def on_block(node)
55
+ return if loop_block?(node) || !node.body
56
+
57
+ %i[ next break ].each { report_each IllegalExits.new(node.body, it) }
58
+ end
59
+
60
+ alias on_numblock on_block
61
+ alias on_itblock on_block
62
+
63
+ private
64
+ # The exits a body takes past the single guard clause it is allowed.
65
+ class IllegalExits
66
+ include RuboCop::Callbacksystems::Helpers
67
+
68
+ MESSAGE = "Avoid early returns. Only a single guard clause on the first line is allowed."
69
+ BLOCK_MESSAGE = "Avoid early next/break. Only a single guard clause on the first line is allowed."
70
+
71
+ def initialize(body, exit_type)
72
+ @body = body
73
+ @exit_type = exit_type
74
+ end
75
+
76
+ def each_offense
77
+ exits.each { yield RuboCop::Callbacksystems::Offense.new(it, message) }
78
+ end
79
+
80
+ private
81
+ attr_reader :body, :exit_type
82
+
83
+ def exits
84
+ BodyExits.new(body, exit_type, walk_blocks: returning?)
85
+ .reject { it.equal?(allowed_exit) || handled_by_each_search?(it) }
86
+ end
87
+
88
+ # A method's `return` leaves the blocks written inside it, while a block's `next` or `break` stays in its own.
89
+ def returning?
90
+ exit_type == :return
91
+ end
92
+
93
+ def allowed_exit
94
+ @allowed_exit ||= Guard.new(body, exit_type).allowed_exit
95
+ end
96
+
97
+ def handled_by_each_search?(exit)
98
+ returning? && exit.each_ancestor(:any_block).any? do |block|
99
+ block.method?(:each) && block.body&.source_range&.contains?(exit.source_range) &&
100
+ conditional_return_of_block_argument?(exit, block: block)
101
+ end
102
+ end
103
+
104
+ def message
105
+ returning? ? MESSAGE : BLOCK_MESSAGE
106
+ end
107
+ end
108
+
109
+ # The exits of one node, gathered branch by branch through the nodes written inside it.
110
+ class BodyExits
111
+ include Enumerable
112
+ include RuboCop::Callbacksystems::Helpers
113
+
114
+ def initialize(node, target_type, walk_blocks:)
115
+ @node = node
116
+ @target_type = target_type
117
+ @walk_blocks = walk_blocks
118
+ end
119
+
120
+ def each
121
+ if block_given?
122
+ @pending = [ node ].compact
123
+ until pending.empty?
124
+ advance
125
+ visit { yield it }
126
+ end
127
+ else
128
+ to_enum(__method__)
129
+ end
130
+ end
131
+
132
+ private
133
+ attr_reader :node, :target_type, :walk_blocks, :pending, :current
134
+
135
+ def advance
136
+ @current = pending.pop
137
+ end
138
+
139
+ def visit
140
+ if current.type == target_type
141
+ yield current
142
+ else
143
+ pending.concat(children_to_walk.reverse)
144
+ end
145
+ end
146
+
147
+ def children_to_walk
148
+ if any_block_type?(current)
149
+ block_children
150
+ elsif lexical_definition?(current)
151
+ immediate_inputs_of(current)
152
+ elsif nested_iteration?
153
+ nested_iteration_inputs
154
+ else
155
+ current.each_child_node.to_a
156
+ end
157
+ end
158
+
159
+ # A block's receiver and arguments run in the surrounding scope even when its body is a deferred callable.
160
+ def block_children
161
+ immediate_inputs_of(current) + walked_block_body
162
+ end
163
+
164
+ def walked_block_body
165
+ walkable_block? ? [ current.body ] : []
166
+ end
167
+
168
+ def walkable_block?
169
+ walk_blocks && !deferred_callable_block?(current) && !method_definition_block?(current) &&
170
+ !loop_block?(current) && current.body
171
+ end
172
+
173
+ # A `next` or `break` inside a language loop belongs to that loop rather than the surrounding block.
174
+ def nested_iteration?
175
+ !walk_blocks && current.type?(:while, :while_post, :until, :until_post, :for)
176
+ end
177
+
178
+ # A `for` collection runs before that loop owns `next` and `break`; its body runs after.
179
+ def nested_iteration_inputs
180
+ current.for_type? ? [ current.collection ] : []
181
+ end
182
+ end
183
+
184
+ class Guard
185
+ include RuboCop::Callbacksystems::Helpers
186
+
187
+ def initialize(body, exit_type)
188
+ @body = body
189
+ @exit_type = exit_type
190
+ end
191
+
192
+ def allowed_exit
193
+ if_guard || unless_guard if first_statement&.if_type?
194
+ end
195
+
196
+ private
197
+ attr_reader :body, :exit_type
198
+
199
+ def first_statement
200
+ @first_statement ||= first_statement_in(body)
201
+ end
202
+
203
+ def if_guard
204
+ first_statement.if_branch if exit?(first_statement.if_branch) && first_statement.else_branch.nil?
205
+ end
206
+
207
+ def exit?(branch)
208
+ branch&.type == exit_type
209
+ end
210
+
211
+ def unless_guard
212
+ first_statement.else_branch if exit?(first_statement.else_branch) && first_statement.if_branch.nil?
213
+ end
214
+ end
215
+ end