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,242 @@
1
+ # Detects methods that only delegate to a same-named method on a delegatable
2
+ # receiver (`def size; node.size; end`). The `delegate` macro says it
3
+ # declaratively, whether the receiver is a constant, a call on self or a chain
4
+ # of them.
5
+ #
6
+ # Rails/Delegate covers the public single-hop case, so what is left here is the
7
+ # private one (the macro defines public methods unless told otherwise), the
8
+ # nested one, where a chain of calls becomes a dotted target that Rails/Delegate
9
+ # does not recognize, and the one that forwards a block, which it does not read
10
+ # because it matches positional arguments only. The macro forwards the block on
11
+ # its own.
12
+ #
13
+ # A private method taking no parameters reads its arguments from its own state,
14
+ # so delegating it changes arity and every call site has to start passing them,
15
+ # which is why the offense is only reported. The same public method is left
16
+ # alone, since its callers are not all in view and a command line is one of
17
+ # them. So is a method that combines its own parameters with data of its own,
18
+ # a partial application that every call site would have to repeat.
19
+ #
20
+ # The autocorrection folds the method into an existing same-target `delegate`
21
+ # when one is present, keeping them on one line, and otherwise writes a fresh
22
+ # macro where the section already declares things, below its mixins when it
23
+ # declares nothing yet.
24
+ #
25
+ # @example
26
+ # # bad - hand-written private delegation
27
+ # private
28
+ # delegate :name, to: :node, private: true
29
+ #
30
+ # def size
31
+ # node.size
32
+ # end
33
+ #
34
+ # # good - folded into the existing delegate
35
+ # private
36
+ # delegate :name, :size, to: :node, private: true
37
+ #
38
+ # # bad - hand-written nested delegation
39
+ # def database_names
40
+ # config.postgres.database_names
41
+ # end
42
+ #
43
+ # # good - the chain becomes a dotted target
44
+ # delegate :database_names, to: "config.postgres"
45
+ #
46
+ # # bad - the method exists only to hand the block over
47
+ # def each(&block)
48
+ # blocks.each(&block)
49
+ # end
50
+ #
51
+ # # good
52
+ # delegate :each, to: :blocks
53
+ #
54
+ # # bad - callers must pass the argument: subunit_factor(options[:currency])
55
+ # private
56
+ # def subunit_factor
57
+ # Currency.subunit_factor(options[:currency])
58
+ # end
59
+ #
60
+ # # good - delegating would make every call site repeat the slot names
61
+ # def parse(raw)
62
+ # WALPressure.parse(raw, slot_names: SLOTS)
63
+ # end
64
+ #
65
+ class RuboCop::Cop::Callbacksystems::PreferDelegate < RuboCop::Cop::Callbacksystems::Base
66
+ extend RuboCop::Cop::AutoCorrector
67
+
68
+ def on_new_investigation
69
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
70
+ end
71
+
72
+ def on_def(node)
73
+ report ManualDelegation.new(node, @source_comments)
74
+ end
75
+
76
+ private
77
+ class ManualDelegation
78
+ include RuboCop::Callbacksystems::Helpers
79
+
80
+ MESSAGE = "Use `%<macro>s` instead of a hand-written delegation%<arguments_note>s."
81
+
82
+ def initialize(node, source_comments)
83
+ @node = node
84
+ @source_comments = source_comments
85
+ end
86
+
87
+ def offense
88
+ if declarable?
89
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) }
90
+ end
91
+ end
92
+
93
+ private
94
+ attr_reader :node, :source_comments
95
+ delegate :body, to: :node, private: true
96
+
97
+ def declarable?
98
+ declarable_delegation? && (forwards_own_parameters? ? beyond_rails_delegate? : binds_own_state?)
99
+ end
100
+
101
+ # `delegate` declares public and private methods, so a protected one has no macro to stand in its place.
102
+ def declarable_delegation?
103
+ body&.send_type? && body.method?(node.method_name) && target.delegable? &&
104
+ !protected_method?(node) && declared_in_class_body?
105
+ end
106
+
107
+ def target
108
+ @target ||= RuboCop::Callbacksystems::Methods::DelegationTarget.new(body.receiver)
109
+ end
110
+
111
+ # A macro stands where the definition stands, so one nested in a conditional or a block has no place to give it.
112
+ def declared_in_class_body?
113
+ statements_in(enclosing_definition_of(node)&.body).any? { it.equal?(statement) }
114
+ end
115
+
116
+ def statement
117
+ @statement ||= visibility_applied_to(node.parent, node) ? node.parent : node
118
+ end
119
+
120
+ def forwards_own_parameters?
121
+ body.arguments.map(&:source) == node.arguments.map(&:source)
122
+ end
123
+
124
+ def beyond_rails_delegate?
125
+ private_method?(node) || target.nested? || forwards_block?
126
+ end
127
+
128
+ def forwards_block?
129
+ node.arguments.any?(&:blockarg_type?)
130
+ end
131
+
132
+ def binds_own_state?
133
+ node.arguments.empty? && private_method?(node)
134
+ end
135
+
136
+ def message
137
+ format MESSAGE,
138
+ macro: conversion.macro_source,
139
+ arguments_note: forwards_own_parameters? ? "" : " and pass the arguments at the call sites"
140
+ end
141
+
142
+ def conversion
143
+ @conversion ||= Conversion.new(node, target, source_comments)
144
+ end
145
+
146
+ def correctable?
147
+ forwards_own_parameters? && conversion.placeable?
148
+ end
149
+
150
+ def correct(corrector)
151
+ conversion.apply(corrector)
152
+ end
153
+ end
154
+
155
+ class Conversion
156
+ include RuboCop::Callbacksystems::Helpers
157
+
158
+ def initialize(node, target, source_comments)
159
+ @node = node
160
+ @target = target
161
+ @source_comments = source_comments
162
+ end
163
+
164
+ def placeable?
165
+ (sibling_delegate.present? || anchor.present?) && !comments_attached_to_definition?
166
+ end
167
+
168
+ def apply(corrector)
169
+ write(corrector)
170
+ corrector.remove(statement_removal_range_for(node))
171
+ end
172
+
173
+ def macro_source
174
+ "delegate :#{node.method_name}, to: #{target.source}#{private_option}"
175
+ end
176
+
177
+ private
178
+ attr_reader :node, :target, :source_comments
179
+
180
+ def sibling_delegate
181
+ @sibling_delegate ||= container_macros.find { sibling?(it) }
182
+ end
183
+
184
+ def container_macros
185
+ container_statements.map { RuboCop::Callbacksystems::Methods::DelegateMacro.new(it) }
186
+ end
187
+
188
+ def container_statements
189
+ statements_in(enclosing_definition_of(node).body)
190
+ end
191
+
192
+ def sibling?(macro)
193
+ macro.macro? && macro.target == target.name && macro.plain?(private: private_delegation?)
194
+ end
195
+
196
+ def private_delegation?
197
+ private_method?(node)
198
+ end
199
+
200
+ def anchor
201
+ @anchor ||= declarations.last || private_modifier || mixins.last
202
+ end
203
+
204
+ def declarations
205
+ preceding_statements.select { declaration_macro?(it) }
206
+ end
207
+
208
+ def preceding_statements
209
+ container_statements.take_while { it != node }
210
+ end
211
+
212
+ def private_modifier
213
+ preceding_statements.find { visibility_modifier_of(it) == :private }
214
+ end
215
+
216
+ def mixins
217
+ preceding_statements.select { mixin_macro?(it) }
218
+ end
219
+
220
+ def comments_attached_to_definition?
221
+ source_comments.any_on_lines?(node.first_line..node.last_line) ||
222
+ source_comments.own_line_comment_on(node.first_line - 1).present?
223
+ end
224
+
225
+ def write(corrector)
226
+ if sibling_delegate
227
+ corrector.insert_before(sibling_delegate.first_option, ":#{node.method_name}, ")
228
+ else
229
+ corrector.insert_after(anchor, "#{separator}#{indentation_of(node)}#{macro_source}")
230
+ end
231
+ end
232
+
233
+ # After a mixin the macro keeps the blank line that belongs there, and elsewhere it joins the line above.
234
+ def separator
235
+ mixins.any? { it.equal?(anchor) } ? "\n\n" : "\n"
236
+ end
237
+
238
+ def private_option
239
+ ", private: true" if private_delegation?
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,175 @@
1
+ # Detects `@x = receiver.x` style assignments inside `initialize` that mirror
2
+ # a method call. These eager assignments are better expressed as `delegate`
3
+ # declarations: they read declaratively, avoid duplicating the name, and stop
4
+ # inviting bigger refactors (the very anti-pattern flagged by
5
+ # `NoRedundantWrapperMethod` and `NoAnemicDelegation`).
6
+ #
7
+ # Only assignments where the variable name matches the called method name are
8
+ # flagged, and only when the receiver itself is also assigned to a variable of
9
+ # the same name in the same `initialize` (so the receiver is reachable via
10
+ # `attr_reader` from the surrounding class).
11
+ #
12
+ # @example
13
+ # # bad - eager assignment that mirrors `node.body`
14
+ # def initialize(node)
15
+ # @node = node
16
+ # @body = node.body
17
+ # end
18
+ #
19
+ # private
20
+ # attr_reader :node, :body
21
+ #
22
+ # # good - declare the delegation
23
+ # def initialize(node)
24
+ # @node = node
25
+ # end
26
+ #
27
+ # private
28
+ # attr_reader :node
29
+ # delegate :body, to: :node, private: true
30
+ #
31
+ # # ok - the assignment transforms the value (different name from the call)
32
+ # def initialize(name)
33
+ # @name = name
34
+ # @name_parts = name.split("_")
35
+ # end
36
+ #
37
+ class RuboCop::Cop::Callbacksystems::PreferDelegateOverInstanceVariableAssignment < RuboCop::Cop::Callbacksystems::Base
38
+ def on_ivasgn(node)
39
+ report Assignment.new(node)
40
+ end
41
+
42
+ private
43
+ class Assignment
44
+ extend RuboCop::AST::NodePattern::Macros
45
+ include RuboCop::Callbacksystems::Helpers
46
+
47
+ MESSAGE = "Replace `@%<name>s = %<receiver>s.%<name>s` with `delegate :%<name>s, to: :%<receiver>s`."
48
+
49
+ # @!method mirroring_assignment?(node)
50
+ def_node_matcher :mirroring_assignment?, <<~PATTERN
51
+ (ivasgn $_ (send (lvar $_) $_))
52
+ PATTERN
53
+
54
+ # @!method receiver_assignment?(node, variable_name, lvar_name)
55
+ def_node_matcher :receiver_assignment?, <<~PATTERN
56
+ (ivasgn %1 (lvar %2))
57
+ PATTERN
58
+
59
+ def initialize(node)
60
+ @node = node
61
+ end
62
+
63
+ def offense
64
+ RuboCop::Callbacksystems::Offense.new(node, message) if mirroring_delegation?
65
+ end
66
+
67
+ private
68
+ attr_reader :node
69
+
70
+ def mirroring_delegation?
71
+ variable_name && variable_matches_method? && stable_initialization?
72
+ end
73
+
74
+ def variable_name
75
+ captures&.first
76
+ end
77
+
78
+ def captures
79
+ @captures ||= mirroring_assignment?(node)
80
+ end
81
+
82
+ def variable_matches_method?
83
+ name_without_sigil(variable_name).to_sym == method_name
84
+ end
85
+
86
+ def method_name
87
+ captures.third
88
+ end
89
+
90
+ def stable_initialization?
91
+ direct_receiver_pair? && stable_state?
92
+ end
93
+
94
+ def direct_receiver_pair?
95
+ direct_initialize_assignment? && receiver_assignment_precedes?
96
+ end
97
+
98
+ def direct_initialize_assignment?
99
+ initialize_method && direct_statements.any? { it.equal?(node) }
100
+ end
101
+
102
+ def initialize_method
103
+ @initialize_method ||= node.each_ancestor(:any_def).find { it.method?(:initialize) }
104
+ end
105
+
106
+ def direct_statements
107
+ @direct_statements ||= statements_in(initialize_method.body)
108
+ end
109
+
110
+ def receiver_assignment_precedes?
111
+ receiver_assignment && receiver_assignment.source_range.begin_pos < node.source_range.begin_pos
112
+ end
113
+
114
+ def receiver_assignment
115
+ @receiver_assignment ||= direct_statements.find do |statement|
116
+ receiver_assignment?(statement, :"@#{receiver_name}", receiver_name)
117
+ end
118
+ end
119
+
120
+ def receiver_name
121
+ captures.second
122
+ end
123
+
124
+ def stable_state?
125
+ receiver_local_stable? && instance_state_stable?
126
+ end
127
+
128
+ def receiver_local_stable?
129
+ receiver_rebindings.none? { between_receiver_and_mirror?(it) }
130
+ end
131
+
132
+ def receiver_rebindings
133
+ nodes_in(initialize_method.body, :lvasgn, :match_var).select { variable_name_of(it) == receiver_name }
134
+ end
135
+
136
+ def between_receiver_and_mirror?(write)
137
+ write.source_range.begin_pos.between?(receiver_assignment.source_range.end_pos, node.source_range.begin_pos)
138
+ end
139
+
140
+ def instance_state_stable?
141
+ sole_instance_write?(receiver_variable_name, receiver_assignment) && sole_instance_write?(variable_name, node)
142
+ end
143
+
144
+ def sole_instance_write?(name, expected)
145
+ instance_writes_to(name).then { it.one? && it.first.equal?(expected) }
146
+ end
147
+
148
+ def instance_writes_to(name)
149
+ nodes_in(state_scope&.body, :ivasgn).select { it.name == name && same_instance?(it) }
150
+ end
151
+
152
+ def state_scope
153
+ state_domain.container
154
+ end
155
+
156
+ def state_domain
157
+ @state_domain ||= RuboCop::Callbacksystems::Methods::Domain.new(node)
158
+ end
159
+
160
+ def same_instance?(write)
161
+ domain = RuboCop::Callbacksystems::Methods::Domain.new(write)
162
+
163
+ domain.container.equal?(state_scope) && domain.identity == state_domain.identity &&
164
+ self_preserved_between?(write, boundary: enclosing_method_of(write) || state_scope)
165
+ end
166
+
167
+ def receiver_variable_name
168
+ :"@#{receiver_name}"
169
+ end
170
+
171
+ def message
172
+ format(MESSAGE, name: method_name, receiver: receiver_name)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,204 @@
1
+ # Detects array subtraction and record-ID exclusions that can use `excluding`.
2
+ # The method names the excluded object directly, instead of wrapping it in a
3
+ # throwaway array or spelling out its database identifier.
4
+ #
5
+ # `excluding` flattens the arguments it receives, and Enumerable supplies a
6
+ # version whose result is an Array. Automatic correction therefore needs
7
+ # positive evidence that the receiver is an Array and the element is not one;
8
+ # an unknown value is still reported for a human who knows its domain.
9
+ #
10
+ # Relation exclusions are suggestions for human review. Active Record requires
11
+ # an instance of the relation's model and excludes its primary key, which may
12
+ # differ from the `id` column. Visible mismatches and custom primary keys are
13
+ # left alone, as are safe navigation and compound negative conditions.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # users - [admin]
18
+ # items - [first_item]
19
+ # User.where.not(id: user.id)
20
+ #
21
+ # # good
22
+ # users.excluding(admin)
23
+ # items.excluding(first_item)
24
+ # User.excluding(user)
25
+ #
26
+ class RuboCop::Cop::Callbacksystems::PreferExcluding < RuboCop::Cop::Callbacksystems::Base
27
+ extend RuboCop::Cop::AutoCorrector
28
+
29
+ def on_send(node)
30
+ report Subtraction.new(node, source_comments:)
31
+ report RecordExclusion.new(node, source_root: processed_source.ast)
32
+ end
33
+
34
+ alias on_csend on_send
35
+
36
+ private
37
+ class Subtraction
38
+ include RuboCop::Callbacksystems::Helpers
39
+ extend RuboCop::AST::NodePattern::Macros
40
+
41
+ MESSAGE = "Use `excluding` instead of `- [element]`."
42
+ NON_ARRAY_LITERAL_TYPES =
43
+ %i[ complex dstr dsym erange false float hash int irange nil rational regexp str sym true ]
44
+
45
+ # @!method single_element_subtraction(node)
46
+ def_node_matcher :single_element_subtraction, <<~PATTERN
47
+ (call $_ :- (array $_element))
48
+ PATTERN
49
+
50
+ def initialize(node, source_comments:)
51
+ @node = node
52
+ @source_comments = source_comments
53
+ @receiver, @element = single_element_subtraction(node)
54
+ end
55
+
56
+ def offense
57
+ if receiver
58
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: correction_equivalent?) { correct(it) }
59
+ end
60
+ end
61
+
62
+ private
63
+ attr_reader :node, :source_comments, :receiver, :element
64
+
65
+ def correction_equivalent?
66
+ array_value?(receiver) && non_array_value?(element) && standalone_element_source? &&
67
+ !source_comments.any_within?(node)
68
+ end
69
+
70
+ def array_value?(candidate)
71
+ reads_as_array?(candidate) || adjacent_assigned_value_of(candidate)&.then { reads_as_array?(it) }
72
+ end
73
+
74
+ def adjacent_assigned_value_of(candidate)
75
+ AdjacentAssignment.new(candidate, beside: node).value
76
+ end
77
+
78
+ def non_array_value?(candidate)
79
+ known_non_array?(candidate) || adjacent_assigned_value_of(candidate)&.then { known_non_array?(it) }
80
+ end
81
+
82
+ def known_non_array?(candidate)
83
+ NON_ARRAY_LITERAL_TYPES.include?(candidate.type) ||
84
+ (candidate.begin_type? && candidate.children.one? && known_non_array?(candidate.children.first))
85
+ end
86
+
87
+ def standalone_element_source?
88
+ !element.parent.percent_literal? || element.type?(:str, :sym)
89
+ end
90
+
91
+ def correct(corrector)
92
+ corrector.replace(node, "#{receiver.source}#{call_operator}excluding(#{argument})")
93
+ end
94
+
95
+ def call_operator
96
+ node.csend_type? ? "&." : "."
97
+ end
98
+
99
+ # Inside a percent literal an element is written bare, so its source alone would not survive on its own.
100
+ def argument
101
+ element.parent.percent_literal? ? element.value.inspect : element.source
102
+ end
103
+ end
104
+
105
+ class RecordExclusion
106
+ include RuboCop::Callbacksystems::Helpers
107
+ extend RuboCop::AST::NodePattern::Macros
108
+
109
+ MESSAGE = "Consider `excluding(%<record>s)` when the record belongs to this relation's model " \
110
+ "and its primary key is `id`."
111
+
112
+ # @!method record_exclusion(node)
113
+ def_node_matcher :record_exclusion, <<~PATTERN
114
+ (send (send $_relation :where) :not (hash (pair {(sym :id) (str "id")} (send $_record :id))))
115
+ PATTERN
116
+
117
+ def initialize(node, source_root:)
118
+ @node = node
119
+ @source_root = source_root
120
+ @relation, @record = record_exclusion(node)
121
+ end
122
+
123
+ def offense
124
+ if record && applicable?
125
+ RuboCop::Callbacksystems::Offense.new(node, format(MESSAGE, record: record.source))
126
+ end
127
+ end
128
+
129
+ private
130
+ attr_reader :node, :source_root, :relation, :record
131
+
132
+ def applicable?
133
+ node.each_node(:csend).none? && !non_record_value? && !different_models? && !custom_primary_key?
134
+ end
135
+
136
+ def non_record_value?
137
+ record_value.literal? || record_value.const_type? || relation&.literal?
138
+ end
139
+
140
+ def record_value
141
+ @record_value ||= AdjacentAssignment.new(record, beside: node).value || record
142
+ end
143
+
144
+ def different_models?
145
+ relation_model && record_model && relation_model != record_model
146
+ end
147
+
148
+ def relation_model
149
+ @relation_model ||= model_name_of(relation)
150
+ end
151
+
152
+ def model_name_of(candidate)
153
+ candidate = candidate.receiver while candidate&.call_type?
154
+ constant_name_of(candidate)&.delete_prefix("::")
155
+ end
156
+
157
+ def record_model
158
+ @record_model ||= model_name_of(record_value)
159
+ end
160
+
161
+ def custom_primary_key?
162
+ source_root.each_node(:send, :defs).any? do |candidate|
163
+ if candidate.defs_type?
164
+ candidate.method?(:primary_key)
165
+ else
166
+ candidate.method?(:primary_key=) && !default_primary_key?(candidate.first_argument)
167
+ end
168
+ end
169
+ end
170
+
171
+ def default_primary_key?(value)
172
+ value.type?(:str, :sym) && value.value.to_s == "id"
173
+ end
174
+ end
175
+
176
+ # A local's immediately preceding assignment, shared by both forms of exclusion.
177
+ class AdjacentAssignment
178
+ def initialize(variable, beside:)
179
+ @variable = variable
180
+ @node = beside
181
+ end
182
+
183
+ def value
184
+ if variable.lvar_type? && matching_assignment?
185
+ previous.expression
186
+ end
187
+ end
188
+
189
+ private
190
+ attr_reader :variable, :node
191
+
192
+ def matching_assignment?
193
+ previous&.lvasgn_type? && previous.name == variable.name
194
+ end
195
+
196
+ def previous
197
+ @previous ||= statement_containing_node&.left_sibling
198
+ end
199
+
200
+ def statement_containing_node
201
+ @statement_containing_node ||= [ node, *node.each_ancestor ].find { it.parent&.type?(:begin, :kwbegin) }
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,54 @@
1
+ # Prefers `extract_associated` when a model query preloads one association and
2
+ # then collects that same association. Active Record supplies this exact
3
+ # operation as one method, keeping the association's name in one place.
4
+ #
5
+ # The receiver is assumed to be an Active Record relation. Comments prevent
6
+ # autocorrection, and a guarded preload followed by an unguarded map is left
7
+ # alone because collapsing it would change what happens when the receiver is nil.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # posts.preload(:author).map(&:author)
12
+ # memberships.preload(:user).collect(&:user)
13
+ #
14
+ # # good
15
+ # posts.extract_associated(:author)
16
+ # memberships.extract_associated(:user)
17
+ #
18
+ class RuboCop::Cop::Callbacksystems::PreferExtractAssociated < RuboCop::Cop::Callbacksystems::Base
19
+ extend RuboCop::Cop::AutoCorrector
20
+
21
+ MESSAGE = "Use `extract_associated(%<association>s)` instead of preloading and mapping the same association."
22
+
23
+ # @!method association_extraction(node)
24
+ def_node_matcher :association_extraction, <<~PATTERN
25
+ (call $(call _ :preload $(sym _association)) {:map :collect} (block_pass (sym _association)))
26
+ PATTERN
27
+
28
+ def on_send(node)
29
+ association_extraction(node) do |preload, association|
30
+ next if preload.csend_type? && node.send_type?
31
+
32
+ add_extraction_offense(node, preload:, association:)
33
+ end
34
+ end
35
+
36
+ alias on_csend on_send
37
+
38
+ private
39
+ def add_extraction_offense(node, preload:, association:)
40
+ message = format(MESSAGE, association: association.source)
41
+
42
+ if source_comments.any_within?(node)
43
+ add_offense(node, message:)
44
+ else
45
+ add_offense(node, message:) do |corrector|
46
+ corrector.replace(node, replacement_for(preload, association:))
47
+ end
48
+ end
49
+ end
50
+
51
+ def replacement_for(preload, association:)
52
+ "#{call_prefix_of(preload)}extract_associated(#{association.source})"
53
+ end
54
+ end