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,478 @@
1
+ # A class body reads in one order: the mixins and the class attribute assignments that say what it is made of, then
2
+ # the values it holds, then the classes it declares in a line, then the `attr_reader`, `attribute` and
3
+ # `class_attribute` that hold its state, then the associations it stands in, then the `delegate` that reaches through
4
+ # them, then the macros that declare its behavior, then the methods, and last the nested classes with a body of their
5
+ # own.
6
+ #
7
+ # A constant tells values from classes by the case of its name rather than by what built it: `Naming/ConstantName` asks
8
+ # for SCREAMING_SNAKE_CASE on a plain value and only lets a name off when the value comes from a call, which is how a
9
+ # class gets built. So `COLUMNS` reads as a value and `Row` as a class, whether it came from `Data.define`, `Struct.new`
10
+ # or a method of your own.
11
+ #
12
+ # A method is never the one at fault. `MethodInvocationOrder` owns the order of those, so a nested class written above
13
+ # the methods is reported once, on the class, rather than once per method it sits over.
14
+ #
15
+ # Nothing moves over a name it reads. A value built from a nested class, or a mixin naming a module declared there, is
16
+ # read while the body loads, so the two keep the order they were written in.
17
+ #
18
+ # `Layout/ClassStructure` lists no category for constants, since putting one there would forbid declaring them in the
19
+ # private section, and it lets an `attr_reader` sit below a method, so the whole order lives here instead and that cop
20
+ # ships off. Leaving both on has them rewrite the same statement in one pass, which breaks the file.
21
+ #
22
+ # @example
23
+ # # bad - the constant sits below the method that reads it
24
+ # class Report
25
+ # def total
26
+ # COLUMNS.size
27
+ # end
28
+ #
29
+ # COLUMNS = [ :name, :amount ]
30
+ # end
31
+ #
32
+ # # good
33
+ # class Report
34
+ # COLUMNS = [ :name, :amount ]
35
+ #
36
+ # def total
37
+ # COLUMNS.size
38
+ # end
39
+ # end
40
+ #
41
+ # # bad - the reader comes before the constant, and the class before both
42
+ # class Report
43
+ # Row = Data.define(:name)
44
+ # attr_reader :rows
45
+ # COLUMNS = [ :name ]
46
+ # end
47
+ #
48
+ # # good
49
+ # class Report
50
+ # COLUMNS = [ :name ]
51
+ # Row = Data.define(:name)
52
+ # attr_reader :rows
53
+ # end
54
+ #
55
+ # # bad - the nested class sits above the methods
56
+ # class Report
57
+ # class Row
58
+ # def to_s = name
59
+ # end
60
+ #
61
+ # def rows
62
+ # [ Row.new ]
63
+ # end
64
+ # end
65
+ #
66
+ # # good
67
+ # class Report
68
+ # def rows
69
+ # [ Row.new ]
70
+ # end
71
+ #
72
+ # class Row
73
+ # def to_s = name
74
+ # end
75
+ # end
76
+ #
77
+ class RuboCop::Cop::Callbacksystems::BodyOrder < RuboCop::Cop::Callbacksystems::Base
78
+ extend RuboCop::Cop::AutoCorrector
79
+
80
+ def on_new_investigation
81
+ report_each Bodies.new(processed_source)
82
+ end
83
+
84
+ private
85
+ class Bodies
86
+ include RuboCop::Callbacksystems::Helpers
87
+
88
+ def initialize(processed_source)
89
+ @processed_source = processed_source
90
+ end
91
+
92
+ def each_offense(&block)
93
+ lifts.each { it.each_offense(&block) }
94
+ sinks.each { it.each_offense(&block) }
95
+ end
96
+
97
+ private
98
+ attr_reader :processed_source
99
+
100
+ def lifts
101
+ @lifts ||= sections.map { Lifts.new(it) }
102
+ end
103
+
104
+ def sections
105
+ @sections ||= bodies.flat_map { sections_of(it) }
106
+ end
107
+
108
+ def bodies
109
+ nodes_in(processed_source.ast, :class, :module, :sclass)
110
+ end
111
+
112
+ def sections_of(node)
113
+ statements_in(node.body)
114
+ .slice_when { |_, right| visibility_modifier_of(right) }
115
+ .map { Entries.new(it.reject { visibility_modifier_of(it) }, processed_source) }
116
+ end
117
+
118
+ def sinks
119
+ @sinks ||= sections.map { Sinks.new(it, permit: permit) }
120
+ end
121
+
122
+ # A lift rewrites the span the classes sit in, and a class carries everything inside it, so one per pass.
123
+ def permit
124
+ @permit ||= RuboCop::Callbacksystems::Autocorrection::RewritePermit.new(granted: lifts.none?(&:any?))
125
+ end
126
+ end
127
+
128
+ class Entries
129
+ include Enumerable
130
+
131
+ delegate :each, to: :statements
132
+
133
+ def initialize(nodes, processed_source)
134
+ @nodes = nodes
135
+ @processed_source = processed_source
136
+ end
137
+
138
+ def blocker_for(declaration)
139
+ take_while { it.begin_position < declaration.begin_position }.find { it.rank > declaration.rank }
140
+ end
141
+
142
+ def methods_below(nested_class)
143
+ select { it.method? && it.begin_position > nested_class.begin_position }
144
+ end
145
+
146
+ def declarations
147
+ select(&:liftable?)
148
+ end
149
+
150
+ def method_precedence_kept?
151
+ reordered_pairs.all?(&:precedence_kept?)
152
+ end
153
+
154
+ # A comment unattached to either statement divides the body into semantic sections.
155
+ def uninterrupted?
156
+ each_cons(2).none? { |above, below| above.range.end.join(below.range.begin).source.strip.present? }
157
+ end
158
+
159
+ def tooling_scope_kept?
160
+ none?(&:contains_tooling_comment?)
161
+ end
162
+
163
+ def anchor
164
+ find(&:body?)
165
+ end
166
+
167
+ private
168
+ attr_reader :nodes, :processed_source
169
+
170
+ def statements
171
+ @statements ||= nodes.map { Statement.new(it, nodes, processed_source) }
172
+ end
173
+
174
+ def reordered_pairs
175
+ each_with_index.flat_map do |first, index|
176
+ drop(index.next).map { ReorderedPair.new(first, it) }.select(&:order_changes?)
177
+ end
178
+ end
179
+
180
+ class ReorderedPair < Data.define(:first, :second)
181
+ def order_changes?
182
+ second.liftable? && (!first.liftable? || first.rank > second.rank)
183
+ end
184
+
185
+ def precedence_kept?
186
+ if first_names&.empty? || second_names&.empty?
187
+ true
188
+ else
189
+ first_names && second_names && !first_names.intersect?(second_names)
190
+ end
191
+ end
192
+
193
+ private
194
+ def first_names
195
+ first.defined_method_names
196
+ end
197
+
198
+ def second_names
199
+ second.defined_method_names
200
+ end
201
+ end
202
+ end
203
+
204
+ # Statements out of place, whose first offense carries the rewrite for the whole group.
205
+ class Misplacements
206
+ include Enumerable
207
+
208
+ ORDER = "a body reads what it holds, then what it does, then its nested classes."
209
+
210
+ delegate :each, to: :misplaced
211
+
212
+ def initialize(entries)
213
+ @entries = entries
214
+ end
215
+
216
+ def each_offense
217
+ rewriting = correctable?
218
+ each_with_index do |statement, index|
219
+ yield RuboCop::Callbacksystems::Offense.new(statement.node, message_for(statement),
220
+ correcting: rewriting && index.zero?) { rewrite(it) }
221
+ end
222
+ end
223
+
224
+ private
225
+ attr_reader :entries
226
+ end
227
+
228
+ class Lifts < Misplacements
229
+ MESSAGE = "Move `%<name>s` above `%<blocker>s`; #{ORDER}"
230
+
231
+ private
232
+ def misplaced
233
+ @misplaced ||= entries.select { it.liftable? && entries.blocker_for(it) && it.movable? }
234
+ end
235
+
236
+ # The rewrite spans every declaration at once, so one that cannot move would be written over.
237
+ def correctable?
238
+ entries.uninterrupted? && entries.tooling_scope_kept? && entries.declarations.all?(&:movable?) &&
239
+ entries.method_precedence_kept?
240
+ end
241
+
242
+ def message_for(declaration)
243
+ format(MESSAGE, name: declaration.name, blocker: entries.blocker_for(declaration).opening_line)
244
+ end
245
+
246
+ def rewrite(corrector)
247
+ Reordering.new(entries.declarations, entries.anchor, corrector).rewrite
248
+ end
249
+ end
250
+
251
+ # The nested classes sitting above what the body does, which `MethodInvocationOrder` never moves.
252
+ class Sinks < Misplacements
253
+ MESSAGE = "Move `%<name>s` below `%<blocked>s`; #{ORDER}"
254
+
255
+ def initialize(entries, permit:)
256
+ super(entries)
257
+ @permit = permit
258
+ end
259
+
260
+ private
261
+ attr_reader :permit
262
+
263
+ def correctable?
264
+ any? && entries.uninterrupted? && entries.tooling_scope_kept? && permit.claim
265
+ end
266
+
267
+ def message_for(nested_class)
268
+ format(MESSAGE, name: nested_class.name, blocked: entries.methods_below(nested_class).first.opening_line)
269
+ end
270
+
271
+ def rewrite(corrector)
272
+ Sinking.new(misplaced, entries.to_a.last, corrector).rewrite
273
+ end
274
+
275
+ def misplaced
276
+ @misplaced ||= entries.select { it.nested_class? && entries.methods_below(it).any? && sinkable?(it) }
277
+ end
278
+
279
+ # A declaration naming the class reads it while the body loads.
280
+ def sinkable?(nested_class)
281
+ entries.declarations.none? { it.reads?(nested_class.declared_name) }
282
+ end
283
+ end
284
+
285
+ class Reordering
286
+ include RuboCop::Callbacksystems::Helpers
287
+
288
+ def initialize(declarations, anchor, corrector)
289
+ @declarations = declarations
290
+ @anchor = anchor
291
+ @corrector = corrector
292
+ end
293
+
294
+ def rewrite
295
+ leading.any? ? replace_leading_span : insert_above_anchor
296
+ statement_removal_ranges_of(trailing.map(&:range)).each { corrector.remove(it) }
297
+ end
298
+
299
+ private
300
+ attr_reader :declarations, :anchor, :corrector
301
+
302
+ def leading
303
+ anchor ? declarations.reject { below_anchor?(it) } : declarations
304
+ end
305
+
306
+ def below_anchor?(declaration)
307
+ declaration.begin_position > anchor.begin_position
308
+ end
309
+
310
+ def replace_leading_span
311
+ corrector.replace(span_of(leading.map(&:range)), ordered_source)
312
+ end
313
+
314
+ def ordered_source
315
+ ordered.map(&:source).zip(separators).join
316
+ end
317
+
318
+ def ordered
319
+ @ordered ||= declarations.sort_by.with_index { |declaration, index| [ declaration.rank, index ] }
320
+ end
321
+
322
+ def separators
323
+ ordered.each_cons(2).map { |above, below| Neighbors.new(above:, below:, declarations:).separator } + [ "" ]
324
+ end
325
+
326
+ def insert_above_anchor
327
+ corrector.insert_before(anchor.range, "#{ordered_source}\n\n")
328
+ end
329
+
330
+ def trailing
331
+ declarations - leading
332
+ end
333
+ end
334
+
335
+ # Two declarations landing next to each other, kept apart only by a blank line the author wrote between them.
336
+ class Neighbors
337
+ BLANK_LINE = /\n[ \t]*\n/
338
+
339
+ def initialize(above:, below:, declarations:)
340
+ @above = above
341
+ @below = below
342
+ @declarations = declarations
343
+ end
344
+
345
+ def separator
346
+ blank_written? ? "\n\n" : "\n"
347
+ end
348
+
349
+ private
350
+ attr_reader :above, :below, :declarations
351
+
352
+ def blank_written?
353
+ declarations.each_cons(2).any? do |first, second|
354
+ written_span.cover?(first.begin_position) && blank_line_between?(first, second)
355
+ end
356
+ end
357
+
358
+ def written_span
359
+ earlier, later = [ above, below ].map(&:begin_position).sort
360
+ earlier...later
361
+ end
362
+
363
+ def blank_line_between?(first, second)
364
+ first.range.end.join(second.range.begin).source.match?(BLANK_LINE)
365
+ end
366
+ end
367
+
368
+ class Sinking
369
+ include RuboCop::Callbacksystems::Helpers
370
+
371
+ def initialize(nested_classes, tail, corrector)
372
+ @nested_classes = nested_classes
373
+ @tail = tail
374
+ @corrector = corrector
375
+ end
376
+
377
+ def rewrite
378
+ corrector.insert_after(tail.range, "\n\n#{ordered_source}")
379
+ statement_removal_ranges_of(nested_classes.map(&:range)).each { corrector.remove(it) }
380
+ end
381
+
382
+ private
383
+ attr_reader :nested_classes, :tail, :corrector
384
+
385
+ def ordered_source
386
+ nested_classes.map(&:source).join("\n\n")
387
+ end
388
+ end
389
+
390
+ class Statement
391
+ include RuboCop::Callbacksystems::Helpers
392
+
393
+ attr_reader :node
394
+
395
+ delegate :range, :source, :contains_tooling_comment?, to: :block
396
+
397
+ def initialize(node, statements, processed_source)
398
+ @node = node
399
+ @statements = statements
400
+ @processed_source = processed_source
401
+ end
402
+
403
+ def liftable?
404
+ rank <= RuboCop::Callbacksystems::ClassStructure::StatementRank::MACRO
405
+ end
406
+
407
+ def rank
408
+ @rank ||= RuboCop::Callbacksystems::ClassStructure::StatementRank.new(node, statements).to_i
409
+ end
410
+
411
+ def method?
412
+ rank == RuboCop::Callbacksystems::ClassStructure::StatementRank::METHOD
413
+ end
414
+
415
+ def nested_class?
416
+ rank == RuboCop::Callbacksystems::ClassStructure::StatementRank::NESTED_CLASS
417
+ end
418
+
419
+ def body?
420
+ rank >= RuboCop::Callbacksystems::ClassStructure::StatementRank::METHOD
421
+ end
422
+
423
+ # Only a read declaration of a higher rank would sort above this one, so any other moves along with it.
424
+ def movable?
425
+ declarations_read_before.none? { it.rank > rank }
426
+ end
427
+
428
+ def reads?(constant_name)
429
+ constant_name && names_read.include?(constant_name)
430
+ end
431
+
432
+ def declared_name
433
+ declared_name_of(node)
434
+ end
435
+
436
+ def begin_position
437
+ node.source_range.begin_pos
438
+ end
439
+
440
+ def name
441
+ node.casgn_type? ? node.name : opening_line
442
+ end
443
+
444
+ def opening_line
445
+ node.source.lines.first.strip
446
+ end
447
+
448
+ def defined_method_names
449
+ @defined_method_names ||= RuboCop::Callbacksystems::Methods::StatementDefinitions.new(node).names
450
+ end
451
+
452
+ private
453
+ attr_reader :statements, :processed_source
454
+
455
+ def block
456
+ @block ||= RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source)
457
+ end
458
+
459
+ def declarations_read_before
460
+ statements.take_while { !it.equal?(node) }
461
+ .select { names_read.include?(declared_name_of(it)) }
462
+ .map { self.class.new(it, statements, processed_source) }
463
+ end
464
+
465
+ # A `private_constant :X` reads the `X` it hides.
466
+ def names_read
467
+ @names_read ||= node.each_descendant(:const, :lvar, :send).filter_map { read_name_of(it) }.to_set
468
+ .merge(private_constant_names_of(node))
469
+ end
470
+
471
+ def read_name_of(descendant)
472
+ if descendant.const_type? then descendant.short_name
473
+ elsif descendant.lvar_type? then descendant.name
474
+ elsif bare_send?(descendant) && descendant.arguments.empty? then descendant.method_name
475
+ end
476
+ end
477
+ end
478
+ end
@@ -0,0 +1,74 @@
1
+ # Requires parentheses on builder method calls (`.new`, ActiveRecord persistence,
2
+ # finders, strong parameters) when they have arguments. A call that builds or
3
+ # looks up a record is an expression handing back a value, and the parentheses
4
+ # mark it as one, while a bare call reads as a declaration the way a macro does
5
+ # (`validates :name`). Bang methods and backslash continuations are exempt.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # User.new name: "John"
10
+ # User.create name: "John", email: "j@example.com"
11
+ # User.find_by name: "John"
12
+ # params.permit :name, :email
13
+ #
14
+ # # good
15
+ # User.new(name: "John")
16
+ # User.create(name: "John", email: "j@example.com")
17
+ # User.find_by(name: "John")
18
+ # params.permit(:name, :email)
19
+ #
20
+ # # good - bang methods exempt
21
+ # User.create! name: "John"
22
+ #
23
+ # # good - backslash continuation
24
+ # User.create \
25
+ # name: "John"
26
+ #
27
+ class RuboCop::Cop::Callbacksystems::BuilderMethodParentheses < RuboCop::Cop::Callbacksystems::Base
28
+ extend RuboCop::Cop::AutoCorrector
29
+
30
+ def on_send(node)
31
+ report BuilderCall.new(node) if builder_method?(node)
32
+ end
33
+
34
+ alias on_csend on_send
35
+
36
+ private
37
+ class BuilderCall
38
+ MESSAGE = "Use parentheses for `%<method>s` when passing arguments."
39
+
40
+ def initialize(node)
41
+ @node = node
42
+ end
43
+
44
+ def offense
45
+ RuboCop::Callbacksystems::Offense.new(node, message) { correct(it) } if missing_parentheses?
46
+ end
47
+
48
+ private
49
+ attr_reader :node
50
+
51
+ def missing_parentheses?
52
+ node.arguments? && !node.parenthesized? && !backslash_continuation?
53
+ end
54
+
55
+ def backslash_continuation?
56
+ gap_range.source.include?("\\")
57
+ end
58
+
59
+ def gap_range
60
+ Parser::Source::Range.new \
61
+ node.source_range.source_buffer, node.loc.selector.end_pos,
62
+ node.first_argument.source_range.begin_pos
63
+ end
64
+
65
+ def message
66
+ format(MESSAGE, method: node.method_name)
67
+ end
68
+
69
+ def correct(corrector)
70
+ corrector.replace(gap_range, "(")
71
+ corrector.insert_after(node.last_argument, ")")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,140 @@
1
+ # Detects multiline expressions that can fit on a single line and collapses them.
2
+ # Applies to hash literals, array literals, and method calls with backslash
3
+ # continuation or parenthesized arguments. An expression spread over several
4
+ # lines when one would hold it costs the reader lines for nothing and hides how
5
+ # short the expression is.
6
+ #
7
+ # @example
8
+ # # bad - hash that fits on one line
9
+ # x = {
10
+ # foo: 1,
11
+ # bar: 2
12
+ # }
13
+ #
14
+ # # good
15
+ # x = { foo: 1, bar: 2 }
16
+ #
17
+ # # bad - array that fits on one line
18
+ # x = [
19
+ # 1,
20
+ # 2,
21
+ # 3
22
+ # ]
23
+ #
24
+ # # good
25
+ # x = [ 1, 2, 3 ]
26
+ #
27
+ # # bad - backslash continuation that fits on one line
28
+ # redirect_to \
29
+ # users_path, notice: "Done"
30
+ #
31
+ # # good
32
+ # redirect_to users_path, notice: "Done"
33
+ #
34
+ # # bad - parenthesized call that fits on one line
35
+ # User.new(
36
+ # name: "John"
37
+ # )
38
+ #
39
+ # # good
40
+ # User.new(name: "John")
41
+ #
42
+ class RuboCop::Cop::Callbacksystems::CollapseMultilineExpression < RuboCop::Cop::Callbacksystems::Base
43
+ extend RuboCop::Cop::AutoCorrector
44
+
45
+ def on_hash(node)
46
+ report_if_uncommented CollapsibleExpression.new(SingleLineLiteral.new(node, max_line_length, HASH_SHAPE)), node:
47
+ end
48
+
49
+ def on_array(node)
50
+ report_if_uncommented CollapsibleExpression.new(SingleLineLiteral.new(node, max_line_length, ARRAY_SHAPE)), node:
51
+ end
52
+
53
+ def on_send(node)
54
+ report_if_uncommented \
55
+ CollapsibleExpression.new(RuboCop::Callbacksystems::Source::SingleLineCall.new(node, max_line_length)),
56
+ node:
57
+ end
58
+
59
+ alias on_csend on_send
60
+
61
+ private
62
+ Shape = Data.define(:open, :close, :items)
63
+
64
+ HASH_SHAPE = Shape.new(open: "{", close: "}", items: :children)
65
+ ARRAY_SHAPE = Shape.new(open: "[", close: "]", items: :values)
66
+
67
+ def report_if_uncommented(analysis, node:)
68
+ report analysis if node.multiline? && !comments_within?(node)
69
+ end
70
+
71
+ def comments_within?(node)
72
+ source_comments.any_within?(node)
73
+ end
74
+
75
+ class CollapsibleExpression
76
+ MESSAGE = "This expression can fit on a single line."
77
+
78
+ def initialize(single_line)
79
+ @single_line = single_line
80
+ end
81
+
82
+ def offense
83
+ RuboCop::Callbacksystems::Offense.new(single_line.node, MESSAGE) { correct(it) } if single_line.fits?
84
+ end
85
+
86
+ private
87
+ attr_reader :single_line
88
+
89
+ def correct(corrector)
90
+ corrector.replace(single_line.node, single_line.source)
91
+ end
92
+ end
93
+
94
+ class SingleLineLiteral
95
+ include RuboCop::Callbacksystems::Helpers
96
+
97
+ attr_reader :node
98
+
99
+ def initialize(node, max_line_length, shape)
100
+ @node = node
101
+ @max_line_length = max_line_length
102
+ @shape = shape
103
+ end
104
+
105
+ def fits?
106
+ delimiter_match? && multiline? && no_multiline_items? && !carries_heredoc?(node) && fits_on_one_line?
107
+ end
108
+
109
+ def source
110
+ @source ||= "#{shape.open} #{items_source} #{shape.close}"
111
+ end
112
+
113
+ private
114
+ attr_reader :max_line_length, :shape
115
+
116
+ def delimiter_match?
117
+ node.loc.begin&.source == shape.open
118
+ end
119
+
120
+ def multiline?
121
+ !node.single_line?
122
+ end
123
+
124
+ def no_multiline_items?
125
+ items.all?(&:single_line?)
126
+ end
127
+
128
+ def items
129
+ node.public_send(shape.items)
130
+ end
131
+
132
+ def fits_on_one_line?
133
+ fits_on_line?(node, source, max_line_length)
134
+ end
135
+
136
+ def items_source
137
+ items.map { it.source.strip }.join(", ")
138
+ end
139
+ end
140
+ end