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,532 @@
1
+ # Detects a single value threaded through many method calls: a parameter or a
2
+ # local passed as an argument to several different receiverless methods, directly
3
+ # or down a pipeline. Such a value is instance state in disguise: it reads
4
+ # better as the state of a class, with the methods that receive it becoming
5
+ # instance methods that no longer need the argument.
6
+ #
7
+ # The value is followed by *data flow*: every receiverless method it is passed to
8
+ # as an argument, then (through that callee's matching parameter) wherever it
9
+ # flows next, transitively across the class. So a value handed from `rewrite` to
10
+ # `process` to `decorate` counts all three, even though no single method passes it
11
+ # four times. The flow is not followed through a *recursive* method: there the
12
+ # value is a moving cursor over a structure, not one value held as state.
13
+ #
14
+ # This complements DataClump, which catches *groups* of arguments shared across
15
+ # method *signatures*. Here a *single* value threaded around is the smell.
16
+ #
17
+ # @example
18
+ # # bad - `node` is passed to every helper
19
+ # def rewrite(node)
20
+ # validate(node)
21
+ # decorate(node)
22
+ # persist(node)
23
+ # index(node)
24
+ # end
25
+ #
26
+ # # bad - `node` is threaded down a pipeline
27
+ # def rewrite(node)
28
+ # process(node)
29
+ # end
30
+ #
31
+ # def process(value)
32
+ # decorate(value)
33
+ # persist(value)
34
+ # index(value)
35
+ # end
36
+ #
37
+ # # good - `node` becomes the state of a class; the helpers become its methods
38
+ # def rewrite(node)
39
+ # Rewrite.new(node).perform
40
+ # end
41
+ #
42
+ # class Rewrite
43
+ # def initialize(node)
44
+ # @node = node
45
+ # end
46
+ #
47
+ # def perform
48
+ # validate
49
+ # decorate
50
+ # persist
51
+ # index
52
+ # end
53
+ # end
54
+ #
55
+ class RuboCop::Cop::Callbacksystems::PreferClassForState < RuboCop::Cop::Callbacksystems::Base
56
+ def on_new_investigation
57
+ @method_indexes = MethodIndexes.new(processed_source.ast)
58
+ end
59
+
60
+ def on_def(node)
61
+ report_each ThreadedValues.new(node, index: method_indexes.for(node), min_methods: cop_config["MinMethods"])
62
+ end
63
+
64
+ alias on_defs on_def
65
+
66
+ private
67
+ Call = Data.define(:method_name, :position)
68
+ FlowEdge = Data.define(:method_name, :target)
69
+ ReachResult = Data.define(:method_names, :cyclic)
70
+
71
+ attr_reader :method_indexes
72
+
73
+ class ThreadedValues
74
+ include RuboCop::Callbacksystems::Helpers
75
+
76
+ MESSAGE = "`%<name>s` is threaded through %<count>d methods. Make it the state of a class."
77
+
78
+ def initialize(node, index:, min_methods:)
79
+ @node = node
80
+ @index = index
81
+ @min_methods = min_methods
82
+ end
83
+
84
+ def each_offense
85
+ threaded.each do |name, count|
86
+ yield RuboCop::Callbacksystems::Offense.new \
87
+ definitions.fetch(name),
88
+ format(MESSAGE, name: name, count: count)
89
+ end
90
+ end
91
+
92
+ private
93
+ attr_reader :node, :index, :min_methods
94
+
95
+ def threaded
96
+ return {} if recursive_origin?
97
+
98
+ reach_counts.select { |_, count| count >= min_methods }
99
+ end
100
+
101
+ def recursive_origin?
102
+ index.recursive?(node)
103
+ end
104
+
105
+ def reach_counts
106
+ definitions.keys.to_h { [ it, reach_size_for(it) ] }
107
+ end
108
+
109
+ def definitions
110
+ @definitions ||= parameter_definitions.merge(local_definitions)
111
+ end
112
+
113
+ def parameter_definitions
114
+ parameter_nodes_of(node).index_by { it.children.first.to_s }.except(*method_assignments.names)
115
+ end
116
+
117
+ def method_assignments
118
+ @method_assignments ||= index.assignments_for(node)
119
+ end
120
+
121
+ def local_definitions
122
+ method_assignments.unique_local_definitions.except(*parameter_names)
123
+ end
124
+
125
+ def parameter_names
126
+ @parameter_names ||= parameter_nodes_of(node).to_set { it.children.first.to_s }
127
+ end
128
+
129
+ def reach_size_for(name)
130
+ index.reach_size(index.flow(name, node))
131
+ end
132
+ end
133
+
134
+ # One index per class and method domain keeps receiverless instance calls away from singleton definitions, and lets
135
+ # every origin reuse the same value-flow graph.
136
+ class MethodIndexes
137
+ include RuboCop::Callbacksystems::Helpers
138
+
139
+ def initialize(ast)
140
+ @methods_by_container = grouped_methods_in(ast)
141
+ end
142
+
143
+ def for(method)
144
+ domain = MethodDomain.new(method)
145
+ if domain.scope
146
+ indexes_for(domain.container)[domain.scope] ||= MethodIndex.new(methods_in(domain))
147
+ else
148
+ MethodIndex.new([])
149
+ end
150
+ end
151
+
152
+ private
153
+ attr_reader :methods_by_container
154
+
155
+ def grouped_methods_in(ast)
156
+ nodes_in(ast, :class, :module).each_with_object({}.compare_by_identity) do |container, groups|
157
+ groups[container] = direct_method_nodes_in(container.body).group_by { MethodDomain.new(it).scope }
158
+ end
159
+ end
160
+
161
+ def indexes_for(container)
162
+ container ? indexes_by_container[container] ||= {} : top_level_indexes
163
+ end
164
+
165
+ def indexes_by_container
166
+ @indexes_by_container ||= {}.compare_by_identity
167
+ end
168
+
169
+ def top_level_indexes
170
+ @top_level_indexes ||= {}
171
+ end
172
+
173
+ def methods_in(domain)
174
+ methods_by_container.fetch(domain.container, {}).fetch(domain.scope, [])
175
+ end
176
+ end
177
+
178
+ class MethodDomain
179
+ delegate :container, to: :domain
180
+
181
+ def initialize(node)
182
+ @node = node
183
+ end
184
+
185
+ def scope
186
+ domain.identity if resolvable?
187
+ end
188
+
189
+ private
190
+ attr_reader :node
191
+
192
+ def resolvable?
193
+ !node.defs_type? || node.receiver.self_type?
194
+ end
195
+
196
+ def domain
197
+ @domain ||= RuboCop::Callbacksystems::Methods::Domain.new(node)
198
+ end
199
+ end
200
+
201
+ # Resolves calls within one method domain and memoizes the transitive set reached from every parameter.
202
+ class MethodIndex
203
+ def initialize(methods)
204
+ @methods = methods
205
+ end
206
+
207
+ def flow(name, method_node)
208
+ flows_for(method_node)[name] ||= ValueFlow.new(name, method_node, self)
209
+ end
210
+
211
+ def parameter_flow(name, position)
212
+ method = methods_by_name[name]
213
+ Parameter.new(method, position, self).flow unless method.nil? || recursive?(method)
214
+ end
215
+
216
+ def recursive?(method)
217
+ calls_for(method).body_method_names.include?(method.method_name)
218
+ end
219
+
220
+ def assignments_for(method)
221
+ assignments_by_method[method] ||= MethodAssignments.new(method)
222
+ end
223
+
224
+ def threading_calls_for(name, method:)
225
+ calls_for(method).named(name)
226
+ end
227
+
228
+ def reach_size(flow)
229
+ reachability.result_for(flow).then { it.cyclic ? 0 : it.method_names.size }
230
+ end
231
+
232
+ private
233
+ attr_reader :methods
234
+
235
+ def flows_for(method)
236
+ flows_by_method[method] ||= {}
237
+ end
238
+
239
+ def flows_by_method
240
+ @flows_by_method ||= {}.compare_by_identity
241
+ end
242
+
243
+ def methods_by_name
244
+ @methods_by_name ||= methods.index_by(&:method_name)
245
+ end
246
+
247
+ def calls_for(method)
248
+ calls_by_method[method] ||= ThreadingCalls.new(method)
249
+ end
250
+
251
+ def calls_by_method
252
+ @calls_by_method ||= {}.compare_by_identity
253
+ end
254
+
255
+ def assignments_by_method
256
+ @assignments_by_method ||= {}.compare_by_identity
257
+ end
258
+
259
+ def reachability
260
+ @reachability ||= Reachability.new(self)
261
+ end
262
+ end
263
+
264
+ # One value, a name within a single method body, and the receiverless calls it is passed to.
265
+ class ValueFlow < Data.define(:name, :method_node, :index)
266
+ def threading_calls
267
+ index.threading_calls_for(name, method: method_node)
268
+ end
269
+ end
270
+
271
+ # Receiverless calls indexed by every unshadowed local value they carry, with one traversal of the method.
272
+ class ThreadingCalls
273
+ include RuboCop::Callbacksystems::Helpers
274
+
275
+ def initialize(method_node)
276
+ @method_node = method_node
277
+ end
278
+
279
+ def named(name)
280
+ calls_by_name.fetch(name) { [] }
281
+ end
282
+
283
+ def body_method_names
284
+ calls_by_name
285
+ @body_method_names
286
+ end
287
+
288
+ private
289
+ attr_reader :method_node
290
+
291
+ def calls_by_name
292
+ @calls_by_name ||= {}.tap do |calls|
293
+ @body_method_names = Set.new
294
+ method_node.each_descendant(:send).each { add(it, to: calls) if bare_send?(it) }
295
+ end
296
+ end
297
+
298
+ def add(send_node, to:)
299
+ @body_method_names << send_node.method_name if inside_body?(send_node)
300
+ indexed_names = Set.new
301
+ position_known = true
302
+
303
+ send_node.arguments.each_with_index do |argument, position|
304
+ name = unshadowed_name_of(argument)
305
+ add_call(send_node, name, position, to:) if new_name?(name, among: indexed_names) && position_known
306
+ position_known &&= !positional_width_ambiguous?(argument)
307
+ end
308
+ end
309
+
310
+ def inside_body?(send_node)
311
+ method_node.body&.source_range&.contains?(send_node.source_range)
312
+ end
313
+
314
+ def unshadowed_name_of(argument)
315
+ if argument.lvar_type? && !name_rebound_between?(argument.name, node: argument, boundary: method_node)
316
+ argument.name.to_s
317
+ end
318
+ end
319
+
320
+ def new_name?(name, among:)
321
+ name && among.add?(name)
322
+ end
323
+
324
+ def add_call(send_node, name, position, to:)
325
+ (to[name] ||= []) << Call.new(send_node.method_name, position)
326
+ end
327
+
328
+ def positional_width_ambiguous?(argument)
329
+ argument.type?(:splat, :kwsplat, :forwarded_args) || argument.each_descendant(:kwsplat).any?
330
+ end
331
+ end
332
+
333
+ class Parameter < Data.define(:method_node, :position, :index)
334
+ include RuboCop::Callbacksystems::Helpers
335
+
336
+ def flow
337
+ index.flow(name, method_node) if name && index.assignments_for(method_node).exclude?(name)
338
+ end
339
+
340
+ def name
341
+ parameter = method_node.arguments.children[position]
342
+ parameter.children.first.to_s if parameter&.arg_type?
343
+ end
344
+ end
345
+
346
+ # Effective local assignments in one method, excluding nested local scopes and explicitly shadowed block locals.
347
+ class MethodAssignments
348
+ include RuboCop::Callbacksystems::Helpers
349
+
350
+ delegate :exclude?, to: :names
351
+
352
+ def initialize(method_node)
353
+ @method_node = method_node
354
+ end
355
+
356
+ def names
357
+ @names ||= assignments.to_set { assignment_name_of(it) }
358
+ end
359
+
360
+ def unique_local_definitions
361
+ assignments.group_by { assignment_name_of(it) }.filter_map do |name, definitions|
362
+ [ name, definitions.first ] if definitions.one? && definitions.first.lvasgn_type?
363
+ end.to_h
364
+ end
365
+
366
+ private
367
+ attr_reader :method_node
368
+
369
+ def assignments
370
+ @assignments ||= AssignmentsWithin.new(method_node.body).reject do |assignment|
371
+ name_rebound_between?(assignment_name_of(assignment), node: assignment, boundary: method_node)
372
+ end
373
+ end
374
+
375
+ def assignment_name_of(assignment)
376
+ assignment.children.first.to_s
377
+ end
378
+
379
+ class AssignmentsWithin
380
+ include Enumerable
381
+ include RuboCop::Callbacksystems::Helpers
382
+
383
+ def initialize(root)
384
+ @root = root
385
+ end
386
+
387
+ def each
388
+ if block_given?
389
+ each_assignment { yield it }
390
+ else
391
+ to_enum(__method__)
392
+ end
393
+ end
394
+
395
+ private
396
+ attr_reader :root
397
+
398
+ def each_assignment
399
+ pending = [ root ].compact
400
+ until pending.empty?
401
+ current = pending.pop
402
+ yield current if current.type?(:lvasgn, :match_var)
403
+ children_of(current).to_a.reverse_each { pending << it }
404
+ end
405
+ end
406
+
407
+ def children_of(node)
408
+ lexical_definition?(node) ? immediate_inputs_of(node) : node.each_child_node
409
+ end
410
+ end
411
+ end
412
+
413
+ # The cached graph shared by every root within one method domain.
414
+ class Reachability
415
+ attr_reader :results
416
+
417
+ def initialize(index)
418
+ @index = index
419
+ @results = {}.compare_by_identity
420
+ @edges = {}.compare_by_identity
421
+ end
422
+
423
+ def result_for(flow)
424
+ results[flow] ||= ReachabilityWalk.new(flow, self).result
425
+ end
426
+
427
+ def edges_for(flow)
428
+ edges[flow] ||= flow.threading_calls.map do |call|
429
+ FlowEdge.new(call.method_name, index.parameter_flow(call.method_name, call.position))
430
+ end
431
+ end
432
+
433
+ private
434
+ attr_reader :index, :edges
435
+ end
436
+
437
+ # An iterative walk of the graph rooted at one value flow.
438
+ class ReachabilityWalk
439
+ def initialize(root, reachability)
440
+ @root = root
441
+ @reachability = reachability
442
+ @stack = []
443
+ @visiting = {}.compare_by_identity
444
+ end
445
+
446
+ def result
447
+ start(root)
448
+ advance until stack.empty?
449
+ results.fetch(root)
450
+ end
451
+
452
+ private
453
+ attr_reader :root, :reachability, :stack, :visiting
454
+
455
+ delegate :edges_for, :results, to: :reachability, private: true
456
+ delegate :edge, to: :frame, private: true
457
+
458
+ def start(flow)
459
+ visiting[flow] = true
460
+ stack << FlowFrame.new(flow, edges_for(flow))
461
+ end
462
+
463
+ def advance
464
+ if frame.complete?
465
+ finish
466
+ elsif edge.target.nil?
467
+ frame.absorb
468
+ else
469
+ advance_to_target
470
+ end
471
+ end
472
+
473
+ def frame
474
+ stack.last
475
+ end
476
+
477
+ def finish
478
+ results[frame.flow] = frame.result
479
+ visiting.delete(frame.flow)
480
+ stack.pop
481
+ end
482
+
483
+ def advance_to_target
484
+ if results.key?(edge.target)
485
+ frame.absorb(results.fetch(edge.target))
486
+ elsif visiting.key?(edge.target)
487
+ frame.absorb(cyclic: true)
488
+ else
489
+ start(edge.target)
490
+ end
491
+ end
492
+ end
493
+
494
+ # One explicit stack frame keeps large pipelines off Ruby's call stack.
495
+ class FlowFrame
496
+ attr_reader :flow
497
+
498
+ def initialize(flow, edges)
499
+ @flow = flow
500
+ @edges = edges
501
+ @position = 0
502
+ @method_names = Set.new
503
+ @cyclic = false
504
+ end
505
+
506
+ def complete?
507
+ position >= edges.size
508
+ end
509
+
510
+ def absorb(result = nil, cyclic: false)
511
+ method_names << edge.method_name
512
+ method_names.merge(result.method_names) if result
513
+ remember_cycle(result, explicit: cyclic)
514
+ @position += 1
515
+ end
516
+
517
+ def edge
518
+ edges[position]
519
+ end
520
+
521
+ def result
522
+ ReachResult.new(method_names:, cyclic:)
523
+ end
524
+
525
+ private
526
+ attr_reader :edges, :position, :method_names, :cyclic
527
+
528
+ def remember_cycle(result, explicit:)
529
+ @cyclic = cyclic || explicit || result&.cyclic || false
530
+ end
531
+ end
532
+ end