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,161 @@
1
+ # Detects private methods that only call another method or read a constant
2
+ # without adding value. Such a wrapper is one more name for a reader to
3
+ # resolve on the way to the thing it stands for, and it hides the call site
4
+ # from whoever searches for the method it wraps. Public methods and predicates
5
+ # are excluded as they often provide semantic value.
6
+ #
7
+ # @example
8
+ # # bad - private wrapper just calls another method
9
+ # private
10
+ # def find_all
11
+ # find(@node)
12
+ # end
13
+ #
14
+ # # bad - private wrapper carries the name of the constant it reads
15
+ # MESSAGE = "Remove this."
16
+ #
17
+ # private
18
+ # def message
19
+ # MESSAGE
20
+ # end
21
+ #
22
+ # # good - read the constant where it is needed
23
+ # MESSAGE = "Remove this."
24
+ #
25
+ # private
26
+ # def offense
27
+ # Offense.new(node, MESSAGE)
28
+ # end
29
+ #
30
+ # # good - the method names a concept, and a subclass answers with its own constant
31
+ # private
32
+ # def marker_tag
33
+ # DEFAULT_MARKER_TAG
34
+ # end
35
+ #
36
+ # # good - nothing in the class calls it, so a superclass or a subclass does
37
+ # private
38
+ # def path_after_saving
39
+ # root_path
40
+ # end
41
+ #
42
+ # # good - read from several places, so it is the one spelling of that value
43
+ # private
44
+ # def scope
45
+ # account.widgets
46
+ # end
47
+ #
48
+ # # good - public method (may be API or provide semantic meaning)
49
+ # def authenticated?
50
+ # resume_session
51
+ # end
52
+ #
53
+ # # good - predicate provides semantic alias
54
+ # def fulfillable?
55
+ # paid?
56
+ # end
57
+ #
58
+ # # good - method does something meaningful
59
+ # def find_all
60
+ # find(@node).compact
61
+ # end
62
+ #
63
+ class RuboCop::Cop::Callbacksystems::NoRedundantWrapperMethod < RuboCop::Cop::Callbacksystems::Base
64
+ def on_new_investigation
65
+ @method_calls = RuboCop::Callbacksystems::Methods::Calls.new(processed_source.ast)
66
+ @macro_referenced_methods = RuboCop::Callbacksystems::Methods::MacroReferences::ContainerIndex.new
67
+ end
68
+
69
+ def on_def(node)
70
+ if private_non_predicate?(node)
71
+ report WrapperMethod.new(node, method_calls:, macro_referenced_methods:)
72
+ end
73
+ end
74
+
75
+ alias on_defs on_def
76
+
77
+ private
78
+ attr_reader :macro_referenced_methods, :method_calls
79
+
80
+ class WrapperMethod
81
+ include RuboCop::Callbacksystems::Helpers
82
+
83
+ MESSAGE = "Method `%<wrapper>s` only wraps `%<target>s` without adding value. Consider removing the indirection."
84
+
85
+ def initialize(node, method_calls:, macro_referenced_methods:)
86
+ @node = node
87
+ @method_calls = method_calls
88
+ @macro_referenced_methods = macro_referenced_methods
89
+ end
90
+
91
+ def offense
92
+ RuboCop::Callbacksystems::Offense.new(node, message) if redundant? && inlineable?
93
+ end
94
+
95
+ private
96
+ attr_reader :node, :method_calls, :macro_referenced_methods
97
+ delegate :body, to: :node, private: true
98
+
99
+ def redundant?
100
+ wraps_a_call? || reads_a_constant?
101
+ end
102
+
103
+ def wraps_a_call?
104
+ body&.send_type? && simple_method_call? && arguments_are_pass_through?
105
+ end
106
+
107
+ def simple_method_call?
108
+ body.receiver.nil? || body.receiver.self_type?
109
+ end
110
+
111
+ def arguments_are_pass_through?
112
+ body.arguments.all? { passes_through?(it) }
113
+ end
114
+
115
+ def passes_through?(argument)
116
+ argument.ivar_type? || parameter_names_of(node).include?(spelling_of(argument))
117
+ end
118
+
119
+ def spelling_of(argument)
120
+ argument.lvar_type? ? argument.name.to_s : argument.source
121
+ end
122
+
123
+ def reads_a_constant?
124
+ body&.const_type? && node.arguments.empty? && named_after_the_constant?
125
+ end
126
+
127
+ # A method naming the constant something else states a concept, usually a hook a subclass answers with its own.
128
+ def named_after_the_constant?
129
+ node.method_name.to_s.upcase == body.short_name.to_s
130
+ end
131
+
132
+ def inlineable?
133
+ called_in_its_class? && !sole_spelling_of_a_value? && !macro_referenced?
134
+ end
135
+
136
+ # A private method nothing in its class calls is the hook a superclass or a subclass answers.
137
+ def called_in_its_class?
138
+ calls.any?
139
+ end
140
+
141
+ def calls
142
+ @calls ||= method_calls.named(node.method_name, from: node)
143
+ end
144
+
145
+ def sole_spelling_of_a_value?
146
+ node.arguments.empty? && calls.many?
147
+ end
148
+
149
+ def macro_referenced?
150
+ macro_referenced_methods.include?(node.method_name, beside: node)
151
+ end
152
+
153
+ def message
154
+ format(MESSAGE, wrapper: node.method_name, target: target)
155
+ end
156
+
157
+ def target
158
+ body.const_type? ? body.source : body.method_name
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,105 @@
1
+ # ASCII-art section dividers in comments add no information: a row of dashes,
2
+ # equals, or box-drawing glyphs is noise to the toolchain and signals padding
3
+ # over substance. A pure divider should be removed; a divider wrapping a title
4
+ # (`=== Setup ===`) should keep only its inner text as a normal comment.
5
+ #
6
+ # @example
7
+ # # bad - a pure divider, here closing the statement it follows
8
+ # configure_defaults # ==========
9
+ #
10
+ # # bad - a title wrapped in dividers
11
+ # # ===== Setup =====
12
+ #
13
+ # # good - a plain comment
14
+ # # Setup
15
+ #
16
+ class RuboCop::Cop::Callbacksystems::NoSectionDividerComments < RuboCop::Cop::Callbacksystems::Base
17
+ extend RuboCop::Cop::AutoCorrector
18
+
19
+ def on_new_investigation
20
+ processed_source.comments.each { report DividerComment.new(it), rewrites_comments: true }
21
+ end
22
+
23
+ private
24
+ class DividerComment
25
+ include RuboCop::Callbacksystems::Helpers
26
+
27
+ PURE_MESSAGE = "Section-divider comments add no information. Remove."
28
+ WRAPPED_MESSAGE = "Strip the divider decorations; keep the inner text as a normal comment."
29
+ # `-=*_~#`, the dashes, and the Box Drawing and Block Elements blocks, by ordinal so this file stays ASCII.
30
+ DIVIDER_POINTS = Set[0x2D, 0x3D, 0x2A, 0x5F, 0x7E, 0x23, 0x2013, 0x2014]
31
+ DIVIDER_RANGE = (0x2500..0x259F)
32
+
33
+ def initialize(comment)
34
+ @comment = comment
35
+ end
36
+
37
+ def offense
38
+ RuboCop::Callbacksystems::Offense.new(comment, message) { correct(it) } if divider_comment?
39
+ end
40
+
41
+ private
42
+ attr_reader :comment
43
+
44
+ def divider_comment?
45
+ !tooling_comment?(comment) && (pure? || wrapped?)
46
+ end
47
+
48
+ def pure?
49
+ present? && only_divider_characters?(trimmed) && divider_run?(trimmed)
50
+ end
51
+
52
+ def present?
53
+ !trimmed.empty?
54
+ end
55
+
56
+ def trimmed
57
+ @trimmed ||= comment.text.delete_prefix("#").strip
58
+ end
59
+
60
+ def only_divider_characters?(text)
61
+ text.each_char.all? { divider_or_space?(it) }
62
+ end
63
+
64
+ def divider_or_space?(character)
65
+ divider?(character) || character.match?(/\s/)
66
+ end
67
+
68
+ def divider?(character)
69
+ DIVIDER_POINTS.include?(character.ord) || DIVIDER_RANGE.cover?(character.ord)
70
+ end
71
+
72
+ def divider_run?(text)
73
+ text.each_char.chunk_while { |left, right| divider?(left) && divider?(right) }
74
+ .any? { it.size >= 2 && divider?(it.first) }
75
+ end
76
+
77
+ def wrapped?
78
+ present? && !inner_text.nil?
79
+ end
80
+
81
+ def inner_text
82
+ @inner_text ||= strip_run(trimmed, side: :leading)&.then { strip_run(it, side: :trailing) }
83
+ end
84
+
85
+ def strip_run(text, side:)
86
+ characters = side == :leading ? text.each_char : text.reverse.each_char
87
+ run = characters.take_while { divider?(it) }
88
+ if run.size >= 2
89
+ side == :leading ? text[run.size..].lstrip : text[0...(text.size - run.size)].rstrip
90
+ end
91
+ end
92
+
93
+ def message
94
+ pure? ? PURE_MESSAGE : WRAPPED_MESSAGE
95
+ end
96
+
97
+ def correct(corrector)
98
+ if pure?
99
+ corrector.remove(comment_removal_range_for(comment))
100
+ else
101
+ corrector.replace(comment, "# #{inner_text}")
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,75 @@
1
+ # Prohibits using `send` or `__send__` inside test blocks.
2
+ # Private methods are private for a reason; tests should verify
3
+ # behavior and outcomes, not call private methods directly.
4
+ #
5
+ # @example
6
+ # # bad
7
+ # test "processes payment" do
8
+ # result = order.send(:process_payment)
9
+ # assert result
10
+ # end
11
+ #
12
+ # # bad
13
+ # test "processes payment" do
14
+ # order.__send__(:process_payment)
15
+ # end
16
+ #
17
+ # # good - test the public interface
18
+ # test "processes payment" do
19
+ # order.checkout
20
+ # assert order.paid?
21
+ # end
22
+ #
23
+ class RuboCop::Cop::Callbacksystems::NoSendInTests < RuboCop::Cop::Callbacksystems::Base
24
+ include RuboCop::Callbacksystems::Testing::CopHelpers
25
+
26
+ def on_block(node)
27
+ report_each RestrictedCalls.new(node) if test_block?(node)
28
+ end
29
+
30
+ alias on_numblock on_block
31
+ alias on_itblock on_block
32
+
33
+ private
34
+ class RestrictedCalls
35
+ include RuboCop::Callbacksystems::Helpers
36
+
37
+ MESSAGE = "Don't use `%<method>s` to test private methods. Test behavior through the public interface instead."
38
+ RESTRICTED_METHODS = %i[ send __send__ ]
39
+
40
+ def initialize(test_block)
41
+ @test_block = test_block
42
+ end
43
+
44
+ def each_offense
45
+ restricted_calls.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
46
+ end
47
+
48
+ private
49
+ attr_reader :test_block
50
+
51
+ def restricted_calls
52
+ immediate_execution.select { restricted?(it) }
53
+ end
54
+
55
+ def immediate_execution
56
+ RuboCop::Callbacksystems::Execution::Immediate.new(test_block.body, deferred_blocks: deferred_blocks)
57
+ end
58
+
59
+ def deferred_blocks
60
+ nodes_in(test_block.body, :any_block).select { method_definition_block?(it) || nested_test_block?(it) }
61
+ end
62
+
63
+ def nested_test_block?(node)
64
+ any_block_type?(node) && bare_send?(call_of(node)) && call_of(node).method?(:test)
65
+ end
66
+
67
+ def restricted?(send_node)
68
+ send_node.call_type? && RESTRICTED_METHODS.include?(send_node.method_name) && !send_node.receiver.nil?
69
+ end
70
+
71
+ def message_for(send_node)
72
+ format(MESSAGE, method: send_node.method_name)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,64 @@
1
+ # Prohibits files in service object-style directories under `app/`. A service
2
+ # is a verb with no noun, so the behavior it holds drifts away from the model
3
+ # that owns the data it acts on, and the model is left anemic. The behavior
4
+ # belongs to that model, or to a plain object in `app/models` named after the
5
+ # concept it stands for.
6
+ #
7
+ # Only applies to Rails applications (paths under `app/`). Gems or libraries
8
+ # that use directories like `commands/` or `queries/` outside of `app/` are
9
+ # untouched.
10
+ #
11
+ # @example
12
+ # # bad - file in app/services/
13
+ # # app/services/order_processor.rb
14
+ #
15
+ # # bad - file in app/decorators/
16
+ # # app/decorators/user_decorator.rb
17
+ #
18
+ # # bad - file in app/interactors/
19
+ # # app/interactors/create_order.rb
20
+ #
21
+ # # bad - file in app/presenters/
22
+ # # app/presenters/user_presenter.rb
23
+ #
24
+ # # bad - file in app/forms/
25
+ # # app/forms/registration_form.rb
26
+ #
27
+ # # good - use models
28
+ # # app/models/order.rb
29
+ #
30
+ # # good - non-Rails gem with commands in lib/
31
+ # # lib/my_gem/commands/run.rb
32
+ #
33
+ class RuboCop::Cop::Callbacksystems::NoServiceObjects < RuboCop::Cop::Callbacksystems::Base
34
+ MESSAGE = "Files in `app/%<directory>s/` are not allowed. Use models or POROs in `app/models/` instead."
35
+
36
+ def on_new_investigation
37
+ forbidden_directory&.then { add_offense(offense_range, message: format(MESSAGE, directory: it)) }
38
+ end
39
+
40
+ private
41
+ FORBIDDEN_DIRECTORIES = %w[
42
+ services
43
+ decorators
44
+ interactors
45
+ presenters
46
+ forms
47
+ operations
48
+ commands
49
+ queries
50
+ use_cases
51
+ ]
52
+
53
+ def forbidden_directory
54
+ FORBIDDEN_DIRECTORIES.find { in_directory?(it) }
55
+ end
56
+
57
+ def in_directory?(directory)
58
+ processed_source.file_path.match?(%r{(^|/)app/#{Regexp.escape(directory)}/})
59
+ end
60
+
61
+ def offense_range
62
+ processed_source.ast || processed_source.buffer.source_range
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ # Detects single-letter names for variables, block arguments, and method
2
+ # parameters. A single letter says nothing about what it holds, so a reader
3
+ # has to go back to where it was assigned to learn what `i` or `e` stands for,
4
+ # where a descriptive name answers on the spot.
5
+ #
6
+ # @example
7
+ # # bad
8
+ # x = 5
9
+ # items.each { |i| process(i) }
10
+ # def foo(n); end
11
+ # items.map { |e| e.name }
12
+ # hash.each { |k, v| puts k }
13
+ #
14
+ # # good
15
+ # count = 5
16
+ # items.each { |item| process(item) }
17
+ # def foo(number); end
18
+ # items.map { |element| element.name }
19
+ # hash.each { |key, value| puts key }
20
+ #
21
+ # # ok - underscore convention for unused
22
+ # items.each { |_| process }
23
+ # [1,2].each_with_index { |_, index| puts index }
24
+ #
25
+ class RuboCop::Cop::Callbacksystems::NoSingleLetterNames < RuboCop::Cop::Callbacksystems::Base
26
+ MESSAGE = "Avoid single-letter name `%<name>s`. Use a descriptive name instead."
27
+
28
+ def on_lvasgn(node)
29
+ name = variable_name_of(node)
30
+ add_offense(node, message: format(MESSAGE, name:)) if single_letter?(name)
31
+ end
32
+
33
+ alias on_arg on_lvasgn
34
+ alias on_optarg on_lvasgn
35
+ alias on_restarg on_lvasgn
36
+ alias on_kwarg on_lvasgn
37
+ alias on_kwoptarg on_lvasgn
38
+ alias on_kwrestarg on_lvasgn
39
+ alias on_blockarg on_lvasgn
40
+ alias on_match_var on_lvasgn
41
+ alias on_shadowarg on_lvasgn
42
+
43
+ private
44
+ def single_letter?(name)
45
+ name && name.length == 1 && !name.start_with?("_")
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # Forbids using `class << self` inside modules.
2
+ # Use `extend self` instead to define module-level methods.
3
+ #
4
+ # `class << self` in modules is an imperative way to define methods
5
+ # when a more declarative `extend self` approach exists.
6
+ #
7
+ # @example
8
+ # # bad - using class << self in module
9
+ # module Calculator
10
+ # class << self
11
+ # def add(a, b)
12
+ # a + b
13
+ # end
14
+ # end
15
+ # end
16
+ #
17
+ # # good - using extend self
18
+ # module Calculator
19
+ # extend self
20
+ #
21
+ # def add(a, b)
22
+ # a + b
23
+ # end
24
+ # end
25
+ #
26
+ class RuboCop::Cop::Callbacksystems::NoSingletonClassInModules < RuboCop::Cop::Callbacksystems::Base
27
+ MESSAGE = "Use `extend self` instead of `class << self` in modules."
28
+
29
+ def on_sclass(node)
30
+ add_offense(node, message: MESSAGE) if singleton_section?(node) && lexical_owner_of(node)&.module_type?
31
+ end
32
+
33
+ private
34
+ def lexical_owner_of(node)
35
+ node.each_ancestor.find { lexical_boundary?(it) }
36
+ end
37
+
38
+ # A block may be evaluated with another receiver (`included`, `class_eval`, and friends), so its `self` cannot be
39
+ # attributed to the module around the block from syntax alone.
40
+ def lexical_boundary?(node)
41
+ node.type?(:class, :module, :sclass, :def, :defs) || any_block_type?(node)
42
+ end
43
+ end
@@ -0,0 +1,173 @@
1
+ # Detects using tap to build collections imperatively. A `tap` on an empty
2
+ # collection is a loop that fills it by hand, so a reader has to run the block
3
+ # to learn what comes out, where `map`, `select`, `index_by` or `group_by`
4
+ # names the result in the call itself.
5
+ #
6
+ # @example
7
+ # # bad - building array with tap
8
+ # [].tap { |arr| items.each { |i| arr << i.name } }
9
+ #
10
+ # # good - use map
11
+ # items.map { it.name }
12
+ #
13
+ # # bad - building hash with tap
14
+ # {}.tap { |h| items.each { |i| h[i.id] = i } }
15
+ #
16
+ # # good - use index_by or to_h
17
+ # items.index_by { it.id }
18
+ #
19
+ # # bad - building Set with tap
20
+ # Set.new.tap { |s| items.each { |i| s << i.id } }
21
+ #
22
+ # # good - use Set.new with block or to_set
23
+ # items.map(&:id).to_set
24
+ #
25
+ # # bad - building Hash with default value and tap
26
+ # Hash.new { |h, k| h[k] = [] }.tap { |h| items.each { |i| h[i.type] << i } }
27
+ #
28
+ # # good - use group_by
29
+ # items.group_by(&:type)
30
+ #
31
+ # # ok - tap for logging (not collection building)
32
+ # user.tap { |u| logger.info(u.id) }
33
+ #
34
+ # # ok - tap for configuring object
35
+ # User.new.tap { |u| u.name = "John" }
36
+ #
37
+ class RuboCop::Cop::Callbacksystems::NoTapCollectionBuilding < RuboCop::Cop::Callbacksystems::Base
38
+ def on_block(node)
39
+ report TapBlock.new(node)
40
+ end
41
+
42
+ alias on_numblock on_block
43
+ alias on_itblock on_block
44
+
45
+ private
46
+ class TapBlock
47
+ include RuboCop::Callbacksystems::Helpers
48
+
49
+ MESSAGE = "Don't use `tap` to build collections. Use `map`, `select`, `index_by`, `group_by`, or `to_h` instead."
50
+ MUTATION_METHODS = %i[ << push append []= store merge! ]
51
+
52
+ def initialize(node)
53
+ @node = node
54
+ end
55
+
56
+ def offense
57
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE) if tap_on_empty_collection? && mutation_in_block?
58
+ end
59
+
60
+ private
61
+ attr_reader :node
62
+
63
+ def tap_on_empty_collection?
64
+ node.method?(:tap) && CollectionReceiver.new(node.receiver).empty_collection?
65
+ end
66
+
67
+ def mutation_in_block?
68
+ RuboCop::Callbacksystems::Execution::Immediate.new(node.body).any? do |candidate|
69
+ candidate.call_type? && mutates_collection?(candidate)
70
+ end
71
+ end
72
+
73
+ def mutates_collection?(call)
74
+ MUTATION_METHODS.include?(call.method_name) && yielded_collection?(call.receiver)
75
+ end
76
+
77
+ def yielded_collection?(receiver)
78
+ directly_yielded_collection?(receiver) || stored_default_collection?(receiver)
79
+ end
80
+
81
+ def directly_yielded_collection?(receiver)
82
+ receiver&.lvar_type? && node.argument_list.any? { it.name == receiver.name } &&
83
+ !name_rebound_between?(receiver.name, node: receiver, boundary: node)
84
+ end
85
+
86
+ def stored_default_collection?(receiver)
87
+ receiver&.call_type? && receiver.method?(:[]) && directly_yielded_collection?(receiver.receiver) &&
88
+ StoredHashDefault.new(node.receiver).collection?
89
+ end
90
+ end
91
+
92
+ class StoredHashDefault
93
+ def initialize(block)
94
+ @block = block
95
+ end
96
+
97
+ def collection?
98
+ default_block? && stores_default? &&
99
+ CollectionReceiver.new(default_value).empty_collection?
100
+ end
101
+
102
+ private
103
+ attr_reader :block
104
+
105
+ def default_block?
106
+ block&.any_block_type? && argument_names.size == 2 && block.argument_list.all?(&:arg_type?)
107
+ end
108
+
109
+ def argument_names
110
+ @argument_names ||= block.argument_list.map(&:name)
111
+ end
112
+
113
+ def stores_default?
114
+ hash_argument_assigned? && key_argument_used?
115
+ end
116
+
117
+ def hash_argument_assigned?
118
+ assignment&.call_type? && assignment.method?(:[]=) && assignment.receiver&.lvar_type? &&
119
+ assignment.receiver.name == argument_names.first
120
+ end
121
+
122
+ def assignment
123
+ block.body
124
+ end
125
+
126
+ def key_argument_used?
127
+ key&.lvar_type? && key.name == argument_names.second
128
+ end
129
+
130
+ def key
131
+ assignment.first_argument
132
+ end
133
+
134
+ def default_value
135
+ assignment.last_argument
136
+ end
137
+ end
138
+
139
+ class CollectionReceiver
140
+ include RuboCop::Callbacksystems::Helpers
141
+
142
+ COLLECTION_CLASSES = %i[ Set Hash Array ]
143
+
144
+ def initialize(receiver)
145
+ @receiver = receiver
146
+ end
147
+
148
+ def empty_collection?
149
+ receiver && (empty_collection_literal?(receiver) || collection_class_constructor?)
150
+ end
151
+
152
+ private
153
+ attr_reader :receiver
154
+
155
+ def collection_class_constructor?
156
+ constructor.send_type? && collection_constructor? &&
157
+ constructor_without_entries?
158
+ end
159
+
160
+ def constructor
161
+ @constructor ||= call_of(receiver)
162
+ end
163
+
164
+ def collection_constructor?
165
+ constructor.method?(:new) && core_constant?(constructor.receiver) &&
166
+ COLLECTION_CLASSES.include?(constructor.receiver.short_name)
167
+ end
168
+
169
+ def constructor_without_entries?
170
+ constructor.arguments.empty? || constructor.receiver.short_name == :Hash
171
+ end
172
+ end
173
+ end