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,309 @@
1
+ # A hash literal of several fields, built in one place and read from several
2
+ # others, is a concept with no behavior: every caller reaches past it into its
3
+ # fields because it has nothing of its own. That is a data clump wearing braces,
4
+ # and the same refactor applies. Give it a class, and the methods reading its
5
+ # fields become its methods.
6
+ #
7
+ # What matters is whether the record travels. One built and consumed on the spot,
8
+ # an options argument or a payload passed straight to a call, is a return value
9
+ # rather than a concept, so only a record that is returned or assigned counts.
10
+ #
11
+ # A read counts for a record only when it goes through where the record lives:
12
+ # the constant or variable it was assigned to, the method returning it, or a
13
+ # local a method assigned from one of those. Sharing a few key names proves
14
+ # nothing, so `entry[:id]` on a form entry never counts against an unrelated
15
+ # hash that also carries an `id`.
16
+ #
17
+ # A hash returned from `as_json`, `to_h`, `to_hash` or `serializable_hash` is
18
+ # the wire format rather than a concept, so it is never a record.
19
+ #
20
+ # `DataClump` sees the same smell spelled as parameter lists. Bagging a clump into
21
+ # a hash silences that cop without changing anything, so this one closes the gap.
22
+ #
23
+ # @example
24
+ # # bad - three fields and no behavior, read across methods
25
+ # SHAPE = { open: "{", close: "}", items: :children }
26
+ #
27
+ # def opening?
28
+ # source == SHAPE[:open]
29
+ # end
30
+ #
31
+ # def items
32
+ # node.public_send(SHAPE[:items])
33
+ # end
34
+ #
35
+ # # good - the record becomes a class and the readers become its methods
36
+ # class Shape
37
+ # def initialize(open, close, items)
38
+ # @open, @close, @items = open, close, items
39
+ # end
40
+ #
41
+ # def opening?(source)
42
+ # source == open
43
+ # end
44
+ # end
45
+ #
46
+ # # good - built and consumed on the spot
47
+ # render json: { status: "ok", id: id, name: name }
48
+ #
49
+ class RuboCop::Cop::Callbacksystems::NoAnemicRecord < RuboCop::Cop::Callbacksystems::Base
50
+ def on_new_investigation
51
+ report_each Records.new \
52
+ processed_source.ast,
53
+ min_fields: cop_config["MinFields"],
54
+ min_readers: cop_config["MinReaders"]
55
+ end
56
+
57
+ private
58
+ Home = Data.define(:kind, :name, :domain)
59
+
60
+ # The traveling hash literals of a file, each with the reads that go through its home.
61
+ class Records
62
+ include RuboCop::Callbacksystems::Helpers
63
+
64
+ def initialize(ast, min_fields:, min_readers:)
65
+ @ast = ast
66
+ @min_fields = min_fields
67
+ @min_readers = min_readers
68
+ @local_variables = RuboCop::Callbacksystems::Execution::LocalVariableOccurrences.new(ast)
69
+ end
70
+
71
+ def each_offense
72
+ reached.select { it.readers >= min_readers }.each { yield it.offense }
73
+ end
74
+
75
+ private
76
+ attr_reader :ast, :min_fields, :min_readers, :local_variables
77
+ delegate :reading_methods_of, to: :read_index, private: true
78
+
79
+ def read_index
80
+ @read_index ||= ReadIndex.new(nodes_in(ast, :send, :csend).map { Read.new(it, local_variables:) })
81
+ end
82
+
83
+ def reached
84
+ candidates.map { ReachedRecord.new(it, reading_methods_of(it)) }
85
+ end
86
+
87
+ def candidates
88
+ nodes_in(ast, :hash).map { Record.new(it) }.select { it.travels? && it.fields.size >= min_fields }
89
+ end
90
+ end
91
+
92
+ # Field reads grouped by the object carrying them, so each record visits only its own reads.
93
+ class ReadIndex
94
+ def initialize(reads)
95
+ @by_container = {}.compare_by_identity
96
+ reads.each { add(it) if it.field && it.home }
97
+ end
98
+
99
+ def reading_methods_of(record)
100
+ reads_for(record).filter_map { it.reader if record.fields.include?(it.field) }
101
+ end
102
+
103
+ private
104
+ attr_reader :by_container
105
+
106
+ def add(read)
107
+ homes = by_container[read.container] ||= {}
108
+ (homes[read.home] ||= []) << read
109
+ end
110
+
111
+ def reads_for(record)
112
+ by_container.fetch(record.container) { {} }.fetch(record.home) { [] }
113
+ end
114
+ end
115
+
116
+ class Read
117
+ include RuboCop::Callbacksystems::Helpers
118
+
119
+ READING_METHODS = %i[ [] fetch dig ]
120
+
121
+ def initialize(node, local_variables:)
122
+ @node = node
123
+ @local_variables = local_variables
124
+ end
125
+
126
+ def field
127
+ @field ||= node.first_argument.value if reads_a_field?
128
+ end
129
+
130
+ # The name the read goes through, which is where the record it reaches has to live.
131
+ def home
132
+ @home ||= home_of(node.receiver)
133
+ end
134
+
135
+ def reader
136
+ enclosing_method_of(node)
137
+ end
138
+
139
+ def container
140
+ @container ||= domain.container
141
+ end
142
+
143
+ private
144
+ attr_reader :node, :local_variables
145
+
146
+ def reads_a_field?
147
+ READING_METHODS.include?(node.method_name) && single_symbol_argument?
148
+ end
149
+
150
+ def single_symbol_argument?
151
+ node.arguments.one? && node.first_argument.sym_type?
152
+ end
153
+
154
+ def home_of(expression)
155
+ case expression&.type
156
+ when :const then Home.new(:constant, constant_name_of(expression), nil)
157
+ when :ivar then Home.new(:instance_variable, expression.name, method_domain)
158
+ when :cvar, :gvar then Home.new(expression.type, expression.name, nil)
159
+ when :send, :csend then method_home_of(expression)
160
+ when :lvar then home_of(Local.new(expression, occurrences: local_variables).origin)
161
+ end
162
+ end
163
+
164
+ def method_domain
165
+ domain.identity
166
+ end
167
+
168
+ def domain
169
+ @domain ||= RuboCop::Callbacksystems::Methods::Domain.new(node)
170
+ end
171
+
172
+ def method_home_of(expression)
173
+ Home.new(:method, expression.method_name, method_domain) if bare_send?(expression) ||
174
+ expression.receiver.self_type?
175
+ end
176
+ end
177
+
178
+ # A local stands for the value its method assigned it once, unless that value is another local.
179
+ class Local
180
+ include RuboCop::Callbacksystems::Helpers
181
+
182
+ def initialize(variable, occurrences:)
183
+ @variable = variable
184
+ @occurrences = occurrences
185
+ end
186
+
187
+ def origin
188
+ expression unless expression&.lvar_type?
189
+ end
190
+
191
+ private
192
+ attr_reader :variable, :occurrences
193
+
194
+ def expression
195
+ assignment&.expression
196
+ end
197
+
198
+ def assignment
199
+ assignments.first if assignments.one? && assignments.first.lvasgn_type?
200
+ end
201
+
202
+ def assignments
203
+ occurrences.named_in_method(variable.name, around: variable).select { it.type?(:lvasgn, :match_var) }
204
+ end
205
+ end
206
+
207
+ class ReachedRecord
208
+ MESSAGE = "This record carries [%<fields>s] and no behavior, and %<readers>d methods reach into its fields. " \
209
+ "Give it a class and let those become its methods."
210
+
211
+ def initialize(record, reading_methods)
212
+ @record = record
213
+ @reading_methods = reading_methods
214
+ end
215
+
216
+ def offense
217
+ RuboCop::Callbacksystems::Offense.new(record.node, message)
218
+ end
219
+
220
+ def readers
221
+ reading_methods.uniq.size
222
+ end
223
+
224
+ private
225
+ attr_reader :record, :reading_methods
226
+
227
+ def message
228
+ format(MESSAGE, fields: record.fields.to_a.join(", "), readers: readers)
229
+ end
230
+ end
231
+
232
+ class Record
233
+ include RuboCop::Callbacksystems::Helpers
234
+
235
+ SERIALIZATION_METHODS = %i[ as_json to_h to_hash serializable_hash ]
236
+ VARIABLE_READ_TYPES = { cvasgn: :cvar, gvasgn: :gvar }
237
+
238
+ attr_reader :node
239
+
240
+ def initialize(node)
241
+ @node = node
242
+ end
243
+
244
+ def travels?
245
+ symbol_keyed? && home.present? && !serialization_output?
246
+ end
247
+
248
+ def home
249
+ @home ||= assigned_home || returning_method_home
250
+ end
251
+
252
+ def fields
253
+ @fields ||= node.pairs.to_set { it.key.value }
254
+ end
255
+
256
+ def container
257
+ @container ||= domain.container
258
+ end
259
+
260
+ private
261
+ def symbol_keyed?
262
+ node.pairs.any? && node.pairs.all? { it.key.sym_type? }
263
+ end
264
+
265
+ def assigned_home
266
+ case node.parent&.type
267
+ when :casgn then Home.new(:constant, constant_assignment_name, nil)
268
+ when :ivasgn then Home.new(:instance_variable, node.parent.name, method_domain)
269
+ when :cvasgn, :gvasgn then Home.new(VARIABLE_READ_TYPES.fetch(node.parent.type), node.parent.name, nil)
270
+ end
271
+ end
272
+
273
+ def constant_assignment_name
274
+ [ constant_name_of(node.parent.children.first), node.parent.name ].compact.join("::")
275
+ end
276
+
277
+ def method_domain
278
+ domain.identity
279
+ end
280
+
281
+ def domain
282
+ @domain ||= RuboCop::Callbacksystems::Methods::Domain.new(node)
283
+ end
284
+
285
+ def returning_method_home
286
+ Home.new(:method, returning_method_name, method_domain) if returning_method_name
287
+ end
288
+
289
+ def returning_method_name
290
+ enclosing_method.method_name if returned?
291
+ end
292
+
293
+ def returned?
294
+ enclosing_method.present? && (node.parent.return_type? || closes_its_method?)
295
+ end
296
+
297
+ def enclosing_method
298
+ @enclosing_method ||= enclosing_method_of(node)
299
+ end
300
+
301
+ def closes_its_method?
302
+ last_statement_in(enclosing_method.body).equal?(node)
303
+ end
304
+
305
+ def serialization_output?
306
+ SERIALIZATION_METHODS.include?(returning_method_name)
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,31 @@
1
+ # Prohibits using assert_select in tests.
2
+ #
3
+ # Testing HTML content couples tests to markup implementation details.
4
+ # Tests should verify behavior through response status, redirects, and
5
+ # data changes, not by inspecting HTML structure.
6
+ #
7
+ # @example
8
+ # # bad - testing HTML content
9
+ # assert_select "div.notice", text: /Welcome/
10
+ # assert_select "h1", "Dashboard"
11
+ # assert_select "input[type=email]"
12
+ #
13
+ # # good - test response and behavior
14
+ # assert_response :success
15
+ # assert_redirected_to dashboard_path
16
+ # assert user.confirmed?
17
+ #
18
+ class RuboCop::Cop::Callbacksystems::NoAssertSelect < RuboCop::Cop::Callbacksystems::Base
19
+ MESSAGE = "Don't use `assert_select` to test HTML content. Test response status, redirects, and data changes instead."
20
+
21
+ # @!method assert_select_call?(node)
22
+ def_node_matcher :assert_select_call?, <<~PATTERN
23
+ (call {nil? (self)} :assert_select ...)
24
+ PATTERN
25
+
26
+ def on_send(node)
27
+ add_offense(node, message: MESSAGE) if assert_select_call?(node)
28
+ end
29
+
30
+ alias on_csend on_send
31
+ end