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,269 @@
1
+ # Detects public methods in visually private nested classes that are never
2
+ # called from outside. When the project index confirms that no other file can
3
+ # reach or subclass the class, a method none of its local callers names is an
4
+ # implementation detail and belongs in the private section.
5
+ #
6
+ # @example
7
+ # # bad - unused_method is public but never called from outside
8
+ # class Foo
9
+ # def process
10
+ # Bar.new(node).used_method
11
+ # nil
12
+ # end
13
+ #
14
+ # private
15
+ # class Bar
16
+ # def used_method; end
17
+ # def unused_method; end # should be private
18
+ # end
19
+ # end
20
+ #
21
+ # # good - internal methods are private
22
+ # class Foo
23
+ # def process
24
+ # Bar.new(node).used_method
25
+ # nil
26
+ # end
27
+ #
28
+ # private
29
+ # class Bar
30
+ # def used_method; end
31
+ #
32
+ # private
33
+ # def internal_method; end
34
+ # end
35
+ # end
36
+ #
37
+ class RuboCop::Cop::Callbacksystems::PrivateNestedClassMethodVisibility < RuboCop::Cop::Callbacksystems::Base
38
+ include RuboCop::Callbacksystems::ProjectIndex::Support
39
+
40
+ def on_new_investigation
41
+ @file_references = FileReferences.new(processed_source.ast) if project_index
42
+ end
43
+
44
+ def on_class(node)
45
+ if project_index_reliable?
46
+ private_nested_classes_in(node).select { confined_without_descendants?(it) }.each do |nested_class|
47
+ report_each UnusedPublicMethods.new(nested_class, references: file_references.for(nested_class))
48
+ end
49
+ end
50
+ end
51
+
52
+ alias on_module on_class
53
+
54
+ private
55
+ attr_reader :file_references
56
+
57
+ def confined_without_descendants?(nested_class)
58
+ confined_declaration_of(nested_class)&.then do |declaration|
59
+ declaration.descendants.none? { it.name != declaration.name }
60
+ end
61
+ end
62
+
63
+ # References in the whole file compared with those inside one nested class, indexed once for every reader.
64
+ class FileReferences
65
+ def initialize(root)
66
+ @root = root
67
+ @by_class = {}.compare_by_identity
68
+ end
69
+
70
+ def for(nested_class)
71
+ by_class[nested_class] ||= ClassReferences.new(all, References.new(nested_class))
72
+ end
73
+
74
+ private
75
+ attr_reader :root, :by_class
76
+
77
+ def all
78
+ @all ||= References.new(root)
79
+ end
80
+
81
+ class ClassReferences
82
+ delegate :public_visibility_uncertain?, to: :all
83
+
84
+ def initialize(all, inside)
85
+ @all = all
86
+ @inside = inside
87
+ end
88
+
89
+ def mentions_outside?(method_name)
90
+ all.call_count(method_name) > inside.call_count(method_name) ||
91
+ all.block_pass_count(method_name) > inside.block_pass_count(method_name) ||
92
+ all.delegation_count(method_name) > inside.delegation_count(method_name) ||
93
+ all.dynamic_delegation_count > inside.dynamic_delegation_count
94
+ end
95
+
96
+ def public_visibility_required?(method_name)
97
+ all.public_visibility_count(method_name).positive?
98
+ end
99
+
100
+ private
101
+ attr_reader :all, :inside
102
+ end
103
+ end
104
+
105
+ class References
106
+ include RuboCop::Callbacksystems::Helpers
107
+
108
+ PUBLIC_LOOKUPS = %i[ protected public public_method public_send respond_to? try try! ]
109
+ VISIBILITY_ENUMERATIONS = %i[
110
+ instance_methods methods private_instance_methods private_methods protected_instance_methods
111
+ protected_methods public_instance_methods public_methods singleton_methods
112
+ ]
113
+
114
+ def initialize(root)
115
+ @evidence = RuboCop::Callbacksystems::Methods::ReferenceEvidence.for(root)
116
+ end
117
+
118
+ def call_count(name)
119
+ call_counts.fetch(name, 0)
120
+ end
121
+
122
+ def public_visibility_count(name)
123
+ explicit_call_counts.fetch(name, 0) + block_pass_count(name) + delegation_count(name) +
124
+ public_lookup_counts.fetch(name, 0) + (evidence.literal_method_names.include?(name.to_s) ? 1 : 0)
125
+ end
126
+
127
+ def block_pass_count(name)
128
+ block_pass_counts.fetch(name, 0)
129
+ end
130
+
131
+ def delegation_count(name)
132
+ delegation_counts.fetch(name, 0)
133
+ end
134
+
135
+ def public_visibility_uncertain?
136
+ dynamic_public_lookup? || dynamic_block_pass? || dynamic_delegation_count.positive? ||
137
+ interpolated_name? || visibility_enumerated?
138
+ end
139
+
140
+ def dynamic_delegation_count
141
+ @dynamic_delegation_count ||= evidence.calls.count { dynamic_delegation?(it) }
142
+ end
143
+
144
+ private
145
+ attr_reader :evidence
146
+
147
+ def call_counts
148
+ @call_counts ||= evidence.calls.map(&:method_name).tally
149
+ end
150
+
151
+ def explicit_call_counts
152
+ @explicit_call_counts ||= evidence.calls.filter_map do |send_node|
153
+ send_node.method_name if send_node.receiver && !send_node.receiver.self_type?
154
+ end.tally
155
+ end
156
+
157
+ def block_pass_counts
158
+ @block_pass_counts ||= evidence.block_passes.filter_map { block_pass_name_of(it) }.tally
159
+ end
160
+
161
+ def block_pass_name_of(node)
162
+ node.children.first.value if node.children.first&.sym_type?
163
+ end
164
+
165
+ def delegation_counts
166
+ @delegation_counts ||= evidence.calls.flat_map { delegated_names_of(it) }.tally
167
+ end
168
+
169
+ def delegated_names_of(send_node)
170
+ if delegate_macro?(send_node)
171
+ RuboCop::Callbacksystems::Methods::DelegateMacro.new(send_node).target_method_names || []
172
+ else
173
+ []
174
+ end
175
+ end
176
+
177
+ def public_lookup_counts
178
+ @public_lookup_counts ||= public_lookups.filter_map { public_lookup_name_of(it) }.tally
179
+ end
180
+
181
+ def public_lookups
182
+ @public_lookups ||= evidence.calls.select { PUBLIC_LOOKUPS.include?(it.method_name) }
183
+ end
184
+
185
+ def public_lookup_name_of(send_node)
186
+ send_node.first_argument&.then do |argument|
187
+ if argument.type?(:sym, :str)
188
+ argument.value.to_sym if argument.value.to_s.valid_encoding?
189
+ elsif argument.any_def_type?
190
+ argument.method_name
191
+ end
192
+ end
193
+ end
194
+
195
+ def dynamic_public_lookup?
196
+ public_lookups.any? { public_lookup_name_of(it).nil? }
197
+ end
198
+
199
+ def dynamic_block_pass?
200
+ evidence.block_passes.any? do |block_pass|
201
+ block_pass.children.first && !block_pass.children.first.sym_type?
202
+ end
203
+ end
204
+
205
+ def dynamic_delegation?(send_node)
206
+ delegate_macro?(send_node) && RuboCop::Callbacksystems::Methods::DelegateMacro.new(send_node).target_method_names.nil?
207
+ end
208
+
209
+ def interpolated_name?
210
+ evidence.interpolated_literals.any?
211
+ end
212
+
213
+ def visibility_enumerated?
214
+ evidence.calls.any? { VISIBILITY_ENUMERATIONS.include?(it.method_name) }
215
+ end
216
+ end
217
+
218
+ class UnusedPublicMethods
219
+ include RuboCop::Callbacksystems::Helpers
220
+
221
+ MESSAGE = "Method `%<method>s` in visually private nested class `%<class>s` is never called from outside. " \
222
+ "Make it private."
223
+
224
+ # Ruby calls these structurally (a double splat calls `to_hash`, `case` calls `===`), so no source names them.
225
+ PROTOCOL_METHODS = RuboCop::Callbacksystems::Methods::ImplicitInvocations::METHOD_NAMES
226
+
227
+ def initialize(nested_class, references:)
228
+ @nested_class = nested_class
229
+ @references = references
230
+ end
231
+
232
+ def each_offense
233
+ unused_public_methods.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
234
+ end
235
+
236
+ private
237
+ attr_reader :nested_class, :references
238
+
239
+ def unused_public_methods
240
+ if references && !references.public_visibility_uncertain?
241
+ uncalled_public_methods
242
+ else
243
+ []
244
+ end
245
+ end
246
+
247
+ def uncalled_public_methods
248
+ public_methods_in(nested_class).reject { mentioned_outside?(it.method_name) }
249
+ end
250
+
251
+ # An instance travels beyond one file's reading, so any mention of the name outside the class counts as a use.
252
+ def mentioned_outside?(method_name)
253
+ PROTOCOL_METHODS.include?(method_name) || macro_referenced_methods.include?(method_name) ||
254
+ references.mentions_outside?(method_name) || references.public_visibility_required?(method_name)
255
+ end
256
+
257
+ def macro_referenced_methods
258
+ @macro_referenced_methods ||= RuboCop::Callbacksystems::Methods::MacroReferences.new(nested_class.body).to_a
259
+ end
260
+
261
+ def message_for(method_node)
262
+ format(MESSAGE, method: method_node.method_name, class: class_name)
263
+ end
264
+
265
+ def class_name
266
+ nested_class.identifier.short_name
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,94 @@
1
+ # Requires nested classes to be declared in the private section. A nested class
2
+ # is an implementation detail, so the class holding one says as much by where it
3
+ # puts it. One that other files build has no business being nested at all: it
4
+ # goes in its own file, where Zeitwerk expects it.
5
+ #
6
+ # A constant assigned from `Data.define`, `Struct.new` or `Class.new` defines a
7
+ # class in everything but syntax, so it reads under the same rule.
8
+ #
9
+ # A definition with no body is left alone. An error class or a bare list of
10
+ # members declares what something is rather than how it works, and a file of its
11
+ # own would cost more than it says.
12
+ #
13
+ # @example
14
+ # # bad - public nested class
15
+ # class Order
16
+ # class LineItem
17
+ # def total
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # # bad - a constant that builds a class with behavior
23
+ # class Order
24
+ # LineItem = Data.define(:sku, :quantity) do
25
+ # def total
26
+ # end
27
+ # end
28
+ #
29
+ # private
30
+ # def process
31
+ # end
32
+ # end
33
+ #
34
+ # # good - nested class in private section
35
+ # class Order
36
+ # def process
37
+ # end
38
+ #
39
+ # private
40
+ # class LineItem
41
+ # def total
42
+ # end
43
+ # end
44
+ # end
45
+ #
46
+ # # good - declarations with no body, wherever they read best
47
+ # class Order
48
+ # class Rejected < StandardError; end
49
+ #
50
+ # LineItem = Data.define(:sku, :quantity)
51
+ # end
52
+ #
53
+ class RuboCop::Cop::Callbacksystems::PrivateNestedClasses < RuboCop::Cop::Callbacksystems::Base
54
+ def on_class(node)
55
+ report NestedClass.new(node)
56
+ end
57
+
58
+ alias on_casgn on_class
59
+
60
+ private
61
+ class NestedClass
62
+ include RuboCop::Callbacksystems::Helpers
63
+
64
+ MESSAGE = "Nested class `%<name>s` should be in the private section, or live in its own file if it is part of " \
65
+ "the API."
66
+
67
+ def initialize(node)
68
+ @node = node
69
+ end
70
+
71
+ def offense
72
+ RuboCop::Callbacksystems::Offense.new(node, message) if outside_private_section?
73
+ end
74
+
75
+ private
76
+ attr_reader :node
77
+
78
+ def outside_private_section?
79
+ class_with_body?(node) && nested? && !in_private_section?(node, enclosing.body)
80
+ end
81
+
82
+ def nested?
83
+ enclosing.present?
84
+ end
85
+
86
+ def enclosing
87
+ @enclosing ||= enclosing_class_or_module_of(node)
88
+ end
89
+
90
+ def message
91
+ format(MESSAGE, name: class_name_of(node))
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,190 @@
1
+ # Ensures every public method of a class or module has a test, found by the
2
+ # convention that a test description opens with the name of the method it
3
+ # covers. A public method is the API of the class, so one without a test is a
4
+ # promise nothing checks, and the naming convention is what lets a reader go
5
+ # from the method to its tests and back.
6
+ #
7
+ # Methods of private nested classes are implementation details and stay out,
8
+ # and so do methods a macro references (callbacks, delegates), since the macro
9
+ # is what exercises them. A method returning a plain literal needs no test that
10
+ # would only restate the literal. Scopes and methods in `class_methods` blocks
11
+ # count.
12
+ #
13
+ # Test files are located based on the source file path:
14
+ # - lib/foo/bar.rb -> test/lib/foo/bar_test.rb or test/foo/bar_test.rb
15
+ # - app/models/user.rb -> test/models/user_test.rb
16
+ #
17
+ # @example
18
+ # # bad - public method without test
19
+ # class User
20
+ # def full_name
21
+ # "#{first_name} #{last_name}"
22
+ # end
23
+ # end
24
+ # # And test file has no test "full_name ..." block
25
+ #
26
+ # # good - public method has test
27
+ # class User
28
+ # def full_name
29
+ # "#{first_name} #{last_name}"
30
+ # end
31
+ # end
32
+ # # test "full_name returns concatenated names" do ... end
33
+ #
34
+ # # good - method returning a plain literal needs no test
35
+ # class User
36
+ # def role
37
+ # "member"
38
+ # end
39
+ # end
40
+ #
41
+ class RuboCop::Cop::Callbacksystems::PublicMethodsMustHaveTests < RuboCop::Cop::Callbacksystems::Base
42
+ def external_dependency_checksum
43
+ [
44
+ RuboCop::Callbacksystems::Source::FilesChecksum.for("test/**/*_test.rb", root: project_root),
45
+ RuboCop::Callbacksystems::Source::FilesChecksum.for("*.gemspec", root: project_root, include_contents: false)
46
+ ].join
47
+ end
48
+
49
+ def on_new_investigation
50
+ @path_mapping = RuboCop::Callbacksystems::Testing::PathMapping.new(processed_source.file_path)
51
+ @tested_methods = path_mapping.test_path ? TestedMethods.new(path_mapping.test_path, ruby_version:) : Set.new
52
+ end
53
+
54
+ def on_class(node)
55
+ unless private_nested_class?(node)
56
+ report_each TestCoverage.new(node, processed_source, path_mapping:, tested_methods:)
57
+ end
58
+ end
59
+
60
+ alias on_module on_class
61
+
62
+ private
63
+ attr_reader :path_mapping, :tested_methods
64
+ delegate :ruby_version, to: :processed_source, private: true
65
+
66
+ class TestCoverage
67
+ MESSAGE = "Public method `%<method>s` has no test. " \
68
+ "Convention: tests begin with the method name followed by what it does, " \
69
+ "e.g. `test \"%<method>s returns the expected value\" do`"
70
+ EXCLUDED_METHODS = %i[ initialize ].to_set
71
+
72
+ def initialize(node, processed_source, path_mapping:, tested_methods:)
73
+ @node = node
74
+ @processed_source = processed_source
75
+ @path_mapping = path_mapping
76
+ @tested_methods = tested_methods
77
+ end
78
+
79
+ def each_offense
80
+ untested_methods.each { yield RuboCop::Callbacksystems::Offense.new(it.node, message_for(it)) }
81
+ end
82
+
83
+ private
84
+ attr_reader :node, :processed_source, :path_mapping, :tested_methods
85
+
86
+ delegate :file_path, to: :processed_source, private: true
87
+
88
+ def untested_methods
89
+ checked? ? testable_public_methods.reject { covered?(it) } : []
90
+ end
91
+
92
+ def checked?
93
+ file_path.exclude?("/test/") && !lib_file_in_non_gem_project? && test_source_valid?
94
+ end
95
+
96
+ def lib_file_in_non_gem_project?
97
+ file_path.match?(%r{(^|/)lib/}) && !path_mapping.gem_project?
98
+ end
99
+
100
+ def test_source_valid?
101
+ path_mapping.test_path.nil? || tested_methods.valid?
102
+ end
103
+
104
+ def testable_public_methods
105
+ RuboCop::Callbacksystems::Methods::Definitions.new(node).reject { exempt?(it) }
106
+ end
107
+
108
+ def exempt?(definition)
109
+ EXCLUDED_METHODS.include?(definition.name) || returns_literal?(definition.node)
110
+ end
111
+
112
+ def returns_literal?(method_node)
113
+ method_node.type?(:any_def) && method_node.body&.recursive_literal?
114
+ end
115
+
116
+ def covered?(definition)
117
+ MethodCoverage.new(definition, tested: tested_methods, macro_referenced: macro_referenced_methods).covered?
118
+ end
119
+
120
+ def macro_referenced_methods
121
+ @macro_referenced_methods ||= RuboCop::Callbacksystems::Methods::MacroReferences.new(node.body).to_set
122
+ end
123
+
124
+ def message_for(definition)
125
+ format(MESSAGE, method: definition.name)
126
+ end
127
+
128
+ # A scope a macro names is still API a test reads, so the macro exempts every method but a scope.
129
+ class MethodCoverage
130
+ def initialize(definition, tested:, macro_referenced:)
131
+ @definition = definition
132
+ @tested = tested
133
+ @macro_referenced = macro_referenced
134
+ end
135
+
136
+ def covered?
137
+ tested? || macro_referenced_non_scope?
138
+ end
139
+
140
+ private
141
+ attr_reader :definition, :tested, :macro_referenced
142
+
143
+ def tested?
144
+ tested.include?(definition.name.to_s)
145
+ end
146
+
147
+ def macro_referenced_non_scope?
148
+ macro_referenced.include?(definition.name) && !scope?
149
+ end
150
+
151
+ def scope?
152
+ definition.node.send_type? && definition.node.method?(:scope)
153
+ end
154
+ end
155
+ end
156
+
157
+ class TestedMethods
158
+ extend RuboCop::AST::NodePattern::Macros
159
+ include Enumerable
160
+ include RuboCop::Callbacksystems::Testing::CopHelpers
161
+ include RuboCop::Callbacksystems::Helpers
162
+
163
+ delegate :each, to: :names
164
+
165
+ def initialize(test_file_path, ruby_version:)
166
+ @test_file_path = test_file_path
167
+ @ruby_version = ruby_version
168
+ end
169
+
170
+ def valid?
171
+ processed_test_source&.valid_syntax? || false
172
+ end
173
+
174
+ private
175
+ attr_reader :test_file_path, :ruby_version
176
+
177
+ def names
178
+ @names ||= descriptions.filter_map { it.split(/\s+/).first }
179
+ end
180
+
181
+ def descriptions
182
+ nodes_in(processed_test_source&.ast, :any_block).filter_map { test_block?(it) }
183
+ end
184
+
185
+ def processed_test_source
186
+ @processed_test_source ||= RuboCop::Callbacksystems::Source::FileAst.processed_source \
187
+ test_file_path, ruby_version:
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,157 @@
1
+ # Detects an `extend ActiveSupport::Concern` the module never uses. The extend
2
+ # promises `included`, `class_methods` or `prepended` hooks, or concern
3
+ # dependencies declared with `include` or `prepend`, so a reader goes looking
4
+ # for them. A module with none is a plain mixin carrying a promise it never
5
+ # keeps.
6
+ #
7
+ # @example
8
+ # # bad - extend without using concern features
9
+ # module Searchable
10
+ # extend ActiveSupport::Concern
11
+ #
12
+ # def search
13
+ # # ...
14
+ # end
15
+ # end
16
+ #
17
+ # # good - using included block
18
+ # module Searchable
19
+ # extend ActiveSupport::Concern
20
+ #
21
+ # included do
22
+ # scope :search, -> { ... }
23
+ # end
24
+ # end
25
+ #
26
+ # # good - using class_methods
27
+ # module Searchable
28
+ # extend ActiveSupport::Concern
29
+ #
30
+ # class_methods do
31
+ # def search(query)
32
+ # # ...
33
+ # end
34
+ # end
35
+ # end
36
+ #
37
+ # # good - no extend needed for simple module
38
+ # module Searchable
39
+ # def search
40
+ # # ...
41
+ # end
42
+ # end
43
+ #
44
+ class RuboCop::Cop::Callbacksystems::RedundantConcernExtend < RuboCop::Cop::Callbacksystems::Base
45
+ extend RuboCop::Cop::AutoCorrector
46
+
47
+ def on_module(node)
48
+ report ConcernModule.new(node, source_comments:)
49
+ end
50
+
51
+ private
52
+ class ConcernModule
53
+ include RuboCop::Callbacksystems::Helpers
54
+
55
+ MESSAGE = "Unnecessary `extend ActiveSupport::Concern`. " \
56
+ "Remove it or use concern hooks or dependencies."
57
+ CONCERN_CONSTANTS = %w[ ActiveSupport::Concern Concern ]
58
+ CONCERN_METHODS = %i[ included class_methods prepended ]
59
+ DEPENDENCY_METHODS = %i[ include prepend ]
60
+
61
+ def initialize(node, source_comments:)
62
+ @node = node
63
+ @source_comments = source_comments
64
+ end
65
+
66
+ def offense
67
+ if redundant?
68
+ RuboCop::Callbacksystems::Offense.new \
69
+ concern_extend, MESSAGE, correcting: correction_uncommented? do |corrector|
70
+ correct(corrector)
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+ attr_reader :node, :source_comments
77
+
78
+ def redundant?
79
+ concern_extend && !uses_concern_features?
80
+ end
81
+
82
+ def concern_extend
83
+ @concern_extend ||= statements_in(node.body).find { concern_extend?(it) }
84
+ end
85
+
86
+ def concern_extend?(statement)
87
+ statement.send_type? && statement.method?(:extend) && statement.arguments.any? { concern_constant?(it) }
88
+ end
89
+
90
+ def concern_constant?(argument)
91
+ CONCERN_CONSTANTS.include?(constant_name_of(argument)&.delete_prefix("::"))
92
+ end
93
+
94
+ def uses_concern_features?
95
+ concern_hooks.any? || concern_dependencies.any? || class_methods_module?
96
+ end
97
+
98
+ def concern_hooks
99
+ nodes_in(node.body, :any_block).select do |block|
100
+ bare_send?(block.send_node) && CONCERN_METHODS.include?(block.method_name)
101
+ end
102
+ end
103
+
104
+ def concern_dependencies
105
+ nodes_in(node.body, :send).select do |call|
106
+ bare_send?(call) && DEPENDENCY_METHODS.include?(call.method_name) &&
107
+ call.each_ancestor(:def, :defs, :class, :module).first.equal?(node)
108
+ end
109
+ end
110
+
111
+ def class_methods_module?
112
+ statements_in(node.body).any? do |statement|
113
+ statement.module_type? && constant_name_of(statement.identifier) == "ClassMethods"
114
+ end
115
+ end
116
+
117
+ def correction_uncommented?
118
+ !source_comments.any_within?(correction_range)
119
+ end
120
+
121
+ def correction_range
122
+ concern_extend.arguments.one? ? statement_removal_range_for(concern_extend) : concern_argument_removal_range
123
+ end
124
+
125
+ def concern_argument_removal_range
126
+ if argument_after_concern
127
+ concern_argument.source_range.with(end_pos: argument_after_concern.source_range.begin_pos)
128
+ else
129
+ argument_before_concern.source_range.end.join(concern_argument.source_range.end)
130
+ end
131
+ end
132
+
133
+ def argument_after_concern
134
+ concern_extend.arguments[concern_argument_index + 1]
135
+ end
136
+
137
+ def concern_argument_index
138
+ concern_extend.arguments.index(concern_argument)
139
+ end
140
+
141
+ def concern_argument
142
+ @concern_argument ||= concern_extend.arguments.find { concern_constant?(it) }
143
+ end
144
+
145
+ def argument_before_concern
146
+ concern_extend.arguments[concern_argument_index - 1]
147
+ end
148
+
149
+ def correct(corrector)
150
+ if concern_extend.arguments.one?
151
+ corrector.remove(statement_removal_range_for(concern_extend))
152
+ else
153
+ corrector.remove(concern_argument_removal_range)
154
+ end
155
+ end
156
+ end
157
+ end