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,286 @@
1
+ # A class whose whole body is class-level code should be a module with
2
+ # `extend self`. Classes are for making instances.
3
+ #
4
+ # `Style/StaticClass` covers the plain case, but it abstains as soon as the
5
+ # singleton section has a private part, because the `module_function` it
6
+ # corrects to stops copying methods to the singleton once `private` switches
7
+ # the mode, which breaks every call to them. `extend self` carries the
8
+ # visibility over untouched, so that case is reported here instead.
9
+ #
10
+ # @example
11
+ # # bad - class methods only, with a private helper
12
+ # class Architecture
13
+ # class << self
14
+ # def resolve(reports)
15
+ # normalize(reports)
16
+ # end
17
+ #
18
+ # private
19
+ # def normalize(raw)
20
+ # raw.downcase
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # # good
26
+ # module Architecture
27
+ # extend self
28
+ #
29
+ # def resolve(reports)
30
+ # normalize(reports)
31
+ # end
32
+ #
33
+ # private
34
+ # def normalize(raw)
35
+ # raw.downcase
36
+ # end
37
+ # end
38
+ #
39
+ class RuboCop::Cop::Callbacksystems::PreferModuleForStaticClass < RuboCop::Cop::Callbacksystems::Base
40
+ include RuboCop::Callbacksystems::ProjectIndex::Support
41
+
42
+ def on_class(node)
43
+ if project_index_reliable?
44
+ inspect_candidate StaticClass.new(node), node:
45
+ end
46
+ end
47
+
48
+ private
49
+ def inspect_candidate(analysis, node:)
50
+ declaration = convertible_declaration_of(node) if analysis.candidate?
51
+ report analysis if declaration && module_preferred?(analysis, declaration:)
52
+ end
53
+
54
+ def convertible_declaration_of(node)
55
+ declaration = resolve_constant_in_index(node.identifier)
56
+ declaration if declaration.is_a?(Rubydex::Class) && project_declaration_reliable?(declaration)
57
+ end
58
+
59
+ def module_preferred?(analysis, declaration:)
60
+ analysis.compatible_with_module?(declaration) && !class_required?(declaration)
61
+ end
62
+
63
+ def class_required?(declaration)
64
+ inherited_callback_observable? || definitions_in_other_files(declaration.definitions).any? ||
65
+ !declaration.definitions.one? ||
66
+ declaration.descendants.any? { it.name != declaration.name } ||
67
+ !project_references.compatible_with_module?(declaration)
68
+ end
69
+
70
+ def inherited_callback_observable?
71
+ [ project_index["Object"].singleton_class, project_index["Class"] ].compact.any? do |namespace|
72
+ namespace.find_member("inherited()")&.definitions&.any?
73
+ end
74
+ end
75
+
76
+ def project_references
77
+ ProjectReferences.for(project_index)
78
+ end
79
+
80
+ # Every reference must prove that replacing the declaration with a module preserves what that line does.
81
+ class ProjectReferences
82
+ extend RuboCop::Callbacksystems::ProjectIndex::Cache
83
+
84
+ def initialize(project_index)
85
+ @method_references_by_receiver = grouped_references(project_index.method_references) { receiver_name_of(it) }
86
+ @constant_references_by_owner = grouped_references(project_index.constant_references) { owner_name_of(it) }
87
+ end
88
+
89
+ def compatible_with_module?(declaration)
90
+ declaration.references.all? do |reference|
91
+ ReferenceUse.new(
92
+ reference,
93
+ declaration:,
94
+ method_references: method_references_for(declaration),
95
+ constant_references: constant_references_for(declaration)
96
+ ).compatible?
97
+ end
98
+ end
99
+
100
+ private
101
+ attr_reader :method_references_by_receiver, :constant_references_by_owner
102
+
103
+ def grouped_references(references)
104
+ references.each_with_object({}) do |reference, groups|
105
+ yield(reference)&.then do |group|
106
+ locations = groups[group] ||= {}
107
+ (locations[location_key(reference.location)] ||= []) << reference
108
+ end
109
+ end
110
+ end
111
+
112
+ def location_key(location)
113
+ [ location.uri, location.start_line, location.start_column ]
114
+ end
115
+
116
+ def receiver_name_of(reference)
117
+ receiver = reference.receiver
118
+ receiver.attached_class.name if receiver.is_a?(Rubydex::SingletonClass)
119
+ end
120
+
121
+ def owner_name_of(reference)
122
+ reference.declaration.owner.name if reference.is_a?(Rubydex::ResolvedConstantReference)
123
+ end
124
+
125
+ def method_references_for(declaration)
126
+ method_references_by_receiver.fetch(declaration.name) { {} }
127
+ end
128
+
129
+ def constant_references_for(declaration)
130
+ constant_references_by_owner.fetch(declaration.name) { {} }
131
+ end
132
+
133
+ class ReferenceUse
134
+ def initialize(reference, declaration:, method_references:, constant_references:)
135
+ @reference = reference
136
+ @declaration = declaration
137
+ @method_references = method_references
138
+ @constant_references = constant_references
139
+ end
140
+
141
+ def compatible?
142
+ direct_declared_method? || namespace_reference?
143
+ end
144
+
145
+ private
146
+ attr_reader :reference, :declaration, :method_references, :constant_references
147
+
148
+ def direct_declared_method?
149
+ adjacent_references_in(method_references).any? { declared_on_singleton?(it.name) }
150
+ end
151
+
152
+ def adjacent_references_in(references, separator_width: nil)
153
+ expected_columns(separator_width).flat_map do |column|
154
+ references.fetch(location_key(column)) { [] }
155
+ end
156
+ end
157
+
158
+ def expected_columns(separator_width)
159
+ widths = separator_width ? [ separator_width ] : [ 1, 2 ]
160
+ widths.to_set { reference.location.end_column + it }
161
+ end
162
+
163
+ def location_key(column)
164
+ [ reference.location.uri, reference.location.end_line, column ]
165
+ end
166
+
167
+ def declared_on_singleton?(method_name)
168
+ declaration.singleton_class.member("#{method_name}()").present?
169
+ end
170
+
171
+ def namespace_reference?
172
+ adjacent_references_in(constant_references, separator_width: 2).any?
173
+ end
174
+ end
175
+ end
176
+
177
+ class StaticClass
178
+ include RuboCop::Callbacksystems::Helpers
179
+
180
+ MESSAGE = "Prefer a module with `extend self` to a class holding only class methods."
181
+ CLASS_ONLY_CALLBACKS = %i[ inherited ]
182
+ VISIBILITY_METHODS = %i[ private protected public ]
183
+
184
+ def initialize(node)
185
+ @node = node
186
+ end
187
+
188
+ def offense
189
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE)
190
+ end
191
+
192
+ def candidate?
193
+ convertible? && private_singleton_section?
194
+ end
195
+
196
+ def compatible_with_module?(declaration)
197
+ !class_semantics_used? && singleton_calls_owned_by?(declaration)
198
+ end
199
+
200
+ private
201
+ attr_reader :node
202
+
203
+ def convertible?
204
+ node.parent_class.nil? && only_class_level_statements?
205
+ end
206
+
207
+ def only_class_level_statements?
208
+ statements.any? && statements.all? { class_level_statement?(it) }
209
+ end
210
+
211
+ def statements
212
+ @statements ||= statements_in(node.body)
213
+ end
214
+
215
+ def class_level_statement?(statement)
216
+ statement.type?(:casgn, :defs) || extend_call?(statement) || singleton_section?(statement)
217
+ end
218
+
219
+ def extend_call?(statement)
220
+ bare_send?(statement) && statement.method?(:extend)
221
+ end
222
+
223
+ def private_singleton_section?
224
+ statements.select { singleton_section?(it) }.any? { holds_private_section?(it) }
225
+ end
226
+
227
+ def holds_private_section?(section)
228
+ private_modifier_in(section.body).present?
229
+ end
230
+
231
+ def class_semantics_used?
232
+ class_only_callback_defined? || method_name_rebound? || singleton_super? || self_value_used?
233
+ end
234
+
235
+ def class_only_callback_defined?
236
+ nodes_in(node.body, :def, :defs).any? { CLASS_ONLY_CALLBACKS.include?(it.method_name) }
237
+ end
238
+
239
+ def method_name_rebound?
240
+ nodes_in(node.body, :alias, :undef).any? || nodes_in(node.body, :send, :csend).any? do |send_node|
241
+ send_node.method?(:alias_method) || send_node.method?(:undef_method)
242
+ end
243
+ end
244
+
245
+ def singleton_super?
246
+ nodes_in(node.body, :super, :zsuper).any?
247
+ end
248
+
249
+ def self_value_used?
250
+ nodes_in(node.body, :self).any? do |self_node|
251
+ enclosing_class_or_module_of(self_node).equal?(node) && !structural_self?(self_node)
252
+ end
253
+ end
254
+
255
+ def singleton_calls_owned_by?(declaration)
256
+ candidate_singleton_calls.all? do |send_node|
257
+ visibility_modifier?(send_node) || owned_method?(send_node.method_name, declaration:)
258
+ end
259
+ end
260
+
261
+ def candidate_singleton_calls
262
+ nodes_in(node.body, :send, :csend).select do |send_node|
263
+ call_on_self?(send_node) && candidate_singleton_domain?(send_node)
264
+ end
265
+ end
266
+
267
+ def candidate_singleton_domain?(send_node)
268
+ RuboCop::Callbacksystems::Methods::Domain.new(send_node).then do |domain|
269
+ (domain.container.equal?(node) && domain.identity.present? && domain.identity.all? { it == :self }) ||
270
+ opaque_block_in_candidate?(domain.container)
271
+ end
272
+ end
273
+
274
+ def opaque_block_in_candidate?(container)
275
+ any_block_type?(container) && enclosing_class_or_module_of(container).equal?(node)
276
+ end
277
+
278
+ def visibility_modifier?(send_node)
279
+ enclosing_method_of(send_node).nil? && VISIBILITY_METHODS.include?(send_node.method_name)
280
+ end
281
+
282
+ def owned_method?(method_name, declaration:)
283
+ declaration.singleton_class.member("#{method_name}()").present?
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,136 @@
1
+ # Detects array access with numeric indices that could use ordinal methods.
2
+ # `items.second` says which element it wants, where `items[1]` makes the
3
+ # reader count from zero.
4
+ #
5
+ # `[]` answers for a Hash, a MatchData and a String too, and the ordinal methods
6
+ # come from Array alone, so a receiver we can read as one of those is left alone.
7
+ # That includes a receiver read out of another `[]`, whose type the file never
8
+ # shows, and a local variable whose assignment in the same scope hands it one of
9
+ # those.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # items[1]
14
+ # items[2]
15
+ # items[3]
16
+ # items[4]
17
+ # items[-2]
18
+ # items[-3]
19
+ #
20
+ # # good
21
+ # items.second
22
+ # items.third
23
+ # items.fourth
24
+ # items.fifth
25
+ # items.second_to_last
26
+ # items.third_to_last
27
+ #
28
+ class RuboCop::Cop::Callbacksystems::PreferOrdinalArrayAccess < RuboCop::Cop::Callbacksystems::Base
29
+ extend RuboCop::Cop::AutoCorrector
30
+
31
+ def on_new_investigation
32
+ @local_variables = RuboCop::Callbacksystems::Execution::LocalVariableOccurrences.new(processed_source.ast)
33
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
34
+ end
35
+
36
+ def on_send(node)
37
+ report IndexedRead.new(node, @local_variables, @source_comments)
38
+ end
39
+
40
+ alias on_csend on_send
41
+
42
+ private
43
+ ORDINAL_METHODS = {
44
+ 1 => :second,
45
+ 2 => :third,
46
+ 3 => :fourth,
47
+ 4 => :fifth,
48
+ -2 => :second_to_last,
49
+ -3 => :third_to_last
50
+ }
51
+
52
+ NON_ARRAY_LITERAL_TYPES = %i[ dsym hash int regexp sym ]
53
+ # `[]` reads out of anything, `match` and its friends answer a MatchData, and the rest answer a Hash.
54
+ NON_ARRAY_RESULT_METHODS = %i[
55
+ [] last_match match match? named_captures to_h to_hash group_by index_by index_with tally params
56
+ ]
57
+
58
+ # One `items[1]`, offending when an ordinal method names that position and nothing says the receiver is no array.
59
+ class IndexedRead
60
+ include RuboCop::Callbacksystems::Helpers
61
+ extend RuboCop::AST::NodePattern::Macros
62
+
63
+ MESSAGE = "Use `%<method>s` instead of `[%<index>s]`."
64
+
65
+ # @!method bracket_access_with_int?(node)
66
+ def_node_matcher :bracket_access_with_int?, <<~PATTERN
67
+ (call $_ :[] (int $_index))
68
+ PATTERN
69
+
70
+ def initialize(node, local_variables, source_comments)
71
+ @node = node
72
+ @local_variables = local_variables
73
+ @source_comments = source_comments
74
+ end
75
+
76
+ def offense
77
+ if ordinal_method && array_receiver?
78
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: correction_keeps_comments?) { correct(it) }
79
+ end
80
+ end
81
+
82
+ private
83
+ attr_reader :node, :local_variables, :source_comments
84
+
85
+ def ordinal_method
86
+ ORDINAL_METHODS[index] if captures
87
+ end
88
+
89
+ def captures
90
+ @captures ||= bracket_access_with_int?(node)
91
+ end
92
+
93
+ def index
94
+ captures.second
95
+ end
96
+
97
+ def array_receiver?
98
+ !non_array_value?(receiver) && !assigned_a_non_array?
99
+ end
100
+
101
+ def non_array_value?(value)
102
+ reads_as_string?(value) ||
103
+ reads_as?(value, literals: NON_ARRAY_LITERAL_TYPES, methods: NON_ARRAY_RESULT_METHODS)
104
+ end
105
+
106
+ def receiver
107
+ captures.first
108
+ end
109
+
110
+ def assigned_a_non_array?
111
+ receiver.lvar_type? && assignments.any? do |assignment|
112
+ assignment.match_var_type? || assignment.expression.nil? || non_array_value?(assignment.expression)
113
+ end
114
+ end
115
+
116
+ def assignments
117
+ local_variables.named(receiver.name, around: receiver).select { it.type?(:lvasgn, :match_var) }
118
+ end
119
+
120
+ def message
121
+ format(MESSAGE, method: ordinal_method, index: index)
122
+ end
123
+
124
+ def correction_keeps_comments?
125
+ !source_comments.any_within?(node)
126
+ end
127
+
128
+ def correct(corrector)
129
+ corrector.replace(node, "#{receiver.source}#{call_operator}#{ordinal_method}")
130
+ end
131
+
132
+ def call_operator
133
+ node.csend_type? ? "&." : "."
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,265 @@
1
+ # A negative `return unless` guard followed by a happy path reads more naturally
2
+ # as `if condition; ...; end`. The wrap adds one `if` block, so it is only applied when
3
+ # the happy path's own nesting plus that extra level still fits inside the
4
+ # configured `Metrics/BlockNesting` limit; anything past that would push the
5
+ # method past the nesting limit. Method-length rules already cap how long a
6
+ # happy path can be, so the only real cost of the wrap is the indent level it adds.
7
+ #
8
+ # @example
9
+ # # bad - leading negative guard
10
+ # def label
11
+ # return unless ready?
12
+ # compute_label
13
+ # end
14
+ #
15
+ # # good - positive wrap
16
+ # def label
17
+ # if ready?
18
+ # compute_label
19
+ # end
20
+ # end
21
+ #
22
+ class RuboCop::Cop::Callbacksystems::PreferPositiveWrap < RuboCop::Cop::Callbacksystems::Base
23
+ extend RuboCop::Cop::AutoCorrector
24
+
25
+ def on_def(node)
26
+ report Guard.new(node.body, processed_source, nesting: block_nesting)
27
+ end
28
+
29
+ alias on_defs on_def
30
+
31
+ private
32
+ def block_nesting
33
+ BlockNesting.new(config.for_cop("Metrics/BlockNesting"))
34
+ end
35
+
36
+ # The configured core metric, reduced to the one question this rewrite needs to ask.
37
+ class BlockNesting
38
+ NESTING_TYPES = %i[ case case_match if while while_post until until_post for resbody ].to_set
39
+
40
+ def initialize(config)
41
+ @max = config["Max"]
42
+ @count_blocks = config.fetch("CountBlocks", false)
43
+ @count_modifier_forms = config.fetch("CountModifierForms", false)
44
+ end
45
+
46
+ def allows_wrap?(nodes)
47
+ nodes.map { depth_of(it) }.max.to_i + 1 <= max
48
+ end
49
+
50
+ private
51
+ attr_reader :max, :count_blocks, :count_modifier_forms
52
+
53
+ def depth_of(node)
54
+ {}.compare_by_identity.then do |depths|
55
+ Descendants.new(node).to_a.reverse_each do |descendant|
56
+ depths[descendant] = (counts?(descendant) ? 1 : 0) +
57
+ descendant.each_child_node.map { depths.fetch(it) }.max.to_i
58
+ end
59
+ depths.fetch(node)
60
+ end
61
+ end
62
+
63
+ def counts?(node)
64
+ NESTING_TYPES.include?(node.type) ? counts_control_flow?(node) : count_blocks && node.any_block_type?
65
+ end
66
+
67
+ def counts_control_flow?(node)
68
+ !node.if_type? || counts_if?(node)
69
+ end
70
+
71
+ def counts_if?(node)
72
+ !node.elsif? && (!node.modifier_form? || count_modifier_forms)
73
+ end
74
+
75
+ class Descendants
76
+ include Enumerable
77
+
78
+ def initialize(root)
79
+ @root = root
80
+ end
81
+
82
+ def each
83
+ if block_given?
84
+ pending = [ root ]
85
+ until pending.empty?
86
+ node = pending.pop
87
+ yield node
88
+ pending.concat(node.each_child_node.to_a.reverse)
89
+ end
90
+ else
91
+ to_enum(__method__)
92
+ end
93
+ end
94
+
95
+ private
96
+ attr_reader :root
97
+ end
98
+ end
99
+
100
+ # The leading negative guard inside a method body, plus the happy path that follows it.
101
+ class Guard
102
+ include RuboCop::Callbacksystems::Helpers
103
+
104
+ MESSAGE = "Wrap positively: `if condition; ...; end` instead of a leading negative guard."
105
+
106
+ def initialize(body, processed_source, nesting:)
107
+ @body = body
108
+ @processed_source = processed_source
109
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
110
+ @nesting = nesting
111
+ end
112
+
113
+ def offense
114
+ RuboCop::Callbacksystems::Offense.new(guard, MESSAGE, correcting: correctable?) { correct(it) } if wrappable?
115
+ end
116
+
117
+ private
118
+ attr_reader :body, :processed_source, :source_comments, :nesting
119
+
120
+ def wrappable?
121
+ statements.size >= 2 && negative_return_guard?(guard) && nesting.allows_wrap?(happy)
122
+ end
123
+
124
+ def statements
125
+ statements_in(body)
126
+ end
127
+
128
+ def negative_return_guard?(node)
129
+ node.if_type? && node.unless? && node.if_branch&.return_type?
130
+ end
131
+
132
+ def guard
133
+ statements.first
134
+ end
135
+
136
+ def happy
137
+ statements.drop(1)
138
+ end
139
+
140
+ # An existing `else` is observable on the positive path and cannot be folded into this rewrite.
141
+ def correctable?
142
+ guard_shape_correctable? && rewrite_preserves_source?
143
+ end
144
+
145
+ def guard_shape_correctable?
146
+ guard.else_branch.nil? && guard_value_stands_alone?
147
+ end
148
+
149
+ def guard_value_stands_alone?
150
+ guard_values.size != 1 || !unpackaged_splat?(guard_values.first)
151
+ end
152
+
153
+ def guard_values
154
+ guard.if_branch.children
155
+ end
156
+
157
+ def unpackaged_splat?(value)
158
+ value.splat_type? || (value.hash_type? && !value.braces?)
159
+ end
160
+
161
+ def rewrite_preserves_source?
162
+ comments_preserved? && heredocs_preserved?
163
+ end
164
+
165
+ def comments_preserved?
166
+ comments_in_replacement.all? { preserved_comment?(it) }
167
+ end
168
+
169
+ def comments_in_replacement
170
+ source_comments.within(replacement_range)
171
+ end
172
+
173
+ def replacement_range
174
+ guard.source_range.with(end_pos: happy_range.end_pos)
175
+ end
176
+
177
+ def happy_range
178
+ @happy_range ||= first_happy_block.range.with(end_pos: last_happy_block.range.end_pos)
179
+ end
180
+
181
+ def first_happy_block
182
+ @first_happy_block ||= statement_block_for(happy.first)
183
+ end
184
+
185
+ def statement_block_for(node)
186
+ RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source)
187
+ end
188
+
189
+ def last_happy_block
190
+ @last_happy_block ||= statement_block_for(happy.last)
191
+ end
192
+
193
+ def preserved_comment?(comment)
194
+ comment.equal?(guard_trailing_comment) || preserved_comment_ranges.any? { it.contains?(comment.source_range) }
195
+ end
196
+
197
+ def guard_trailing_comment
198
+ @guard_trailing_comment ||= source_comments.trailing_comment_on(guard.last_line)
199
+ end
200
+
201
+ def preserved_comment_ranges
202
+ [ happy_range, guard.condition.source_range, *guard_values.map(&:source_range) ]
203
+ end
204
+
205
+ def heredocs_preserved?
206
+ !carries_heredoc?(guard) && happy_heredocs.all? { it.source.start_with?("<<~") }
207
+ end
208
+
209
+ def happy_heredocs
210
+ happy.flat_map { it.each_node(:str, :dstr, :xstr).select { it.loc?(:heredoc_body) } }
211
+ end
212
+
213
+ def correct(corrector)
214
+ corrector.replace(replacement_range, wrapped_source)
215
+ end
216
+
217
+ def wrapped_source
218
+ "#{guard_header}\n#{indented_happy}\n#{else_branch}#{indent}end"
219
+ end
220
+
221
+ def guard_header
222
+ "if #{guard.condition.source}#{guard_comment_source}"
223
+ end
224
+
225
+ def guard_comment_source
226
+ guard_trailing_comment&.then { " #{it.text}" }.to_s
227
+ end
228
+
229
+ def indented_happy
230
+ [ "#{first_line_indent}#{happy_lines.first}", *happy_lines.drop(1).map { indented_line(it) } ].join
231
+ end
232
+
233
+ def first_line_indent
234
+ happy_range.column.zero? ? " " : "#{indent} "
235
+ end
236
+
237
+ def indent
238
+ indentation_of(guard)
239
+ end
240
+
241
+ def happy_lines
242
+ @happy_lines ||= happy_source.lines
243
+ end
244
+
245
+ def happy_source
246
+ happy_range.source
247
+ end
248
+
249
+ def indented_line(line)
250
+ line.strip.empty? ? line : " #{line}"
251
+ end
252
+
253
+ def else_branch
254
+ guard_value_source ? "#{indent}else\n#{indent} #{guard_value_source}\n" : ""
255
+ end
256
+
257
+ def guard_value_source
258
+ case guard_values
259
+ in [] then nil
260
+ in [ value ] then value.source
261
+ else "[ #{guard_values.map(&:source).join(", ")} ]"
262
+ end
263
+ end
264
+ end
265
+ end