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,339 @@
1
+ # Detects a set of parameter names that travel together across a class's or
2
+ # module's private methods, suggesting they belong to an object of their own,
3
+ # usually a private nested class holding them as instance state. Only private
4
+ # methods are examined, since a public method's signature answers to an
5
+ # interface, not internal threading, which is also why the JS rule no-data-clump
6
+ # skips exported functions and public members. Methods under `class << self`
7
+ # join the group of the class that holds them.
8
+ #
9
+ # The set is found as a connected component in the co-occurrence graph of shared
10
+ # parameter names, meaning names used by two or more methods, linked whenever they
11
+ # appear together in one method's signature. Because membership follows the
12
+ # graph, not an exact repeated tuple, this catches names sharing *varying*
13
+ # combinations, as in `m1(a, b)`, `m2(a, c)`, `m3(b, c)`, where no single pair
14
+ # repeats. Only methods carrying two or more of the names count toward a set,
15
+ # since one name is a value arriving, not a concept being passed, and counting
16
+ # such bystanders would let a lone name clear the set threshold instead of its
17
+ # own higher one.
18
+ # A one-letter name is left out, since it is a placeholder `NoSingleLetterNames` already flags.
19
+ #
20
+ # How far a set has to spread depends on how much its shape already tells. A
21
+ # signature repeated verbatim counts from `MinMethodsForRepeatedSignature`
22
+ # methods, two by default, because nothing in those parameter lists explains the
23
+ # co-occurrence except the concept itself. Names that come with extras of their
24
+ # own might merely have met, so those need `MinMethods`, three by default, and a
25
+ # lone name threaded through the class has to reach `MinMethodsForSingleParam`,
26
+ # six by default. The recursion subject is exempt, since a name passed both as
27
+ # itself and as a derivative of itself in the same call (`walk(node.child, node)`)
28
+ # changes at every step and cannot become shared state.
29
+ #
30
+ # @example
31
+ # # bad - a signature repeated verbatim is telling from two methods already
32
+ # class Order
33
+ # private
34
+ # def validate(user, account); end
35
+ # def execute(user, account); end
36
+ # end
37
+ #
38
+ # # bad - a shared core with extras of its own, once it reaches three methods
39
+ # class Order
40
+ # private
41
+ # def create(name, email, phone); end
42
+ # def validate(name, email, address); end
43
+ # def save(name, phone, address); end
44
+ # end
45
+ #
46
+ # # bad - one value threaded through six methods
47
+ # class Visitor
48
+ # private
49
+ # def first(node); end
50
+ # def second(node); end
51
+ # def third(node); end
52
+ # def fourth(node); end
53
+ # def fifth(node); end
54
+ # def sixth(node); end
55
+ # end
56
+ #
57
+ # # good - a private nested class holds the values as state
58
+ # class Order
59
+ # def process
60
+ # context = Context.new(user, account)
61
+ # context.validate
62
+ # context.execute
63
+ # end
64
+ #
65
+ # private
66
+ # class Context
67
+ # def initialize(user, account)
68
+ # @user = user
69
+ # @account = account
70
+ # end
71
+ #
72
+ # def validate; end
73
+ # def execute; end
74
+ # end
75
+ # end
76
+ #
77
+ class RuboCop::Cop::Callbacksystems::DataClump < RuboCop::Cop::Callbacksystems::Base
78
+ def on_class(node)
79
+ report_each ParameterClumps.new(node, limits)
80
+ end
81
+
82
+ alias on_module on_class
83
+
84
+ private
85
+ def limits
86
+ Limits.new \
87
+ min_methods: cop_config["MinMethods"],
88
+ min_methods_for_repeated_signature: cop_config["MinMethodsForRepeatedSignature"],
89
+ min_methods_for_single_parameter: cop_config["MinMethodsForSingleParam"]
90
+ end
91
+
92
+ class ParameterClumps
93
+ include RuboCop::Callbacksystems::Helpers
94
+
95
+ def initialize(node, limits)
96
+ @node = node
97
+ @limits = limits
98
+ end
99
+
100
+ def each_offense
101
+ clumps.each { yield RuboCop::Callbacksystems::Offense.new(it.location, it.message) }
102
+ end
103
+
104
+ private
105
+ attr_reader :node, :limits
106
+
107
+ def clumps
108
+ SignatureGroup.new(signatures, limits).clumps
109
+ end
110
+
111
+ def signatures
112
+ internal_methods.map { Signature.new(it, exempt_names) }.select { it.names.any? }
113
+ end
114
+
115
+ def internal_methods
116
+ @internal_methods ||= direct_method_nodes_in(node.body).select { internal_method?(it) }
117
+ end
118
+
119
+ def internal_method?(method_node)
120
+ private_nested_class?(node) || private_method?(method_node)
121
+ end
122
+
123
+ def exempt_names
124
+ @exempt_names ||= internal_methods.flat_map { recursion_subject_names_of(it) }.to_set
125
+ end
126
+
127
+ def recursion_subject_names_of(method_node)
128
+ RuboCop::Callbacksystems::Execution::Immediate.new(method_node.body).nodes_of_type(:send, :csend)
129
+ .flat_map { recursion_subjects_in(it) }
130
+ end
131
+ end
132
+
133
+ # Reads a method group's shared names as connected components, each its own hidden object.
134
+ class SignatureGroup
135
+ MIN_SHARING_METHODS = 2
136
+
137
+ def initialize(signatures, limits)
138
+ @signatures = signatures
139
+ @limits = limits
140
+ end
141
+
142
+ def clumps
143
+ candidates.select { limits.reached_by?(it) }
144
+ end
145
+
146
+ private
147
+ attr_reader :signatures, :limits
148
+
149
+ def candidates
150
+ graph.map { clump_for(it) }
151
+ end
152
+
153
+ def graph
154
+ Graph.new(shared_names, signatures)
155
+ end
156
+
157
+ def shared_names
158
+ name_counts.select { |_, count| count >= MIN_SHARING_METHODS }.keys
159
+ end
160
+
161
+ def name_counts
162
+ signatures.flat_map(&:names).tally
163
+ end
164
+
165
+ def clump_for(names)
166
+ Clump.new(signatures.select { it.counts_toward?(names) }, names)
167
+ end
168
+ end
169
+
170
+ # The shared names linked by the signatures they appear in together, read as its connected components.
171
+ class Graph
172
+ include Enumerable
173
+
174
+ delegate :each, to: :components
175
+
176
+ def initialize(shared_names, signatures)
177
+ @shared_names = shared_names
178
+ @signatures = signatures
179
+ end
180
+
181
+ private
182
+ attr_reader :shared_names, :signatures
183
+
184
+ def components
185
+ each_component.to_a
186
+ end
187
+
188
+ def each_component
189
+ if block_given?
190
+ remaining = shared_names.to_set
191
+ until remaining.empty?
192
+ component = component_of(remaining.first)
193
+ yield component
194
+ remaining.subtract(component)
195
+ end
196
+ else
197
+ enum_for(__method__)
198
+ end
199
+ end
200
+
201
+ def component_of(name)
202
+ closure_of(Set[name])
203
+ end
204
+
205
+ def closure_of(names)
206
+ names.dup.tap do |reached|
207
+ pending = names.to_a
208
+ position = 0
209
+ visited_signatures = {}.compare_by_identity
210
+
211
+ until position == pending.size
212
+ signatures_by_name.fetch(pending[position]).each do |signature|
213
+ next if visited_signatures.key?(signature)
214
+
215
+ visited_signatures[signature] = true
216
+ signature.names.each do |neighbor|
217
+ pending << neighbor if shared_name?(neighbor) && reached.add?(neighbor)
218
+ end
219
+ end
220
+ position += 1
221
+ end
222
+ end
223
+ end
224
+
225
+ def signatures_by_name
226
+ @signatures_by_name ||= shared_names.index_with { [] }.tap do |index|
227
+ signatures.each do |signature|
228
+ signature.names.each { index.fetch(it) << signature if index.key?(it) }
229
+ end
230
+ end
231
+ end
232
+
233
+ def shared_name?(name)
234
+ shared_name_index.include?(name)
235
+ end
236
+
237
+ def shared_name_index
238
+ @shared_name_index ||= shared_names.to_set
239
+ end
240
+ end
241
+
242
+ class Clump < Data.define(:signatures, :names)
243
+ MESSAGE = "Parameters `%<parameters>s` travel together through %<count>d private methods (`%<methods>s`). " \
244
+ "Consider a private nested class holding them as instance state."
245
+ SINGLE_PARAMETER_MESSAGE = "Parameter `%<parameter>s` is threaded through %<count>d private methods " \
246
+ "(`%<methods>s`). Consider a private nested class holding it as instance state."
247
+
248
+ def message
249
+ lone_name? ? threaded_parameter_message : shared_parameters_message
250
+ end
251
+
252
+ def lone_name?
253
+ names.one?
254
+ end
255
+
256
+ # Stronger than a shared core, since the methods take the set and nothing besides.
257
+ def repeated_signature?
258
+ signatures.map(&:sorted_names).uniq.one?
259
+ end
260
+
261
+ def location
262
+ signatures.first.node.loc.name
263
+ end
264
+
265
+ def count
266
+ signatures.size
267
+ end
268
+
269
+ private
270
+ def threaded_parameter_message
271
+ format(SINGLE_PARAMETER_MESSAGE, parameter: sorted_parameter_names.first, count:, methods: method_names)
272
+ end
273
+
274
+ def sorted_parameter_names
275
+ names.sort
276
+ end
277
+
278
+ def method_names
279
+ signatures.map(&:method_name).join(", ")
280
+ end
281
+
282
+ def shared_parameters_message
283
+ format(MESSAGE, parameters: sorted_parameter_names.join(", "), count:, methods: method_names)
284
+ end
285
+ end
286
+
287
+ class Signature
288
+ include RuboCop::Callbacksystems::Helpers
289
+
290
+ attr_reader :node, :names
291
+
292
+ delegate :method_name, to: :node
293
+
294
+ def initialize(node, exempt_names)
295
+ @node = node
296
+ @names = parameter_names_of(node).uniq.reject { exempt_names.include?(it) || it.length == 1 }
297
+ end
298
+
299
+ # A method carrying one name of a set is a bystander, so a set needs two of its names to appear in a signature.
300
+ def counts_toward?(wanted)
301
+ shared_count_in(wanted) >= (wanted.many? ? 2 : 1)
302
+ end
303
+
304
+ def shared_count_in(wanted)
305
+ names.count { wanted.include?(it) }
306
+ end
307
+
308
+ # A one-letter name is a placeholder rather than a concept, and `NoSingleLetterNames` already asks for a word.
309
+ def sorted_names
310
+ @sorted_names ||= names.sort
311
+ end
312
+ end
313
+
314
+ # The thresholds mirror the JS rule no-data-clump, so the two configs agree on what qualifies.
315
+ class Limits
316
+ def initialize(min_methods:, min_methods_for_repeated_signature:, min_methods_for_single_parameter:)
317
+ @min_methods = min_methods
318
+ @min_methods_for_repeated_signature = min_methods_for_repeated_signature
319
+ @min_methods_for_single_parameter = min_methods_for_single_parameter
320
+ end
321
+
322
+ def reached_by?(clump)
323
+ clump.count >= reach_for(clump)
324
+ end
325
+
326
+ private
327
+ attr_reader :min_methods, :min_methods_for_repeated_signature, :min_methods_for_single_parameter
328
+
329
+ def reach_for(clump)
330
+ if clump.lone_name?
331
+ min_methods_for_single_parameter
332
+ elsif clump.repeated_signature?
333
+ min_methods_for_repeated_signature
334
+ else
335
+ min_methods
336
+ end
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,350 @@
1
+ # A method that returns a value should be named for the value it hands back
2
+ # (`total`, `user_by_id`), not for the imperative action it performs
3
+ # (`compute_total`, `get_user`). The imperative name reads as a command when the
4
+ # thing is really a query. We flag a method whose name leads with a producer verb
5
+ # and carries a noun we can rename it to (`compute_total` -> `total`). A bare verb
6
+ # (`fetch`) is left alone: with no noun there is nothing to rename to. A verb
7
+ # followed straight by a connector (`find_in_scope`, `load_for`) is left alone
8
+ # too: the phrase that remains (`in_scope`, `for`) names no value on its own, and
9
+ # the leading word often reads as a noun in context (`load_for person` is "the
10
+ # load for a person"), so the rename is too contextual to pick. With arguments and
11
+ # no connector in the name we only flag it (`get_user(id)`): a bare noun beside an
12
+ # argument (`user(id)`) relates nothing, and the relator is the author's call.
13
+ #
14
+ # The verb list is curated, not taken from a lexicon: `get`/`find`/`build` are not
15
+ # dropped from any dictionary, but are bad as method prefixes by code convention.
16
+ # Excluded on purpose: noun-verbs that name a value fine (`count`, `name`,
17
+ # `value`, `order`, `state`...) and command verbs that act rather than produce
18
+ # (`save`, `render`, `update`, `send`, `handle`, `process`...).
19
+ #
20
+ # We flag a producer-verb method only when it just *delivers* a value: it
21
+ # changes no state, calls no mutator, does no I/O. A method that *does* something
22
+ # earns its verb and is left alone, even named with a producer verb: `create_user`
23
+ # that calls `save!`, an association `build` that mutates the collection.
24
+ # Construction is delivery, not doing: `Foo.new(args)` only hands a value back, so
25
+ # a pure factory is flagged. Building a collaborator and then asking it for
26
+ # something is a third case: what happens inside is not readable here, so
27
+ # `Commenter.new(card).comment` leaves the name alone.
28
+ #
29
+ # Two more cases are left alone, both signs the name is intentional rather than
30
+ # careless. A method that delegates within its own verb family (`format_money`
31
+ # calling `format_short`, `find_account_by_cookie` calling `find_by`) mirrors a
32
+ # real operation of the same name, so renaming it would break the family. And a
33
+ # method whose suggested noun already names a sibling (`build_prices` beside
34
+ # `attr_reader :prices`, `lookup_key` beside `key`) is the separate-builder
35
+ # pattern: the value already has its declarative name, this is its helper.
36
+ #
37
+ # Ruby divergence from the JS rule: JS can gate on "returns a value" because a
38
+ # function can fall off the end returning `undefined`. In Ruby every method
39
+ # returns its last expression, so "delivers" is the *absence of effects*, not the
40
+ # presence of a return. Predicates (`valid?`), bang methods (`save!`), setters
41
+ # (`foo=`), operators, and `initialize` stay exempt by Ruby convention.
42
+ #
43
+ # @example
44
+ # # bad - imperative name for a value (no arguments): name it for the value
45
+ # def compute_total
46
+ # line_items.sum(&:price)
47
+ # end
48
+ #
49
+ # # bad - imperative name for a value built from an argument
50
+ # def get_user(id)
51
+ # users[id]
52
+ # end
53
+ #
54
+ # # good - the bare verb is allowed: there is no noun to rename it to
55
+ # def compute
56
+ # heavy_work
57
+ # end
58
+ #
59
+ # # good - verb followed straight by a connector names no value to rename to
60
+ # def load_for(person)
61
+ # events_for(person) + blocks_for(person)
62
+ # end
63
+ #
64
+ # # good - delegates within its own verb family: it mirrors a real operation
65
+ # def format_money(money)
66
+ # Worldwide.currency(money).format_short(money)
67
+ # end
68
+ #
69
+ # # good - the suggested noun already names a sibling (`attr_reader :prices`)
70
+ # def build_prices
71
+ # properties.to_h { |currency, cents| [ currency, Money.new(cents) ] }
72
+ # end
73
+ #
74
+ # # good - named for the value it returns
75
+ # def total
76
+ # line_items.sum(&:price)
77
+ # end
78
+ #
79
+ # # good - noun-verb reads as a noun
80
+ # def count
81
+ # items.length
82
+ # end
83
+ #
84
+ # # good - predicates, bang methods, and setters are exempt by convention
85
+ # def valid?
86
+ # errors.empty?
87
+ # end
88
+ #
89
+ class RuboCop::Cop::Callbacksystems::DeclarativeMethodNaming < RuboCop::Cop::Callbacksystems::Base
90
+ def on_new_investigation
91
+ @declared_values = DeclaredValues.new
92
+ end
93
+
94
+ def on_def(node)
95
+ report MethodName.new(node, declared_values:)
96
+ end
97
+
98
+ alias on_defs on_def
99
+
100
+ private
101
+ PRODUCER_VERBS = %w[
102
+ get fetch retrieve obtain acquire load
103
+ compute calculate derive determine evaluate deduce infer
104
+ generate produce fabricate assemble construct build make create
105
+ convert transform translate cast coerce
106
+ serialize deserialize stringify normalize denormalize parse extract
107
+ format summarize aggregate tabulate
108
+ find lookup locate search select pick choose
109
+ collect gather accumulate compile
110
+ resolve
111
+ ].to_set
112
+
113
+ CONNECTORS = %w[
114
+ for of from at in on by with within without
115
+ into onto over under above below beneath behind
116
+ beside before after between across through throughout
117
+ toward towards alongside during via to per until
118
+ since against among around near beyond
119
+ ].to_set
120
+
121
+ EFFECTS = %i[
122
+ save update create build destroy delete insert
123
+ insert_all upsert upsert_all update_all delete_all destroy_all
124
+ push pop shift unshift prepend concat append << store replace add clear
125
+ each each_with_index puts print pp write
126
+ touch mkdir mkdir_p mkpath makedirs rm rm_f rm_rf rmdir remove_entry remove_dir
127
+ cp cp_r mv ln_s chmod chown
128
+ ].to_set
129
+
130
+ READER_MACROS = %i[ attr_reader attr_accessor attr_writer ].to_set
131
+ SIDE_EFFECT_NODE_TYPES = %i[ casgn cvasgn gvasgn ivasgn super xstr yield zsuper ]
132
+ NETWORK_METHODS = %i[ get post put patch delete request start ].to_set
133
+ FILESYSTEM_RECEIVERS = %w[ File IO ].to_set
134
+ FILESYSTEM_METHODS = %i[ binread foreach open read readlines sysopen ].to_set
135
+
136
+ attr_reader :declared_values
137
+
138
+ # Value names indexed once for each body whose methods ask whether their suggested noun is already taken.
139
+ class DeclaredValues
140
+ include RuboCop::Callbacksystems::Helpers
141
+
142
+ def initialize
143
+ @sibling_methods = RuboCop::Callbacksystems::Methods::Siblings.new
144
+ @reader_names_by_container = {}.compare_by_identity
145
+ end
146
+
147
+ def include?(name, beside:)
148
+ sibling_methods.named(name.to_sym, beside:).any? || reader_names_beside(beside).include?(name)
149
+ end
150
+
151
+ private
152
+ attr_reader :sibling_methods, :reader_names_by_container
153
+
154
+ def reader_names_beside(method)
155
+ domain = RuboCop::Callbacksystems::Methods::Domain.new(method)
156
+ reader_names_in(domain.container).fetch(domain.identity) { Set.new }
157
+ end
158
+
159
+ def reader_names_in(container)
160
+ if container
161
+ reader_names_by_container[container] ||=
162
+ ReaderMacros.new(container.body).each_with_object({}) do |macro, names|
163
+ (names[declaration_domain_of(macro)] ||= Set.new)
164
+ .merge(macro.arguments.select(&:sym_type?).map { it.value.to_s })
165
+ end
166
+ else
167
+ {}
168
+ end
169
+ end
170
+
171
+ def declaration_domain_of(macro)
172
+ identity = RuboCop::Callbacksystems::Methods::Domain.new(macro).identity
173
+ identity.take(identity.size.pred).freeze
174
+ end
175
+
176
+ # Reader declarations reached through body sequences, singleton sections, and nested call arguments.
177
+ class ReaderMacros
178
+ include Enumerable
179
+ include RuboCop::Callbacksystems::Helpers
180
+
181
+ def initialize(body)
182
+ @body = body
183
+ end
184
+
185
+ def each
186
+ if block_given?
187
+ @pending = [ body ].compact
188
+ until pending.empty?
189
+ advance
190
+ visit { yield it }
191
+ end
192
+ else
193
+ to_enum(__method__)
194
+ end
195
+ end
196
+
197
+ private
198
+ attr_reader :body, :pending, :current
199
+
200
+ def advance
201
+ @current = pending.pop
202
+ end
203
+
204
+ def visit
205
+ case current.type
206
+ when :begin, :kwbegin then queue current.each_child_node
207
+ when :sclass then queue [ current.body ].compact
208
+ when :send then visit_send { yield it }
209
+ end
210
+ end
211
+
212
+ def queue(nodes)
213
+ pending.concat(nodes.to_a.reverse)
214
+ end
215
+
216
+ def visit_send
217
+ yield current if reader_macro?
218
+ queue current.arguments
219
+ end
220
+
221
+ def reader_macro?
222
+ bare_send?(current) && READER_MACROS.include?(current.method_name)
223
+ end
224
+ end
225
+ end
226
+
227
+ class MethodName
228
+ include RuboCop::Callbacksystems::Helpers
229
+
230
+ IMPERATIVE_NAME = "Rename `%<name>s` to `%<suggestion>s`: name it for the value, not the action `%<verb>s`."
231
+ IMPERATIVE_NAME_RELATE = "Rename `%<name>s` for the value, related to its argument, not the action `%<verb>s`."
232
+
233
+ def initialize(node, declared_values:)
234
+ @node = node
235
+ @declared_values = declared_values
236
+ end
237
+
238
+ def offense
239
+ RuboCop::Callbacksystems::Offense.new(node.loc.name, message) if imperatively_named?
240
+ end
241
+
242
+ private
243
+ attr_reader :node, :declared_values
244
+
245
+ def imperatively_named?
246
+ eligible? && imperative_producer? && delivers_only? &&
247
+ !delegates_within_verb_family? && !value_already_named?
248
+ end
249
+
250
+ def eligible?
251
+ !node.predicate_method? && !node.bang_method? && !node.assignment_method? &&
252
+ !node.operator_method? && !node.method?(:initialize)
253
+ end
254
+
255
+ def imperative_producer?
256
+ PRODUCER_VERBS.include?(verb) && !noun.nil?
257
+ end
258
+
259
+ def verb
260
+ segments.first
261
+ end
262
+
263
+ def segments
264
+ @segments ||= node.method_name.to_s.split("_")
265
+ end
266
+
267
+ # A rest led by a connector (`find_in_block`) is a prepositional phrase naming nothing, so it yields no noun.
268
+ def noun
269
+ rest.join("_") if rest.any? && CONNECTORS.exclude?(rest.first)
270
+ end
271
+
272
+ def rest
273
+ segments.drop(1)
274
+ end
275
+
276
+ def delivers_only?
277
+ RuboCop::Callbacksystems::Execution::Immediate.new(node.body).none? do |descendant|
278
+ descendant.type?(*SIDE_EFFECT_NODE_TYPES, :call) && SideEffect.new(descendant).present?
279
+ end
280
+ end
281
+
282
+ def delegates_within_verb_family?
283
+ node.each_descendant(:call).any? { it.method_name.to_s.split("_").first == verb }
284
+ end
285
+
286
+ def value_already_named?
287
+ declared_values.include?(noun, beside: node)
288
+ end
289
+
290
+ def message
291
+ format(message_template, name: node.method_name, verb: verb, suggestion: noun)
292
+ end
293
+
294
+ def message_template
295
+ needs_relator? ? IMPERATIVE_NAME_RELATE : IMPERATIVE_NAME
296
+ end
297
+
298
+ def needs_relator?
299
+ arguments? && !carries_connector?
300
+ end
301
+
302
+ def arguments?
303
+ node.arguments.any?
304
+ end
305
+
306
+ def carries_connector?
307
+ rest.any? { CONNECTORS.include?(it) }
308
+ end
309
+ end
310
+
311
+ class SideEffect
312
+ include RuboCop::Callbacksystems::Helpers
313
+
314
+ def initialize(node)
315
+ @node = node
316
+ end
317
+
318
+ def present?
319
+ !node.call_type? || mutating_call?
320
+ end
321
+
322
+ private
323
+ attr_reader :node
324
+
325
+ def mutating_call?
326
+ node.bang_method? || node.setter_method? || EFFECTS.include?(node.method_name) ||
327
+ asks_a_built_collaborator? || performs_io?
328
+ end
329
+
330
+ def asks_a_built_collaborator?
331
+ node.receiver&.call_type? && node.receiver.method?(:new)
332
+ end
333
+
334
+ def performs_io?
335
+ performs_network_io? || performs_filesystem_io?
336
+ end
337
+
338
+ def performs_network_io?
339
+ receiver_constant.to_s.start_with?("Net::HTTP") && NETWORK_METHODS.include?(node.method_name)
340
+ end
341
+
342
+ def receiver_constant
343
+ @receiver_constant ||= constant_name_of(node.receiver)&.delete_prefix("::")
344
+ end
345
+
346
+ def performs_filesystem_io?
347
+ FILESYSTEM_RECEIVERS.include?(receiver_constant) && FILESYSTEM_METHODS.include?(node.method_name)
348
+ end
349
+ end
350
+ end