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,336 @@
1
+ # Ensures a `module:` option repeated across routes is extracted to a `scope`
2
+ # block. Repeated, the namespace sits on every line and a reader compares them
3
+ # to learn that the routes share a controller directory, where one
4
+ # `scope module:` says it once and lets the routes read bare.
5
+ #
6
+ # Autocorrection consolidates the routes only when they are contiguous, so it
7
+ # never reorders a route past another and changes matching precedence.
8
+ #
9
+ # @example
10
+ # # bad - module: repeated multiple times
11
+ # resources :users, module: :admin
12
+ # resources :posts, module: :admin
13
+ # resources :comments, module: :admin
14
+ #
15
+ # # good - use scope block
16
+ # scope module: :admin do
17
+ # resources :users
18
+ # resources :posts
19
+ # resources :comments
20
+ # end
21
+ #
22
+ class RuboCop::Cop::Callbacksystems::RoutesModuleScope < RuboCop::Cop::Callbacksystems::Base
23
+ extend RuboCop::Cop::AutoCorrector
24
+
25
+ def on_new_investigation
26
+ if routes_file?(processed_source.file_path)
27
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
28
+ @statement_positions = StatementPositions.new
29
+ repeated_route_groups.each { report_each RouteGroup.new(it, source_comments:, statement_positions:) }
30
+ end
31
+ end
32
+
33
+ private
34
+ ROUTE_METHODS = %i[ resources resource get post put patch delete match root ]
35
+
36
+ attr_reader :source_comments, :statement_positions
37
+
38
+ def repeated_route_groups
39
+ routes_with_module.group_by(&:group_key).values.select(&:many?)
40
+ end
41
+
42
+ def routes_with_module
43
+ route_calls.map { Route.new(it) }.select(&:module_value)
44
+ end
45
+
46
+ def route_calls
47
+ RuboCop::Callbacksystems::Execution::Immediate.new(processed_source.ast).nodes_of_type(:send)
48
+ .select { bare_send?(it) && ROUTE_METHODS.include?(it.method_name) }
49
+ end
50
+
51
+ # Routes repeating one `module:`, where only the first offense carries the correction that rewrites the whole run.
52
+ class RouteGroup
53
+ MESSAGE = "Extract repeated `module: %<module>s` to a `scope module: %<module>s do` block."
54
+
55
+ def initialize(routes, source_comments:, statement_positions:)
56
+ @routes = routes
57
+ @source_comments = source_comments
58
+ @statement_positions = statement_positions
59
+ end
60
+
61
+ def each_offense
62
+ yield offense_for(routes.first, correcting: consolidation.applicable?)
63
+ routes.drop(1).each { yield offense_for(it, correcting: false) }
64
+ end
65
+
66
+ private
67
+ attr_reader :routes, :source_comments, :statement_positions
68
+
69
+ def offense_for(route, correcting:)
70
+ RuboCop::Callbacksystems::Offense.new(route.node, message_for(route), correcting:) { consolidation.rewrite(it) }
71
+ end
72
+
73
+ def message_for(route)
74
+ format(MESSAGE, module: route.module_value)
75
+ end
76
+
77
+ def consolidation
78
+ @consolidation ||= Consolidation.new(routes, source_comments:, statement_positions:)
79
+ end
80
+ end
81
+
82
+ # One group of routes rewritten as a `scope module:` block, when they sit together with no comment among them.
83
+ class Consolidation
84
+ include RuboCop::Callbacksystems::Helpers
85
+
86
+ def initialize(routes, source_comments:, statement_positions:)
87
+ @routes = routes
88
+ @source_comments = source_comments
89
+ @statement_positions = statement_positions
90
+ end
91
+
92
+ def applicable?
93
+ contiguous? && routes.all?(&:literal_module?) && routes.all?(&:module_removable?) && source_self_contained? &&
94
+ !comments_within_span?
95
+ end
96
+
97
+ def rewrite(corrector)
98
+ corrector.replace(span, scope_block)
99
+ end
100
+
101
+ private
102
+ attr_reader :routes, :source_comments, :statement_positions
103
+
104
+ def contiguous?
105
+ container.begin_type? && nodes.all? { it.parent.equal?(container) } && consecutive?(positions_in_container)
106
+ end
107
+
108
+ def container
109
+ @container ||= nodes.first.parent
110
+ end
111
+
112
+ def nodes
113
+ @nodes ||= routes.map(&:node)
114
+ end
115
+
116
+ def consecutive?(indexes)
117
+ indexes.sort.each_cons(2).all? { |left, right| right == left + 1 }
118
+ end
119
+
120
+ def positions_in_container
121
+ nodes.map { statement_positions.position_of(it) }
122
+ end
123
+
124
+ def source_self_contained?
125
+ routes.none? { carries_heredoc?(it.node) }
126
+ end
127
+
128
+ def comments_within_span?
129
+ source_comments.any_on_lines?(span.first_line..span.last_line)
130
+ end
131
+
132
+ def span
133
+ @span ||= range_spanning(ordered_routes.map(&:node))
134
+ end
135
+
136
+ def ordered_routes
137
+ @ordered_routes ||= routes.sort_by { it.node.source_range.begin_pos }
138
+ end
139
+
140
+ def scope_block
141
+ [ "scope module: #{module_source} do", *route_lines, "#{indent}end" ].join("\n")
142
+ end
143
+
144
+ def module_source
145
+ routes.first.module_value
146
+ end
147
+
148
+ def route_lines
149
+ ordered_routes.map { "#{indent} #{it.source_without_module}" }
150
+ end
151
+
152
+ def indent
153
+ @indent ||= indentation_of(ordered_routes.first.node)
154
+ end
155
+ end
156
+
157
+ # Statement positions shared by route groups, preserving node identity when two routes have identical syntax.
158
+ class StatementPositions
159
+ def initialize
160
+ @by_container = {}.compare_by_identity
161
+ end
162
+
163
+ def position_of(node)
164
+ positions_in(node.parent).fetch(node)
165
+ end
166
+
167
+ private
168
+ attr_reader :by_container
169
+
170
+ def positions_in(container)
171
+ by_container[container] ||= positions_of(container.children)
172
+ end
173
+
174
+ def positions_of(statements)
175
+ statements.each_with_index.with_object({}.compare_by_identity) do |(statement, position), positions|
176
+ positions[statement] = position
177
+ end
178
+ end
179
+ end
180
+
181
+ class Route
182
+ include RuboCop::Callbacksystems::Helpers
183
+
184
+ attr_reader :node
185
+
186
+ def initialize(node)
187
+ @node = node
188
+ end
189
+
190
+ def group_key
191
+ [ parent_block, normalized_module ]
192
+ end
193
+
194
+ def module_value
195
+ module_node&.source
196
+ end
197
+
198
+ def literal_module?
199
+ module_node.type?(:sym, :str)
200
+ end
201
+
202
+ def module_removable?
203
+ module_pairs.one?
204
+ end
205
+
206
+ def source_without_module
207
+ before_module.source + after_module.source
208
+ end
209
+
210
+ private
211
+ # Two blocks written alike compare equal as nodes, so the position tells the parents apart.
212
+ def parent_block
213
+ node.each_ancestor(*BLOCK_NODE_TYPES).first&.then { it.source_range.begin_pos } || :root
214
+ end
215
+
216
+ def normalized_module
217
+ [ module_node.type, literal_name_of(module_node) ]
218
+ end
219
+
220
+ def module_node
221
+ module_pair&.value
222
+ end
223
+
224
+ def module_pair
225
+ keyword_options.option_for(:module)
226
+ end
227
+
228
+ def keyword_options
229
+ @keyword_options ||= RuboCop::Callbacksystems::Hashes::KeywordOptions.new(*node.arguments.select(&:hash_type?))
230
+ end
231
+
232
+ def module_pairs
233
+ keyword_options.explicit_options_for(:module)
234
+ end
235
+
236
+ def before_module
237
+ node.source_range.with(end_pos: module_gap.begin_pos)
238
+ end
239
+
240
+ def module_gap
241
+ explicit_options? ? explicit_module_gap : implicit_module_gap
242
+ end
243
+
244
+ def explicit_options?
245
+ options.loc.begin
246
+ end
247
+
248
+ def options
249
+ module_pair.parent
250
+ end
251
+
252
+ def explicit_module_gap
253
+ if option_after_module
254
+ module_pair.source_range.with(end_pos: option_after_module.source_range.begin_pos)
255
+ elsif option_before_module
256
+ option_before_module.source_range.end.join(module_pair.source_range.end)
257
+ else
258
+ options_argument_gap
259
+ end
260
+ end
261
+
262
+ def option_after_module
263
+ option_elements[option_index + 1]
264
+ end
265
+
266
+ def option_elements
267
+ options.children
268
+ end
269
+
270
+ def option_index
271
+ option_elements.index { it.equal?(module_pair) }
272
+ end
273
+
274
+ def option_before_module
275
+ option_elements[option_index - 1] if option_index.positive?
276
+ end
277
+
278
+ def options_argument_gap
279
+ following_options_gap || preceding_options_gap || sole_options_gap
280
+ end
281
+
282
+ def following_options_gap
283
+ argument_after_options&.then { options.source_range.with(end_pos: it.source_range.begin_pos) }
284
+ end
285
+
286
+ def argument_after_options
287
+ node.arguments[options_index + 1]
288
+ end
289
+
290
+ def options_index
291
+ node.arguments.index { it.equal?(options) }
292
+ end
293
+
294
+ def preceding_options_gap
295
+ argument_before_options&.then { it.source_range.end.join(options.source_range.end) }
296
+ end
297
+
298
+ def argument_before_options
299
+ node.arguments[options_index - 1] if options_index.positive?
300
+ end
301
+
302
+ def sole_options_gap
303
+ node.loc.begin.join(node.loc.end)
304
+ end
305
+
306
+ def implicit_module_gap
307
+ if element_after_module
308
+ module_pair.source_range.join(element_after_module.source_range.begin)
309
+ elsif element_before_module
310
+ element_before_module.source_range.end.join(module_pair.source_range)
311
+ else
312
+ node.loc.selector.end.join(node.source_range.end)
313
+ end
314
+ end
315
+
316
+ def element_after_module
317
+ elements[module_index + 1]
318
+ end
319
+
320
+ def elements
321
+ @elements ||= node.arguments.flat_map { it.hash_type? ? it.children : [ it ] }
322
+ end
323
+
324
+ def module_index
325
+ elements.index { it.equal?(module_pair) }
326
+ end
327
+
328
+ def element_before_module
329
+ elements[module_index - 1] if module_index.positive?
330
+ end
331
+
332
+ def after_module
333
+ node.source_range.with(begin_pos: module_gap.end_pos)
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,107 @@
1
+ # Asks for `{ }` over `do end` on a single-line setup or teardown block. The
2
+ # `do end` form is the shape of a body with several lines, so wrapping one
3
+ # statement in it spends two lines on a body that never grew.
4
+ #
5
+ # @example
6
+ # # bad
7
+ # setup do
8
+ # @user = users(:bruno)
9
+ # end
10
+ #
11
+ # # good
12
+ # setup { @user = users(:bruno) }
13
+ #
14
+ # # good (multi-line is fine with do/end)
15
+ # setup do
16
+ # @user = users(:bruno)
17
+ # @account = accounts(:callback)
18
+ # end
19
+ #
20
+ class RuboCop::Cop::Callbacksystems::SingleLineSetupBlock < RuboCop::Cop::Callbacksystems::Base
21
+ extend RuboCop::Cop::AutoCorrector
22
+
23
+ def on_new_investigation
24
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
25
+ end
26
+
27
+ def on_block(node)
28
+ report SetupBlock.new(node, @source_comments)
29
+ end
30
+
31
+ alias on_numblock on_block
32
+ alias on_itblock on_block
33
+
34
+ private
35
+ class SetupBlock
36
+ include RuboCop::Callbacksystems::Helpers
37
+ extend RuboCop::AST::NodePattern::Macros
38
+
39
+ MESSAGE = "Use `%<method>s { ... }` for single-line blocks instead of `do ... end`."
40
+
41
+ # @!method setup_or_teardown_block?(node)
42
+ def_node_matcher :setup_or_teardown_block?, <<~PATTERN
43
+ (any_block (send nil? ${:setup :teardown} ...) ...)
44
+ PATTERN
45
+
46
+ def initialize(node, source_comments)
47
+ @node = node
48
+ @source_comments = source_comments
49
+ end
50
+
51
+ def offense
52
+ offense_for_block if expanded_one_liner?
53
+ end
54
+
55
+ private
56
+ attr_reader :node, :source_comments
57
+
58
+ def expanded_one_liner?
59
+ setup_or_teardown_block?(node) && !node.braces? && node.body&.single_line? && statements_in(node.body).one?
60
+ end
61
+
62
+ def offense_for_block
63
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) }
64
+ end
65
+
66
+ def message
67
+ format(MESSAGE, method: node.method_name)
68
+ end
69
+
70
+ # Braces bind tighter than command-style arguments, so a multiline command stays untouched until its comments
71
+ # and indentation can be preserved. A single-line command can become an explicit argument list.
72
+ def correctable?
73
+ comments_movable? && !carries_heredoc?(node.body) &&
74
+ (node.send_node.arguments.empty? || node.send_node.parenthesized? || node.send_node.single_line?)
75
+ end
76
+
77
+ # One prose comment has an unambiguous home after the compact block. Several comments encode a layout of their
78
+ # own, and tooling directives can change meaning when moved, so neither is rewritten.
79
+ def comments_movable?
80
+ comments_on_block_lines.size <= 1 && comments_on_block_lines.none? { tooling_comment?(it) }
81
+ end
82
+
83
+ def comments_on_block_lines
84
+ @comments_on_block_lines ||= source_comments.on_lines(node.first_line..node.last_line)
85
+ end
86
+
87
+ def correct(corrector)
88
+ corrector.replace(node, "#{call_source} { #{parameters}#{node.body.source.strip} }")
89
+ end
90
+
91
+ def call_source
92
+ if node.send_node.arguments? && !node.send_node.parenthesized?
93
+ "#{node.method_name}(#{call_arguments_source})"
94
+ else
95
+ node.send_node.source
96
+ end
97
+ end
98
+
99
+ def call_arguments_source
100
+ node.send_node.first_argument.source_range.join(node.send_node.last_argument.source_range).source
101
+ end
102
+
103
+ def parameters
104
+ "#{node.arguments.source} " unless node.arguments.empty?
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,49 @@
1
+ # A class declares its class-level methods in a single `class << self` section.
2
+ # Splitting them across several blocks scatters the class API and hides which
3
+ # part of it is private.
4
+ #
5
+ # @example
6
+ # # bad - two singleton sections
7
+ # class Configuration
8
+ # class << self
9
+ # def create_from(file)
10
+ # end
11
+ # end
12
+ #
13
+ # def initialize(raw_config)
14
+ # end
15
+ #
16
+ # class << self
17
+ # private
18
+ # def load_config_files(file)
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # # good - one singleton section
24
+ # class Configuration
25
+ # class << self
26
+ # def create_from(file)
27
+ # end
28
+ #
29
+ # private
30
+ # def load_config_files(file)
31
+ # end
32
+ # end
33
+ #
34
+ # def initialize(raw_config)
35
+ # end
36
+ # end
37
+ #
38
+ class RuboCop::Cop::Callbacksystems::SingleSingletonClass < RuboCop::Cop::Callbacksystems::Base
39
+ MESSAGE = "Merge this `class << self` into the one declared above; a class has a single singleton section."
40
+
41
+ def on_class(node)
42
+ singleton_sections_in(node).drop(1).each { add_offense(it, message: MESSAGE) }
43
+ end
44
+
45
+ private
46
+ def singleton_sections_in(node)
47
+ statements_in(node.body).select { singleton_section?(it) }
48
+ end
49
+ end
@@ -0,0 +1,160 @@
1
+ # Forbids a second class or module at the top level of a file. Zeitwerk expects
2
+ # the path of a file to name the constant it defines, so a second definition is
3
+ # one nothing autoloads, and a reader looking for it by name never finds the
4
+ # file it sits in.
5
+ #
6
+ # A class extending one written above it in the same file is left alone. Moving it
7
+ # out would need a `require` back, so the two sitting together is a hierarchy
8
+ # rather than the sprawl this looks for.
9
+ #
10
+ # @example
11
+ # # bad - multiple top-level classes
12
+ # class User
13
+ # end
14
+ #
15
+ # class Admin
16
+ # end
17
+ #
18
+ # # bad - multiple top-level modules
19
+ # module Authentication
20
+ # end
21
+ #
22
+ # module Authorization
23
+ # end
24
+ #
25
+ # # bad - mixed top-level class and module
26
+ # class User
27
+ # end
28
+ #
29
+ # module UserHelpers
30
+ # end
31
+ #
32
+ # # good - single top-level class
33
+ # class User
34
+ # end
35
+ #
36
+ # # good - single top-level module
37
+ # module Authentication
38
+ # end
39
+ #
40
+ # # good - nested classes/modules inside one top-level
41
+ # class User
42
+ # class Profile
43
+ # end
44
+ #
45
+ # module Validations
46
+ # end
47
+ # end
48
+ #
49
+ # # good - a hierarchy the file declares together
50
+ # class BaseTest < ActiveSupport::TestCase
51
+ # end
52
+ #
53
+ # class VariantTest < BaseTest
54
+ # end
55
+ #
56
+ class RuboCop::Cop::Callbacksystems::SingleTopLevelClassOrModule < RuboCop::Cop::Callbacksystems::Base
57
+ def on_new_investigation
58
+ report_each TopLevelDefinitions.new(processed_source.ast)
59
+ end
60
+
61
+ private
62
+ class TopLevelDefinitions
63
+ include RuboCop::Callbacksystems::Helpers
64
+
65
+ MESSAGE = "Only one top-level class or module is allowed per file."
66
+ TRANSPARENT_NODES = %i[
67
+ begin kwbegin if case case_match when in_pattern rescue resbody ensure
68
+ ]
69
+
70
+ def initialize(ast)
71
+ @ast = ast
72
+ end
73
+
74
+ def each_offense
75
+ standalone.each { yield RuboCop::Callbacksystems::Offense.new(it, MESSAGE) }
76
+ end
77
+
78
+ private
79
+ attr_reader :ast
80
+
81
+ def standalone
82
+ definitions.drop(1).reject { it.extends?(definitions) }.map(&:node)
83
+ end
84
+
85
+ def definitions
86
+ @definitions ||= definitions_in(ast)
87
+ end
88
+
89
+ def definitions_in(node)
90
+ Enumerator.new do |definitions|
91
+ pending = [ node ].compact
92
+ until pending.empty?
93
+ current = pending.pop
94
+ definition = Definition.new(current)
95
+ if definition.declared?
96
+ definitions << definition
97
+ elsif current.type?(*TRANSPARENT_NODES)
98
+ pending.concat(current.child_nodes.reverse)
99
+ end
100
+ end
101
+ end.to_a
102
+ end
103
+
104
+ class Definition
105
+ include RuboCop::Callbacksystems::Helpers
106
+
107
+ attr_reader :node
108
+
109
+ def initialize(node)
110
+ @node = node
111
+ end
112
+
113
+ def declared?
114
+ node.type?(:class, :module) || builder_declaration?
115
+ end
116
+
117
+ def extends?(definitions)
118
+ parent_class.then { |parent| parent && definitions.any? { it.name == parent.source } }
119
+ end
120
+
121
+ def name
122
+ node.type?(:class, :module) ? node.identifier.source : assigned_constant_name
123
+ end
124
+
125
+ private
126
+ def builder_declaration?
127
+ if node.casgn_type?
128
+ builder_assignment.builds_class? || builder_assignment.builds_module?
129
+ else
130
+ false
131
+ end
132
+ end
133
+
134
+ def builder_assignment
135
+ @builder_assignment ||= RuboCop::Callbacksystems::ClassStructure::BuilderAssignment.new(node)
136
+ end
137
+
138
+ def parent_class
139
+ if node.class_type?
140
+ node.parent_class
141
+ elsif node.casgn_type? && class_new_builder?
142
+ builder_call.first_argument
143
+ end
144
+ end
145
+
146
+ def class_new_builder?
147
+ builder_call&.method?(:new) && core_constant?(builder_call.receiver) &&
148
+ builder_call.receiver.short_name == :Class
149
+ end
150
+
151
+ def builder_call
152
+ @builder_call ||= call_of(node.expression)
153
+ end
154
+
155
+ def assigned_constant_name
156
+ node.source_range.with(end_pos: node.loc.operator.begin_pos).source.rstrip
157
+ end
158
+ end
159
+ end
160
+ end