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,168 @@
1
+ # Enforces an empty line before method definitions. The empty line is what
2
+ # separates one definition from the next when reading down a class, so a method
3
+ # pressed against a macro or the method above reads as a continuation of it.
4
+ #
5
+ # @example
6
+ # # bad - macro directly before method
7
+ # before_action :authenticate
8
+ # def index
9
+ # end
10
+ #
11
+ # # bad - consecutive methods without empty line
12
+ # def name
13
+ # end
14
+ # def email
15
+ # end
16
+ #
17
+ # # good - empty line before method
18
+ # before_action :authenticate
19
+ #
20
+ # def index
21
+ # end
22
+ #
23
+ # # good - empty line between methods
24
+ # def name
25
+ # end
26
+ #
27
+ # def email
28
+ # end
29
+ #
30
+ # # good - consecutive macros are fine
31
+ # validates :name
32
+ # validates :email
33
+ #
34
+ # # good - first method in class body needs no preceding empty line
35
+ # class User
36
+ # def name
37
+ # end
38
+ # end
39
+ #
40
+ # # good - method after visibility modifier needs no empty line
41
+ # private
42
+ # def helper
43
+ # end
44
+ #
45
+ class RuboCop::Cop::Callbacksystems::EmptyLineBeforeMethod < RuboCop::Cop::Callbacksystems::Base
46
+ extend RuboCop::Cop::AutoCorrector
47
+
48
+ def on_def(node)
49
+ report MethodDefinition.new(node, describing_macros)
50
+ end
51
+
52
+ alias on_defs on_def
53
+
54
+ private
55
+ def describing_macros
56
+ Array(cop_config["DescribingMacros"]).map(&:to_sym)
57
+ end
58
+
59
+ class MethodDefinition
60
+ include RuboCop::Callbacksystems::Helpers
61
+
62
+ MESSAGE = "Add an empty line before `%<method>s`."
63
+
64
+ def initialize(node, describing_macros)
65
+ @node = node
66
+ @describing_macros = describing_macros
67
+ end
68
+
69
+ def offense
70
+ if crowded?
71
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: source_ordered?) { correct(it) }
72
+ end
73
+ end
74
+
75
+ private
76
+ attr_reader :node, :describing_macros
77
+ delegate :adjacent_to_previous?, :leading_node, :previous_node, to: :description_group, private: true
78
+
79
+ def description_group
80
+ @description_group ||= DescriptionGroup.new(node, describing_macros)
81
+ end
82
+
83
+ def crowded?
84
+ previous_node && !after_visibility_modifier? && missing_empty_line?
85
+ end
86
+
87
+ def after_visibility_modifier?
88
+ !visibility_modifier_of(previous_node).nil?
89
+ end
90
+
91
+ def missing_empty_line?
92
+ adjacent_to_previous?
93
+ end
94
+
95
+ def message
96
+ format(MESSAGE, method: node.method_name)
97
+ end
98
+
99
+ def source_ordered?
100
+ range_through_heredocs(previous_node).end_pos <= leading_node.source_range.begin_pos
101
+ end
102
+
103
+ def correct(corrector)
104
+ if previous_node.last_line == leading_node.first_line
105
+ corrector.replace \
106
+ previous_node.source_range.end.join(leading_node.source_range.begin),
107
+ "\n\n#{indentation_of(previous_node)}"
108
+ else
109
+ corrector.insert_before(line_start_of(leading_node), "\n")
110
+ end
111
+ end
112
+
113
+ def line_start_of(statement)
114
+ position = line_start_position_of(statement)
115
+ statement.source_range.with(begin_pos: position, end_pos: position)
116
+ end
117
+
118
+ # The consecutive describing macros attached to a method and the statement immediately before that group.
119
+ class DescriptionGroup
120
+ include RuboCop::Callbacksystems::Helpers
121
+
122
+ def initialize(node, describing_macros)
123
+ @node = node
124
+ @describing_macros = describing_macros
125
+ end
126
+
127
+ def adjacent_to_previous?
128
+ previous_node && adjacent?(above: previous_node, below: leading_node)
129
+ end
130
+
131
+ def previous_node
132
+ siblings[leading_position.pred] if leading_position.positive?
133
+ end
134
+
135
+ def leading_node
136
+ siblings.fetch(leading_position)
137
+ end
138
+
139
+ private
140
+ attr_reader :node, :describing_macros
141
+
142
+ def leading_position
143
+ @leading_position ||= node_position.downto(1).find { !joins_description_at?(it) } || 0
144
+ end
145
+
146
+ def node_position
147
+ @node_position ||= siblings.index(node)
148
+ end
149
+
150
+ def siblings
151
+ @siblings ||= node.parent&.begin_type? ? node.parent.child_nodes : [ node ]
152
+ end
153
+
154
+ def joins_description_at?(position)
155
+ above = siblings.fetch(position.pred)
156
+ describing?(above) && adjacent?(above:, below: siblings.fetch(position))
157
+ end
158
+
159
+ def describing?(statement)
160
+ bare_send?(statement) && describing_macros.include?(statement.method_name)
161
+ end
162
+
163
+ def adjacent?(above:, below:)
164
+ (below.first_line - range_through_heredocs(above).last_line) <= 1
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,169 @@
1
+ # A class body reads in sections: the mixins, the constants, the declarations of its state, the macros, the methods,
2
+ # and the nested classes. A blank line is what tells the reader one section has ended and the next begins, so two
3
+ # adjacent statements of different sections need one between them, while the statements of one section sit together
4
+ # or apart as the author likes. `BodyOrder` owns the order the sections come in, and this cop asks only for the blank
5
+ # line, whichever way round the two statements sit.
6
+ #
7
+ # The sections are coarser than the ranks `BodyOrder` reads. An `attribute`, an association and a `delegate` all
8
+ # declare what the object holds, so they are one section, the way the authentication generator writes
9
+ # `attribute :session` right above `delegate :user, ...` and `has_secure_password` right above `has_many :sessions`.
10
+ #
11
+ # A visibility modifier belongs to no section, so the pair it makes with the statement on either side is left to the
12
+ # Layout cops.
13
+ #
14
+ # @example
15
+ # # bad - the token sits pressed against the constant
16
+ # class Key < ApplicationRecord
17
+ # PREFIX = "key_"
18
+ # has_secure_token :value
19
+ # end
20
+ #
21
+ # # good
22
+ # class Key < ApplicationRecord
23
+ # PREFIX = "key_"
24
+ #
25
+ # has_secure_token :value
26
+ # end
27
+ #
28
+ # # good - the state declarations are one section
29
+ # class Current < ActiveSupport::CurrentAttributes
30
+ # attribute :session
31
+ # delegate :user, to: :session, allow_nil: true
32
+ # end
33
+ #
34
+ class RuboCop::Cop::Callbacksystems::EmptyLineBetweenSections < RuboCop::Cop::Callbacksystems::Base
35
+ extend RuboCop::Cop::AutoCorrector
36
+
37
+ def on_class(node)
38
+ report_each Body.new(node.body, processed_source)
39
+ end
40
+
41
+ alias on_module on_class
42
+ alias on_sclass on_class
43
+
44
+ private
45
+ class Body
46
+ include RuboCop::Callbacksystems::Helpers
47
+
48
+ def initialize(body, processed_source)
49
+ @body = body
50
+ @processed_source = processed_source
51
+ end
52
+
53
+ def each_offense(&block)
54
+ statements.each_cons(2).filter_map { |above, below| Boundary.new(above:, below:).offense }.each(&block)
55
+ end
56
+
57
+ private
58
+ attr_reader :body, :processed_source
59
+
60
+ def statements
61
+ @statements ||= nodes.map { Statement.new(it, nodes, processed_source) }
62
+ end
63
+
64
+ def nodes
65
+ @nodes ||= statements_in(body)
66
+ end
67
+ end
68
+
69
+ # Two statements written one after the other, read for the sections they belong to.
70
+ class Boundary
71
+ include RuboCop::Callbacksystems::Helpers
72
+
73
+ MESSAGE = "Separate `%<statement>s` from the %<above>s above with a blank line, since the %<below>s are " \
74
+ "another section."
75
+
76
+ def initialize(above:, below:)
77
+ @above = above
78
+ @below = below
79
+ end
80
+
81
+ def offense
82
+ if crowded?
83
+ RuboCop::Callbacksystems::Offense.new(below.node, message, correcting: source_ordered?) { correct(it) }
84
+ end
85
+ end
86
+
87
+ private
88
+ attr_reader :above, :below
89
+
90
+ def crowded?
91
+ crosses_sections? && !below.separated_from?(above)
92
+ end
93
+
94
+ def crosses_sections?
95
+ [ above, below ].none?(&:visibility_modifier?) && above.section != below.section
96
+ end
97
+
98
+ def message
99
+ format(MESSAGE, statement: below.opening_line, above: above.section, below: below.section)
100
+ end
101
+
102
+ def source_ordered?
103
+ above.last_line <= below.node.first_line
104
+ end
105
+
106
+ def correct(corrector)
107
+ if above.last_line == below.node.first_line
108
+ corrector.replace \
109
+ above.node.source_range.end.join(below.node.source_range.begin),
110
+ "\n\n#{indentation_of(above.node)}"
111
+ else
112
+ corrector.insert_before(below.line_start, "\n")
113
+ end
114
+ end
115
+ end
116
+
117
+ class Statement
118
+ include RuboCop::Callbacksystems::Helpers
119
+
120
+ SECTIONS = {
121
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::MIXIN => "mixins",
122
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::VALUE => "constants",
123
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::CLASS_DECLARATION => "constants",
124
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::ATTRIBUTE => "declarations",
125
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::ASSOCIATION => "declarations",
126
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::DELEGATE => "declarations",
127
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::MACRO => "macros",
128
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::METHOD => "methods",
129
+ RuboCop::Callbacksystems::ClassStructure::StatementRank::NESTED_CLASS => "nested classes"
130
+ }
131
+
132
+ attr_reader :node
133
+
134
+ def initialize(node, siblings, processed_source)
135
+ @node = node
136
+ @siblings = siblings
137
+ @processed_source = processed_source
138
+ end
139
+
140
+ def visibility_modifier?
141
+ visibility_modifier_of(node).present?
142
+ end
143
+
144
+ def section
145
+ @section ||= SECTIONS.fetch(RuboCop::Callbacksystems::ClassStructure::StatementRank.new(node, siblings).to_i)
146
+ end
147
+
148
+ def separated_from?(above)
149
+ processed_source.lines[above.last_line...(node.first_line - 1)].any?(&:blank?)
150
+ end
151
+
152
+ def last_line
153
+ range_through_heredocs(node).last_line
154
+ end
155
+
156
+ def opening_line
157
+ node.source.lines.first.strip
158
+ end
159
+
160
+ # The blank line goes above the comments introducing the statement, which travel with it.
161
+ def line_start
162
+ position = RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source).begin_position
163
+ node.source_range.with(begin_pos: position, end_pos: position)
164
+ end
165
+
166
+ private
167
+ attr_reader :siblings, :processed_source
168
+ end
169
+ end
@@ -0,0 +1,107 @@
1
+ # A `private` keyword with nothing after it declares a section that holds
2
+ # nothing. Remove it and let the class end where it ends.
3
+ #
4
+ # The correction stays away when another expression observes the value of the
5
+ # class or module definition: `private` returns `nil`, while the statement above
6
+ # it may return something else.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class Order
11
+ # def total
12
+ # end
13
+ #
14
+ # private
15
+ # end
16
+ #
17
+ # # good
18
+ # class Order
19
+ # def total
20
+ # end
21
+ # end
22
+ #
23
+ class RuboCop::Cop::Callbacksystems::EmptyPrivateSection < RuboCop::Cop::Callbacksystems::Base
24
+ extend RuboCop::Cop::AutoCorrector
25
+
26
+ def on_send(node)
27
+ report PrivateSection.new(node, trailing_comment: trailing_comment_on(node))
28
+ end
29
+
30
+ alias on_csend on_send
31
+
32
+ private
33
+ def trailing_comment_on(node)
34
+ RuboCop::Callbacksystems::Source::Comments.for(processed_source).trailing_comment_on(node.last_line)
35
+ end
36
+
37
+ class PrivateSection
38
+ include RuboCop::Callbacksystems::Helpers
39
+
40
+ MESSAGE = "Remove the empty `private` section."
41
+
42
+ def initialize(node, trailing_comment:)
43
+ @node = node
44
+ @trailing_comment = trailing_comment
45
+ end
46
+
47
+ def offense
48
+ if empty?
49
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: correctable?) do |corrector|
50
+ correct(corrector)
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+ attr_reader :node, :trailing_comment
57
+
58
+ def empty?
59
+ private_modifier? && closes_enclosing_body?
60
+ end
61
+
62
+ def private_modifier?
63
+ visibility_modifier_of(node) == :private
64
+ end
65
+
66
+ def closes_enclosing_body?
67
+ statements_in(enclosing_body).last.equal?(node)
68
+ end
69
+
70
+ # `enclosing_body_for` reports only multi-statement bodies, and a body of just the modifier has one statement.
71
+ def enclosing_body
72
+ enclosing_definition_of(node)&.body
73
+ end
74
+
75
+ def correctable?
76
+ definition_result_discarded? && !moves_tooling_comment?
77
+ end
78
+
79
+ def definition_result_discarded?
80
+ RuboCop::Callbacksystems::Execution::DiscardedExpression.new(enclosing_definition_of(node)).discarded?
81
+ end
82
+
83
+ def moves_tooling_comment?
84
+ trailing_comment && tooling_comment?(trailing_comment)
85
+ end
86
+
87
+ def correct(corrector)
88
+ if inline_removal_range && trailing_comment
89
+ corrector.replace(inline_removal_range.join(trailing_comment.source_range), standing_comment)
90
+ else
91
+ corrector.remove(statement_removal_range_for(node))
92
+ end
93
+ end
94
+
95
+ def inline_removal_range
96
+ @inline_removal_range ||= inline_statement_removal_range_for(node)
97
+ end
98
+
99
+ def standing_comment
100
+ "\n#{line_indentation}#{trailing_comment.text}"
101
+ end
102
+
103
+ def line_indentation
104
+ node.source_range.source_line[/\A[ \t]*/]
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,71 @@
1
+ # An empty class or module keeps its `end` on a line of its own, the way
2
+ # `Style/EmptyMethod` already asks of an empty method.
3
+ #
4
+ # A nested empty class is left to `PreferClassNewForEmptyNestedClass`, which
5
+ # turns it into a constant rather than a definition.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # class PgBox::ConfigurationError < StandardError; end
10
+ #
11
+ # # good
12
+ # class PgBox::ConfigurationError < StandardError
13
+ # end
14
+ #
15
+ class RuboCop::Cop::Callbacksystems::ExpandedEmptyClassOrModule < RuboCop::Cop::Callbacksystems::Base
16
+ extend RuboCop::Cop::AutoCorrector
17
+
18
+ def on_class(node)
19
+ report SingleLineDefinition.new(node)
20
+ end
21
+
22
+ alias on_module on_class
23
+
24
+ private
25
+ class SingleLineDefinition
26
+ include RuboCop::Callbacksystems::Helpers
27
+
28
+ MESSAGE = "Put `end` on its own line."
29
+
30
+ def initialize(node)
31
+ @node = node
32
+ end
33
+
34
+ def offense
35
+ if expandable?
36
+ RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: superclass_without_heredoc?) { correct(it) }
37
+ end
38
+ end
39
+
40
+ private
41
+ attr_reader :node
42
+
43
+ def expandable?
44
+ node.body.nil? && node.single_line? && stays_a_definition?
45
+ end
46
+
47
+ def stays_a_definition?
48
+ node.module_type? || enclosing_class_or_module_of(node).nil?
49
+ end
50
+
51
+ def superclass_without_heredoc?
52
+ node.module_type? || node.parent_class.nil? || !carries_heredoc?(node.parent_class)
53
+ end
54
+
55
+ def correct(corrector)
56
+ corrector.replace(gap_before_end, "\n#{indentation_of(node)}")
57
+ end
58
+
59
+ def gap_before_end
60
+ node.source_range.with(begin_pos: header_end, end_pos: node.loc.end.begin_pos)
61
+ end
62
+
63
+ def header_end
64
+ (superclass || node.identifier).source_range.end_pos
65
+ end
66
+
67
+ def superclass
68
+ node.parent_class if node.class_type?
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,110 @@
1
+ # Detects memoization methods that delegate to another method in the same class.
2
+ # The second method exists only to serve the first, so the reading is split
3
+ # across two definitions for nothing, and the computation belongs in the method
4
+ # that caches it.
5
+ #
6
+ # @example
7
+ # # bad - delegates to another method
8
+ # def user
9
+ # @user ||= find_user
10
+ # end
11
+ #
12
+ # private
13
+ # def find_user
14
+ # User.find(params[:id])
15
+ # end
16
+ #
17
+ # # good - computation is inline
18
+ # def user
19
+ # @user ||= User.find(params[:id])
20
+ # end
21
+ #
22
+ # # good - calls a method on a receiver (not delegation)
23
+ # def owner
24
+ # @owner ||= account.owner
25
+ # end
26
+ #
27
+ # # good - calls method with arguments
28
+ # def formatted_name
29
+ # @formatted_name ||= format_name(first, last)
30
+ # end
31
+ #
32
+ # # good - has additional logic
33
+ # def user
34
+ # @user ||= find_user || default_user
35
+ # end
36
+ #
37
+ class RuboCop::Cop::Callbacksystems::InlineMemoizedComputation < RuboCop::Cop::Callbacksystems::Base
38
+ def on_new_investigation
39
+ @method_calls = RuboCop::Callbacksystems::Methods::Calls.new(processed_source.ast)
40
+ @sibling_methods = RuboCop::Callbacksystems::Methods::Siblings.new
41
+ @macro_referenced_methods = RuboCop::Callbacksystems::Methods::MacroReferences::ContainerIndex.new
42
+ end
43
+
44
+ def on_def(node)
45
+ report Memoization.new(node, method_calls:, sibling_methods:, macro_referenced_methods:)
46
+ end
47
+
48
+ alias on_defs on_def
49
+
50
+ private
51
+ attr_reader :macro_referenced_methods, :method_calls, :sibling_methods
52
+
53
+ class Memoization
54
+ include RuboCop::Callbacksystems::Helpers
55
+
56
+ MESSAGE = "Inline the computation from `%<method>s` instead of delegating."
57
+
58
+ def initialize(node, method_calls:, sibling_methods:, macro_referenced_methods:)
59
+ @node = node
60
+ @method_calls = method_calls
61
+ @sibling_methods = sibling_methods
62
+ @macro_referenced_methods = macro_referenced_methods
63
+ end
64
+
65
+ def offense
66
+ RuboCop::Callbacksystems::Offense.new(node, message) if delegates_to_own_method?
67
+ end
68
+
69
+ private
70
+ attr_reader :node, :method_calls, :sibling_methods, :macro_referenced_methods
71
+ delegate :expression, to: "node.body", private: true
72
+
73
+ def delegates_to_own_method?
74
+ node.body&.or_asgn_type? && delegating_call? && private_delegated_definition? && sole_call_site? &&
75
+ !macro_referenced?
76
+ end
77
+
78
+ def delegating_call?
79
+ bare_send?(expression) && expression.arguments.empty?
80
+ end
81
+
82
+ def private_delegated_definition?
83
+ delegated_definition && private_method?(delegated_definition)
84
+ end
85
+
86
+ def delegated_definition
87
+ @delegated_definition ||= sibling_methods.named(delegated_method, beside: node).first
88
+ end
89
+
90
+ def delegated_method
91
+ expression.method_name
92
+ end
93
+
94
+ def sole_call_site?
95
+ calls_to_delegated_method.one? && calls_to_delegated_method.first.equal?(expression)
96
+ end
97
+
98
+ def calls_to_delegated_method
99
+ @calls_to_delegated_method ||= method_calls.named(delegated_method, from: node)
100
+ end
101
+
102
+ def macro_referenced?
103
+ macro_referenced_methods.include?(delegated_method, beside: node)
104
+ end
105
+
106
+ def message
107
+ format(MESSAGE, method: delegated_method)
108
+ end
109
+ end
110
+ end