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,47 @@
1
+ # Asks for singular scope names, except under the `with_` prefix. A scope reads
2
+ # as an adjective on the relation, `Person.customer` the way `Person.active`
3
+ # does, while a plural name restates the collection the relation already is. A
4
+ # `with_` scope names what the records carry, and that is plural as often as
5
+ # not.
6
+ #
7
+ # @example
8
+ # # bad - plural scope name
9
+ # scope :customers, -> { where(customer: true) }
10
+ # scope :admins, -> { where(role: :admin) }
11
+ #
12
+ # # good - singular scope name
13
+ # scope :customer, -> { where(customer: true) }
14
+ # scope :admin, -> { where(role: :admin) }
15
+ #
16
+ # # good - with_* prefix can be plural (for associations)
17
+ # scope :with_posts, -> { includes(:posts) }
18
+ # scope :with_comments, -> { preload(:comments) }
19
+ #
20
+ class RuboCop::Cop::Callbacksystems::SingularScopeNames < RuboCop::Cop::Callbacksystems::Base
21
+ MESSAGE = "Scope names should be singular. Use `%<singular>s` instead of `%<name>s`."
22
+
23
+ def on_send(node)
24
+ add_offense(node.first_argument, message: message_for(node.first_argument.value)) if plural_scope?(node)
25
+ end
26
+
27
+ alias on_csend on_send
28
+
29
+ private
30
+ ASSOCIATION_PREFIX = "with_"
31
+
32
+ def plural_scope?(node)
33
+ scope_definition?(node) && plural?(node.first_argument.value)
34
+ end
35
+
36
+ def plural?(name)
37
+ !name.start_with?(ASSOCIATION_PREFIX) && singular_of(name) != name.to_s
38
+ end
39
+
40
+ def singular_of(name)
41
+ name.to_s.singularize
42
+ end
43
+
44
+ def message_for(name)
45
+ format(MESSAGE, singular: singular_of(name), name:)
46
+ end
47
+ end
@@ -0,0 +1,73 @@
1
+ # Requires a space inside the delimiters of a percent literal, the way an array
2
+ # literal carries one. `Layout/SpaceInsideArrayLiteralBrackets` asks for `[ a, b ]`
3
+ # and `Layout/SpaceInsideHashLiteralBraces` for `{ a: 1 }`, while the core cop for
4
+ # these literals can only forbid the space, so this one asks for it and that one
5
+ # stays off.
6
+ #
7
+ # A literal spanning several lines is left alone, since the line breaks already
8
+ # separate its elements, and so is an empty one, which has nothing to separate.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # %i[save update]
13
+ # %w[a b]
14
+ #
15
+ # # good
16
+ # %i[ save update ]
17
+ # %w[ a b ]
18
+ #
19
+ # # good - the line breaks separate these
20
+ # %i[
21
+ # save update
22
+ # destroy
23
+ # ]
24
+ #
25
+ # # good - nothing to separate
26
+ # %i[]
27
+ #
28
+ class RuboCop::Cop::Callbacksystems::SpaceInsidePercentLiteral < RuboCop::Cop::Callbacksystems::Base
29
+ extend RuboCop::Cop::AutoCorrector
30
+
31
+ def on_array(node)
32
+ report PercentLiteral.new(node) if node.percent_literal?
33
+ end
34
+
35
+ private
36
+ class PercentLiteral
37
+ include RuboCop::Callbacksystems::Helpers
38
+
39
+ MESSAGE = "Add a space inside the delimiters, the way an array literal carries one."
40
+
41
+ def initialize(node)
42
+ @node = node
43
+ end
44
+
45
+ def offense
46
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE) { correct(it) } if unspaced?
47
+ end
48
+
49
+ private
50
+ attr_reader :node
51
+
52
+ def unspaced?
53
+ eligible? && [ opening_gap, closing_gap ].any? { it.source != " " }
54
+ end
55
+
56
+ def eligible?
57
+ node.children.any? && same_line?(node.loc.begin, node.loc.end)
58
+ end
59
+
60
+ def opening_gap
61
+ node.loc.begin.end.join(node.children.first.source_range.begin)
62
+ end
63
+
64
+ def closing_gap
65
+ node.children.last.source_range.end.join(node.loc.end.begin)
66
+ end
67
+
68
+ def correct(corrector)
69
+ corrector.replace(opening_gap, " ")
70
+ corrector.replace(closing_gap, " ")
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ # Keeps system tests on a real browser driver.
2
+ # `rack_test` skips JavaScript entirely, so a Hotwire application under it
3
+ # exercises pages that never behave like production. Integration tests
4
+ # already cover the fast no-browser path.
5
+ #
6
+ # @example
7
+ # # bad - system tests without a browser
8
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
9
+ # driven_by :rack_test
10
+ # end
11
+ #
12
+ # # good - headless browser
13
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
14
+ # driven_by :selenium, using: :headless_chrome
15
+ # end
16
+ #
17
+ class RuboCop::Cop::Callbacksystems::SystemTestBrowserDriver < RuboCop::Cop::Callbacksystems::Base
18
+ MESSAGE = "System tests must drive a real browser. Replace `:rack_test` with `:selenium, using: :headless_chrome`."
19
+
20
+ # @!method rack_test_driver?(node)
21
+ def_node_matcher :rack_test_driver?, <<~PATTERN
22
+ (call {nil? (self)} :driven_by (sym :rack_test) ...)
23
+ PATTERN
24
+
25
+ def on_send(node)
26
+ add_offense(node, message: MESSAGE) if rack_test_driver?(node)
27
+ end
28
+
29
+ alias on_csend on_send
30
+ end
@@ -0,0 +1,86 @@
1
+ # Asks for `test "description" do` over `def test_*`, and for `setup` and
2
+ # `teardown` blocks over methods of those names. A description in plain words
3
+ # says more than an underscored method name, and a `setup` block runs beside
4
+ # the ones a parent class declares, where a `setup` method replaces them unless
5
+ # it remembers to call `super`.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # def test_something
10
+ # assert true
11
+ # end
12
+ #
13
+ # # good
14
+ # test "something" do
15
+ # assert true
16
+ # end
17
+ #
18
+ # # bad
19
+ # def setup
20
+ # @user = users(:bruno)
21
+ # end
22
+ #
23
+ # # good
24
+ # setup do
25
+ # @user = users(:bruno)
26
+ # end
27
+ #
28
+ # # bad
29
+ # def teardown
30
+ # cleanup
31
+ # end
32
+ #
33
+ # # good
34
+ # teardown do
35
+ # cleanup
36
+ # end
37
+ #
38
+ class RuboCop::Cop::Callbacksystems::TestMethodDefinition < RuboCop::Cop::Callbacksystems::Base
39
+ TEST_MESSAGE = "Use `test \"description\" do` instead of `def %<method>s`."
40
+ BLOCK_MESSAGE = "Use `%<method>s do` block instead of `def %<method>s`."
41
+
42
+ def on_def(node)
43
+ report MethodDefinition.new(node)
44
+ end
45
+
46
+ private
47
+ class MethodDefinition
48
+ include RuboCop::Callbacksystems::Helpers
49
+
50
+ TEST_PREFIX = "test_"
51
+ HOOK_METHODS = %i[ setup teardown ]
52
+
53
+ def initialize(node)
54
+ @node = node
55
+ end
56
+
57
+ def offense
58
+ RuboCop::Callbacksystems::Offense.new(node, message) if inside_test_class? && message
59
+ end
60
+
61
+ private
62
+ attr_reader :node
63
+
64
+ def inside_test_class?
65
+ class_node = enclosing_class_or_module_of(node)
66
+ direct_method_definition?(node) && class_node.class_type? && (class_name_of(class_node).end_with?("Test") ||
67
+ rails_test_base_class?(class_node.parent_class))
68
+ end
69
+
70
+ def message
71
+ if test_method?
72
+ format(TEST_MESSAGE, method: node.method_name)
73
+ elsif hook_method?
74
+ format(BLOCK_MESSAGE, method: node.method_name)
75
+ end
76
+ end
77
+
78
+ def test_method?
79
+ node.method_name.start_with?(TEST_PREFIX)
80
+ end
81
+
82
+ def hook_method?
83
+ HOOK_METHODS.include?(node.method_name)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,319 @@
1
+ # Ensures tests follow the order of the methods they cover in the source file.
2
+ # A test file read beside its source then walks the class top to bottom, and a
3
+ # reader looking for the tests of a method finds them where the method sits
4
+ # instead of scanning the whole file for its name.
5
+ #
6
+ # A test names its method with the first word of its description, so only the
7
+ # tests opening with a method of the source are read, and the cop runs on test
8
+ # files whose source file exists.
9
+ #
10
+ # @example
11
+ # # Source file (app/models/user.rb):
12
+ # # def name; end
13
+ # # def email; end
14
+ # # def admin?; end
15
+ #
16
+ # # bad - tests not in method order
17
+ # test "email returns formatted email" do
18
+ # end
19
+ #
20
+ # test "name returns full name" do
21
+ # end
22
+ #
23
+ # # good - tests follow method order
24
+ # test "name returns full name" do
25
+ # end
26
+ #
27
+ # test "email returns formatted email" do
28
+ # end
29
+ #
30
+ # test "admin? returns true for admins" do
31
+ # end
32
+ #
33
+ class RuboCop::Cop::Callbacksystems::TestMethodOrder < RuboCop::Cop::Callbacksystems::Base
34
+ extend RuboCop::Cop::AutoCorrector
35
+ include RuboCop::Callbacksystems::Testing::CopHelpers
36
+
37
+ def external_dependency_checksum
38
+ RuboCop::Callbacksystems::Source::FilesChecksum.for([ "app/**/*.rb", "lib/**/*.rb" ], root: project_root)
39
+ end
40
+
41
+ def on_new_investigation
42
+ @method_positions = nil
43
+ @path_mapping = nil
44
+
45
+ report_each TestOrder.new(processed_source, blocks: test_blocks, positions: method_positions)
46
+ end
47
+
48
+ private
49
+ def method_positions
50
+ @method_positions ||= checkable? ? source_methods.each_with_index.to_a.uniq(&:first).to_h : {}
51
+ end
52
+
53
+ def checkable?
54
+ test_file?(processed_source.file_path) && path_mapping.source_path && File.exist?(path_mapping.source_path)
55
+ end
56
+
57
+ def path_mapping
58
+ @path_mapping ||= RuboCop::Callbacksystems::Testing::PathMapping.new(processed_source.file_path)
59
+ end
60
+
61
+ def source_methods
62
+ source_methods_for(path_mapping.source_path)
63
+ end
64
+
65
+ def source_methods_for(source_path)
66
+ source = RuboCop::Callbacksystems::Source::FileAst.processed_source \
67
+ source_path, ruby_version: processed_source.ruby_version
68
+
69
+ source&.valid_syntax? ? RuboCop::Callbacksystems::Methods::Definitions.new(source.ast).map(&:name) : []
70
+ end
71
+
72
+ class TestOrder
73
+ Test = Data.define(:node, :name, :position)
74
+
75
+ def initialize(processed_source, blocks:, positions:)
76
+ @processed_source = processed_source
77
+ @blocks = blocks
78
+ @positions = positions
79
+ end
80
+
81
+ # Only the first offense carries the correction, since rewriting puts every test in order at once.
82
+ def each_offense
83
+ correction_available = reordering.applicable?
84
+ offenses.each do |offense|
85
+ yield correction_available ? correcting(offense) : offense
86
+ correction_available = false
87
+ end
88
+ end
89
+
90
+ private
91
+ attr_reader :processed_source, :blocks, :positions
92
+
93
+ delegate :rewrite, to: :reordering, private: true
94
+
95
+ def reordering
96
+ @reordering ||= Reordering.new(processed_source, blocks:, tests:)
97
+ end
98
+
99
+ def tests
100
+ @tests ||= blocks.filter_map { test_for(it) }
101
+ end
102
+
103
+ def test_for(block)
104
+ Description.new(block, positions).method_name&.then { Test.new(block, it, positions.fetch(it)) }
105
+ end
106
+
107
+ def offenses
108
+ placements.filter_map(&:offense)
109
+ end
110
+
111
+ def placements
112
+ Placements.new(tests)
113
+ end
114
+
115
+ def correcting(offense)
116
+ RuboCop::Callbacksystems::Offense.new(offense.range, offense.message) { rewrite(it) }
117
+ end
118
+ end
119
+
120
+ # Any executable statement between two tests is a hard boundary, so a correction cannot move a test across setup.
121
+ class Reordering
122
+ include RuboCop::Callbacksystems::Helpers
123
+
124
+ def initialize(processed_source, blocks:, tests:)
125
+ @processed_source = processed_source
126
+ @blocks = blocks
127
+ @tests = tests
128
+ end
129
+
130
+ def rewrite(corrector)
131
+ runs.select(&:reorderable?).each { it.rewrite(corrector) }
132
+ end
133
+
134
+ def applicable?
135
+ runs.any?(&:reorderable?)
136
+ end
137
+
138
+ private
139
+ attr_reader :processed_source, :blocks, :tests
140
+
141
+ def runs
142
+ @runs ||= blocks.sort_by { it.source_range.begin_pos }
143
+ .slice_when { |left, right| interrupted_between?(left, right) }
144
+ .map { Run.new(it, tests, processed_source) }
145
+ end
146
+
147
+ def interrupted_between?(left, right)
148
+ left_end = statement_of(left).range.end_pos
149
+ right_start = statement_of(right).begin_position
150
+ right_start < left_end || content_in?(range_between(left_end, right_start).source)
151
+ end
152
+
153
+ def statement_of(node)
154
+ RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source)
155
+ end
156
+
157
+ def content_in?(source)
158
+ source.lines.any? { it.strip.present? }
159
+ end
160
+ end
161
+
162
+ # One uninterrupted run of test blocks, where a test the source does not know keeps its slot.
163
+ class Run
164
+ def initialize(blocks, tests, processed_source)
165
+ @blocks = blocks
166
+ @tests = tests
167
+ @processed_source = processed_source
168
+ end
169
+
170
+ def rewrite(corrector)
171
+ moved_indexes.each { corrector.replace(statement_of(recognized[it]).range, statement_of(ordered[it]).source) }
172
+ end
173
+
174
+ def reorderable?
175
+ recognized != ordered && tooling_scope_kept?
176
+ end
177
+
178
+ private
179
+ attr_reader :blocks, :tests, :processed_source
180
+
181
+ def moved_indexes
182
+ recognized.each_index.reject { recognized[it].equal?(ordered[it]) }
183
+ end
184
+
185
+ def recognized
186
+ @recognized ||= blocks.filter_map { tests_by_node[it] }
187
+ end
188
+
189
+ def tests_by_node
190
+ @tests_by_node ||= {}.compare_by_identity.tap do |index|
191
+ tests.each { index[it.node] = it }
192
+ end
193
+ end
194
+
195
+ def ordered
196
+ @ordered ||= recognized.sort_by.with_index { |test, index| [ test.position, index ] }
197
+ end
198
+
199
+ def statement_of(test)
200
+ statement_for(test.node)
201
+ end
202
+
203
+ def statement_for(node)
204
+ RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source)
205
+ end
206
+
207
+ def tooling_scope_kept?
208
+ blocks.none? { statement_for(it).contains_tooling_comment? }
209
+ end
210
+ end
211
+
212
+ class Description
213
+ include RuboCop::Callbacksystems::Helpers
214
+
215
+ def initialize(block, positions)
216
+ @block = block
217
+ @positions = positions
218
+ end
219
+
220
+ def method_name
221
+ candidates.find { positions.key?(it) }
222
+ end
223
+
224
+ private
225
+ attr_reader :block, :positions
226
+
227
+ # A scope is named bare where a method might carry a `?` or `!`, so the word after the name tells the two apart.
228
+ def candidates
229
+ if first_word.nil? then []
230
+ elsif scope? then [ first_word.to_sym ]
231
+ else [ first_word.to_sym, :"#{first_word}?", :"#{first_word}!" ]
232
+ end
233
+ end
234
+
235
+ def first_word
236
+ words.first
237
+ end
238
+
239
+ def words
240
+ @words ||= block.send_node.first_argument.value.split(/\s+/)
241
+ end
242
+
243
+ def scope?
244
+ words.second == "scope"
245
+ end
246
+ end
247
+
248
+ # Placements read the prefix once, carrying exactly the facts the next test needs.
249
+ class Placements
250
+ include Enumerable
251
+
252
+ delegate :each, to: :placements
253
+
254
+ def initialize(tests)
255
+ @tests = tests
256
+ @seen_names = Set.new
257
+ end
258
+
259
+ private
260
+ attr_reader :tests, :seen_names, :previous, :anchor
261
+
262
+ def placements
263
+ @placements ||= tests.map { |test| placement_for(test).tap { remember(test) } }
264
+ end
265
+
266
+ def placement_for(test)
267
+ Placement.new(test, repeated: seen_names.include?(test.name), grouped: previous&.name == test.name, anchor:)
268
+ end
269
+
270
+ def remember(test)
271
+ seen_names << test.name
272
+ @previous = test
273
+ @anchor = test if anchor.nil? || test.position > anchor.position
274
+ end
275
+ end
276
+
277
+ class Placement
278
+ GROUPING_MESSAGE = "Tests for `%<method>s` should be grouped together."
279
+ ORDER_MESSAGE = "Test for `%<method>s` appears after `%<previous>s`, but `%<method>s` is defined first in source."
280
+
281
+ def initialize(test, repeated:, grouped:, anchor:)
282
+ @test = test
283
+ @repeated = repeated
284
+ @grouped = grouped
285
+ @anchor = anchor
286
+ end
287
+
288
+ def offense
289
+ RuboCop::Callbacksystems::Offense.new(test.node, message) if separated? || out_of_order?
290
+ end
291
+
292
+ private
293
+ attr_reader :test, :anchor, :repeated, :grouped
294
+
295
+ def separated?
296
+ repeated? && !grouped?
297
+ end
298
+
299
+ def repeated?
300
+ repeated
301
+ end
302
+
303
+ def grouped?
304
+ grouped
305
+ end
306
+
307
+ def out_of_order?
308
+ !repeated? && anchor && test.position < anchor.position
309
+ end
310
+
311
+ def message
312
+ separated? ? format(GROUPING_MESSAGE, method: test.name) : order_message
313
+ end
314
+
315
+ def order_message
316
+ format(ORDER_MESSAGE, method: test.name, previous: anchor.name)
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,78 @@
1
+ # Flags private methods with too many instance variable assignments.
2
+ # Too many instance variable assignments often indicates a method is doing too much.
3
+ # The `initialize` method and public methods are excluded.
4
+ # Public methods (like controller actions) often need multiple instance variables.
5
+ #
6
+ # @example MaxAssignments: 2 (default)
7
+ # # bad - too many instance variable assignments in private method
8
+ # private
9
+ # def setup_request
10
+ # @user = find_user
11
+ # @account = find_account
12
+ # @permissions = load_permissions
13
+ # end
14
+ #
15
+ # # good - single responsibility
16
+ # private
17
+ # def set_user
18
+ # @user = find_user
19
+ # end
20
+ #
21
+ # # good - public methods can have many (e.g., controller actions)
22
+ # def show
23
+ # @user = find_user
24
+ # @account = find_account
25
+ # @permissions = load_permissions
26
+ # end
27
+ #
28
+ # # good - initialize can have many
29
+ # def initialize(user, account)
30
+ # @user = user
31
+ # @account = account
32
+ # @created_at = Time.current
33
+ # end
34
+ #
35
+ class RuboCop::Cop::Callbacksystems::TooManyInstanceVariables < RuboCop::Cop::Callbacksystems::Base
36
+ def on_def(node)
37
+ report InstanceVariableAssignments.new(node, max: cop_config["MaxAssignments"])
38
+ end
39
+
40
+ alias on_defs on_def
41
+
42
+ private
43
+ class InstanceVariableAssignments
44
+ include RuboCop::Callbacksystems::Helpers
45
+
46
+ MESSAGE = "Method has %<count>d instance variable assignments (max %<max>d). " \
47
+ "Consider splitting into separate methods."
48
+
49
+ def initialize(node, max:)
50
+ @node = node
51
+ @max = max
52
+ end
53
+
54
+ def offense
55
+ RuboCop::Callbacksystems::Offense.new(node, message) if private_non_initializer? && count > max
56
+ end
57
+
58
+ private
59
+ attr_reader :node, :max
60
+
61
+ def private_non_initializer?
62
+ direct_method_definition?(node) && !node.method?(:initialize) && !public_method?(node)
63
+ end
64
+
65
+ def count
66
+ @count ||= instance_assignments.to_set(&:name).size
67
+ end
68
+
69
+ def instance_assignments
70
+ RuboCop::Callbacksystems::Execution::Immediate.new(node.body).nodes_of_type(:ivasgn)
71
+ .select { self_preserved_between?(it, boundary: node) }
72
+ end
73
+
74
+ def message
75
+ format(MESSAGE, count:, max:)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,77 @@
1
+ # Flags methods with too many local variable assignments.
2
+ # Too many local variables often indicates imperative code that could
3
+ # be made more declarative by extracting methods.
4
+ #
5
+ # @example MaxAssignments: 3 (default)
6
+ # # bad - imperative with local variables
7
+ # def process
8
+ # user = find_user
9
+ # account = user.account
10
+ # plan = account.plan
11
+ # features = plan.features
12
+ # # ...
13
+ # end
14
+ #
15
+ # # good - declarative with methods
16
+ # def process
17
+ # apply_features
18
+ # end
19
+ #
20
+ # def features
21
+ # plan.features
22
+ # end
23
+ #
24
+ # def plan
25
+ # account.plan
26
+ # end
27
+ #
28
+ # def account
29
+ # user.account
30
+ # end
31
+ #
32
+ # def user
33
+ # User.find(user_id)
34
+ # end
35
+ #
36
+ # # good - attribute assignments don't count
37
+ # def configure
38
+ # config.timeout = 30
39
+ # config.retries = 3
40
+ # config.verbose = true
41
+ # end
42
+ #
43
+ class RuboCop::Cop::Callbacksystems::TooManyLocalVariables < RuboCop::Cop::Callbacksystems::Base
44
+ def on_def(node)
45
+ report LocalVariableAssignments.new(node, max: cop_config["MaxAssignments"])
46
+ end
47
+
48
+ alias on_defs on_def
49
+
50
+ private
51
+ class LocalVariableAssignments
52
+ include RuboCop::Callbacksystems::Helpers
53
+
54
+ MESSAGE = "Method has %<count>d local variable assignments (max %<max>d). " \
55
+ "Extract methods instead of using local variables."
56
+
57
+ def initialize(node, max:)
58
+ @node = node
59
+ @max = max
60
+ end
61
+
62
+ def offense
63
+ RuboCop::Callbacksystems::Offense.new(node, message) if count > max
64
+ end
65
+
66
+ private
67
+ attr_reader :node, :max
68
+
69
+ def count
70
+ @count ||= assignment_count(node.body, :lvasgn, :match_var)
71
+ end
72
+
73
+ def message
74
+ format(MESSAGE, count:, max:)
75
+ end
76
+ end
77
+ end