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,173 @@
1
+ # Prohibits returning a local variable as the last statement of a method. The
2
+ # variable is a name for the value the method already hands back, so the last
3
+ # line reads as a summary of the ones above it, and the whole body is a
4
+ # `tap`, a `then` or an `each_with_object` spelled out by hand.
5
+ #
6
+ # @example
7
+ # # bad - returning a local variable
8
+ # def process
9
+ # result = calculate_something
10
+ # result
11
+ # end
12
+ #
13
+ # # bad - assign, mutate, return
14
+ # def process
15
+ # result = []
16
+ # result << item
17
+ # result
18
+ # end
19
+ #
20
+ # # bad - explicit return of local variable
21
+ # def process
22
+ # result = {}
23
+ # result[:key] = value
24
+ # return result
25
+ # end
26
+ #
27
+ # # good - return the expression directly
28
+ # def process
29
+ # calculate_something
30
+ # end
31
+ #
32
+ # # good - use tap
33
+ # def process
34
+ # [].tap do |result|
35
+ # result << item
36
+ # end
37
+ # end
38
+ #
39
+ # # good - use each_with_object
40
+ # def process
41
+ # items.each_with_object([]) do |item, result|
42
+ # result << transform(item)
43
+ # end
44
+ # end
45
+ #
46
+ # # good - use then
47
+ # def process
48
+ # calculate_something.then do |result|
49
+ # result.merge(extra: value)
50
+ # end
51
+ # end
52
+ #
53
+ # # good - declarative approach
54
+ # def process
55
+ # [item]
56
+ # end
57
+ #
58
+ class RuboCop::Cop::Callbacksystems::NoLocalVariableReturn < RuboCop::Cop::Callbacksystems::Base
59
+ extend RuboCop::Cop::AutoCorrector
60
+
61
+ def on_def(node)
62
+ last_statement_in(node.body)&.then do |statement|
63
+ report ReturnedLocal.new(statement, source_comments: RuboCop::Callbacksystems::Source::Comments.for(processed_source))
64
+ end
65
+ end
66
+
67
+ alias on_defs on_def
68
+
69
+ private
70
+ class ReturnedLocal
71
+ include RuboCop::Callbacksystems::Helpers
72
+
73
+ MESSAGE = "Avoid returning a local variable. " \
74
+ "Return the expression directly, or use `.tap`, `.then`, or `each_with_object`."
75
+ SCOPE_REFLECTION_METHODS = %i[ binding eval local_variables ]
76
+
77
+ def initialize(node, source_comments:)
78
+ @node = node
79
+ @source_comments = source_comments
80
+ end
81
+
82
+ def offense
83
+ if variable&.lvar_type? && assigned?
84
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: inlineable?) { correct(it) }
85
+ end
86
+ end
87
+
88
+ private
89
+ attr_reader :node, :source_comments
90
+
91
+ def variable
92
+ @variable ||= returned_expression_of(node)
93
+ end
94
+
95
+ # A parameter handed back as it came is not a value computed and then returned.
96
+ def assigned?
97
+ occurrences.any? { it.type?(:lvasgn, :match_var) }
98
+ end
99
+
100
+ def occurrences
101
+ @occurrences ||= RuboCop::Callbacksystems::Execution::LocalVariableOccurrences.new(method_node)
102
+ .named_in_method(variable.name, around: node)
103
+ end
104
+
105
+ def method_node
106
+ @method_node ||= enclosing_method_of(node)
107
+ end
108
+
109
+ # A heredoc keeps its body on the lines past the assignment, so inlining the opener would leave the body behind.
110
+ def inlineable?
111
+ assignment && only_read_once? && scope_preserved? && source_preserved?
112
+ end
113
+
114
+ def assignment
115
+ @assignment ||= statements_before(node).last(1).find { it.lvasgn_type? && it.name == variable.name }
116
+ end
117
+
118
+ def only_read_once?
119
+ occurrences.one?(&:lvar_type?) && occurrences.one?(&:lvasgn_type?) && occurrences.none?(&:match_var_type?)
120
+ end
121
+
122
+ def scope_preserved?
123
+ !captured_by_deferred_callable? && !reflects_on_local_scope?
124
+ end
125
+
126
+ def captured_by_deferred_callable?
127
+ RuboCop::Callbacksystems::Execution::DeferredLocalVariables.new(method_node.body).include?(variable.name)
128
+ end
129
+
130
+ # Removing the assignment removes its name from bindings even when Ruby source never reads that name directly.
131
+ def reflects_on_local_scope?
132
+ nodes_in(method_node.body, :send, :csend).any? do |call|
133
+ SCOPE_REFLECTION_METHODS.include?(call.method_name) && scope_reader?(call)
134
+ end
135
+ end
136
+
137
+ def scope_reader?(call)
138
+ call_on_self?(call) || (core_constant?(call.receiver) && call.receiver.short_name == :Kernel)
139
+ end
140
+
141
+ def source_preserved?
142
+ !carries_heredoc?(assignment.expression) && !reads_source_line? && !moves_tooling_comment? &&
143
+ !assignment_trailing_comment?
144
+ end
145
+
146
+ def reads_source_line?
147
+ assignment.expression.source.include?("__LINE__")
148
+ end
149
+
150
+ def moves_tooling_comment?
151
+ source_comments.tooling_within?(assignment.source_range.join(node.source_range))
152
+ end
153
+
154
+ def assignment_trailing_comment?
155
+ source_comments.trailing_comment_on(assignment.last_line).present?
156
+ end
157
+
158
+ def correct(corrector)
159
+ corrector.remove(assignment_removal_range)
160
+ corrector.replace(node, replacement)
161
+ end
162
+
163
+ def assignment_removal_range
164
+ inline_statement_removal_range_for(assignment) ||
165
+ blank_line_below(line_removal_range_for(assignment)) ||
166
+ line_removal_range_for(assignment)
167
+ end
168
+
169
+ def replacement
170
+ node.return_type? ? "return #{assignment.expression.source}" : assignment.expression.source
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,188 @@
1
+ # Detects an empty collection filled by hand inside an `each`. An empty array
2
+ # or hash assigned to a variable and then filled from a loop is a `map`,
3
+ # `select`, `filter_map`, `index_by` or `to_h` spread over two statements, so
4
+ # a reader has to run the loop in their head to learn what the collection
5
+ # holds, where the declarative call names it. The collection may be a local or
6
+ # an instance variable, and it need not be returned afterwards. The mutation
7
+ # may sit under `if`, `unless` or `case` branches, as long as every statement
8
+ # the body reaches is that mutation. A loop with any other statement, or one
9
+ # leaving through `next`, `break`, `return` or `raise`, is left alone, since
10
+ # the declarative form would drop that behavior.
11
+ #
12
+ # @example
13
+ # # bad - array accumulation returned by the method
14
+ # def names
15
+ # results = []
16
+ # items.each { |x| results << x.name }
17
+ # results
18
+ # end
19
+ #
20
+ # # good - use map
21
+ # def names
22
+ # items.map { it.name }
23
+ # end
24
+ #
25
+ # # bad - array accumulation with a guard
26
+ # results = []
27
+ # items.each { |x| results << x.name if x.active? }
28
+ #
29
+ # # good - use select or filter_map
30
+ # results = items.filter_map { it.name if it.active? }
31
+ #
32
+ # # bad - array filled by index
33
+ # results = []
34
+ # items.each_with_index { |x, index| results[index] = x.name }
35
+ #
36
+ # # good - use map
37
+ # results = items.map { it.name }
38
+ #
39
+ # # bad - hash accumulation into an instance variable
40
+ # @index = {}
41
+ # items.each { |x| @index[x.id] = x }
42
+ #
43
+ # # good - use index_by or to_h
44
+ # @index = items.index_by { it.id }
45
+ #
46
+ class RuboCop::Cop::Callbacksystems::NoManualAccumulation < RuboCop::Cop::Callbacksystems::Base
47
+ LOOP_METHODS = %i[ each each_with_index ]
48
+
49
+ def on_block(node)
50
+ report Accumulation.new(node) if LOOP_METHODS.include?(node.method_name)
51
+ end
52
+
53
+ alias on_numblock on_block
54
+ alias on_itblock on_block
55
+
56
+ private
57
+ # A loop filling an empty collection that an earlier statement in the same list assigned.
58
+ class Accumulation
59
+ include RuboCop::Callbacksystems::Helpers
60
+
61
+ MESSAGES = {
62
+ array: "Don't fill an array with `%<loop>s`. Use `map`, `select` or `filter_map` instead.",
63
+ hash: "Don't fill a hash with `%<loop>s`. Use `index_by` or `to_h` instead."
64
+ }
65
+ MUTATORS = { array: %i[ << push append []= store ], hash: %i[ []= store merge! ] }
66
+
67
+ def initialize(loop_block)
68
+ @loop_block = loop_block
69
+ end
70
+
71
+ def offense
72
+ RuboCop::Callbacksystems::Offense.new(loop_block.send_node, message) if accumulator && keeps_flow?
73
+ end
74
+
75
+ private
76
+ attr_reader :loop_block
77
+ delegate :body, to: :loop_block, private: true
78
+
79
+ def accumulator
80
+ @accumulator ||= empty_collection_assignments_before.find { fills?(it) }
81
+ end
82
+
83
+ def empty_collection_assignments_before
84
+ preceding_statements.select { empty_collection_assignment?(it) && !reassigned_after?(it) }
85
+ end
86
+
87
+ def preceding_statements
88
+ @preceding_statements ||= statements_before(loop_block)
89
+ end
90
+
91
+ def empty_collection_assignment?(statement)
92
+ statement.type?(:lvasgn, :ivasgn) && empty_collection_literal?(statement.expression)
93
+ end
94
+
95
+ def reassigned_after?(assignment)
96
+ preceding_statements.drop_while { !it.equal?(assignment) }.drop(1).any? do |statement|
97
+ reassigned_within?(statement, assignment)
98
+ end
99
+ end
100
+
101
+ def reassigned_within?(statement, assignment)
102
+ RuboCop::Callbacksystems::Execution::Immediate.new(statement).nodes_of_type(assignment.type).any? do |node|
103
+ node.name == assignment.name && !rebound_local?(node, assignment)
104
+ end
105
+ end
106
+
107
+ def rebound_local?(candidate, assignment)
108
+ assignment.lvasgn_type? &&
109
+ name_rebound_between?(assignment.name, node: candidate, boundary: loop_block.parent)
110
+ end
111
+
112
+ # A statement beyond the mutation is a side effect `map` or `select` would drop, so such a loop is left alone.
113
+ def fills?(assignment)
114
+ leaves.any? && leaves.all? { mutation_of?(assignment, it) }
115
+ end
116
+
117
+ def leaves
118
+ @leaves ||= Leaves.new(body).to_a
119
+ end
120
+
121
+ def mutation_of?(assignment, leaf)
122
+ leaf.call_type? && reads_variable?(leaf.receiver, assignment.name) && mutator_of?(assignment, leaf)
123
+ end
124
+
125
+ def mutator_of?(assignment, call)
126
+ MUTATORS[kind_of(assignment)].include?(call.method_name)
127
+ end
128
+
129
+ def kind_of(assignment)
130
+ assignment.expression.type
131
+ end
132
+
133
+ def keeps_flow?
134
+ RuboCop::Callbacksystems::Execution::Immediate.new(body).none? do |node|
135
+ node.type?(:next, :break, :return) || raise?(node)
136
+ end
137
+ end
138
+
139
+ def raise?(node)
140
+ bare_send?(node) && node.method?(:raise)
141
+ end
142
+
143
+ def message
144
+ format(MESSAGES[kind_of(accumulator)], loop: loop_block.method_name)
145
+ end
146
+
147
+ # Terminal statements reached through conditional branches, in the order the source presents them.
148
+ class Leaves
149
+ include Enumerable
150
+ include RuboCop::Callbacksystems::Helpers
151
+
152
+ def initialize(body)
153
+ @body = body
154
+ end
155
+
156
+ def each
157
+ if block_given?
158
+ pending = statements_in(body).reverse
159
+ until pending.empty?
160
+ statement = pending.pop
161
+ if branching?(statement)
162
+ branches_of(statement).reverse_each { pending.concat(statements_in(it).reverse) }
163
+ else
164
+ yield statement
165
+ end
166
+ end
167
+ else
168
+ to_enum(__method__)
169
+ end
170
+ end
171
+
172
+ private
173
+ attr_reader :body
174
+
175
+ def branching?(statement)
176
+ statement.type?(:if, :case)
177
+ end
178
+
179
+ def branches_of(statement)
180
+ if statement.if_type?
181
+ [ statement.if_branch, statement.else_branch ]
182
+ else
183
+ [ *statement.when_branches.map(&:body), statement.else_branch ]
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,145 @@
1
+ # Detects memoization mixed with other statements in the same method. A
2
+ # memoized method reads as a value, so a statement beside the `||=` runs on the
3
+ # first read and never again, an effect hidden behind a getter that the caller
4
+ # has no way to see. Conditional memoization (with if/unless) and rescue
5
+ # blocks are allowed.
6
+ #
7
+ # @example
8
+ # # bad - memoization with other statements before
9
+ # def user
10
+ # validate_params
11
+ # @user ||= User.find(params[:id])
12
+ # end
13
+ #
14
+ # # bad - memoization with other statements after
15
+ # def user
16
+ # @user ||= User.find(params[:id])
17
+ # log_access
18
+ # end
19
+ #
20
+ # # good - memoization is the entire method body
21
+ # def user
22
+ # @user ||= User.find(params[:id])
23
+ # end
24
+ #
25
+ # # good - conditional memoization
26
+ # def period
27
+ # @period ||= compute_period if params[:starts_on].present?
28
+ # end
29
+ #
30
+ # # good - memoization with rescue
31
+ # def processor
32
+ # @processor ||= find_processor
33
+ # rescue ActiveRecord::RecordNotFound
34
+ # nil
35
+ # end
36
+ #
37
+ # # good - complex computation inside the memoization
38
+ # def user
39
+ # @user ||= begin
40
+ # data = fetch_data
41
+ # process(data)
42
+ # end
43
+ # end
44
+ #
45
+ class RuboCop::Cop::Callbacksystems::NoMixedMemoization < RuboCop::Cop::Callbacksystems::Base
46
+ def on_def(node)
47
+ report MemoizationBody.new(node)
48
+ end
49
+
50
+ alias on_defs on_def
51
+
52
+ private
53
+ class MemoizationBody
54
+ include RuboCop::Callbacksystems::Helpers
55
+
56
+ MESSAGE = "Memoization should be the entire method body. Extract other statements to separate methods."
57
+
58
+ def initialize(method_node)
59
+ @method_node = method_node
60
+ end
61
+
62
+ def offense
63
+ RuboCop::Callbacksystems::Offense.new(memoization_node, MESSAGE) if mixed?
64
+ end
65
+
66
+ private
67
+ attr_reader :method_node
68
+ delegate :body, to: :method_node, private: true
69
+
70
+ def mixed?
71
+ memoization_node && !entire_body_is_memoization?
72
+ end
73
+
74
+ def memoization_node
75
+ @memoization_node ||= RuboCop::Callbacksystems::Execution::Immediate.new(body).nodes_of_type(:or_asgn)
76
+ .find { instance_variable_memoization?(it) }
77
+ end
78
+
79
+ def instance_variable_memoization?(node)
80
+ node&.or_asgn_type? && node.children.first.ivasgn_type?
81
+ end
82
+
83
+ def entire_body_is_memoization?
84
+ simple_memoization? || multiple_memoizations? || conditional_memoization? ||
85
+ memoization_with_rescue? || memoization_then_field_return?
86
+ end
87
+
88
+ def simple_memoization?
89
+ instance_variable_memoization?(effective_body)
90
+ end
91
+
92
+ def effective_body
93
+ @effective_body ||= TransparentExpression.new(body).value
94
+ end
95
+
96
+ def multiple_memoizations?
97
+ effective_body.type?(:begin, :kwbegin) &&
98
+ effective_body.children.all? { instance_variable_memoization?(it) }
99
+ end
100
+
101
+ def conditional_memoization?
102
+ effective_body.if_type? && effective_body.else_branch.nil? &&
103
+ instance_variable_memoization?(effective_body.if_branch)
104
+ end
105
+
106
+ def memoization_with_rescue?
107
+ instance_variable_memoization?(rescue_node&.children&.first)
108
+ end
109
+
110
+ def rescue_node
111
+ effective_body if effective_body.rescue_type?
112
+ end
113
+
114
+ # `@user ||= load; @user` reads back the field it just memoized, which is still a single concern.
115
+ def memoization_then_field_return?
116
+ effective_body.type?(:begin, :kwbegin) && effective_body.children.size == 2 &&
117
+ instance_variable_memoization?(effective_body.children.first) &&
118
+ returns_memoized_variable?(effective_body.children.last)
119
+ end
120
+
121
+ def returns_memoized_variable?(node)
122
+ reads_variable?(returned_expression_of(node), memoization_node.lhs.name)
123
+ end
124
+
125
+ class TransparentExpression
126
+ def initialize(node)
127
+ @node = node
128
+ end
129
+
130
+ def value
131
+ node.then do |current|
132
+ current = current.children.first while transparent?(current)
133
+ current
134
+ end
135
+ end
136
+
137
+ private
138
+ attr_reader :node
139
+
140
+ def transparent?(current)
141
+ current.type?(:begin, :kwbegin) && current.children.one?
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,78 @@
1
+ # Forbids nested modules inside classes.
2
+ # Modules should be defined at the top level or inside other modules,
3
+ # not inside classes.
4
+ #
5
+ # Nested modules in classes are often used to avoid creating proper
6
+ # objects, leading to imperative rather than declarative code.
7
+ #
8
+ # @example
9
+ # # bad - nested module in class
10
+ # class Order
11
+ # module Calculations
12
+ # def total
13
+ # # ...
14
+ # end
15
+ # end
16
+ # end
17
+ #
18
+ # # bad - nested module in private section
19
+ # class Order
20
+ # private
21
+ # module Calculations
22
+ # def total
23
+ # # ...
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # # good - use a nested class instead
29
+ # class Order
30
+ # private
31
+ # class Calculator
32
+ # def total
33
+ # # ...
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # # good - define module at top level
39
+ # module Order::Calculations
40
+ # def total
41
+ # # ...
42
+ # end
43
+ # end
44
+ #
45
+ class RuboCop::Cop::Callbacksystems::NoNestedModulesInClasses < RuboCop::Cop::Callbacksystems::Base
46
+ MESSAGE = "Do not nest modules inside classes. Use a nested class or define the module at the top level."
47
+
48
+ def on_module(node)
49
+ report NestedModule.new(node)
50
+ end
51
+
52
+ alias on_casgn on_module
53
+
54
+ private
55
+ class NestedModule
56
+ include RuboCop::Callbacksystems::Helpers
57
+
58
+ def initialize(node)
59
+ @node = node
60
+ end
61
+
62
+ def offense
63
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE) if declaration? && inside_class?
64
+ end
65
+
66
+ private
67
+ attr_reader :node
68
+
69
+ def declaration?
70
+ node.module_type? ||
71
+ RuboCop::Callbacksystems::ClassStructure::BuilderAssignment.new(node).builds_module?
72
+ end
73
+
74
+ def inside_class?
75
+ enclosing_class_or_module_of(node)&.class_type?
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,74 @@
1
+ # Prohibits returning nil at the end of a method. A trailing `nil` is there to
2
+ # hide the value of the statement above it, which means a caller reads the
3
+ # method for a value it was never meant to give, so the method wants a shape
4
+ # whose last statement is its result. A `nil` that is the only statement is
5
+ # intentional, as in an interface method, and stays.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # def process
10
+ # do_something
11
+ # nil
12
+ # end
13
+ #
14
+ # # bad
15
+ # def process
16
+ # if condition
17
+ # do_something
18
+ # end
19
+ # nil
20
+ # end
21
+ #
22
+ # # good - return value is implicit
23
+ # def process
24
+ # do_something
25
+ # end
26
+ #
27
+ # # good - use early return if needed
28
+ # def process
29
+ # return unless condition
30
+ # do_something
31
+ # end
32
+ #
33
+ # # good - nil is clearly intentional (only statement)
34
+ # def some_interface_method
35
+ # nil
36
+ # end
37
+ #
38
+ class RuboCop::Cop::Callbacksystems::NoNilAtEndOfMethod < RuboCop::Cop::Callbacksystems::Base
39
+ def on_def(node)
40
+ report TrailingNil.new(node)
41
+ end
42
+
43
+ alias on_defs on_def
44
+
45
+ private
46
+ class TrailingNil
47
+ include RuboCop::Callbacksystems::Helpers
48
+
49
+ MESSAGE = "Avoid `nil` at the end of a method. Restructure the code to make it unnecessary."
50
+
51
+ def initialize(method_node)
52
+ @method_node = method_node
53
+ end
54
+
55
+ def offense
56
+ RuboCop::Callbacksystems::Offense.new(last, MESSAGE) if trailing?
57
+ end
58
+
59
+ private
60
+ attr_reader :method_node
61
+
62
+ def trailing?
63
+ last&.nil_type? && previous
64
+ end
65
+
66
+ def last
67
+ @last ||= last_statement_in(method_node.body)
68
+ end
69
+
70
+ def previous
71
+ last.left_sibling if last.parent.type?(:begin, :kwbegin)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,37 @@
1
+ # A `validate` declaration already says what the callback does. Its name should
2
+ # describe the condition it checks, so the declaration reads as a sentence.
3
+ # Renaming needs an understanding of that condition and every caller, so this
4
+ # cop reports the registration without changing the callback or its definition.
5
+ #
6
+ # @example
7
+ # # bad
8
+ # validate :validate_email
9
+ # validate :valid_membership
10
+ #
11
+ # # good
12
+ # validate :ensure_deliverable_email
13
+ # validate :no_expired_membership
14
+ # validate :expiration_date_cannot_be_in_the_past
15
+ #
16
+ class RuboCop::Cop::Callbacksystems::NoRedundantValidationName < RuboCop::Cop::Callbacksystems::Base
17
+ MESSAGE = "Validation callback `%<name>s` repeats `validate`. Name the condition instead, like `ensure_*` or `no_*`."
18
+
19
+ def on_send(node)
20
+ if validation_registration?(node)
21
+ node.arguments.each do |argument|
22
+ add_offense(argument, message: format(MESSAGE, name: argument.value)) if redundant_name?(argument)
23
+ end
24
+ end
25
+ end
26
+
27
+ alias on_csend on_send
28
+
29
+ private
30
+ def validation_registration?(node)
31
+ node.method?(:validate) && call_on_self?(node) && !enclosing_method_of(node)
32
+ end
33
+
34
+ def redundant_name?(argument)
35
+ argument.sym_type? && argument.value.match?(/\A(?:valid|validate)(?:_|[!?]?\z)/)
36
+ end
37
+ end