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,261 @@
1
+ # Detects consecutive appends to one collection. `list << a` followed by
2
+ # `list << b` makes the reader walk the same receiver twice, where a single
3
+ # `list.push(a, b)` states the whole addition at once.
4
+ #
5
+ # The run has to be adjacent statements on the same local variable, instance
6
+ # variable or bare method call, and none of the values may read or rebind the
7
+ # receiver, since `list << list.size` after an append counts what was pushed
8
+ # before it, while `list << (list = other)` changes what the next append reaches.
9
+ # `<<` also writes to a String, a Set or a stream, none of which takes several
10
+ # values through `push`, so the rule needs positive evidence that every visible
11
+ # assignment builds an Array and, for a local, an unconditional assignment above
12
+ # the run. An unknown receiver is left alone. A run carrying a comment or a
13
+ # heredoc is reported without a correction, since collapsing it would drop the
14
+ # comment or strand the heredoc body.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # list = []
19
+ # list << header
20
+ # list << body
21
+ #
22
+ # # bad
23
+ # list = []
24
+ # list.push(header)
25
+ # list << body
26
+ #
27
+ # # good
28
+ # list = []
29
+ # list.push(header, body)
30
+ #
31
+ # # good - the second value reads the receiver
32
+ # list = []
33
+ # list << header
34
+ # list << list.size
35
+ #
36
+ class RuboCop::Cop::Callbacksystems::NoConsecutiveAppends < RuboCop::Cop::Callbacksystems::Base
37
+ extend RuboCop::Cop::AutoCorrector
38
+
39
+ def on_new_investigation
40
+ @assigned_values = AssignedValues.new(processed_source.ast)
41
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
42
+ end
43
+
44
+ def on_begin(node)
45
+ report_each AppendRuns.new(node, assigned_values:, source_comments:)
46
+ end
47
+
48
+ alias on_kwbegin on_begin
49
+
50
+ private
51
+ attr_reader :assigned_values, :source_comments
52
+
53
+ # The runs of adjacent appends to one receiver inside a statement list.
54
+ class AppendRuns
55
+ def initialize(statement_list, assigned_values:, source_comments:)
56
+ @statement_list = statement_list
57
+ @assignment_index = assigned_values
58
+ @source_comments = source_comments
59
+ end
60
+
61
+ def each_offense
62
+ runs.select(&:array_receiver?).each { yield it.offense }
63
+ end
64
+
65
+ private
66
+ attr_reader :statement_list, :assignment_index, :source_comments
67
+
68
+ def runs
69
+ appends.chunk_while { |left, right| left.continued_by?(right) }.select(&:many?).map { run_of(it) }
70
+ end
71
+
72
+ def appends
73
+ statement_list.children.map { Append.new(it) }
74
+ end
75
+
76
+ def run_of(members)
77
+ Run.new(members, assigned_values: assignment_index, source_comments:)
78
+ end
79
+ end
80
+
81
+ # One statement read as an append, or as the statement that ends a run.
82
+ class Append
83
+ include RuboCop::Callbacksystems::Helpers
84
+ extend RuboCop::AST::NodePattern::Macros
85
+
86
+ attr_reader :node
87
+
88
+ # @!method appended(node)
89
+ def_node_matcher :appended, <<~PATTERN
90
+ (send ${lvar ivar (send nil? _)} {:<< :push} $!block_pass+)
91
+ PATTERN
92
+
93
+ def initialize(node)
94
+ @node = node
95
+ end
96
+
97
+ def continued_by?(other)
98
+ append? && other.append? && receiver.source == other.receiver.source
99
+ end
100
+
101
+ def append?
102
+ captures.present? && !touches_receiver?
103
+ end
104
+
105
+ def receiver
106
+ captures.first
107
+ end
108
+
109
+ def values
110
+ captures.second
111
+ end
112
+
113
+ private
114
+ def captures
115
+ @captures ||= appended(node)
116
+ end
117
+
118
+ def touches_receiver?
119
+ values.any? { |value| nodes_in(value, *receiver_node_types).any? { same_receiver?(it) } }
120
+ end
121
+
122
+ def receiver_node_types
123
+ if receiver.lvar_type? then %i[ lvar lvasgn match_var ]
124
+ elsif receiver.ivar_type? then %i[ ivar ivasgn ]
125
+ else [ receiver.type ]
126
+ end
127
+ end
128
+
129
+ def same_receiver?(occurrence)
130
+ if receiver.lvar_type?
131
+ variable_name_of(occurrence) == receiver.name &&
132
+ !name_rebound_between?(receiver.name, node: occurrence, boundary: scope)
133
+ elsif receiver.ivar_type?
134
+ occurrence.name == receiver.name
135
+ else
136
+ occurrence.source == receiver.source
137
+ end
138
+ end
139
+
140
+ def scope
141
+ @scope ||= enclosing_scope_of(node)
142
+ end
143
+ end
144
+
145
+ class Run
146
+ include RuboCop::Callbacksystems::Helpers
147
+
148
+ MESSAGE = "Append `%<values>s` to `%<receiver>s` in one `push` instead of consecutive appends."
149
+
150
+ def initialize(appends, assigned_values:, source_comments:)
151
+ @appends = appends
152
+ @assignment_index = assigned_values
153
+ @source_comments = source_comments
154
+ end
155
+
156
+ def array_receiver?
157
+ assigned_values.any? && assigned_values.all? { array_value?(it) } && initialized_before_run?
158
+ end
159
+
160
+ def offense
161
+ RuboCop::Callbacksystems::Offense.new(range, message, correcting: rewritable?) { correct(it) }
162
+ end
163
+
164
+ private
165
+ attr_reader :appends, :assignment_index, :source_comments
166
+ delegate :receiver, to: "appends.first", private: true
167
+
168
+ def assigned_values
169
+ @assigned_values ||= assignments.map { assigned_value_of(it) unless it.match_var_type? }
170
+ end
171
+
172
+ def assignments
173
+ assignment_index.for(receiver)
174
+ end
175
+
176
+ def assigned_value_of(assignment)
177
+ assignment.parent.or_asgn_type? ? assignment.parent.expression : assignment.expression
178
+ end
179
+
180
+ def array_value?(value)
181
+ reads_as_array?(value)
182
+ end
183
+
184
+ def initialized_before_run?
185
+ !receiver.lvar_type? || assignments.any? { direct_assignment_before_run?(it) }
186
+ end
187
+
188
+ def direct_assignment_before_run?(assignment)
189
+ assignment.parent.equal?(appends.first.node.parent) &&
190
+ assignment.source_range.begin_pos < appends.first.node.source_range.begin_pos
191
+ end
192
+
193
+ def range
194
+ range_spanning(appends.map(&:node))
195
+ end
196
+
197
+ def message
198
+ format(MESSAGE, values: values.map(&:source).join(", "), receiver: receiver.source)
199
+ end
200
+
201
+ def values
202
+ appends.flat_map(&:values)
203
+ end
204
+
205
+ def rewritable?
206
+ !receiver.send_type? && !commented? && appends.none? { carries_heredoc?(it.node) }
207
+ end
208
+
209
+ def commented?
210
+ source_comments.any_on_lines?(range.first_line..range.last_line)
211
+ end
212
+
213
+ def correct(corrector)
214
+ corrector.replace(range, "#{receiver.source}.push(#{values.map(&:source).join(", ")})")
215
+ end
216
+ end
217
+
218
+ # Assignments grouped once by local lexical scope or by instance-variable name for the whole file.
219
+ class AssignedValues
220
+ include RuboCop::Callbacksystems::Helpers
221
+
222
+ def initialize(ast)
223
+ @ast = ast
224
+ @local_variables = RuboCop::Callbacksystems::Execution::LocalVariableOccurrences.new(ast)
225
+ end
226
+
227
+ def for(receiver)
228
+ if receiver.lvar_type?
229
+ local_variables.named(receiver.name, around: receiver).select { it.type?(:lvasgn, :match_var) }
230
+ else
231
+ instance_assignments.fetch(instance_variable_key_of(receiver)) { [] }
232
+ end
233
+ end
234
+
235
+ private
236
+ attr_reader :ast, :local_variables
237
+
238
+ def instance_assignments
239
+ @instance_assignments ||= nodes_in(ast, :ivasgn).select { state_belongs_to_domain?(it) }
240
+ .group_by { instance_variable_key_of(it) }
241
+ end
242
+
243
+ def state_belongs_to_domain?(assignment)
244
+ self_preserved_between?(assignment, boundary: state_boundary_of(assignment))
245
+ end
246
+
247
+ def state_boundary_of(assignment)
248
+ enclosing_method_of(assignment) || RuboCop::Callbacksystems::Methods::Domain.new(assignment).container
249
+ end
250
+
251
+ def instance_variable_key_of(node)
252
+ domain = RuboCop::Callbacksystems::Methods::Domain.new(node)
253
+
254
+ [ instance_variable_name_of(node), domain.container&.object_id, domain.identity ]
255
+ end
256
+
257
+ def instance_variable_name_of(receiver)
258
+ receiver.send_type? ? :"@#{receiver.method_name}" : receiver.name
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,124 @@
1
+ # Keeps routes on the standard resource actions. A custom action is a verb
2
+ # bolted onto a controller, and the noun it acts on is a resource of its own
3
+ # waiting to be named, so `confirm` on messages becomes a `confirmation`
4
+ # resource with a `create` action and the controller stays one noun with the
5
+ # seven actions a reader expects.
6
+ #
7
+ # @example
8
+ # # bad - custom action through an option
9
+ # resources :messages do
10
+ # post :confirm, on: :member
11
+ # end
12
+ #
13
+ # # bad - custom actions through a block
14
+ # resources :messages do
15
+ # member do
16
+ # post :confirm
17
+ # end
18
+ # end
19
+ #
20
+ # # good - the action becomes a resource
21
+ # resources :messages do
22
+ # resource :confirmation, only: :create
23
+ # end
24
+ #
25
+ class RuboCop::Cop::Callbacksystems::NoCustomRouteActions < RuboCop::Cop::Callbacksystems::Base
26
+ def on_new_investigation
27
+ @immediate_nodes = {}.compare_by_identity
28
+ RuboCop::Callbacksystems::Execution::Immediate.new(processed_source.ast).each { @immediate_nodes[it] = true }
29
+ end
30
+
31
+ def on_block(node)
32
+ report ScopeBlock.new(node) if in_routes_file? && immediately_executed?(node)
33
+ end
34
+ alias on_numblock on_block
35
+ alias on_itblock on_block
36
+
37
+ def on_send(node)
38
+ report VerbRoute.new(node) if in_routes_file? && immediately_executed?(node)
39
+ end
40
+ alias on_csend on_send
41
+
42
+ private
43
+ CUSTOM_SCOPES = %i[ member collection new ]
44
+
45
+ def in_routes_file?
46
+ routes_file?(processed_source.file_path)
47
+ end
48
+
49
+ def immediately_executed?(node)
50
+ @immediate_nodes.key?(node)
51
+ end
52
+
53
+ class ScopeBlock
54
+ include RuboCop::Callbacksystems::Helpers
55
+
56
+ MESSAGE = "Custom actions belong to their own resource. Replace the `%<scope>s` block with nested resources."
57
+
58
+ def initialize(node)
59
+ @node = node
60
+ end
61
+
62
+ def offense
63
+ if scoped_block?
64
+ RuboCop::Callbacksystems::Offense.new(node.send_node, format(MESSAGE, scope: node.method_name))
65
+ end
66
+ end
67
+
68
+ private
69
+ attr_reader :node
70
+
71
+ def scoped_block?
72
+ CUSTOM_SCOPES.include?(node.method_name) && bare_send?(node.send_node) &&
73
+ node.send_node.arguments.empty? && inside_resources?
74
+ end
75
+
76
+ def inside_resources?
77
+ node.each_ancestor(*BLOCK_NODE_TYPES).any? do |block|
78
+ bare_send?(block.send_node) && %i[ resources resource ].include?(block.method_name)
79
+ end
80
+ end
81
+ end
82
+
83
+ class VerbRoute
84
+ include RuboCop::Callbacksystems::Helpers
85
+
86
+ MESSAGE = "Custom actions belong to their own resource. Route `%<action>s` as a resource instead of `on: " \
87
+ ":%<scope>s`."
88
+ ROUTE_VERBS = %i[ get post patch put delete match ]
89
+
90
+ def initialize(node)
91
+ @node = node
92
+ end
93
+
94
+ def offense
95
+ if scoped_route?
96
+ RuboCop::Callbacksystems::Offense.new(node, format(MESSAGE, action: action_name, scope: custom_scope))
97
+ end
98
+ end
99
+
100
+ private
101
+ attr_reader :node
102
+ delegate :first_argument, to: :node, private: true
103
+
104
+ def scoped_route?
105
+ ROUTE_VERBS.include?(node.method_name) && bare_send?(node) && custom_scope.present?
106
+ end
107
+
108
+ def custom_scope
109
+ @custom_scope ||= on_option_value&.then { it.value if it.sym_type? && CUSTOM_SCOPES.include?(it.value) }
110
+ end
111
+
112
+ def on_option_value
113
+ keyword_options.option_for(:on)&.value
114
+ end
115
+
116
+ def keyword_options
117
+ @keyword_options ||= RuboCop::Callbacksystems::Hashes::KeywordOptions.new(*node.arguments.select(&:hash_type?))
118
+ end
119
+
120
+ def action_name
121
+ literal_name_of(first_argument)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,90 @@
1
+ # Keeps preload, eager_load, and includes inside app/models. Which associations
2
+ # a query loads is knowledge about the model, so every caller asks for a named
3
+ # scope instead of repeating that knowledge at each call site.
4
+ # Methods defined on a known receiver in the same source are left alone; calls
5
+ # on unknown receivers are assumed to use the Active Record query API.
6
+ #
7
+ # @example
8
+ # # bad - direct eager loading in controller
9
+ # class UsersController < ApplicationController
10
+ # def index
11
+ # @users = User.includes(:posts).all
12
+ # end
13
+ # end
14
+ #
15
+ # # good - use a scope
16
+ # class UsersController < ApplicationController
17
+ # def index
18
+ # @users = User.with_posts.all
19
+ # end
20
+ # end
21
+ #
22
+ # # In the model:
23
+ # class User < ApplicationRecord
24
+ # scope :with_posts, -> { includes(:posts) }
25
+ # end
26
+ #
27
+ class RuboCop::Cop::Callbacksystems::NoDirectEagerLoading < RuboCop::Cop::Callbacksystems::Base
28
+ MESSAGE = "Don't use `%<method>s` outside `app/models`. Define a scope in the model instead."
29
+
30
+ def on_new_investigation
31
+ @custom_methods = CustomMethods.new(processed_source.ast)
32
+ end
33
+
34
+ def on_send(node)
35
+ add_offense(node, message: format(MESSAGE, method: node.method_name)) if direct_eager_loading?(node)
36
+ end
37
+
38
+ alias on_csend on_send
39
+
40
+ private
41
+ attr_reader :custom_methods
42
+
43
+ def direct_eager_loading?(node)
44
+ !model_file?(processed_source.file_path) && EAGER_LOADING_METHODS.include?(node.method_name) &&
45
+ node.arguments.any? && !custom_methods.defines?(node)
46
+ end
47
+
48
+ # Explicit definitions distinguish an application method from the homonymous query API without guessing types.
49
+ class CustomMethods
50
+ include RuboCop::Callbacksystems::Helpers
51
+
52
+ def initialize(root)
53
+ @root = root
54
+ end
55
+
56
+ def defines?(call)
57
+ if call_on_self?(call)
58
+ siblings.named(call.method_name, beside: call).any?
59
+ elsif call.receiver.const_type?
60
+ singleton_definition_for?(call)
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ private
67
+ attr_reader :root
68
+
69
+ def siblings
70
+ @siblings ||= RuboCop::Callbacksystems::Methods::Siblings.new
71
+ end
72
+
73
+ def singleton_definition_for?(call)
74
+ singleton_definitions.any? do |definition|
75
+ definition.method?(call.method_name) && receiver_names_match?(call.receiver, definition:)
76
+ end
77
+ end
78
+
79
+ def singleton_definitions
80
+ @singleton_definitions ||= nodes_in(root, :def, :defs).select do |definition|
81
+ RuboCop::Callbacksystems::Methods::Domain.new(definition).identity == [ :self ] &&
82
+ direct_method_definition?(definition)
83
+ end
84
+ end
85
+
86
+ def receiver_names_match?(receiver, definition:)
87
+ enclosing_class_or_module_of(definition).identifier.short_name == receiver.short_name
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,58 @@
1
+ # Detects `each` blocks that `return` an element from inside a condition. The
2
+ # loop is a search written by hand, and `find` says so in one line, while the
3
+ # `each` form makes a reader trace the `return` to learn that the method hands
4
+ # back the first match and `nil` otherwise.
5
+ #
6
+ # @example
7
+ # # bad - each with early return
8
+ # items.each do |item|
9
+ # return item if item.valid?
10
+ # end
11
+ # nil
12
+ #
13
+ # # good - use find
14
+ # items.find { it.valid? }
15
+ #
16
+ # # bad - each with early return (implicit nil)
17
+ # def find_valid
18
+ # items.each do |item|
19
+ # return item if item.valid?
20
+ # end
21
+ # end
22
+ #
23
+ # # good
24
+ # def find_valid
25
+ # items.find { it.valid? }
26
+ # end
27
+ #
28
+ class RuboCop::Cop::Callbacksystems::NoEachWithEarlyReturn < RuboCop::Cop::Callbacksystems::Base
29
+ def on_block(node)
30
+ report EachBlock.new(node)
31
+ end
32
+
33
+ alias on_numblock on_block
34
+ alias on_itblock on_block
35
+
36
+ private
37
+ class EachBlock
38
+ include RuboCop::Callbacksystems::Helpers
39
+
40
+ MESSAGE = "Don't use `each` with early `return`. Use `find` instead."
41
+
42
+ def initialize(node)
43
+ @node = node
44
+ end
45
+
46
+ def offense
47
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE) if node.method?(:each) && returns_conditionally?
48
+ end
49
+
50
+ private
51
+ attr_reader :node
52
+
53
+ def returns_conditionally?
54
+ RuboCop::Callbacksystems::Execution::Immediate.new(node.body).nodes_of_type(:return)
55
+ .any? { conditional_return_of_block_argument?(it, block: node) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,81 @@
1
+ # Do not use an explicit `public` modifier. A class reads top-down, public
2
+ # methods first and a single `private` after them, so a `public` that reopens
3
+ # visibility later makes a reader track which section each method sits in
4
+ # instead of trusting the order.
5
+ #
6
+ # @example
7
+ # # bad - using explicit public modifier
8
+ # class Foo
9
+ # private
10
+ # def private_method; end
11
+ #
12
+ # public
13
+ # def public_method; end
14
+ # end
15
+ #
16
+ # # bad - using public with method name
17
+ # class Foo
18
+ # def some_method; end
19
+ # public :some_method
20
+ # end
21
+ #
22
+ # # good - public methods at the beginning
23
+ # class Foo
24
+ # def public_method; end
25
+ #
26
+ # private
27
+ # def private_method; end
28
+ # end
29
+ #
30
+ class RuboCop::Cop::Callbacksystems::NoExplicitPublicModifier < RuboCop::Cop::Callbacksystems::Base
31
+ extend RuboCop::Cop::AutoCorrector
32
+
33
+ def on_send(node)
34
+ report PublicModifier.new(node, source_comments:)
35
+ end
36
+
37
+ alias on_csend on_send
38
+
39
+ private
40
+ class PublicModifier
41
+ include RuboCop::Callbacksystems::Helpers
42
+
43
+ MESSAGE = "Do not use `public` modifier. Public methods should be defined at the beginning of the class."
44
+
45
+ def initialize(node, source_comments:)
46
+ @node = node
47
+ @source_comments = source_comments
48
+ end
49
+
50
+ def offense
51
+ if explicit?
52
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: redundant? && uncommented?) { correct(it) }
53
+ end
54
+ end
55
+
56
+ private
57
+ attr_reader :node, :source_comments
58
+
59
+ def explicit?
60
+ bare_send?(node) && node.method?(:public) && !inside_nested_class?
61
+ end
62
+
63
+ def inside_nested_class?
64
+ node.each_ancestor(:class, :module).drop(1).any?
65
+ end
66
+
67
+ # A bare `public` after `private` is what keeps the methods below it public, so a human reorders that one.
68
+ def redundant?
69
+ node.arguments.empty? && enclosing_definition_of(node) &&
70
+ statements_before(node).none? { leaves_public_section?(it) }
71
+ end
72
+
73
+ def uncommented?
74
+ !source_comments.any_on_lines?(node.first_line..node.last_line)
75
+ end
76
+
77
+ def correct(corrector)
78
+ corrector.remove(statement_removal_range_for(node))
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,68 @@
1
+ # Detects `Hash.new` with a default block that initializes missing keys to an
2
+ # empty collection. The pattern reaches for an imperative accumulator when the
3
+ # declarative equivalent (`group_by`, `group_by + transform_values`) is almost
4
+ # always clearer.
5
+ #
6
+ # `NoTapCollectionBuilding` only catches the `.tap { ... }` flavor of this
7
+ # pattern; this cop also catches it when used standalone, passed to
8
+ # `each_with_object`, assigned to a variable, etc.
9
+ #
10
+ # @example
11
+ # # bad - building a hash of groups imperatively
12
+ # groups = Hash.new { |h, k| h[k] = [] }
13
+ # items.each { |i| groups[i.type] << i }
14
+ #
15
+ # # bad - same with each_with_object
16
+ # items.each_with_object(Hash.new { |h, k| h[k] = [] }) do |item, groups|
17
+ # groups[item.type] << item
18
+ # end
19
+ #
20
+ # # bad - hash of sets
21
+ # tags = Hash.new { |h, k| h[k] = Set.new }
22
+ #
23
+ # # good - use group_by
24
+ # items.group_by(&:type)
25
+ #
26
+ # # good - group_by + transform_values when the values need more shaping
27
+ # items.group_by(&:type).transform_values { Set.new(it.map(&:tag)) }
28
+ #
29
+ class RuboCop::Cop::Callbacksystems::NoHashWithDefaultBlock < RuboCop::Cop::Callbacksystems::Base
30
+ MESSAGE = "Avoid `Hash.new { |h, k| h[k] = ... }`. Use `group_by` (or `group_by + transform_values`) instead."
31
+
32
+ # @!method value_assigned_to_key(body, hash_name, key_name)
33
+ def_node_matcher :value_assigned_to_key, <<~PATTERN
34
+ (send (lvar %1) :[]= (lvar %2) $_)
35
+ PATTERN
36
+
37
+ def on_block(node)
38
+ if hash_new_block?(node) && core_collection_constructor?(node.send_node, :Hash)
39
+ hash_name, key_name = node.argument_list.map(&:name)
40
+ if empty_collection?(value_assigned_to_key(node.body, hash_name, key_name))
41
+ add_offense(node, message: MESSAGE)
42
+ end
43
+ end
44
+ end
45
+
46
+ alias on_numblock on_block
47
+ alias on_itblock on_block
48
+
49
+ private
50
+ def hash_new_block?(node)
51
+ call = node.send_node
52
+ call.send_type? && call.method?(:new) && node.argument_list.size == 2 &&
53
+ node.argument_list.all?(&:arg_type?)
54
+ end
55
+
56
+ def core_collection_constructor?(node, *receivers)
57
+ node&.send_type? && node.method?(:new) && core_constant?(node.receiver) &&
58
+ receivers.include?(node.receiver.short_name)
59
+ end
60
+
61
+ def empty_collection?(node)
62
+ empty_collection_literal?(node) || collection_constructor?(node)
63
+ end
64
+
65
+ def collection_constructor?(node)
66
+ node&.send_type? && node.arguments.empty? && core_collection_constructor?(node, :Array, :Hash, :Set)
67
+ end
68
+ end