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,156 @@
1
+ # Detects single-line brace blocks with a single parameter that should use `it`
2
+ # instead. Since Ruby 3.4 a block can read its one parameter as `it`, and on a
3
+ # single line a name for it only repeats what the receiver already said.
4
+ #
5
+ # @example
6
+ # # bad
7
+ # items.map { |item| item.name }
8
+ # users.select { |user| user.active? }
9
+ # items.each { |item| process(item) }
10
+ # numbers.map { |n| n.to_s }
11
+ #
12
+ # # good
13
+ # items.map { it.name }
14
+ # users.select { it.active? }
15
+ # items.each { process(it) }
16
+ # numbers.map { it.to_s }
17
+ #
18
+ # # good - multi-line block
19
+ # items.map do |item|
20
+ # item.name
21
+ # end
22
+ #
23
+ # # good - multiple parameters
24
+ # hash.each { |key, value| puts key }
25
+ #
26
+ # # good - lambda or proc, where the parameter name documents the signature
27
+ # double = ->(number) { number * 2 }
28
+ # greet = proc { |name| puts name }
29
+ #
30
+ # # good - define_method, where the block sets the defined method's signature
31
+ # define_method(:double) { |number| number * 2 }
32
+ #
33
+ class RuboCop::Cop::Callbacksystems::PreferItBlockParameter < RuboCop::Cop::Callbacksystems::Base
34
+ extend RuboCop::Cop::AutoCorrector
35
+
36
+ def on_block(node)
37
+ report NamedParameterBlock.new(node)
38
+ end
39
+
40
+ alias on_numblock on_block
41
+ alias on_itblock on_block
42
+
43
+ private
44
+ class NamedParameterBlock
45
+ include RuboCop::Callbacksystems::Helpers
46
+
47
+ MESSAGE = "Use `it` instead of explicit block parameter `|%<parameter>s|`."
48
+ METHOD_DEFINITIONS = %i[ define_method define_singleton_method ]
49
+
50
+ def initialize(node)
51
+ @node = node
52
+ end
53
+
54
+ def offense
55
+ offense_for_parameter if convertible?
56
+ end
57
+
58
+ private
59
+ attr_reader :node
60
+
61
+ def convertible?
62
+ single_line_braced? && single_replaceable_parameter? &&
63
+ !node.lambda_or_proc? && !method_definition? && !nested_in_convertible_block? &&
64
+ !read_inside_an_inner_block? && !it_has_an_existing_meaning?
65
+ end
66
+
67
+ def single_line_braced?
68
+ node.body && node.braces? && node.single_line?
69
+ end
70
+
71
+ def single_replaceable_parameter?
72
+ node.arguments.size == 1 && node.first_argument.arg_type? && !parameter_reassigned?
73
+ end
74
+
75
+ def parameter_reassigned?
76
+ nodes_in(node.body, :lvasgn, :match_var).any? { it.children.first == parameter_name }
77
+ end
78
+
79
+ def parameter_name
80
+ node.first_argument.name
81
+ end
82
+
83
+ def method_definition?
84
+ METHOD_DEFINITIONS.include?(node.method_name)
85
+ end
86
+
87
+ def nested_in_convertible_block?
88
+ node.each_ancestor(:any_block).any? { it.arguments.size == 1 && it.braces? && it.single_line? }
89
+ end
90
+
91
+ def read_inside_an_inner_block?
92
+ parameter_reads.any? { |read| read.each_ancestor(*BLOCK_NODE_TYPES).take_while { !it.equal?(node) }.any? }
93
+ end
94
+
95
+ def parameter_reads
96
+ @parameter_reads ||= reads_of(parameter_name, within: node.body)
97
+ end
98
+
99
+ def it_has_an_existing_meaning?
100
+ parameter_name != :it && (it_variable_nodes.any? || bare_it_calls.any?)
101
+ end
102
+
103
+ def it_variable_nodes
104
+ nodes_in(
105
+ node.body,
106
+ :lvar, :lvasgn, :match_var, :shadowarg, :arg, :optarg, :restarg, :kwarg, :kwoptarg, :kwrestarg, :blockarg
107
+ ).select { it.children.first == :it }
108
+ end
109
+
110
+ def bare_it_calls
111
+ nodes_in(node.body, :send).select { bare_send?(it) && it.method?(:it) }
112
+ end
113
+
114
+ def offense_for_parameter
115
+ if parameter_reads.empty?
116
+ RuboCop::Callbacksystems::Offense.new(node, message)
117
+ else
118
+ RuboCop::Callbacksystems::Offense.new(node, message) { correct(it) }
119
+ end
120
+ end
121
+
122
+ def message
123
+ format(MESSAGE, parameter: node.first_argument.source)
124
+ end
125
+
126
+ def correct(corrector)
127
+ Correction.new(corrector, node, reads: parameter_reads).apply
128
+ end
129
+ end
130
+
131
+ class Correction
132
+ include RuboCop::Callbacksystems::Helpers
133
+
134
+ def initialize(corrector, node, reads:)
135
+ @corrector = corrector
136
+ @node = node
137
+ @reads = reads
138
+ end
139
+
140
+ def apply
141
+ replace_parameter_reads
142
+ remove_parameters
143
+ end
144
+
145
+ private
146
+ attr_reader :corrector, :node, :reads
147
+
148
+ def replace_parameter_reads
149
+ reads.each { replace_expression(corrector, it, with: "it") }
150
+ end
151
+
152
+ def remove_parameters
153
+ corrector.remove(node.arguments.source_range.join(node.arguments.source_range.end.adjust(end_pos: 1)))
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,57 @@
1
+ # Detects `collection.size > 1` patterns that should use `many?`. The predicate
2
+ # asks the question in one word, where the comparison makes the reader count.
3
+ #
4
+ # `size`, `length` and `count` answer for a String too, while `many?` comes from
5
+ # `Enumerable` and a String is not one, so a receiver we can read as a String is
6
+ # left alone. A receiver whose type the file does not show stays flagged: getting
7
+ # it wrong raises on the first call rather than passing quietly.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # users.size > 1
12
+ # users.length > 1
13
+ # users.count > 1
14
+ #
15
+ # # good
16
+ # users.many?
17
+ #
18
+ # # good - a String has no `many?`
19
+ # name.strip.length > 1
20
+ #
21
+ class RuboCop::Cop::Callbacksystems::PreferMany < RuboCop::Cop::Callbacksystems::Base
22
+ extend RuboCop::Cop::AutoCorrector
23
+
24
+ MESSAGE = "Use `many?` instead of `%<method>s > 1`."
25
+
26
+ # @!method size_greater_than_one?(node)
27
+ def_node_matcher :size_greater_than_one?, <<~PATTERN
28
+ (call $(call _ ${:size :length :count}) :> (int 1))
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ size_greater_than_one?(node) do |size_call, method|
33
+ next if string_receiver?(size_call.receiver)
34
+
35
+ add_many_offense(node, call: size_call, method:)
36
+ end
37
+ end
38
+
39
+ alias on_csend on_send
40
+
41
+ private
42
+ def string_receiver?(receiver)
43
+ reads_as_string?(receiver)
44
+ end
45
+
46
+ def add_many_offense(node, call:, method:)
47
+ if source_comments.any_within?(node)
48
+ add_offense(node, message: format(MESSAGE, method:))
49
+ else
50
+ add_offense(node, message: format(MESSAGE, method:)) { it.replace(node, replacement_for(call)) }
51
+ end
52
+ end
53
+
54
+ def replacement_for(call)
55
+ "#{call_prefix_of(call)}many?"
56
+ end
57
+ end
@@ -0,0 +1,211 @@
1
+ # Detects a lambda or proc assigned to a local variable inside a method when
2
+ # its body reads nothing from that method. A closure that closes over no
3
+ # local and no parameter is a method in disguise: giving it a name of its own
4
+ # lets it be read and tested alone, and keeps the enclosing method to its
5
+ # job. Instance variables, methods on `self` and constants do not count as
6
+ # captures, since a method reads those too, and neither do the closure's own
7
+ # parameters and locals.
8
+ #
9
+ # A closure that reads or reassigns a local or a parameter of the method is
10
+ # left alone, since that capture is the point of the closure. So is one that
11
+ # yields to the method's block or calls `super`, one defined outside a method,
12
+ # such as a scope or a callback, and one not held in a local, whose shape
13
+ # other cops judge.
14
+ #
15
+ # @example
16
+ # # bad - the lambda reads nothing from `total`
17
+ # def total(orders)
18
+ # price_of = ->(order) { order.quantity * order.unit_price }
19
+ # orders.sum { price_of.call(it) }
20
+ # end
21
+ #
22
+ # # good - a method of its own
23
+ # def total(orders)
24
+ # orders.sum { price_of(it) }
25
+ # end
26
+ #
27
+ # def price_of(order)
28
+ # order.quantity * order.unit_price
29
+ # end
30
+ #
31
+ # # good - the lambda closes over `rate`
32
+ # def total(orders, rate)
33
+ # price_of = ->(order) { order.quantity * rate }
34
+ # orders.sum { price_of.call(it) }
35
+ # end
36
+ #
37
+ class RuboCop::Cop::Callbacksystems::PreferMethodOverLambda < RuboCop::Cop::Callbacksystems::Base
38
+ def on_lvasgn(node)
39
+ node.each_ancestor(:any_def, :class, :module, :sclass).first&.then do |definition|
40
+ report HeldClosure.new(node, method_node: definition) if definition.any_def_type?
41
+ end
42
+ end
43
+
44
+ private
45
+ # A local assignment inside a method, read as a closure against the names that method binds around it.
46
+ class HeldClosure
47
+ extend RuboCop::AST::NodePattern::Macros
48
+ include RuboCop::Callbacksystems::Helpers
49
+
50
+ MESSAGE = "`%<name>s` captures nothing from `%<method>s`, so it can be a method of its own."
51
+
52
+ # @!method closure?(node)
53
+ def_node_matcher :closure?, <<~PATTERN
54
+ (any_block {(lambda) (send nil? {:lambda :proc}) (send (const {nil? cbase} :Proc) :new)} ...)
55
+ PATTERN
56
+
57
+ def initialize(node, method_node:)
58
+ @node = node
59
+ @method_node = method_node
60
+ end
61
+
62
+ def offense
63
+ RuboCop::Callbacksystems::Offense.new(node, message) if closure?(closure) && captures_nothing?
64
+ end
65
+
66
+ private
67
+ attr_reader :node, :method_node
68
+
69
+ def closure
70
+ node.expression
71
+ end
72
+
73
+ def captures_nothing?
74
+ captured_uses.none? && !depends_on_method_context?
75
+ end
76
+
77
+ def captured_uses
78
+ LocalVariables.used_in(closure).select do |use|
79
+ method_names.include?(use.name) && !bound_within_closure?(use)
80
+ end
81
+ end
82
+
83
+ # The closure's own name is left out so that a recursive call counts as no capture.
84
+ def method_names
85
+ @method_names ||= bindings_around_closure.filter_map(&:name).excluding(node.name)
86
+ end
87
+
88
+ def bindings_around_closure
89
+ LocalVariables.bound_in(method_node).select { binding_visible_around_closure?(it) }
90
+ end
91
+
92
+ def binding_visible_around_closure?(binding)
93
+ binding.node.source_range.begin_pos < closure.source_range.begin_pos &&
94
+ binding_scopes.include?(lexical_scope_of(binding.node))
95
+ end
96
+
97
+ def binding_scopes
98
+ @binding_scopes ||= closure.each_ancestor(:any_block, :any_def)
99
+ .take_while { !it.equal?(method_node) }
100
+ .push(method_node)
101
+ end
102
+
103
+ def lexical_scope_of(binding)
104
+ binding.each_ancestor(:any_def, :any_block, :class, :module, :sclass).first
105
+ end
106
+
107
+ def bound_within_closure?(use)
108
+ blocks_from(use).any? { block_binds?(it, use.name) }
109
+ end
110
+
111
+ def blocks_from(use)
112
+ use.node.each_ancestor(:any_block).take_while { !it.equal?(closure) }.push(closure)
113
+ end
114
+
115
+ def block_binds?(block, name)
116
+ block.argument_list.any? { it.name == name }
117
+ end
118
+
119
+ def depends_on_method_context?
120
+ nodes_in(closure, :yield, :super, :zsuper, :return, :break, :next, :redo, :retry).any? ||
121
+ nodes_in(closure, :send, :csend).any? { ContextualCall.new(it).contextual? }
122
+ end
123
+
124
+ def message
125
+ format(MESSAGE, name: node.name, method: method_node.method_name)
126
+ end
127
+
128
+ class ContextualCall
129
+ include RuboCop::Callbacksystems::Helpers
130
+
131
+ CONTEXTUAL_METHODS = %i[ binding block_given? __method__ __callee__ eval local_variables ]
132
+ REFLECTIVE_METHODS = %i[ send __send__ method ]
133
+ STRING_EVAL_METHODS = %i[ instance_eval class_eval module_eval ]
134
+
135
+ def initialize(node)
136
+ @node = node
137
+ end
138
+
139
+ def contextual?
140
+ direct? || reflective? || string_eval?
141
+ end
142
+
143
+ private
144
+ attr_reader :node
145
+
146
+ def direct?
147
+ contextual_receiver? && CONTEXTUAL_METHODS.include?(node.method_name)
148
+ end
149
+
150
+ def contextual_receiver?
151
+ call_on_self?(node) || (core_constant?(node.receiver) && node.receiver.short_name == :Kernel)
152
+ end
153
+
154
+ def reflective?
155
+ node.first_argument&.then do |argument|
156
+ REFLECTIVE_METHODS.include?(node.method_name) &&
157
+ (!argument.type?(:sym, :str) || contextual_reflection?(argument.value.to_sym))
158
+ end
159
+ end
160
+
161
+ def contextual_reflection?(method_name)
162
+ STRING_EVAL_METHODS.include?(method_name) ||
163
+ (contextual_receiver? && CONTEXTUAL_METHODS.include?(method_name))
164
+ end
165
+
166
+ def string_eval?
167
+ STRING_EVAL_METHODS.include?(node.method_name) && node.arguments.any?
168
+ end
169
+ end
170
+
171
+ class LocalVariables
172
+ include RuboCop::Callbacksystems::Helpers
173
+
174
+ BINDING_TYPES = %i[ arg optarg restarg kwarg kwoptarg kwrestarg blockarg shadowarg lvasgn match_var ]
175
+ USE_TYPES = %i[ lvar lvasgn match_var ]
176
+ Occurrence = Data.define(:node, :name)
177
+
178
+ class << self
179
+ def bound_in(tree)
180
+ new(tree, types: BINDING_TYPES).to_a
181
+ end
182
+
183
+ def used_in(tree)
184
+ new(tree, types: USE_TYPES).to_a
185
+ end
186
+ end
187
+
188
+ def initialize(tree, types:)
189
+ @tree = tree
190
+ @types = types
191
+ end
192
+
193
+ def to_a
194
+ node_occurrences + regexp_occurrences
195
+ end
196
+
197
+ private
198
+ attr_reader :tree, :types
199
+
200
+ def node_occurrences
201
+ nodes_in(tree, *types).map { Occurrence.new(it, variable_name_of(it)) }
202
+ end
203
+
204
+ def regexp_occurrences
205
+ nodes_in(tree, :match_with_lvasgn).flat_map do |match|
206
+ match.children.first.to_regexp.named_captures.each_key.map { Occurrence.new(match, it.to_sym) }
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,172 @@
1
+ # Detects local variable assignments that could be methods instead.
2
+ # Assigning to a variable with the same name as the method being called
3
+ # suggests the code would be more declarative with a method or delegate.
4
+ #
5
+ # @example
6
+ # # bad - variable name matches method call (simple receiver)
7
+ # account = user.account
8
+ # plan = @account.plan
9
+ #
10
+ # # good - use delegate when method names match
11
+ # delegate :account, to: :user
12
+ # delegate :plan, to: :account
13
+ #
14
+ # # good - or extract method
15
+ # def account
16
+ # user.account
17
+ # end
18
+ #
19
+ # # bad - finder pattern
20
+ # user = find_user
21
+ # user = User.find(params[:id])
22
+ # order = Order.find_by(number: number)
23
+ #
24
+ # # good - extract to method
25
+ # def user
26
+ # User.find(params[:id])
27
+ # end
28
+ #
29
+ # # ok - assignment in conditional
30
+ # if user = find_user
31
+ # user.activate
32
+ # end
33
+ #
34
+ # # ok - chain receiver (delegate doesn't apply)
35
+ # length = answer.value.to_s.length
36
+ #
37
+ # # ok - receiver is a method parameter (delegate doesn't apply)
38
+ # def process(user)
39
+ # account = user.account # user is a parameter, not a method
40
+ # end
41
+ #
42
+ # # ok - different name indicates transformation
43
+ # formatted_date = date.strftime("%Y-%m-%d")
44
+ # active_users = users.select(&:active?)
45
+ #
46
+ class RuboCop::Cop::Callbacksystems::PreferMethodOverVariable < RuboCop::Cop::Callbacksystems::Base
47
+ def on_lvasgn(node)
48
+ report Assignment.new(node)
49
+ end
50
+
51
+ private
52
+ class Assignment
53
+ DELEGATE_MESSAGE = "Variable `%<variable>s` mirrors method call. Use `delegate :%<variable>s, " \
54
+ "to: :%<receiver>s` or extract a method."
55
+ FINDER_MESSAGE = "Variable `%<variable>s` assigned from finder. Consider extracting a `%<variable>s` method."
56
+ CONDITIONAL_TYPES = %i[ if while until while_post until_post case case_match ]
57
+
58
+ def initialize(node)
59
+ @node = node
60
+ end
61
+
62
+ def offense
63
+ RuboCop::Callbacksystems::Offense.new(node, message) if message
64
+ end
65
+
66
+ private
67
+ attr_reader :node
68
+
69
+ def message
70
+ @message ||= delegation_message || finder_message if assigns_a_call? && !inside_conditional?
71
+ end
72
+
73
+ def assigns_a_call?
74
+ node.expression&.send_type?
75
+ end
76
+
77
+ def inside_conditional?
78
+ node.each_ancestor(*CONDITIONAL_TYPES).any? { condition_holds_assignment?(it.condition) }
79
+ end
80
+
81
+ def condition_holds_assignment?(condition)
82
+ condition&.then { it.equal?(node) || it.source_range.contains?(node.source_range) }
83
+ end
84
+
85
+ def delegation_message
86
+ format(DELEGATE_MESSAGE, variable: variable_name, receiver: receiver.name) if same_name_with_simple_receiver?
87
+ end
88
+
89
+ def same_name_with_simple_receiver?
90
+ value_node.method?(variable_name) && delegatable_receiver?
91
+ end
92
+
93
+ def value_node
94
+ node.expression
95
+ end
96
+
97
+ def variable_name
98
+ node.name
99
+ end
100
+
101
+ # `delegate` needs a method or a reader to send to, which also keeps `now = Time.now` holding its one instant.
102
+ def delegatable_receiver?
103
+ value_node.receiver && !value_node.receiver.lvar_type? && !receiver.chained? && !receiver.name.nil?
104
+ end
105
+
106
+ def receiver
107
+ @receiver ||= Receiver.new(value_node)
108
+ end
109
+
110
+ def finder_message
111
+ format(FINDER_MESSAGE, variable: variable_name) if finder_call.finds_the_variable?
112
+ end
113
+
114
+ def finder_call
115
+ FinderCall.new(variable_name, value_node)
116
+ end
117
+ end
118
+
119
+ class Receiver
120
+ include RuboCop::Callbacksystems::Helpers
121
+
122
+ def initialize(value_node)
123
+ @value_node = value_node
124
+ end
125
+
126
+ def chained?
127
+ value_node.receiver.send_type? && value_node.receiver.receiver
128
+ end
129
+
130
+ def name
131
+ case value_node.receiver.type
132
+ when :send then value_node.receiver.method_name
133
+ when :ivar then name_without_sigil(value_node.receiver.name)
134
+ end
135
+ end
136
+
137
+ private
138
+ attr_reader :value_node
139
+ end
140
+
141
+ class FinderCall
142
+ FINDER_PREFIXES = %w[ find_ get_ fetch_ load_ ]
143
+ CLASS_FINDERS = %w[ find find_by find_by! ]
144
+
145
+ def initialize(variable_name, value_node)
146
+ @variable_name = variable_name
147
+ @value_node = value_node
148
+ end
149
+
150
+ def finds_the_variable?
151
+ (finder_method? || class_finder?) && !reads_a_local?
152
+ end
153
+
154
+ private
155
+ attr_reader :variable_name, :value_node
156
+
157
+ def finder_method?
158
+ FINDER_PREFIXES.any? { value_node.method_name.to_s == "#{it}#{variable_name}" }
159
+ end
160
+
161
+ def class_finder?
162
+ value_node.receiver&.const_type? &&
163
+ CLASS_FINDERS.include?(value_node.method_name.to_s) &&
164
+ variable_name.to_s == value_node.receiver.short_name.to_s.underscore
165
+ end
166
+
167
+ # A method extracted out of here could not read a local of the caller, so a finder given one stays a variable.
168
+ def reads_a_local?
169
+ value_node.each_node(:lvar).any?
170
+ end
171
+ end
172
+ end