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,451 @@
1
+ # Prohibits bang methods (ending with `!`) unless a non-bang counterpart exists.
2
+ # The `!` suffix promises a version without it that is safer, so a lone bang is a
3
+ # name that raises a question it never answers.
4
+ #
5
+ # Instance and singleton methods answer for themselves. A `def self.reload` is no
6
+ # counterpart to the `reload!` of an instance, so each side is read against the
7
+ # methods that land where it does: the body's own defs, what a `class << self` or a
8
+ # `class_methods do` adds to the class, and what an `included do` adds to
9
+ # instances.
10
+ #
11
+ # Counterparts defined in reopenings, ancestors and mixins count wherever the
12
+ # project index can resolve the whole ancestry. When any link is unknown, the
13
+ # cop abstains rather than calling a method orphaned on incomplete evidence.
14
+ #
15
+ # @example
16
+ # # bad - no non-bang counterpart
17
+ # def process!
18
+ # # ...
19
+ # end
20
+ #
21
+ # # good - has non-bang counterpart
22
+ # def save
23
+ # # safe version
24
+ # end
25
+ #
26
+ # def save!
27
+ # save || raise(RecordNotSaved)
28
+ # end
29
+ #
30
+ # # good - no bang needed
31
+ # def process
32
+ # # ...
33
+ # end
34
+ #
35
+ class RuboCop::Cop::Callbacksystems::NoBangMethodWithoutCounterpart < RuboCop::Cop::Callbacksystems::Base
36
+ include RuboCop::Callbacksystems::ProjectIndex::Support
37
+
38
+ def on_new_investigation
39
+ @project_index_available = project_index_reliable? && runtime_definitions_absent?
40
+ @local_definitions = LocalDefinitions.new(local_definition_entries)
41
+ @counterparts_by_namespace = {}
42
+ end
43
+
44
+ def on_class(node)
45
+ namespace_for(node)&.then do |namespace|
46
+ report_each OrphanBangMethods.new \
47
+ local_definitions.definitions_for(node), counterparts: counterparts_for(namespace)
48
+ end
49
+ end
50
+
51
+ alias on_module on_class
52
+
53
+ private
54
+ attr_reader :local_definitions, :counterparts_by_namespace, :project_index_available
55
+
56
+ def runtime_definitions_absent?
57
+ RuntimeDefinitions.for(project_index).absent?
58
+ end
59
+
60
+ def local_definition_entries
61
+ nodes_in(processed_source.ast, :class, :module).filter_map do |container|
62
+ namespace_for(container)&.then { [ container, it.name ] }
63
+ end
64
+ end
65
+
66
+ def namespace_for(container)
67
+ return unless project_index_available && container.identifier.const_type?
68
+
69
+ resolve_constant_in_index(container.identifier).then { it if it.is_a?(Rubydex::Namespace) }
70
+ rescue
71
+ nil
72
+ end
73
+
74
+ def counterparts_for(namespace)
75
+ counterparts_by_namespace[namespace.name] ||= Counterparts.new \
76
+ namespace, local_definitions:, project_index:, processed_source:
77
+ end
78
+
79
+ # A project-wide runtime definition leaves the set of possible counterparts open.
80
+ class RuntimeDefinitions
81
+ extend RuboCop::Callbacksystems::ProjectIndex::Cache
82
+
83
+ METHOD_NAMES = %w[ define_method define_singleton_method ].to_set
84
+
85
+ def initialize(project_index)
86
+ @absent = project_index.method_references.none? do |reference|
87
+ METHOD_NAMES.include?(reference.name.delete_suffix("()"))
88
+ end
89
+ end
90
+
91
+ def absent?
92
+ absent
93
+ end
94
+
95
+ private
96
+ attr_reader :absent
97
+ end
98
+
99
+ # Method definitions classified by the side of the container where Ruby installs them.
100
+ class LocalDefinitions
101
+ def initialize(entries)
102
+ definitions = entries.map { |container, name| [ container, name, ContainerDefinitions.new(container) ] }
103
+ @definitions_by_container = definitions.to_h { |container, _, value| [ container, value ] }.compare_by_identity
104
+ @containers_by_namespace = definitions.group_by { |_, name, _| name }
105
+ .transform_values { it.map(&:last) }
106
+ @possibilities = {}
107
+ end
108
+
109
+ def definitions_for(container)
110
+ definitions_by_container.fetch(container)
111
+ end
112
+
113
+ def possibly_defines?(namespace_name, name, domain:)
114
+ query = Query.new(namespace_name, name, domain)
115
+
116
+ possibilities.fetch(query) do
117
+ possibilities[query] = containers_by_namespace.fetch(namespace_name, []).any? do |container|
118
+ container.possibly_defines?(name, domain:)
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+ Query = Data.define(:namespace_name, :name, :domain)
125
+
126
+ attr_reader :definitions_by_container, :containers_by_namespace, :possibilities
127
+
128
+ # Definitions that belong to one lexical class or module body.
129
+ class ContainerDefinitions
130
+ include RuboCop::Callbacksystems::Helpers
131
+
132
+ INSTANCE_MACROS = %i[ included prepended ]
133
+ SINGLETON_MACRO = :class_methods
134
+ CONCERN_MACROS = (INSTANCE_MACROS + [ SINGLETON_MACRO ]).to_set
135
+ ALWAYS_UNCERTAIN_CALLS = %i[ delegate_missing_to has_secure_password has_secure_token ].to_set
136
+ NON_DECLARATIVE_CALLS = %i[ extend include prepend private protected public using ].to_set
137
+ UNKNOWN_NAME = Object.new
138
+ Definition = Data.define(:node, :domain)
139
+
140
+ def initialize(container)
141
+ @container = container
142
+ end
143
+
144
+ def possibly_defines?(name, domain:)
145
+ by_domain.fetch(domain, []).any? { it.method?(name) } ||
146
+ runtime_names.include?(name.to_s) || runtime_names.include?(UNKNOWN_NAME)
147
+ end
148
+
149
+ def by_domain
150
+ @by_domain ||= definitions.group_by(&:domain).transform_values { it.map(&:node) }
151
+ end
152
+
153
+ private
154
+ attr_reader :container
155
+
156
+ def definitions
157
+ @definitions ||= method_definitions + macro_definitions
158
+ end
159
+
160
+ def method_definitions
161
+ direct_method_nodes_in(container.body).filter_map do |definition|
162
+ if definition.def_type? || definition.receiver.self_type?
163
+ Definition.new(definition, RuboCop::Callbacksystems::Methods::Domain.new(definition).identity)
164
+ end
165
+ end
166
+ end
167
+
168
+ def macro_definitions
169
+ INSTANCE_MACROS.flat_map { definitions_in_macro(it, domain: []) } +
170
+ definitions_in_macro(SINGLETON_MACRO, domain: [ :self ])
171
+ end
172
+
173
+ def definitions_in_macro(macro, domain:)
174
+ statements_in(container.body).flat_map do |statement|
175
+ if macro_block?(statement, macro:)
176
+ direct_definitions_in(statement.body, :def).map { Definition.new(it, domain) }
177
+ else
178
+ []
179
+ end
180
+ end
181
+ end
182
+
183
+ def macro_block?(statement, macro:)
184
+ if any_block_type?(statement)
185
+ call_on_self?(call_of(statement)) && statement.method?(macro)
186
+ else
187
+ false
188
+ end
189
+ end
190
+
191
+ def runtime_names
192
+ @runtime_names ||= runtime_names_from_calls.to_set |
193
+ (ambiguous_definition? ? Set[UNKNOWN_NAME] : Set.new)
194
+ end
195
+
196
+ def runtime_names_from_calls
197
+ scoped_calls.flat_map { runtime_names_from(it) }
198
+ end
199
+
200
+ def scoped_calls
201
+ @scoped_calls ||= nodes_in(container.body, :send, :csend).select { belongs_to_container?(it) }
202
+ end
203
+
204
+ def belongs_to_container?(node)
205
+ enclosing_class_or_module_of(node).equal?(container) && enclosing_method_of(node).nil?
206
+ end
207
+
208
+ def runtime_names_from(call)
209
+ indexed_names = RuboCop::Callbacksystems::Methods::StatementDefinitions.new(call).names
210
+
211
+ if always_uncertain?(call) || indexed_names.nil?
212
+ [ UNKNOWN_NAME ]
213
+ elsif indexed_names.any?
214
+ indexed_names.map(&:to_s)
215
+ elsif declarative_call?(call)
216
+ names_in_arguments(call.arguments)
217
+ else
218
+ []
219
+ end
220
+ end
221
+
222
+ def always_uncertain?(call)
223
+ ALWAYS_UNCERTAIN_CALLS.include?(call.method_name) ||
224
+ (call.method?(:module_function) && call.arguments.empty?)
225
+ end
226
+
227
+ def declarative_call?(call)
228
+ [ call.receiver.nil? || call.receiver.self_type?, NON_DECLARATIVE_CALLS.exclude?(call.method_name) ].all?
229
+ end
230
+
231
+ def names_in_arguments(arguments)
232
+ arguments.map { it.type?(:sym, :str) ? it.value.to_s : UNKNOWN_NAME }
233
+ end
234
+
235
+ def ambiguous_definition?
236
+ scoped_definitions.any? do |definition|
237
+ owner = RuboCop::Callbacksystems::Methods::Domain.new(definition).container
238
+
239
+ !owner.equal?(container) && !direct_concern_definition?(definition, owner:)
240
+ end
241
+ end
242
+
243
+ def scoped_definitions
244
+ @scoped_definitions ||= nodes_in(container.body, :def, :defs).select { belongs_to_container?(it) }
245
+ end
246
+
247
+ def direct_concern_definition?(definition, owner:)
248
+ any_block_type?(owner) && call_on_self?(call_of(owner)) && CONCERN_MACROS.include?(owner.method_name) &&
249
+ direct_definitions_in(owner.body, :def).include?(definition)
250
+ end
251
+ end
252
+ end
253
+
254
+ # Names that might answer a bang method, with absence trusted only across complete ancestry.
255
+ class Counterparts
256
+ include RuboCop::Cop::ProjectIndexHelp
257
+
258
+ SINGLETON_DOMAIN = [ :self ]
259
+ DYNAMIC_DISPATCH_METHODS = %i[ method_missing respond_to_missing? ]
260
+ Query = Data.define(:name, :domain)
261
+
262
+ attr_reader :namespace, :local_definitions, :project_index, :processed_source
263
+
264
+ def initialize(namespace, local_definitions:, project_index:, processed_source:)
265
+ @namespace = namespace
266
+ @local_definitions = local_definitions
267
+ @project_index = project_index
268
+ @processed_source = processed_source
269
+ @absences = {}
270
+ @complete_domains = {}
271
+ end
272
+
273
+ def absent?(name, domain:)
274
+ query = Query.new(name, domain)
275
+
276
+ absences.fetch(query) { absences[it] = CounterpartSearch.new(it, context: self).absent? }
277
+ end
278
+
279
+ def complete?(indexed_domain, domain:)
280
+ complete_domains.fetch(domain) do
281
+ ancestors = (namespace.ancestors.to_a + indexed_domain.ancestors.to_a).uniq(&:name)
282
+
283
+ complete_domains[domain] = resolved_ancestry?(indexed_domain, ignore_extend: domain.empty?) &&
284
+ definitions_in_other_files(ancestors.flat_map { it.definitions.to_a }).empty?
285
+ end
286
+ rescue
287
+ false
288
+ end
289
+
290
+ private
291
+ attr_reader :absences, :complete_domains
292
+
293
+ def resolved_ancestry?(indexed_domain, ignore_extend:)
294
+ fully_resolved_index_ancestry?(namespace, ignore_extend:) &&
295
+ fully_resolved_index_ancestry?(indexed_domain)
296
+ end
297
+
298
+ class CounterpartSearch
299
+ def initialize(query, context:)
300
+ @query = query
301
+ @context = context
302
+ end
303
+
304
+ def absent?
305
+ if query.domain.all? { it == :self } && indexed_domain
306
+ !counterpart_possible? && !dynamic_dispatch_possible? &&
307
+ context.complete?(indexed_domain, domain: query.domain)
308
+ else
309
+ false
310
+ end
311
+ rescue
312
+ false
313
+ end
314
+
315
+ private
316
+ attr_reader :query, :context
317
+ delegate :namespace, to: :context, private: true
318
+
319
+ def indexed_domain
320
+ @indexed_domain ||= query.domain.inject(namespace) { |owner, _| owner&.singleton_class }
321
+ end
322
+
323
+ def counterpart_possible?
324
+ indexed_name_possible?(query.name, namespaces: indexed_domain.ancestors) ||
325
+ concern_name_possible?(query.name)
326
+ end
327
+
328
+ def indexed_name_possible?(name, namespaces:, local_domain: nil, project_only: false)
329
+ namespaces.any? do |indexed_namespace|
330
+ NamespaceMember.new(indexed_namespace, name:, context:, local_domain:, project_only:).possible?
331
+ end
332
+ end
333
+
334
+ def concern_name_possible?(name, project_only: false)
335
+ query.domain == SINGLETON_DOMAIN && indexed_name_possible?(
336
+ name, namespaces: namespace.ancestors, local_domain: SINGLETON_DOMAIN, project_only:
337
+ )
338
+ end
339
+
340
+ def dynamic_dispatch_possible?
341
+ DYNAMIC_DISPATCH_METHODS.any? do |method_name|
342
+ indexed_name_possible?(method_name, namespaces: indexed_domain.ancestors, project_only: true) ||
343
+ concern_name_possible?(method_name, project_only: true)
344
+ end
345
+ end
346
+ end
347
+
348
+ class NamespaceMember
349
+ def initialize(indexed_namespace, name:, context:, local_domain:, project_only:)
350
+ @indexed_namespace = indexed_namespace
351
+ @name = name
352
+ @context = context
353
+ @local_domain = local_domain
354
+ @project_only = project_only
355
+ end
356
+
357
+ def possible?
358
+ locally_possible? || indexed_declaration_possible?
359
+ rescue
360
+ true
361
+ end
362
+
363
+ private
364
+ attr_reader :indexed_namespace, :name, :context, :local_domain, :project_only
365
+
366
+ def locally_possible?
367
+ namespace = indexed_namespace
368
+ domain = []
369
+
370
+ while namespace.is_a?(Rubydex::SingletonClass)
371
+ domain << :self
372
+ namespace = namespace.owner
373
+ end
374
+
375
+ context.local_definitions.possibly_defines?(namespace.name, name, domain: local_domain || domain)
376
+ end
377
+
378
+ def indexed_declaration_possible?
379
+ if declaration.is_a?(Rubydex::Method)
380
+ project_scope_allows? && readable? && usable?
381
+ else
382
+ false
383
+ end
384
+ end
385
+
386
+ def declaration
387
+ @declaration ||= indexed_namespace.member("#{name}()")
388
+ end
389
+
390
+ def project_scope_allows?
391
+ !project_only || definitions.any? do |definition|
392
+ definition.location.uri.start_with?(RuboCop::Cop::ProjectIndexHelp::FILE_URI_PREFIX)
393
+ end
394
+ end
395
+
396
+ def definitions
397
+ @definitions ||= declaration.definitions.to_a
398
+ end
399
+
400
+ # Rubydex 0.4 indexes an `attr_writer :name` member as `name()`, although Ruby only defines `name=`.
401
+ def readable?
402
+ definitions.empty? || definitions.any? { it.class.name != "Rubydex::AttrWriterDefinition" }
403
+ end
404
+
405
+ def usable?
406
+ definitions.empty? || indexed_namespace.is_a?(Rubydex::SingletonClass) || definitions.any? do |definition|
407
+ !definition.location.uri.start_with?(RuboCop::Cop::ProjectIndexHelp::FILE_URI_PREFIX) ||
408
+ !definition.is_a?(Rubydex::MethodDefinition)
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ # The bang methods of one body with no plain counterpart within reach of where they land.
415
+ class OrphanBangMethods
416
+ include RuboCop::Callbacksystems::Helpers
417
+
418
+ MESSAGE = "Method `%<method>s` has no non-bang counterpart. Only use `!` when a version without `!` exists."
419
+
420
+ def initialize(container_definitions, counterparts:)
421
+ @container_definitions = container_definitions
422
+ @counterparts = counterparts
423
+ end
424
+
425
+ def each_offense
426
+ container_definitions.by_domain.flat_map { |domain, definitions| orphans_among(definitions, domain:) }
427
+ .each { yield offense_for(it) }
428
+ end
429
+
430
+ private
431
+ attr_reader :container_definitions, :counterparts
432
+
433
+ def orphans_among(definitions, domain:)
434
+ definitions.select do |definition|
435
+ conventional_bang_method?(definition) && counterparts.absent?(plain_name_of(definition), domain:)
436
+ end
437
+ end
438
+
439
+ def conventional_bang_method?(definition)
440
+ definition.bang_method? && !definition.method?(:!)
441
+ end
442
+
443
+ def plain_name_of(definition)
444
+ definition.method_name.to_s.chomp("!").to_sym
445
+ end
446
+
447
+ def offense_for(definition)
448
+ RuboCop::Callbacksystems::Offense.new(definition, format(MESSAGE, method: definition.method_name))
449
+ end
450
+ end
451
+ end
@@ -0,0 +1,73 @@
1
+ # Prohibits defining methods named `call`, except for Rack middleware endpoints.
2
+ # This pattern is associated with service objects which we avoid.
3
+ #
4
+ # @example
5
+ # # bad
6
+ # class OrderProcessor
7
+ # def call
8
+ # # ...
9
+ # end
10
+ # end
11
+ #
12
+ # # bad
13
+ # class OrderProcessor
14
+ # def self.call
15
+ # # ...
16
+ # end
17
+ # end
18
+ #
19
+ # # good - use descriptive method names
20
+ # class Order
21
+ # def process
22
+ # # ...
23
+ # end
24
+ # end
25
+ #
26
+ class RuboCop::Cop::Callbacksystems::NoCallMethod < RuboCop::Cop::Callbacksystems::Base
27
+ MESSAGE = "Avoid defining `call` methods. Use descriptive method names instead."
28
+
29
+ def on_def(node)
30
+ report MethodName.new(node)
31
+ end
32
+
33
+ alias on_defs on_def
34
+
35
+ private
36
+ class MethodName
37
+ include RuboCop::Callbacksystems::Helpers
38
+
39
+ def initialize(node)
40
+ @node = node
41
+ end
42
+
43
+ def offense
44
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE) if node.method?(:call) && !rack_protocol?
45
+ end
46
+
47
+ private
48
+ attr_reader :node
49
+
50
+ def rack_protocol?
51
+ direct_method_definition?(node) && instance_method? && parameter_names_of(node) == [ "env" ] &&
52
+ rack_middleware_class?
53
+ end
54
+
55
+ def instance_method?
56
+ RuboCop::Callbacksystems::Methods::Domain.new(node).scope == :instance
57
+ end
58
+
59
+ def rack_middleware_class?
60
+ class_node.class_type? && (class_name_of(class_node).end_with?("Middleware") || accepts_rack_application?)
61
+ end
62
+
63
+ def class_node
64
+ @class_node ||= enclosing_class_or_module_of(node)
65
+ end
66
+
67
+ def accepts_rack_application?
68
+ direct_method_nodes_in(class_node.body).any? do |method_node|
69
+ method_node.method?(:initialize) && parameter_names_of(method_node).include?("app")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,159 @@
1
+ # Detects comments in the direct body of test classes. A test explains itself
2
+ # through its description, so a comment at the class level is a sentence the
3
+ # test names should have carried, and a reader skims past it while looking for
4
+ # the test that matters. Comments inside a test block stay allowed for the
5
+ # non-obvious why.
6
+ #
7
+ # There is no fix. Which sentence a comment was carrying is only visible to
8
+ # whoever wrote it, so deleting one is the author's call and never ours.
9
+ #
10
+ # This cop should only be enabled for test files.
11
+ #
12
+ # @example
13
+ # # bad - comment at class body level
14
+ # class UserTest < ActiveSupport::TestCase
15
+ # # Setup user fixtures
16
+ # setup do
17
+ # @user = users(:john)
18
+ # end
19
+ #
20
+ # test "validates name" do
21
+ # # ...
22
+ # end
23
+ # end
24
+ #
25
+ # # good - no comments at class body level
26
+ # class UserTest < ActiveSupport::TestCase
27
+ # setup do
28
+ # @user = users(:john)
29
+ # end
30
+ #
31
+ # test "validates name" do
32
+ # # This comment inside test is ok if highly relevant
33
+ # end
34
+ # end
35
+ #
36
+ class RuboCop::Cop::Callbacksystems::NoCommentsInTestClassBody < RuboCop::Cop::Callbacksystems::Base
37
+ def on_new_investigation
38
+ report_each ClassBodyComments.new(comments_in_test_file, processed_source.ast)
39
+ end
40
+
41
+ private
42
+ def comments_in_test_file
43
+ if test_file?(processed_source.file_path)
44
+ prose_comments_in(processed_source)
45
+ else
46
+ []
47
+ end
48
+ end
49
+
50
+ class ClassBodyComments
51
+ include RuboCop::Callbacksystems::Helpers
52
+
53
+ MESSAGE = "Avoid comments in test class body. Tests should be self-documenting through descriptive names."
54
+
55
+ def initialize(comments, ast)
56
+ @comments = ScopeComments.new(comments, ast)
57
+ @content_scopes = ContentScopes.new(ast)
58
+ @ast = ast
59
+ end
60
+
61
+ def each_offense
62
+ test_classes.each do |test_class|
63
+ comments_in_body_of(test_class).each { yield RuboCop::Callbacksystems::Offense.new(it, MESSAGE) }
64
+ end
65
+ end
66
+
67
+ private
68
+ attr_reader :comments, :content_scopes, :ast
69
+
70
+ def test_classes
71
+ nodes_in(ast, :class).select { test_class?(it) && class_has_content?(it) }
72
+ end
73
+
74
+ def test_class?(class_node)
75
+ class_name_of(class_node).end_with?("Test") || rails_test_base_class?(class_node.parent_class)
76
+ end
77
+
78
+ def class_has_content?(class_node)
79
+ content_scopes.any_within?(class_node)
80
+ end
81
+
82
+ def comments_in_body_of(class_node)
83
+ comments.directly_within(class_node)
84
+ end
85
+ end
86
+
87
+ # Comments assigned to the innermost lexical scope containing them with one sweep over scopes and source positions.
88
+ class ScopeComments
89
+ include RuboCop::Callbacksystems::Helpers
90
+
91
+ SCOPE_TYPES = [ :class, :module, :sclass, *BLOCK_NODE_TYPES, :def, :defs ]
92
+
93
+ def initialize(comments, ast)
94
+ @scopes = nodes_in(ast, *SCOPE_TYPES).sort_by { [ it.source_range.begin_pos, -it.source_range.end_pos ] }
95
+ @scope_index = 0
96
+ @active_scopes = []
97
+ @by_scope = {}.compare_by_identity
98
+ comments.sort_by { it.source_range.begin_pos }.each { assign(it) }
99
+ end
100
+
101
+ def directly_within(scope)
102
+ by_scope.fetch(scope) { [] }
103
+ end
104
+
105
+ private
106
+ attr_reader :scopes, :active_scopes, :by_scope, :scope_index
107
+
108
+ def assign(comment)
109
+ activate_scopes_through(comment.source_range.begin_pos)
110
+ retire_scopes_before(comment.source_range.end_pos)
111
+ (by_scope[active_scopes.last] ||= []) << comment if contained_by_active_scope?(comment)
112
+ end
113
+
114
+ def activate_scopes_through(position)
115
+ while next_scope && next_scope.source_range.begin_pos <= position
116
+ retire_scopes_before(next_scope.source_range.begin_pos)
117
+ active_scopes << next_scope
118
+ @scope_index += 1
119
+ end
120
+ end
121
+
122
+ def next_scope
123
+ scopes[scope_index]
124
+ end
125
+
126
+ def retire_scopes_before(position)
127
+ active_scopes.pop while active_scopes.last&.then { it.source_range.end_pos < position }
128
+ end
129
+
130
+ def contained_by_active_scope?(comment)
131
+ active_scopes.last&.source_range&.contains?(comment.source_range)
132
+ end
133
+ end
134
+
135
+ # Definitions and blocks indexed by their start, so a class can ask whether its subtree carries test content.
136
+ class ContentScopes
137
+ include RuboCop::Callbacksystems::Helpers
138
+
139
+ def initialize(ast)
140
+ @scopes = nodes_in(ast, :any_def, :any_block).sort_by { it.source_range.begin_pos }
141
+ end
142
+
143
+ def any_within?(node)
144
+ scope = scopes[first_index_at(node.source_range.begin_pos)]
145
+ scope && node.source_range.contains?(scope.source_range)
146
+ end
147
+
148
+ private
149
+ attr_reader :scopes
150
+
151
+ def first_index_at(position)
152
+ begin_positions.bsearch_index { it >= position } || scopes.size
153
+ end
154
+
155
+ def begin_positions
156
+ @begin_positions ||= scopes.map { it.source_range.begin_pos }
157
+ end
158
+ end
159
+ end