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,137 @@
1
+ # A long comment block is prose doing work the code should do. Inside a class
2
+ # body, two lines are enough for the non-obvious why. A block sitting
3
+ # immediately above a top-level class or module sometimes holds two of those
4
+ # whys, so it gets four. A block inside a `test` block narrates the scenario,
5
+ # so it has no limit there. Shortening prose is human judgment, so there is no
6
+ # autocorrection.
7
+ #
8
+ # @example
9
+ # # bad - a three-line block inside a class body
10
+ # class Widget
11
+ # # The cache is warmed by the nightly job, and a miss here
12
+ # # means the job never ran, so we raise instead of recomputing
13
+ # # to surface the broken schedule.
14
+ # def price
15
+ # end
16
+ # end
17
+ #
18
+ # # good - two lines say the non-obvious why
19
+ # class Widget
20
+ # # A cache miss means the nightly job never ran, so we raise
21
+ # # to surface the broken schedule instead of recomputing.
22
+ # def price
23
+ # end
24
+ # end
25
+ #
26
+ class RuboCop::Cop::Callbacksystems::CommentBlockLength < RuboCop::Cop::Callbacksystems::Base
27
+ def on_new_investigation
28
+ @comment_context = CommentContext.new(processed_source.ast)
29
+ comment_blocks_in(processed_source).each { report measured_block_of(it) }
30
+ end
31
+
32
+ private
33
+ attr_reader :comment_context
34
+
35
+ def measured_block_of(comments)
36
+ MeasuredBlock.new(comments, comment_context, max: cop_config["Max"], header_max: cop_config["HeaderMax"])
37
+ end
38
+
39
+ class MeasuredBlock
40
+ include RuboCop::Callbacksystems::Helpers
41
+
42
+ MESSAGE = "This comment block spans %d lines where %d is the limit. Keep the non-obvious why and drop any " \
43
+ "narration of the what."
44
+
45
+ def initialize(comments, context, max:, header_max:)
46
+ @comments = comments
47
+ @context = context
48
+ @max = max
49
+ @header_max = header_max
50
+ end
51
+
52
+ def offense
53
+ RuboCop::Callbacksystems::Offense.new(range, message) if comments.size > limit && !inside_test?(range)
54
+ end
55
+
56
+ private
57
+ attr_reader :comments, :context, :max, :header_max
58
+ delegate :inside_test?, to: :context, private: true
59
+
60
+ def limit
61
+ top_level_header? ? header_max : max
62
+ end
63
+
64
+ def top_level_header?
65
+ context.top_level_header_on?(comments.last.source_range.line)
66
+ end
67
+
68
+ def range
69
+ range_spanning(comments)
70
+ end
71
+
72
+ def message
73
+ format(MESSAGE, comments.size, limit)
74
+ end
75
+ end
76
+
77
+ # File-wide comment contexts indexed once for all blocks in the investigation.
78
+ class CommentContext
79
+ extend RuboCop::AST::NodePattern::Macros
80
+ include RuboCop::Callbacksystems::Testing::CopHelpers
81
+ include RuboCop::Callbacksystems::Helpers
82
+
83
+ def initialize(ast)
84
+ @ast = ast
85
+ end
86
+
87
+ def top_level_header_on?(line)
88
+ top_level_header_lines.include?(line)
89
+ end
90
+
91
+ def inside_test?(range)
92
+ test_ranges.include?(range)
93
+ end
94
+
95
+ private
96
+ attr_reader :ast
97
+
98
+ def top_level_header_lines
99
+ @top_level_header_lines ||= nodes_in(ast, :class, :module).filter_map do |node|
100
+ node.first_line.pred if enclosing_class_or_module_of(node).nil?
101
+ end.to_set
102
+ end
103
+
104
+ def test_ranges
105
+ @test_ranges ||= ContainingRanges.new(test_blocks(ast).map(&:source_range))
106
+ end
107
+
108
+ # Source ranges supporting containment queries in logarithmic time, including nested ranges.
109
+ class ContainingRanges
110
+ def initialize(ranges)
111
+ @ranges = ranges.sort_by(&:begin_pos)
112
+ end
113
+
114
+ def include?(range)
115
+ candidate = first_range_after(range.begin_pos).pred
116
+ candidate >= 0 && maximum_end_positions[candidate] >= range.end_pos
117
+ end
118
+
119
+ private
120
+ attr_reader :ranges
121
+
122
+ def first_range_after(position)
123
+ begin_positions.bsearch_index { it > position } || ranges.size
124
+ end
125
+
126
+ def begin_positions
127
+ @begin_positions ||= ranges.map(&:begin_pos)
128
+ end
129
+
130
+ def maximum_end_positions
131
+ @maximum_end_positions ||= ranges.each_with_object([]) do |range, positions|
132
+ positions << [ positions.last || 0, range.end_pos ].max
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,75 @@
1
+ # Detects complex conditionals with too many boolean operators. A condition
2
+ # chaining several operators has to be read term by term to know what it asks,
3
+ # while a predicate method names the question and holds the terms where they
4
+ # read on their own.
5
+ #
6
+ # @example MaxOperators: 1 (default)
7
+ # # bad - too many operators
8
+ # if a && b && c
9
+ # # ...
10
+ # end
11
+ #
12
+ # # good - extract to predicate method
13
+ # if complex_condition?
14
+ # # ...
15
+ # end
16
+ #
17
+ # def complex_condition?
18
+ # a && b && c
19
+ # end
20
+ #
21
+ # # good - single operator
22
+ # if a && b
23
+ # # ...
24
+ # end
25
+ #
26
+ class RuboCop::Cop::Callbacksystems::ComplexConditional < RuboCop::Cop::Callbacksystems::Base
27
+ MESSAGE = "Conditional has %<count>d boolean operators (max %<max>d). Extract to a predicate method."
28
+
29
+ def on_if(node)
30
+ check_condition(node.condition)
31
+ end
32
+
33
+ alias on_while on_if
34
+ alias on_until on_if
35
+ alias on_while_post on_if
36
+ alias on_until_post on_if
37
+
38
+ private
39
+ def check_condition(condition)
40
+ count = OperatorCount.new(condition).value
41
+ add_offense(condition, message: format(MESSAGE, count: count, max: max_operators)) if count > max_operators
42
+ end
43
+
44
+ def max_operators
45
+ cop_config["MaxOperators"]
46
+ end
47
+
48
+ # The top-level boolean spine, walked iteratively so arbitrarily deep parser output cannot exhaust the call stack.
49
+ class OperatorCount
50
+ def initialize(node)
51
+ @pending = [ node ]
52
+ @count = 0
53
+ end
54
+
55
+ def value
56
+ visit(pending.pop) until pending.empty?
57
+ count
58
+ end
59
+
60
+ private
61
+ attr_reader :pending, :count
62
+
63
+ def visit(node)
64
+ case node&.type
65
+ when :and, :or then count_operator(node)
66
+ when :begin, :kwbegin then pending << node.children.last
67
+ end
68
+ end
69
+
70
+ def count_operator(node)
71
+ @count += 1
72
+ pending.concat(node.children)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,105 @@
1
+ # In controller tests, every test that makes an HTTP request must assert the response.
2
+ # Testing only side effects without verifying the response status or redirect
3
+ # leaves the controller's behavior unverified.
4
+ #
5
+ # @example
6
+ # # bad - no response assertion
7
+ # test "create creates a user" do
8
+ # post users_url, params: { name: "John" }
9
+ # assert_equal 1, User.count
10
+ # end
11
+ #
12
+ # # good - asserts response status
13
+ # test "create creates a user" do
14
+ # post users_url, params: { name: "John" }
15
+ # assert_response :created
16
+ # assert_equal 1, User.count
17
+ # end
18
+ #
19
+ # # good - asserts redirect
20
+ # test "create redirects" do
21
+ # post users_url, params: { name: "John" }
22
+ # assert_redirected_to users_path
23
+ # end
24
+ #
25
+ # # good - no HTTP request (not a controller action test)
26
+ # test "helper works" do
27
+ # assert_equal "expected", helper_method
28
+ # end
29
+ #
30
+ class RuboCop::Cop::Callbacksystems::ControllerTestResponseAssertion < RuboCop::Cop::Callbacksystems::Base
31
+ include RuboCop::Callbacksystems::Testing::CopHelpers
32
+
33
+ def on_block(node)
34
+ report_each RequestResponses.new(node)
35
+ end
36
+
37
+ alias on_numblock on_block
38
+ alias on_itblock on_block
39
+
40
+ private
41
+ REQUEST_ABORTING_ASSERTIONS = %i[ assert_raises ]
42
+
43
+ class RequestResponses
44
+ extend RuboCop::AST::NodePattern::Macros
45
+ include RuboCop::Callbacksystems::Testing::CopHelpers
46
+ include RuboCop::Callbacksystems::Helpers
47
+
48
+ MESSAGE = "Controller test makes an HTTP request but has no response assertion (assert_response or " \
49
+ "assert_redirected_to)."
50
+
51
+ def initialize(node)
52
+ @node = node
53
+ end
54
+
55
+ def each_offense
56
+ missing_requests.each { yield RuboCop::Callbacksystems::Offense.new(it, MESSAGE) }
57
+ end
58
+
59
+ private
60
+ attr_reader :node
61
+
62
+ def missing_requests
63
+ if test_block?(node)
64
+ request_runs.filter_map do |run|
65
+ request = run.first
66
+ request if request_statement?(request) && !request_answered_in?(run)
67
+ end
68
+ else
69
+ []
70
+ end
71
+ end
72
+
73
+ def request_runs
74
+ statements.slice_before { request_statement?(it) }
75
+ end
76
+
77
+ def statements
78
+ @statements ||= statements_in(node.body)
79
+ end
80
+
81
+ def request_statement?(statement)
82
+ immediate_calls_in(statement).any? { http_request?(it) }
83
+ end
84
+
85
+ def request_answered_in?(run)
86
+ run.any? { response_assertion_in?(it) } || request_aborted_in?(run.first)
87
+ end
88
+
89
+ def response_assertion_in?(statement)
90
+ immediate_calls_in(statement).any? { response_assertion?(it) }
91
+ end
92
+
93
+ def request_aborted_in?(statement)
94
+ immediate_calls_in(statement).select { http_request?(it) }.all? do |request|
95
+ request.each_ancestor(:any_block)
96
+ .take_while { it.equal?(statement) || statement.source_range.contains?(it.source_range) }
97
+ .any? { request_aborting_assertion?(it.send_node) }
98
+ end
99
+ end
100
+
101
+ def request_aborting_assertion?(send_node)
102
+ call_on_self?(send_node) && REQUEST_ABORTING_ASSERTIONS.include?(send_node.method_name)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,123 @@
1
+ # In controller tests, the first assertion after an HTTP request must be a
2
+ # response assertion (assert_response or assert_redirected_to). The response is
3
+ # what the request produced, so it is read first: when the controller fails, a
4
+ # side-effect assertion placed before it fails on a symptom (a missing record)
5
+ # and hides the status that explains it. Non-assertion code between the request
6
+ # and the response assertion is allowed, and so are ActiveJob and ActionMailer
7
+ # assertions (assert_enqueued_jobs, assert_enqueued_emails, etc.).
8
+ #
9
+ # @example
10
+ # # bad - first assertion is not about the response
11
+ # test "create" do
12
+ # post users_url, params: { name: "John" }
13
+ # assert_equal 1, User.count
14
+ # assert_response :created
15
+ # end
16
+ #
17
+ # # good - response assertion comes first
18
+ # test "create" do
19
+ # post users_url, params: { name: "John" }
20
+ # assert_response :created
21
+ # assert_equal 1, User.count
22
+ # end
23
+ #
24
+ # # good - non-assertion code before response assertion is fine
25
+ # test "create" do
26
+ # post users_url, params: { name: "John" }
27
+ # user = User.last
28
+ # assert_response :created
29
+ # assert_equal "John", user.name
30
+ # end
31
+ #
32
+ # # good - assert_difference wrapping request, then response assertion
33
+ # test "create" do
34
+ # assert_difference("User.count", 1) do
35
+ # post users_url, params: { name: "John" }
36
+ # end
37
+ # assert_response :created
38
+ # end
39
+ #
40
+ # # good - ActiveJob/ActionMailer assertions before response assertion
41
+ # test "create sends email" do
42
+ # post users_url, params: { name: "John" }
43
+ # assert_enqueued_emails 1
44
+ # assert_response :created
45
+ # end
46
+ #
47
+ class RuboCop::Cop::Callbacksystems::ControllerTestResponseFirstAssertion < RuboCop::Cop::Callbacksystems::Base
48
+ include RuboCop::Callbacksystems::Testing::CopHelpers
49
+
50
+ def on_block(node)
51
+ report_each RequestAssertions.new(node) if test_block?(node)
52
+ end
53
+
54
+ alias on_numblock on_block
55
+ alias on_itblock on_block
56
+
57
+ private
58
+ SIDE_EFFECT_ASSERTIONS = %i[
59
+ assert_enqueued_jobs assert_no_enqueued_jobs assert_performed_jobs assert_no_performed_jobs
60
+ assert_enqueued_with assert_performed_with
61
+ assert_emails assert_no_emails assert_enqueued_emails assert_no_enqueued_emails assert_enqueued_email_with
62
+ ]
63
+
64
+ # The assertions a test reads first after each of its requests, kept when one is not about the response.
65
+ class RequestAssertions
66
+ extend RuboCop::AST::NodePattern::Macros
67
+ include RuboCop::Callbacksystems::Testing::CopHelpers
68
+ include RuboCop::Callbacksystems::Helpers
69
+
70
+ MESSAGE = "The first assertion after an HTTP request must be `assert_response` or `assert_redirected_to`, not " \
71
+ "`%<method>s`."
72
+
73
+ def initialize(node)
74
+ @node = node
75
+ end
76
+
77
+ def each_offense
78
+ misplaced_assertions.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
79
+ end
80
+
81
+ private
82
+ attr_reader :node
83
+
84
+ def misplaced_assertions
85
+ waiting_for_assertion = false
86
+ statements.filter_map do |statement|
87
+ if http_request_statement?(statement)
88
+ waiting_for_assertion = true
89
+ nil
90
+ elsif waiting_for_assertion
91
+ first_assertion_in(statement)&.then do |assertion|
92
+ waiting_for_assertion = false
93
+ assertion unless response_assertion?(assertion)
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def statements
100
+ @statements ||= statements_in(node.body)
101
+ end
102
+
103
+ def http_request_statement?(statement)
104
+ immediate_calls_in(statement).any? { http_request?(it) }
105
+ end
106
+
107
+ def first_assertion_in(statement)
108
+ immediate_calls_in(statement).find { assertion?(it) && !side_effect_assertion?(it) }
109
+ end
110
+
111
+ def assertion?(send_node)
112
+ call_on_self?(send_node) && send_node.method_name.to_s.start_with?("assert", "refute")
113
+ end
114
+
115
+ def side_effect_assertion?(send_node)
116
+ call_on_self?(send_node) && SIDE_EFFECT_ASSERTIONS.include?(send_node.method_name)
117
+ end
118
+
119
+ def message_for(assertion)
120
+ format(MESSAGE, method: assertion.method_name)
121
+ end
122
+ end
123
+ end