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,84 @@
1
+ # Counts only validation guards: leading `return`s that reject input by
2
+ # returning nothing, `nil`, or `false`. Dispatch branches like
3
+ # `return start_value if start?` are a lookup table written as control flow,
4
+ # not validation, so they don't count. Many validations at the start of a
5
+ # method usually means the checks belong in an extracted predicate.
6
+ #
7
+ # @example MaxGuards: 2 (default)
8
+ # # bad - three leading validation guards
9
+ # def process(account)
10
+ # return unless account
11
+ # return false if account.closed?
12
+ # return nil unless account.active?
13
+ # charge(account)
14
+ # end
15
+ #
16
+ # # good - extract the validation
17
+ # def process(account)
18
+ # return unless chargeable?(account)
19
+ # charge(account)
20
+ # end
21
+ #
22
+ class RuboCop::Cop::Callbacksystems::TooManyValidationGuards < RuboCop::Cop::Callbacksystems::Base
23
+ def on_def(node)
24
+ report LeadingGuards.new(node, maximum: cop_config["MaxGuards"])
25
+ end
26
+
27
+ alias on_defs on_def
28
+
29
+ private
30
+ class LeadingGuards
31
+ include RuboCop::Callbacksystems::Helpers
32
+
33
+ MESSAGE = "Method has %<count>d leading validation guards (max %<maximum>d). Extract validation into a helper."
34
+
35
+ def initialize(node, maximum:)
36
+ @node = node
37
+ @maximum = maximum
38
+ end
39
+
40
+ def offense
41
+ RuboCop::Callbacksystems::Offense.new(node, message) if count > maximum
42
+ end
43
+
44
+ private
45
+ attr_reader :node, :maximum
46
+
47
+ def count
48
+ @count ||= leading_zone.count { validation_guard?(it) }
49
+ end
50
+
51
+ def leading_zone
52
+ statements_in(node.body).take_while { in_zone?(it) }
53
+ end
54
+
55
+ def in_zone?(statement)
56
+ statement.lvasgn_type? || validation_guard?(statement)
57
+ end
58
+
59
+ def validation_guard?(statement)
60
+ statement.if_type? && single_branch?(statement) && rejecting_return?(lone_branch_of(statement))
61
+ end
62
+
63
+ # A full `if`/`else` is a two-way choice, not a guard.
64
+ def single_branch?(if_node)
65
+ [ if_node.if_branch, if_node.else_branch ].compact.one?
66
+ end
67
+
68
+ def rejecting_return?(branch)
69
+ branch.return_type? && (branch.children.empty? || single_rejecting_value?(branch))
70
+ end
71
+
72
+ def single_rejecting_value?(return_node)
73
+ return_node.children.one? && return_node.children.first.falsey_literal?
74
+ end
75
+
76
+ def lone_branch_of(if_node)
77
+ if_node.if_branch || if_node.else_branch
78
+ end
79
+
80
+ def message
81
+ format(MESSAGE, count:, maximum:)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,176 @@
1
+ # Detects local variables that alias a method call that already reads as a name.
2
+ # Call the method directly or extract a well-named declarative method instead.
3
+ # A variable that names a computed expression (operator, block-pass, literal
4
+ # receiver) or a multiline call is left alone: there the name does real work.
5
+ #
6
+ # @example
7
+ # # bad - variable just aliases a method call
8
+ # directory = forbidden_directory
9
+ # add_offense(node) if directory
10
+ #
11
+ # # good - call the method directly
12
+ # add_offense(node) if forbidden_directory
13
+ #
14
+ # # ok - variable names a computed expression
15
+ # valid_values = options.map(&:value)
16
+ # answer.errors.add(:base) if (submitted_values - valid_values).any?
17
+ #
18
+ # # ok - call spans multiple lines
19
+ # record = relation.create! \
20
+ # type: "Report",
21
+ # data: {}
22
+ # record.deliver
23
+ #
24
+ # # ok - variable used multiple times
25
+ # user = find_user
26
+ # user.activate
27
+ # user.notify
28
+ #
29
+ # # ok - variable used inside a block (may be mutated across iterations)
30
+ # result = Set.new
31
+ # items.each { |item| result << item }
32
+ #
33
+ # # ok - assignment in conditional
34
+ # if user = find_user
35
+ # user.activate
36
+ # end
37
+ #
38
+ # # ok - variable reassigned
39
+ # current = first
40
+ # current = current.next while current
41
+ #
42
+ class RuboCop::Cop::Callbacksystems::UnnecessaryLocalVariable < RuboCop::Cop::Callbacksystems::Base
43
+ extend RuboCop::Cop::AutoCorrector
44
+
45
+ def on_new_investigation
46
+ @local_variables = RuboCop::Callbacksystems::Execution::LocalVariableOccurrences.new(processed_source.ast)
47
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
48
+ end
49
+
50
+ def on_lvasgn(node)
51
+ report Assignment.new(node, @local_variables, @source_comments)
52
+ end
53
+
54
+ private
55
+ class Assignment
56
+ include RuboCop::Callbacksystems::Helpers
57
+
58
+ MESSAGE = "Variable `%<name>s` is unnecessary. Call the method directly or extract a well-named declarative " \
59
+ "method."
60
+
61
+ def initialize(node, local_variables, source_comments)
62
+ @node = node
63
+ @local_variables = local_variables
64
+ @source_comments = source_comments
65
+ end
66
+
67
+ def offense
68
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: inlineable?) { correct(it) } if unnecessary?
69
+ end
70
+
71
+ private
72
+ attr_reader :node, :local_variables, :source_comments
73
+
74
+ def unnecessary?
75
+ aliases_named_call? && !conditional? && single_use? && !used_inside_nested_block? && !used_for_restoration?
76
+ end
77
+
78
+ def aliases_named_call?
79
+ value&.type?(:call) && !computed_expression? && !value.multiline?
80
+ end
81
+
82
+ def value
83
+ node.expression
84
+ end
85
+
86
+ def computed_expression?
87
+ value.operator_method? || value.arguments.any?(&:block_pass_type?) || literal_receiver?
88
+ end
89
+
90
+ def literal_receiver?
91
+ value.receiver&.then { it.type?(:array, :hash) || it.recursive_literal? }
92
+ end
93
+
94
+ def conditional?
95
+ node.each_ancestor(:if, :while, :until, :case, :and, :or).any?
96
+ end
97
+
98
+ def single_use?
99
+ enclosing_scope && !reassigned? && references.one?
100
+ end
101
+
102
+ def enclosing_scope
103
+ @enclosing_scope ||= enclosing_scope_of(node)
104
+ end
105
+
106
+ def reassigned?
107
+ other_occurrences.any? { it.type?(:lvasgn, :match_var) }
108
+ end
109
+
110
+ def other_occurrences
111
+ @other_occurrences ||= local_variables.named(node.name, around: node).reject { it.equal?(node) }
112
+ end
113
+
114
+ def references
115
+ @references ||= other_occurrences.select(&:lvar_type?)
116
+ end
117
+
118
+ def used_inside_nested_block?
119
+ reference.each_ancestor(:any_block).any? { it != enclosing_scope }
120
+ end
121
+
122
+ def reference
123
+ references.first
124
+ end
125
+
126
+ def used_for_restoration?
127
+ reference.each_ancestor(:ensure, :resbody).any?
128
+ end
129
+
130
+ def message
131
+ format(MESSAGE, name: node.name)
132
+ end
133
+
134
+ # The next statement removes intervening work; its evaluation path must also keep the call eager and ordered.
135
+ def inlineable?
136
+ nodes_in(next_statement, :lvar).any? { it.equal?(reference) } && !read_by_another_assignment? &&
137
+ !carries_heredoc?(node) && relocation_preserves_order? && removal_uncommented?
138
+ end
139
+
140
+ def next_statement
141
+ node.right_sibling if node.parent.begin_type?
142
+ end
143
+
144
+ # Two corrections on one line would overwrite each other, so this one waits for the pass that follows.
145
+ def read_by_another_assignment?
146
+ reference.each_ancestor(:lvasgn).any?
147
+ end
148
+
149
+ def relocation_preserves_order?
150
+ RuboCop::Callbacksystems::Execution::EagerEvaluationPath.new(reference, within: next_statement)
151
+ .preserves_order?
152
+ end
153
+
154
+ def removal_uncommented?
155
+ !source_comments.any_within?(node.source_range.with(end_pos: next_statement.source_range.begin_pos))
156
+ end
157
+
158
+ def correct(corrector)
159
+ corrector.remove(node.source_range.with(end_pos: next_statement.source_range.begin_pos))
160
+ replace_expression(corrector, reference, with: inlined_source)
161
+ end
162
+
163
+ # Bare arguments at the read would swallow the following `.method` or comma, so they get parentheses.
164
+ def inlined_source
165
+ if value.arguments? && !value.parenthesized?
166
+ "#{call_source_through_selector}(#{value.arguments.map(&:source).join(", ")})"
167
+ else
168
+ value.source
169
+ end
170
+ end
171
+
172
+ def call_source_through_selector
173
+ value.source_range.with(end_pos: value.loc.selector.end_pos).source
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,203 @@
1
+ # Detects private methods of a visually private nested class that nothing
2
+ # calls. When the project index confirms that no other file reaches or reopens
3
+ # the class, a private method no line of the file names is dead code a reader
4
+ # still has to account for.
5
+ #
6
+ # @example
7
+ # # bad - unused private method
8
+ # class Foo
9
+ # def process
10
+ # Bar.new.run
11
+ # nil
12
+ # end
13
+ #
14
+ # private
15
+ # class Bar
16
+ # def run
17
+ # helper
18
+ # end
19
+ #
20
+ # private
21
+ # def helper; end
22
+ # def unused; end # never called
23
+ # end
24
+ # end
25
+ #
26
+ # # good - all private methods are used
27
+ # class Foo
28
+ # def process
29
+ # Bar.new.run
30
+ # nil
31
+ # end
32
+ #
33
+ # private
34
+ # class Bar
35
+ # def run
36
+ # helper
37
+ # end
38
+ #
39
+ # private
40
+ # def helper; end
41
+ # end
42
+ # end
43
+ #
44
+ class RuboCop::Cop::Callbacksystems::UnusedPrivateMethodInNestedClass < RuboCop::Cop::Callbacksystems::Base
45
+ include RuboCop::Callbacksystems::ProjectIndex::Support
46
+
47
+ def on_new_investigation
48
+ @file_references = FileReferences.new(processed_source.ast) if project_index
49
+ end
50
+
51
+ def on_class(node)
52
+ if project_index_reliable?
53
+ private_nested_classes_in(node).each do |nested_class|
54
+ confined_declaration_of(nested_class)&.then do |declaration|
55
+ report_each UnusedMethods.new(nested_class, declaration:, references: file_references)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ alias on_module on_class
62
+
63
+ private
64
+ attr_reader :file_references
65
+
66
+ # Every source-level way the file can name or discover one of the nested class's methods, indexed once.
67
+ class FileReferences
68
+ include RuboCop::Callbacksystems::Helpers
69
+
70
+ NAME_PREFIX_PATTERN = /[a-zA-Z_]\w*\z/
71
+ DYNAMIC_METHOD_LOOKUPS = %i[
72
+ send __send__ public_send method public_method private_method instance_method public_instance_method
73
+ respond_to? method_defined? public_method_defined? protected_method_defined? private_method_defined?
74
+ alias_method
75
+ ]
76
+ METHOD_ENUMERATIONS = %i[
77
+ methods public_methods protected_methods private_methods singleton_methods
78
+ instance_methods public_instance_methods protected_instance_methods private_instance_methods
79
+ ]
80
+
81
+ def initialize(root)
82
+ @evidence = RuboCop::Callbacksystems::Methods::ReferenceEvidence.for(root)
83
+ end
84
+
85
+ def complete?
86
+ dynamic_calls_resolved? && !methods_enumerated?
87
+ end
88
+
89
+ def include?(method_name)
90
+ called_methods.include?(method_name) || dynamically_called_methods.include?(method_name) ||
91
+ aliased_methods.include?(method_name) || macro_referenced_methods.include?(method_name) ||
92
+ literal_method_names.include?(method_name.to_s) ||
93
+ literal_method_prefixes.any? { method_name.to_s.start_with?(it) }
94
+ end
95
+
96
+ private
97
+ attr_reader :evidence
98
+
99
+ delegate :macro_referenced_methods, :literal_method_names, to: :evidence, private: true
100
+
101
+ def dynamic_calls_resolved?
102
+ dynamic_calls.all? do |send_node|
103
+ method_name_arguments_of(send_node).all? { it.type?(:sym, :str) }
104
+ end
105
+ end
106
+
107
+ def dynamic_calls
108
+ @dynamic_calls ||= evidence.calls.select { DYNAMIC_METHOD_LOOKUPS.include?(it.method_name) }
109
+ end
110
+
111
+ def method_name_arguments_of(send_node)
112
+ send_node.method?(:alias_method) ? send_node.arguments : [ send_node.first_argument ].compact
113
+ end
114
+
115
+ def methods_enumerated?
116
+ evidence.calls.any? { METHOD_ENUMERATIONS.include?(it.method_name) }
117
+ end
118
+
119
+ def called_methods
120
+ @called_methods ||= evidence.calls.to_set(&:method_name)
121
+ end
122
+
123
+ def dynamically_called_methods
124
+ @dynamically_called_methods ||= dynamic_calls.flat_map do |send_node|
125
+ method_name_arguments_of(send_node).filter_map { literal_method_name_of(it) }
126
+ end.to_set
127
+ end
128
+
129
+ def literal_method_name_of(literal)
130
+ literal.value.to_s.then { it.to_sym if it.valid_encoding? }
131
+ end
132
+
133
+ def aliased_methods
134
+ @aliased_methods ||= evidence.alias_nodes.flat_map do |alias_node|
135
+ alias_node.children.filter_map { literal_method_name_of(it) if it.type?(:sym, :str) }
136
+ end.to_set
137
+ end
138
+
139
+ def literal_method_prefixes
140
+ @literal_method_prefixes ||= evidence.interpolated_literals.filter_map do |literal|
141
+ interpolation_prefix(literal)
142
+ end.to_set
143
+ end
144
+
145
+ def interpolation_prefix(node)
146
+ if node.children.any? { !it.str_type? }
147
+ node.children.first.then { it.value.to_s.scrub[NAME_PREFIX_PATTERN] if it.str_type? }
148
+ end
149
+ end
150
+ end
151
+
152
+ class UnusedMethods
153
+ include RuboCop::Callbacksystems::Helpers
154
+
155
+ MESSAGE = "Private method `%<method>s` in nested class `%<class>s` is never called. Remove it."
156
+ STRUCTURAL_METHODS = RuboCop::Callbacksystems::Methods::ImplicitInvocations::METHOD_NAMES
157
+
158
+ def initialize(nested_class, declaration:, references:)
159
+ @nested_class = nested_class
160
+ @declaration = declaration
161
+ @references = references
162
+ end
163
+
164
+ def each_offense
165
+ unused_methods.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
166
+ end
167
+
168
+ private
169
+ attr_reader :nested_class, :declaration, :references
170
+
171
+ def unused_methods
172
+ provable? ? uncalled_methods : []
173
+ end
174
+
175
+ # A superclass calls the hooks its subclass overrides, and that call is nowhere in this file.
176
+ def provable?
177
+ nested_class.parent_class.nil? && !subclassed_elsewhere? && references.complete?
178
+ end
179
+
180
+ def subclassed_elsewhere?
181
+ declaration.descendants.any? { it.name != declaration.name }
182
+ end
183
+
184
+ def uncalled_methods
185
+ private_methods_in(nested_class).reject do |method_node|
186
+ STRUCTURAL_METHODS.include?(method_node.method_name) || override?(method_node.method_name) ||
187
+ referenced?(method_node.method_name)
188
+ end
189
+ end
190
+
191
+ def override?(method_name)
192
+ declaration.find_member("#{method_name}()", only_inherited: true).present?
193
+ end
194
+
195
+ def referenced?(method_name)
196
+ references.include?(method_name)
197
+ end
198
+
199
+ def message_for(method_node)
200
+ format(MESSAGE, method: method_node.method_name, class: nested_class.identifier.short_name)
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,21 @@
1
+ require "zeitwerk"
2
+ require "rubocop"
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/array/access"
5
+ require "active_support/core_ext/enumerable"
6
+ require "active_support/core_ext/module/delegation"
7
+ require "active_support/core_ext/object/blank"
8
+ require "active_support/core_ext/string/exclude"
9
+ require "active_support/core_ext/string/inflections"
10
+
11
+ module RubocopCallbacksystems
12
+ end
13
+
14
+ loader = Zeitwerk::Loader.new
15
+ loader.tag = "rubocop-callbacksystems"
16
+ loader.push_dir("#{__dir__}/rubocop", namespace: RuboCop)
17
+ loader.ignore("#{__dir__}/rubocop/callbacksystems/cops_document.rb")
18
+ loader.setup
19
+ loader.eager_load
20
+ RuboCop::ProjectIndexLoader.singleton_class.prepend RuboCop::Callbacksystems::ProjectIndex::Loader
21
+ RuboCop::Cop::Team.prepend RuboCop::Callbacksystems::ProjectIndex::Corrections